Skip to content

Commit 395bf6a

Browse files
authored
Merge pull request #25 from pszypowicz/hide-menu-icon
Make the menu bar icon optional
2 parents 1958db7 + 648a094 commit 395bf6a

5 files changed

Lines changed: 49 additions & 11 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ open /Applications/Cyclist.app
8181

8282
The build script signs with your "Apple Development" certificate when one is present so the Accessibility grant survives rebuilds. See `scripts/build-app.sh --help` for options.
8383

84-
On first launch Cyclist prompts for Accessibility permission and activates itself once granted. It lives in the menu bar (no Dock icon); the menu holds Settings, About, and Quit - quitting is how every hook releases and the native shortcuts return. The Settings window holds the list options, trackpad swipe navigation, and a native Launch at Login toggle (registers with System Settings > General > Login Items).
84+
On first launch Cyclist prompts for Accessibility permission and activates itself once granted. It lives in the menu bar (no Dock icon); the menu holds Settings, About, and Quit - quitting is how every hook releases and the native shortcuts return. The Settings window holds the list options, trackpad swipe navigation, and a native Launch at Login toggle (registers with System Settings > General > Login Items). The menu bar icon itself is optional (Settings > General): with it hidden, hold the switcher open and press `,` to reach Settings, or just relaunch Cyclist - reopening the app always presents the Settings window (where Cmd+Q still quits).
8585

8686
## Configuration
8787

@@ -100,6 +100,7 @@ defaults write io.github.pszypowicz.Cyclist switcherShortcut "alt+tab"
100100
| `includeNoWindows` | bool | `false` |
101101
| `trackpadSwipe` | bool | `true` |
102102
| `keyboardSpaceNavigation` | bool | `true` |
103+
| `showMenuBarIcon` | bool | `true` |
103104
| `aerospaceIntegration` | bool | `false` |
104105
| `showHollowWorkspaces` | bool | `false` |
105106
| `switcherShortcut` | string | `cmd+tab` |

Sources/Cyclist/AppDelegate.swift

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
2727
// Start delivering only after every consumer has wired its callbacks.
2828
wsEvents.start()
2929
controller.onTapInvalidated = { [weak self] in self?.scheduleRecovery() }
30-
statusItem.setUp()
31-
// The AeroSpace integration applies through defaults KVO, so the
32-
// Settings toggle and `defaults write` are the same mechanism and
33-
// external writes take effect immediately.
34-
UserDefaults.standard.addObserver(
35-
self, forKeyPath: Settings.aerospaceIntegrationKey, options: [], context: nil)
30+
statusItem.refresh()
31+
// The AeroSpace integration and the menu bar icon apply through
32+
// defaults KVO, so the Settings toggles and `defaults write` are
33+
// the same mechanism and external writes take effect immediately.
34+
for key in [Settings.aerospaceIntegrationKey, Settings.showMenuBarIconKey] {
35+
UserDefaults.standard.addObserver(self, forKeyPath: key, options: [], context: nil)
36+
}
3637
ensurePermissionAndStart()
3738
// Optional: unlocks live titles for windows in other Spaces via
3839
// CGWindowList. Used solely to read titles; Cyclist never captures
@@ -48,10 +49,20 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
4849
change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
4950
// KVO from an external `defaults write` can arrive off-main.
5051
DispatchQueue.main.async { [weak self] in
51-
self?.applyAerospace()
52+
guard let self else { return }
53+
self.applyAerospace()
54+
self.statusItem.refresh()
5255
}
5356
}
5457

