Automatic Dock manager for macOS — different Dock settings for different displays
SmartDock lives in your menu bar and automatically switches Dock configuration when you connect or disconnect an external monitor. Configure separate settings for each mode — position, icon size, magnification, autohide — and SmartDock applies them instantly.
| Feature | Details | |
|---|---|---|
| 🖥️ | Two-mode Dock profiles | Separate settings for external monitor vs. built-in display |
| 📍 | Position control | Bottom, Left, or Right — per mode |
| 📐 | Icon size & magnification | Independent size sliders for each mode |
| 👁️ | Autohide toggle | Show/hide Dock per mode — except while an app is fullscreen |
| ⚡ | Instant detection | Event-driven via CGDisplayRegisterReconfigurationCallback — no polling |
| 🔄 | System sync | Auto-imports Dock changes from System Settings via KVO |
| 🔔 | Notifications | macOS banner when profile switches (optional) |
| ⌨️ | Global hotkeys | 5 customizable shortcuts — toggle autohide, refresh, switch profiles, open settings |
| 🔗 | URL scheme | smartdock:// commands for Raycast, Alfred, Shortcuts.app and shell scripts |
| 🩺 | Diagnostics | Copy Diagnostic Info + Export Logs — both scrubbed of anything identifying |
| 📜 | AppleScript | Scriptable from Script Editor, osascript, Automator and Shortcuts |
| 🎨 | Glass UI | Tabbed settings window (Settings / Shortcuts / About) with NSVisualEffectView |
| 🚀 | Launch at Login | Native SMAppService integration |
| 🛡️ | Smooth transitions | Per-property AppleScript — no Dock restart needed |
| 👋 | Onboarding | Welcome screen on first launch |
brew install --cask alexeikaratai/tap/smartdockAfter install, grant Accessibility permission in System Settings → Privacy & Security → Accessibility.
Download SmartDock.app from Releases. To open unsigned app:
xattr -cr /Applications/SmartDock.app
codesign --force --deep --sign - /Applications/SmartDock.app
open /Applications/SmartDock.appOr: right-click → Open → Open in the dialog.
git clone https://github.com/alexeikaratai/smartdock.git
cd smartdock
make runSmartDock registers a smartdock:// URL scheme, so any tool that can open a URL
can drive it — Raycast, Alfred, Shortcuts.app, or a plain shell script:
open smartdock://refresh # re-apply the config for the current display setup
open smartdock://switch/external # force the External Monitor profile
open smartdock://switch/builtin # force the Built-in Only profile
open smartdock://toggle-autohide # flip auto-hide on the active profile
open smartdock://settings # open the Settings windowSmartDock has native App Intents for four actions, one of them parameterised:
| Action | Parameter |
|---|---|
| Refresh Dock | — |
| Switch Dock Profile | External Monitor / Built-in Display |
| Toggle Dock Auto-Hide | — |
| Open SmartDock Settings | — |
They are deliberately left out of the released build. macOS only opens the App
Intents connection to a bundle it can validate, and SmartDock is ad-hoc signed —
linkd rejects the process outright:
Rejecting invalid client due to requiresValidatedBundle
The actions would still be listed in Spotlight and Shortcuts, and every one of them
would fail with "couldn't communicate with the app" — worse than not offering them.
So make app builds and verifies the metadata but does not put it in the bundle;
make sign does, because that is the Developer ID path.
Until then, use the smartdock:// URLs above — Shortcuts.app drives them through its
Open URL action, and AppleScript works from a Run AppleScript action.
SmartDock ships a scripting dictionary — open it in Script Editor with File → Open Dictionary…, or drive the app directly:
tell application "SmartDock"
refresh -- re-apply the config for the current display setup
switch to external -- force the External Monitor profile
switch to builtin -- force the Built-in Only profile
toggle autohide -- flip auto-hide on the active profile
show settings -- open the Settings window
end tellFrom the shell:
osascript -e 'tell application "SmartDock" to switch to external'Hotkeys, URLs, AppleScript and Shortcuts all run through one code path, so the four can never disagree about what a command does.
make test # run the suite (Swift Testing, in parallel)
make coverage # run it with a per-file coverage table
make lint # verify formatting — CI gates on this
make format # apply formattingSee CONTRIBUTING.md for conventions, the style rules that are switched off on purpose, and what the coverage numbers do and don't cover.
Sources/
├── SmartDockCore/ # Testable business logic
│ ├── DockConfiguration.swift # DockConfiguration + HotkeyBinding + UserPreferences
│ ├── DisplayMonitor.swift # CG callback + debounce for display changes
│ ├── DockController.swift # AppleScript Dock control + KVO system sync
│ ├── SmartDockService.swift # Orchestrator: display state → dock config
│ ├── URLCommand.swift # smartdock:// URL parsing
│ ├── AppleScriptCommand.swift # dock profile ↔ Apple Event code mapping
│ ├── LogExport.swift # log show invocation + redaction
│ ├── DiagnosticReport.swift # Bug-report snapshot formatting
│ └── Log.swift # Logger API (macOS 14+)
└── SmartDock/ # AppKit UI layer
├── App.swift # @main entry, manual NSApplication run loop
├── ScriptingSupport.swift # NSScriptCommand subclasses for the sdef
├── AppIntentsSupport.swift # App Intents for Shortcuts.app and Spotlight
├── StatusBarController.swift # Menu bar icon & dropdown with SF Symbol icons
├── SettingsWindow.swift # Tabbed glass window (Settings / Shortcuts / About)
├── OnboardingWindow.swift # First-launch welcome screen
├── NotificationManager.swift # macOS banner notifications
├── HotkeyManager.swift # Global keyboard shortcuts (5 actions)
├── HotkeyRecorder.swift # Captures a keystroke into a binding
├── AppRelauncher.swift # Safe relaunch — waits for PID exit first
├── AppUpdateWatcher.swift # Detects Homebrew upgrade, prompts relaunch
├── LaunchAtLogin.swift # SMAppService wrapper
├── AccessibilityChecker.swift # First-launch Accessibility prompt
└── Views/ # Self-contained UI pieces
├── UI.swift # Shared control & glass window factories
├── PositionIcon.swift # Cached dock-position thumbnails
├── PositionPicker.swift # Position button row
├── AboutTabView.swift # About tab contents
└── AccessibilityWarningView.swift # Permission banner & reset flow
| Decision | Why |
|---|---|
| AppleScript via System Events | Graceful Dock updates without killall Dock — no visual glitch, no restart |
Per-property tell blocks |
Each setting applied independently — one failure doesn't block others |
| Debounced display callbacks | 1s settle delay filters transient CG callbacks during Mission Control / fullscreen transitions |
| Swift 6 strict concurrency | @MainActor on all UI and service types — no data races |
| Protocol-based DI | DisplayMonitoring / DockControlling protocols enable mock-based testing |
| Event-driven detection | CGDisplayRegisterReconfigurationCallback — no timers, no polling |
| Diff-based apply | Only runs AppleScript for properties that actually changed — no dock flash |
| KVO system sync | Observes com.apple.dock UserDefaults — auto-imports changes from System Settings |
| One command path | Hotkeys, smartdock:// URLs, AppleScript and App Intents all reach HotkeyManager.perform — four front doors, one implementation |
| Hotkey caching | Bindings cached in memory — no UserDefaults reads on every keystroke |
| Wake recovery | Re-applies config after sleep/wake to fix macOS resetting dock state |
Auto-hide does not change while an app is in fullscreen. macOS declines the request — the script runs, reports success, and the Dock stays as it was. Everything else (position, icon size, magnification) applies normally, and auto-hide applies as soon as nothing is fullscreen.
SmartDock detects this rather than assuming: it reads the Dock back after every change and records what actually landed. If a setting was refused you will see it in Settings → About → Copy Diagnostic Info:
- Last apply: Dock ignored autohide (requested autohide) ⚠️
and in the log:
Dock ignored autohide — AppleScript reported success but the setting did not land
The menu bar reports what the Dock is really doing, not what was asked for.
SmartDock asks for two separate permissions:
| Permission | Needed for | When |
|---|---|---|
| Automation (System Events) | Core dock switching via AppleScript | macOS prompts once, the first time SmartDock changes the Dock |
| Accessibility | Global keyboard shortcuts only | Prompted on first launch — optional, everything else works without it |
Both live in System Settings → Privacy & Security. If hotkeys stop working after a Homebrew update, ad-hoc signing has invalidated the Accessibility grant — use Settings → Shortcuts → Reset Permission.
- macOS 14.0+ (Sonoma) to run
- Swift 6.2+ to build — the package declares
swift-tools-version: 6.2 - Xcode 26+ / matching Command Line Tools (
xcode-select --install). Swift 6.2 first shipped in Xcode 26, so earlier Xcode versions cannot build this package.
Alex Karatai
MIT License. See LICENSE for details.
