diff --git a/CHANGELOG.md b/CHANGELOG.md index 467abbe..a7d2963 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ shape matters: ## Unreleased +- Added: A Rules submenu in the menu bar lists your rules in match order. Click one to turn it off or on without opening Settings, or choose Edit Rules… to jump to the Rules pane. - Added: The browser picker can lay out as a horizontal row instead of a vertical list, show icons without names, and use a larger or smaller icon size. Set it under Settings → Browsers → Picker appearance. In the row layout, ←/→ move between choices and ↑/↓ jump a row. The picker names the highlighted choice above the icons, so profiles of the same browser stay distinguishable. - Fixed: When macOS blocks Junction from reading your browsers' profile lists, Junction now says so instead of showing no profiles. Settings → Browsers, the rule editor and the welcome tour explain that Junction needs Full Disk Access, with a button that opens the right page in System Settings and a Check Again button for after you turn it on. - Changed: Google Chrome for Testing, the automation build that Playwright and Puppeteer install, no longer shows up as a browser in rules, the picker, or Settings → Browsers. diff --git a/Sources/JunctionApp/Settings/SettingsWindowController.swift b/Sources/JunctionApp/Settings/SettingsWindowController.swift index c50e67f..80db2a2 100644 --- a/Sources/JunctionApp/Settings/SettingsWindowController.swift +++ b/Sources/JunctionApp/Settings/SettingsWindowController.swift @@ -20,6 +20,12 @@ final class SettingsWindowController: NSObject { name: .junctionPrefillRule, object: nil ) + NotificationCenter.default.addObserver( + self, + selector: #selector(jumpToRules), + name: .junctionShowRules, + object: nil + ) } func show() { diff --git a/Sources/JunctionApp/StatusItemController.swift b/Sources/JunctionApp/StatusItemController.swift index dd24f14..0a7a330 100644 --- a/Sources/JunctionApp/StatusItemController.swift +++ b/Sources/JunctionApp/StatusItemController.swift @@ -188,6 +188,30 @@ final class StatusItemController: NSObject, NSMenuDelegate { } recent.submenu = recentMenu menu.addItem(recent) + + // Rules, in match order, each a checkmark toggle for `enabled`. + let rules = NSMenuItem(title: "Rules", action: nil, keyEquivalent: "") + .withSymbol("list.bullet") + let rulesMenu = NSMenu() + if state.config.rules.isEmpty { + let empty = NSMenuItem(title: "No rules yet", action: nil, keyEquivalent: "") + empty.isEnabled = false + rulesMenu.addItem(empty) + } + for (index, rule) in state.config.rules.enumerated() { + let item = NSMenuItem(title: rule.name, action: #selector(toggleRule(_:)), keyEquivalent: "") + item.target = self + item.state = rule.enabled ? .on : .off + item.representedObject = RuleRef(index: index, name: rule.name) + item.toolTip = rule.enabled ? "Click to turn this rule off" : "Click to turn this rule on" + rulesMenu.addItem(item) + } + rulesMenu.addItem(.separator()) + let editRules = NSMenuItem(title: "Edit Rules…", action: #selector(showRules), keyEquivalent: "") + editRules.target = self + rulesMenu.addItem(editRules) + rules.submenu = rulesMenu + menu.addItem(rules) menu.addItem(.separator()) let settings = NSMenuItem(title: "Settings…", action: #selector(openSettings), keyEquivalent: ",") @@ -210,6 +234,24 @@ final class StatusItemController: NSObject, NSMenuDelegate { @objc private func togglePause() { state.routingPaused.toggle() } + /// Position plus name: rule ids aren't persisted, and the file can change while the menu + /// is open, so only toggle if the rule at that position is still the one that was shown. + private struct RuleRef { + let index: Int + let name: String + } + + @objc private func toggleRule(_ sender: NSMenuItem) { + guard let ref = sender.representedObject as? RuleRef, + state.config.rules.indices.contains(ref.index), + state.config.rules[ref.index].name == ref.name else { return } + state.updateConfig { $0.rules[ref.index].enabled.toggle() } + } + + @objc private func showRules() { + NotificationCenter.default.post(name: .junctionShowRules, object: nil) + } + /// LSUIElement apps aren't frontmost, so Sparkle's window opens behind everything. @objc private func checkForUpdates() { NSApp.activate(ignoringOtherApps: true) @@ -246,5 +288,6 @@ final class StatusItemController: NSObject, NSMenuDelegate { extension Notification.Name { static let junctionPrefillRule = Notification.Name("junctionPrefillRule") + static let junctionShowRules = Notification.Name("junctionShowRules") } #endif diff --git a/web/src/data/docs.json b/web/src/data/docs.json index 75c0ed7..33fe471 100644 --- a/web/src/data/docs.json +++ b/web/src/data/docs.json @@ -124,6 +124,10 @@ "label": "Recent links", "body": "the last 10 links Junction routed, in a submenu. In memory only — never written to disk." }, + { + "label": "Rules", + "body": "every rule in match order, checked when it's on. Click one to turn it off or on without opening Settings; Edit Rules… opens the Rules pane." + }, { "label": "Settings", "body": "fallback (a browser, or the ask-every-time picker), rules, browsers and profiles — including which appear in the picker and how it looks (a vertical list or a horizontal row, with or without names, any icon size) — deep links, updates."