58+
// Reopening the app (Finder double-click, `open -a Cyclist`) presents
59+
// Settings - the universal "where did it go" gesture, and the escape
60+
// hatch when the menu bar icon is hidden.
61+
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
62+
SettingsView.showWindow()
63+
return false
64+
}
65+
5566
private func applyAerospace() {
5667
let on = Settings.aerospaceIntegration
5768
guard on != aerospaceApplied else { return }

Sources/Cyclist/Settings.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ enum Settings {
1212
static let includeNoWindowsKey = "includeNoWindows"
1313
static let trackpadSwipeKey = "trackpadSwipe"
1414
static let keyboardSpaceNavKey = "keyboardSpaceNavigation"
15+
static let showMenuBarIconKey = "showMenuBarIcon"
1516
static let aerospaceIntegrationKey = "aerospaceIntegration"
1617
static let showHollowWorkspacesKey = "showHollowWorkspaces"
1718
static let switcherShortcutKey = "switcherShortcut"
@@ -27,6 +28,7 @@ enum Settings {
2728
includeNoWindowsKey: false,
2829
trackpadSwipeKey: true,
2930
keyboardSpaceNavKey: true,
31+
showMenuBarIconKey: true,
3032
aerospaceIntegrationKey: false,
3133
showHollowWorkspacesKey: false,
3234
switcherShortcutKey: "cmd+tab",
@@ -60,6 +62,10 @@ enum Settings {
6062
UserDefaults.standard.bool(forKey: keyboardSpaceNavKey)
6163
}
6264

65+
static var showMenuBarIcon: Bool {
66+
UserDefaults.standard.bool(forKey: showMenuBarIconKey)
67+
}
68+
6369
static var aerospaceIntegration: Bool {
6470
UserDefaults.standard.bool(forKey: aerospaceIntegrationKey)
6571
}

Sources/Cyclist/SettingsView.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ struct SettingsView: View {
4545
@AppStorage(Settings.includeNoWindowsKey) private var includeNoWindows = false
4646
@AppStorage(Settings.trackpadSwipeKey) private var trackpadSwipe = true
4747
@AppStorage(Settings.keyboardSpaceNavKey) private var keyboardSpaceNav = true
48+
@AppStorage(Settings.showMenuBarIconKey) private var showMenuBarIcon = true
4849
@AppStorage(Settings.aerospaceIntegrationKey) private var aerospaceIntegration = false
4950
@AppStorage(Settings.showHollowWorkspacesKey) private var showHollowWorkspaces = false
5051
@AppStorage(Settings.switcherShortcutKey) private var switcherShortcut = "cmd+tab"
@@ -107,10 +108,16 @@ struct SettingsView: View {
107108
Label("Launch at Login", systemImage: "power")
108109
}
109110
.onChange(of: launchAtLogin) { setLaunchAtLogin($0) }
111+
Toggle(isOn: $showMenuBarIcon) {
112+
HStack(spacing: 4) {
113+
Text("Show menu bar icon")
114+
InfoDot("Cyclist keeps running without the icon. To get back here: hold the switcher open and press comma, or relaunch Cyclist - reopening the app always shows this window.")
115+
}
116+
}
110117
} header: {
111118
Text("General")
112119
} footer: {
113-
Text("A macOS login item; also listed in System Settings > General > Login Items.")
120+
Text("Launch at Login is a macOS login item; also listed in System Settings > General > Login Items.")
114121
.font(.caption)
115122
.foregroundStyle(.secondary)
116123
}

Sources/Cyclist/StatusItem.swift

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,20 @@ import AppKit
33
final class StatusItemController: NSObject {
44
private var statusItem: NSStatusItem?
55

6-
func setUp() {
6+
// Creates or removes the status item to match the setting; the owner
7+
// calls it at launch and from its defaults observation, so hiding or
8+
// showing the icon applies live. Idempotent.
9+
func refresh() {
10+
if Settings.showMenuBarIcon {
11+
guard statusItem == nil else { return }
12+
statusItem = makeItem()
13+
} else if let item = statusItem {
14+
NSStatusBar.system.removeStatusItem(item)
15+
statusItem = nil
16+
}
17+
}
18+
19+
private func makeItem() -> NSStatusItem {
720
let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
821
item.button?.image = NSImage(
922
systemSymbolName: "arrow.triangle.2.circlepath",
@@ -29,7 +42,7 @@ final class StatusItemController: NSObject {
2942
menu.addItem(quitItem)
3043

3144
item.menu = menu
32-
statusItem = item
45+
return item
3346
}
3447

3548
@objc private func showSettings() {

0 commit comments

Comments
 (0)