Skip to content

Repository files navigation

SmartDock icon

SmartDock

Automatic Dock manager for macOS — different Dock settings for different displays

Version 2.5.0 macOS 14+ Swift 6.2 MIT License


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.

✨ Features

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

📸 Screenshot

SmartDock Settings

📦 Installation

From Homebrew

brew install --cask alexeikaratai/tap/smartdock

After install, grant Accessibility permission in System Settings → Privacy & Security → Accessibility.

From GitHub Release

Download SmartDock.app from Releases. To open unsigned app:

xattr -cr /Applications/SmartDock.app
codesign --force --deep --sign - /Applications/SmartDock.app
open /Applications/SmartDock.app

Or: right-click → Open → Open in the dialog.

From Source

git clone https://github.com/alexeikaratai/smartdock.git
cd smartdock
make run

🔗 Automation

SmartDock 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 window

Shortcuts & Spotlight — built, not yet shipped

SmartDock 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.

AppleScript

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 tell

From 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.

🧪 Run Tests

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 formatting

See CONTRIBUTING.md for conventions, the style rules that are switched off on purpose, and what the coverage numbers do and don't cover.

🏗️ Architecture

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

Key Design Decisions

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

⚠️ Known Limitations

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.

🔐 Permissions

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.

🛠️ Requirements

  • 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.

👤 Author

Alex Karatai

📄 License

MIT License. See LICENSE for details.

About

Automatic Dock manager for macOS — different Dock settings for different displays. Lives in menu bar, switches Dock config instantly when external monitor connects or disconnects.

Topics

Resources

Contributing

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages