Skip to content

Repository files navigation

Oriel: Keyboard-driven window manager for macOS

A keyboard-driven window manager for macOS, move and arrange windows with global shortcuts.
A single dependency-free Swift Package, builds with the swift CLI alone, no Xcode project required.

Features

Default shortcut Action
⌃⌥↩ Maximize / restore previous size (toggle)
⌃⌥⌘→ Move to next display
⌃⌥⌘← Move to previous display
⌃⌥← Left half of the current display
⌃⌥→ Right half of the current display
⌃⌥C Center on the current display

Display moves are smarter than a plain teleport:

  • Size is preserved: the window is shrunk only if it's larger than the target screen.
  • Relative position is preserved: the position is converted to a ratio of the free space (screen size − window size) within the screen's visibleFrame, and re-applied on the target screen. A centered window arrives exactly centered; a window flush against an edge stays flush against that edge.
  • Moves wrap around: from the last display, "next" cycles back to the first.

The half shortcuts continue across displays too: pressing ⌃⌥→ on a window that already fills the right half carries it to the left half of the next display, and ⌃⌥← on a left-half window carries it to the right half of the previous display. Repeating the key walks every half position in order. With a single display the halves stay put.

Settings

Open from the menu bar icon → Settings… (a System Settings–style sidebar window).

  • General: launch at login, and hide the menu bar icon.
  • Shortcuts: click any action's button and press a new combination; it is applied and saved immediately. Esc cancels, and at least one of ⌘ / ⌃ / ⌥ is required.

With the menu bar icon hidden, Oriel runs fully invisible. It appears in the Dock only while the settings window is open, and launching Oriel again while it's running reopens Settings.

Note

Launch at login requires the bundled app (SMAppService needs an app bundle). Settings live in UserDefaults, so swift run (non-bundled) and build/Oriel.app use separate domains and don't share them.

Building the app

./Scripts/bundle.sh
open build/Oriel.app

Grant the Accessibility permission to Oriel itself when prompted. The bundle is ad-hoc signed, so the permission survives rebuilds on this machine; distributing to other machines requires a Developer ID signature plus notarization.

The app icon is compiled from the Icon Composer document at Assets/AppIcon.icon (generated by swift Scripts/make-assets.swift), so on macOS 26+ the system renders it live with the Liquid Glass treatment, including the dark, clear, and tinted variants.

Release signing & notarization

The release workflow signs and notarizes automatically when these repository secrets exist (without them, releases fall back to ad-hoc signing):

Secret Value
DEVELOPER_ID_P12 Developer ID Application certificate + private key, exported as .p12 from Keychain Access, then base64-encoded (base64 -i cert.p12)
DEVELOPER_ID_P12_PASSWORD Password chosen when exporting the .p12
NOTARY_KEY_P8 Contents of an App Store Connect API key (.p8, Developer role)
NOTARY_KEY_ID The API key's Key ID
NOTARY_ISSUER_ID The API key's Issuer ID
SPARKLE_ED_PRIVATE_KEY Sparkle EdDSA private key (generate_keys -x) for signing auto-updates, must match SUPublicEDKey in Scripts/Info.plist

Auto-updates (Sparkle)

The app updates itself via Sparkle: it checks the feed at appcast.xml on main (see SUFeedURL in Scripts/Info.plist). After publishing a release, the workflow signs the notarized zip with the Sparkle private key and commits a new appcast entry, no manual steps.

Release notes come from CHANGELOG.md: write a ## <version> section before tagging (the release fails early without one). The workflow publishes that section as the GitHub release body and embeds it (rendered to HTML) in the appcast entry, so the update dialog shows the same notes. The EdDSA keypair lives in the maintainer's login keychain (back it up; losing it means existing installs can't verify future updates).

Sparkle is Developer-ID-distribution only, a future App Store variant must compile out UpdaterController.swift and the Sparkle dependency (App Review 2.5.2 forbids self-updaters).

Local distributable builds work too: CODESIGN_IDENTITY="Developer ID Application" ./Scripts/bundle.sh signs with hardened runtime + timestamp instead of ad-hoc.

Development

swift run                  # run once
./Scripts/dev.sh           # rebuild & relaunch on source changes
./Scripts/dev.sh --settings  # same, with the settings window open

dev.sh polls Sources/ and Package.swift once a second and swaps the app in when a build succeeds (on failure the old instance keeps running, save to retry). Ctrl+C shuts everything down. If you prefer an FSEvents-based watcher: brew install watchexec, then watchexec -r -e swift -- swift run Oriel.

Important

For a process launched from a terminal, macOS attributes the Accessibility permission to the terminal app (the "responsible process"), enable your terminal (iTerm, Terminal, VS Code, …) under System Settings → Privacy & Security → Accessibility. After that, swift run never re-prompts, keeping the dev loop fast.

If shortcuts do nothing, it's almost always this. Toggle the terminal off and on in the Accessibility list, or fully restart it, permission changes apply after a process restart.

Manual test checklist

  1. swift run → the menu bar icon appears.
  2. Focus any window and try ⌃⌥← / ⌃⌥→. The half placements fail the least, so they're a good first check of permissions and coordinates.
  3. ⌃⌥↩ twice maximizes, then returns to the original size and position.
  4. With 2+ displays, ⌃⌥⌘→ / ⌃⌥⌘←. Size preserved, relative position keeps its ratio, nothing lands off-screen on a smaller display.
  5. Edge cases: vertically arranged displays, a Dock on only one monitor, fixed-size windows (resize may be refused, but the position must still move).

Unit tests

./Scripts/test.sh   # equivalent to `swift test`

The display-move math (ScreenMath.relativeReposition) is a pure function, covered in Tests/OrielTests/: center stays centered, edges stay flush, size is preserved, oversized windows shrink, and a round trip returns to the original spot. Swift Testing needs full Xcode, Command Line Tools alone won't run it.

Code structure

Sources/Oriel/
├── main.swift                # Entry point (accessory app, no Dock icon)
├── AppDelegate.swift         # Permissions, shortcut wiring, menu bar item, Dock policy
├── HotKeyCenter.swift        # Carbon RegisterEventHotKey wrapper (global shortcuts)
├── ShortcutStore.swift       # Per-action shortcut specs + UserDefaults persistence
├── AppPreferences.swift      # App-level preferences (hide menu bar icon)
├── SettingsWindow.swift      # Sidebar settings window + shortcut recorder
├── AccessibilityWindow.swift # AXUIElement wrapper (focused window, frame get/set)
├── ScreenMath.swift          # Coordinate-system conversion (the crux!) + position math
└── WindowManager.swift       # The actual logic for the three features

Recommended reading order: ScreenMath.swiftWindowManager.swift.

All window coordinates are normalized to the AX API's coordinate system (origin at the top-left of the primary display, y increasing downward). NSScreen's Cocoa coordinates (bottom-left origin) must go through ScreenMath.flipped(_:) before comparing with window coordinates, 90% of multi-display bugs come from missing this conversion.

The settings UI is dependency-free AppKit. (It began under a Command Line Tools–only environment, where the SwiftUI macro plugin isn't available; everything works, so it stayed.)

License

MIT, see LICENSE. Bundled third-party software and its licenses are listed in THIRD-PARTY-NOTICES.md.

Inspired by Rectangle. No code is shared.

About

A keyboard-driven window manager for macOS

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages