BroV look: swamp-zombie unibrow character, new app and menu bar icons, BroV naming in UI
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -28,13 +28,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
private func setupMenuBarItem() {
|
||||
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
||||
guard let button = statusItem?.button else { return }
|
||||
button.image = NSImage(named: "MenuBarIcon") ?? NSImage(systemSymbolName: "circle.fill", accessibilityDescription: "Coucou")
|
||||
button.image = NSImage(named: "MenuBarIcon") ?? NSImage(systemSymbolName: "circle.fill", accessibilityDescription: "BroV")
|
||||
button.image?.size = NSSize(width: 24, height: 18)
|
||||
button.image?.accessibilityDescription = "Coucou"
|
||||
button.image?.accessibilityDescription = "BroV"
|
||||
button.image?.isTemplate = true
|
||||
|
||||
let menu = NSMenu()
|
||||
menu.addItem(withTitle: "Open Coucou", action: #selector(openIsland), keyEquivalent: "")
|
||||
menu.addItem(withTitle: "Open BroV", action: #selector(openIsland), keyEquivalent: "")
|
||||
menu.addItem(.separator())
|
||||
menu.addItem(withTitle: "Settings…", action: #selector(openSettings), keyEquivalent: ",")
|
||||
menu.addItem(.separator())
|
||||
@@ -69,7 +69,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
let win = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 720, height: 560),
|
||||
styleMask: [.titled, .closable, .miniaturizable, .resizable],
|
||||
backing: .buffered, defer: false)
|
||||
win.title = "Settings — Coucou"
|
||||
win.title = "Settings — BroV"
|
||||
let host = NSHostingView(rootView: SettingsView())
|
||||
host.sizingOptions = [.minSize]
|
||||
win.contentView = host
|
||||
|
||||
@@ -198,9 +198,9 @@ final class ClaudeService {
|
||||
/// neutral otherwise — same wording as the Windows build.
|
||||
private nonisolated static func makeSystemPrompt() -> String {
|
||||
let opening = if let firstName = resolveUserFirstName() {
|
||||
"You are Mochi, \(firstName)'s personal AI assistant embedded in the notch of their Mac."
|
||||
"You are BroV, \(firstName)'s personal AI assistant embedded in the notch of their Mac."
|
||||
} else {
|
||||
"You are Mochi, a personal AI assistant embedded in the notch of the user's Mac."
|
||||
"You are BroV, a personal AI assistant embedded in the notch of the user's Mac."
|
||||
}
|
||||
return """
|
||||
\(opening) \
|
||||
|
||||
@@ -19,7 +19,7 @@ final class HookServer: @unchecked Sendable {
|
||||
static var socketPath: String {
|
||||
#if APPSTORE
|
||||
// Container home root keeps path ≤ 103 bytes (sun_path limit on macOS is 104 incl. NUL)
|
||||
// /Users/louis/Library/Containers/fr.louisraille.Coucou/Data/nb.sock = 66 bytes ✓
|
||||
// /Users/louis/Library/Containers/fr.louisraille.BroV/Data/nb.sock = 66 bytes ✓
|
||||
return NSHomeDirectory() + "/nb.sock"
|
||||
#else
|
||||
return supportDir.appendingPathComponent("nb.sock").path
|
||||
@@ -1093,7 +1093,7 @@ final class HookServer: @unchecked Sendable {
|
||||
|
||||
// MARK: - Outdated hook detection
|
||||
|
||||
/// Returns true if settings.json has a Coucou hook that needs updating:
|
||||
/// Returns true if settings.json has a BroV hook that needs updating:
|
||||
/// either a PermissionRequest hook with timeout < 120s, or the AskUserQuestion
|
||||
/// PreToolUse matcher is missing (requires Claude Code 2.1.85+).
|
||||
static func hooksNeedUpdate() -> Bool {
|
||||
@@ -1104,8 +1104,8 @@ final class HookServer: @unchecked Sendable {
|
||||
let hooks = settings["hooks"] as? [String: Any] else {
|
||||
return false
|
||||
}
|
||||
// Track whether any Coucou hook is installed at all
|
||||
var hasCoucouHooks = false
|
||||
// Track whether any BroV hook is installed at all
|
||||
var hasBroVHooks = false
|
||||
|
||||
if let permReqHooks = hooks["PermissionRequest"] as? [[String: Any]] {
|
||||
for matcher in permReqHooks {
|
||||
@@ -1113,7 +1113,7 @@ final class HookServer: @unchecked Sendable {
|
||||
for hook in hookList {
|
||||
if let cmd = hook["command"] as? String,
|
||||
cmd.contains("NotchBuddy") || cmd.contains("coucou") {
|
||||
hasCoucouHooks = true
|
||||
hasBroVHooks = true
|
||||
if let timeout = hook["timeout"] as? Int, timeout < 120 { return true }
|
||||
}
|
||||
}
|
||||
@@ -1122,7 +1122,7 @@ final class HookServer: @unchecked Sendable {
|
||||
}
|
||||
|
||||
// Check that the AskUserQuestion PreToolUse entry exists
|
||||
if hasCoucouHooks {
|
||||
if hasBroVHooks {
|
||||
let preToolHooks = hooks["PreToolUse"] as? [[String: Any]] ?? []
|
||||
let hasAskEntry = preToolHooks.contains { m in
|
||||
(m["matcher"] as? String) == "AskUserQuestion"
|
||||
@@ -1540,7 +1540,7 @@ final class HookServer: @unchecked Sendable {
|
||||
label: "~/.gemini/settings.json")
|
||||
if let raw = settings["hooks"], !(raw is [String: Any]) {
|
||||
throw NSError(domain: "Coucou", code: 2, userInfo: [
|
||||
NSLocalizedDescriptionKey: "~/.gemini/settings.json: \"hooks\" has an unexpected type — Coucou has not touched it."
|
||||
NSLocalizedDescriptionKey: "~/.gemini/settings.json: \"hooks\" has an unexpected type — BroV has not touched it."
|
||||
])
|
||||
}
|
||||
let base = hookBase()
|
||||
@@ -1557,7 +1557,7 @@ final class HookServer: @unchecked Sendable {
|
||||
for (geminiEvent, normalizedEvent, timeout) in events {
|
||||
if let raw = hooks[geminiEvent], !(raw is [[String: Any]]) {
|
||||
throw NSError(domain: "Coucou", code: 2, userInfo: [
|
||||
NSLocalizedDescriptionKey: "~/.gemini/settings.json: \"hooks\"[\"\(geminiEvent)\"] has an unexpected type — Coucou has not touched it."
|
||||
NSLocalizedDescriptionKey: "~/.gemini/settings.json: \"hooks\"[\"\(geminiEvent)\"] has an unexpected type — BroV has not touched it."
|
||||
])
|
||||
}
|
||||
var groups = hooks[geminiEvent] as? [[String: Any]] ?? []
|
||||
@@ -1581,7 +1581,7 @@ final class HookServer: @unchecked Sendable {
|
||||
label: "~/.gemini/settings.json")
|
||||
if let raw = settings["hooks"], !(raw is [String: Any]) {
|
||||
throw NSError(domain: "Coucou", code: 2, userInfo: [
|
||||
NSLocalizedDescriptionKey: "~/.gemini/settings.json: \"hooks\" has an unexpected type — Coucou has not touched it."
|
||||
NSLocalizedDescriptionKey: "~/.gemini/settings.json: \"hooks\" has an unexpected type — BroV has not touched it."
|
||||
])
|
||||
}
|
||||
if var hooks = settings["hooks"] as? [String: Any] {
|
||||
@@ -1679,12 +1679,12 @@ final class HookServer: @unchecked Sendable {
|
||||
do { data = try Data(contentsOf: url) }
|
||||
catch {
|
||||
throw NSError(domain: "Coucou", code: 2, userInfo: [
|
||||
NSLocalizedDescriptionKey: "\(label) cannot be read — Coucou has not touched it."
|
||||
NSLocalizedDescriptionKey: "\(label) cannot be read — BroV has not touched it."
|
||||
])
|
||||
}
|
||||
guard let obj = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] else {
|
||||
throw NSError(domain: "Coucou", code: 2, userInfo: [
|
||||
NSLocalizedDescriptionKey: "\(label) is not valid JSON — Coucou has not touched it."
|
||||
NSLocalizedDescriptionKey: "\(label) is not valid JSON — BroV has not touched it."
|
||||
])
|
||||
}
|
||||
return obj
|
||||
@@ -1735,7 +1735,7 @@ final class HookServer: @unchecked Sendable {
|
||||
FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".codex/hooks.json")
|
||||
}
|
||||
|
||||
/// True when ~/.codex/hooks.json already routes Codex events to Coucou's nb-hook.
|
||||
/// True when ~/.codex/hooks.json already routes Codex events to BroV's nb-hook.
|
||||
static func codexHooksInstalled() -> Bool {
|
||||
guard let data = try? Data(contentsOf: codexHooksURL),
|
||||
let root = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any],
|
||||
@@ -1790,7 +1790,7 @@ final class HookServer: @unchecked Sendable {
|
||||
var root = try Self.strictReadJSONObject(at: Self.codexHooksURL, label: "~/.codex/hooks.json")
|
||||
if let raw = root["hooks"], !(raw is [String: Any]) {
|
||||
throw NSError(domain: "Coucou", code: 2, userInfo: [
|
||||
NSLocalizedDescriptionKey: "~/.codex/hooks.json: \"hooks\" has an unexpected type — Coucou has not touched it."
|
||||
NSLocalizedDescriptionKey: "~/.codex/hooks.json: \"hooks\" has an unexpected type — BroV has not touched it."
|
||||
])
|
||||
}
|
||||
let base = hookBase()
|
||||
@@ -1800,7 +1800,7 @@ final class HookServer: @unchecked Sendable {
|
||||
("SessionStart", 10, nil),
|
||||
("UserPromptSubmit", 10, nil),
|
||||
("PreToolUse", 10, nil),
|
||||
("PermissionRequest", 120, "Waiting for your answer in the notch (Coucou)"),
|
||||
("PermissionRequest", 120, "Waiting for your answer in the notch (BroV)"),
|
||||
("PostToolUse", 10, nil),
|
||||
("Stop", 10, nil),
|
||||
("SubagentStart", 10, nil),
|
||||
@@ -1812,11 +1812,11 @@ final class HookServer: @unchecked Sendable {
|
||||
for (event, timeout, statusMsg) in events {
|
||||
if let raw = hooks[event], !(raw is [[String: Any]]) {
|
||||
throw NSError(domain: "Coucou", code: 2, userInfo: [
|
||||
NSLocalizedDescriptionKey: "~/.codex/hooks.json: \"hooks\"[\"\(event)\"] has an unexpected type — Coucou has not touched it."
|
||||
NSLocalizedDescriptionKey: "~/.codex/hooks.json: \"hooks\"[\"\(event)\"] has an unexpected type — BroV has not touched it."
|
||||
])
|
||||
}
|
||||
var groups = hooks[event] as? [[String: Any]] ?? []
|
||||
// Remove existing Coucou entries
|
||||
// Remove existing BroV entries
|
||||
groups = removeNbHookEntries(from: groups)
|
||||
var hookEntry: [String: Any] = [
|
||||
"type": "command",
|
||||
@@ -1836,7 +1836,7 @@ final class HookServer: @unchecked Sendable {
|
||||
var root = try Self.strictReadJSONObject(at: Self.codexHooksURL, label: "~/.codex/hooks.json")
|
||||
if let raw = root["hooks"], !(raw is [String: Any]) {
|
||||
throw NSError(domain: "Coucou", code: 2, userInfo: [
|
||||
NSLocalizedDescriptionKey: "~/.codex/hooks.json: \"hooks\" has an unexpected type — Coucou has not touched it."
|
||||
NSLocalizedDescriptionKey: "~/.codex/hooks.json: \"hooks\" has an unexpected type — BroV has not touched it."
|
||||
])
|
||||
}
|
||||
if var hooks = root["hooks"] as? [String: Any] {
|
||||
@@ -1874,7 +1874,7 @@ extension Notification.Name {
|
||||
|
||||
private let nbHookShellWrapper = """
|
||||
#!/bin/sh
|
||||
# Coucou hook relay — always exits 0, never blocks Claude Code
|
||||
# BroV hook relay — always exits 0, never blocks Claude Code
|
||||
HOOK_DIR="$(dirname "$0")"
|
||||
if xcode-select -p >/dev/null 2>&1; then
|
||||
out=$(/usr/bin/python3 "$HOOK_DIR/nb-hook.py" "$@" 2>/dev/null)
|
||||
@@ -1890,8 +1890,8 @@ exit 0
|
||||
|
||||
private let nbHookPythonGitHub = """
|
||||
#!/usr/bin/env python3
|
||||
# nb-hook.py — Coucou hook relay for Claude Code and third-party agents (GitHub version)
|
||||
# Reads JSON from stdin, forwards to Coucou via Unix socket, translates response.
|
||||
# nb-hook.py — BroV hook relay for Claude Code and third-party agents (GitHub version)
|
||||
# Reads JSON from stdin, forwards to BroV via Unix socket, translates response.
|
||||
import sys, json, os, socket
|
||||
|
||||
def normalize_event(name):
|
||||
@@ -1948,7 +1948,7 @@ def main():
|
||||
'~/Library/Application Support/NotchBuddy/nb.sock'
|
||||
)
|
||||
|
||||
# --statusline mode: relay rate_limits to Coucou, then delegate to saved previous
|
||||
# --statusline mode: relay rate_limits to BroV, then delegate to saved previous
|
||||
if '--statusline' in sys.argv[1:]:
|
||||
relay = {
|
||||
'coucou_kind': 'statusline',
|
||||
@@ -2074,7 +2074,7 @@ def main():
|
||||
# socket_path is already defined above
|
||||
|
||||
if event == 'PermissionRequest':
|
||||
# Block and wait for Coucou's decision (Claude Code allows up to 120s)
|
||||
# Block and wait for BroV's decision (Claude Code allows up to 120s)
|
||||
try:
|
||||
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
s.settimeout(118)
|
||||
@@ -2115,7 +2115,7 @@ def main():
|
||||
sys.stdout.flush()
|
||||
sys.exit(0)
|
||||
elif decision == 'deny':
|
||||
out = {'hookSpecificOutput': {'hookEventName': 'PermissionRequest', 'decision': {'behavior': 'deny', 'message': 'Denied from Coucou'}}}
|
||||
out = {'hookSpecificOutput': {'hookEventName': 'PermissionRequest', 'decision': {'behavior': 'deny', 'message': 'Denied from BroV'}}}
|
||||
sys.stdout.write(json.dumps(out) + '\\n')
|
||||
sys.stdout.flush()
|
||||
sys.exit(0)
|
||||
@@ -2157,7 +2157,7 @@ sys.exit(0)
|
||||
|
||||
private let nbHookPythonAppStore = """
|
||||
#!/usr/bin/env python3
|
||||
# nb-hook.py — Coucou (App Store) hook relay for Claude Code and third-party agents
|
||||
# nb-hook.py — BroV (App Store) hook relay for Claude Code and third-party agents
|
||||
# Socket lives inside the sandboxed container; script runs outside the sandbox.
|
||||
import sys, json, os, socket
|
||||
|
||||
@@ -2212,10 +2212,10 @@ def main():
|
||||
return
|
||||
|
||||
socket_path = os.path.expanduser(
|
||||
'~/Library/Containers/fr.louisraille.Coucou/Data/nb.sock'
|
||||
'~/Library/Containers/fr.louisraille.BroV/Data/nb.sock'
|
||||
)
|
||||
|
||||
# --statusline mode: relay rate_limits to Coucou, then delegate to saved previous
|
||||
# --statusline mode: relay rate_limits to BroV, then delegate to saved previous
|
||||
if '--statusline' in sys.argv[1:]:
|
||||
relay = {
|
||||
'coucou_kind': 'statusline',
|
||||
@@ -2340,7 +2340,7 @@ def main():
|
||||
# socket_path is already defined above
|
||||
|
||||
if event == 'PermissionRequest':
|
||||
# Block and wait for Coucou's decision (Claude Code allows up to 120s)
|
||||
# Block and wait for BroV's decision (Claude Code allows up to 120s)
|
||||
try:
|
||||
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
s.settimeout(118)
|
||||
@@ -2381,7 +2381,7 @@ def main():
|
||||
sys.stdout.flush()
|
||||
sys.exit(0)
|
||||
elif decision == 'deny':
|
||||
out = {'hookSpecificOutput': {'hookEventName': 'PermissionRequest', 'decision': {'behavior': 'deny', 'message': 'Denied from Coucou'}}}
|
||||
out = {'hookSpecificOutput': {'hookEventName': 'PermissionRequest', 'decision': {'behavior': 'deny', 'message': 'Denied from BroV'}}}
|
||||
sys.stdout.write(json.dumps(out) + '\\n')
|
||||
sys.stdout.flush()
|
||||
sys.exit(0)
|
||||
|
||||
@@ -3891,7 +3891,7 @@ struct MusicCardView: View {
|
||||
.padding(.leading, 108)
|
||||
.padding(.trailing, 36)
|
||||
|
||||
Text("Allow Coucou to control Music")
|
||||
Text("Allow BroV to control Music")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(Color(hex: "#8E939C"))
|
||||
.padding(.leading, 108)
|
||||
|
||||
@@ -119,7 +119,7 @@ struct SettingsView: View {
|
||||
.resizable()
|
||||
.frame(width: 32, height: 32)
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
Text("Coucou")
|
||||
Text("BroV")
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
Text(appVersion)
|
||||
.font(.system(size: 11))
|
||||
@@ -301,14 +301,14 @@ struct SettingsView: View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Toggle("Show my agent sessions on my iPhone", isOn: $iPhoneSyncEnabled)
|
||||
.onChange(of: iPhoneSyncEnabled) { _, on in CloudProbe.shared.setEnabled(on) }
|
||||
Text("Sends your sessions to your private iCloud for the Coucou iPhone app. Project names, commands and questions are encrypted with your iCloud keys. Turning it off deletes them from iCloud.")
|
||||
Text("Sends your sessions to your private iCloud for the BroV iPhone app. Project names, commands and questions are encrypted with your iCloud keys. Turning it off deletes them from iCloud.")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
Toggle("Move Mochi to my iPhone's Dynamic Island when my Mac is locked", isOn: $iPhoneLiveActivityEnabled)
|
||||
.disabled(!iPhoneSyncEnabled)
|
||||
.onChange(of: iPhoneLiveActivityEnabled) { _, on in LiveActivityRelay.shared.setEnabled(on) }
|
||||
Text("Goes through the Coucou relay to Apple's push service. Only the agent's name and state are sent: no project name, command or path.")
|
||||
Text("Goes through the BroV relay to Apple's push service. Only the agent's name and state are sent: no project name, command or path.")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
@@ -332,7 +332,7 @@ struct SettingsView: View {
|
||||
@ViewBuilder private var activePillsSection: some View {
|
||||
GroupBox {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("Choose the tools you use. Coucou only shows what you declare here.")
|
||||
Text("Choose the tools you use. BroV only shows what you declare here.")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
@@ -534,7 +534,7 @@ struct SettingsView: View {
|
||||
|
||||
GroupBox("Plan usage") {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("Shows your Claude plan usage (5-hour and weekly limits) in the notch header. Coucou adds a status line relay to ~/.claude/settings.json. If you already have a status line, it keeps working as before. Pro and Max plans only.")
|
||||
Text("Shows your Claude plan usage (5-hour and weekly limits) in the notch header. BroV adds a status line relay to ~/.claude/settings.json. If you already have a status line, it keeps working as before. Pro and Max plans only.")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
@@ -936,7 +936,7 @@ struct SettingsView: View {
|
||||
private func installHooksAppStore() {
|
||||
guard let claudeURL = pickClaudeFolder(prompt: "Select") else { return }
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Install Coucou hooks in ~/.claude?"
|
||||
alert.messageText = "Install BroV hooks in ~/.claude?"
|
||||
alert.informativeText = "Will write:\n• ~/.claude/coucou/nb-hook\n• ~/.claude/settings.json (backup created first)"
|
||||
alert.addButton(withTitle: "Install")
|
||||
alert.addButton(withTitle: "Cancel")
|
||||
@@ -1036,7 +1036,7 @@ struct SettingsView: View {
|
||||
pendingGeminiJSON = try HookServer.shared.previewGeminiHooks(install: install)
|
||||
showGeminiDiff = true
|
||||
statusMessage = "Review the JSON below before confirming."
|
||||
} catch let e as NSError where e.domain == "CoucouNoop" {
|
||||
} catch let e as NSError where e.domain == "BroVNoop" {
|
||||
statusMessage = e.localizedDescription
|
||||
} catch {
|
||||
statusMessage = "❌ \(error.localizedDescription)"
|
||||
@@ -1063,7 +1063,7 @@ struct SettingsView: View {
|
||||
pendingAgyJSON = try HookServer.shared.previewAgyHooks(install: install)
|
||||
showAgyDiff = true
|
||||
statusMessage = "Review the JSON below before confirming."
|
||||
} catch let e as NSError where e.domain == "CoucouNoop" {
|
||||
} catch let e as NSError where e.domain == "BroVNoop" {
|
||||
statusMessage = e.localizedDescription
|
||||
} catch {
|
||||
statusMessage = "❌ \(error.localizedDescription)"
|
||||
@@ -1090,7 +1090,7 @@ struct SettingsView: View {
|
||||
pendingCodexJSON = try HookServer.shared.previewCodexHooks(install: install)
|
||||
showCodexDiff = true
|
||||
statusMessage = "Review the JSON below before confirming."
|
||||
} catch let e as NSError where e.domain == "CoucouNoop" {
|
||||
} catch let e as NSError where e.domain == "BroVNoop" {
|
||||
statusMessage = e.localizedDescription
|
||||
} catch {
|
||||
statusMessage = "❌ \(error.localizedDescription)"
|
||||
|
||||
@@ -2,7 +2,7 @@ import Foundation
|
||||
|
||||
// MARK: - Shortcut Action
|
||||
|
||||
/// Every keyboard shortcut Coucou knows about.
|
||||
/// Every keyboard shortcut BroV knows about.
|
||||
/// The first 10 cases are *global* (registered with Carbon and effective from any app).
|
||||
/// Island-local shortcuts are documented in `ShortcutLogic.islandShortcuts` but are
|
||||
/// not managed here — they are handled by a local NSEvent monitor in IslandWindowController.
|
||||
@@ -46,7 +46,7 @@ enum ShortcutAction: String, CaseIterable, Sendable {
|
||||
case .nextPill: return "Next pill"
|
||||
case .prevPill: return "Previous pill"
|
||||
case .muteToggle: return "Mute / unmute sounds"
|
||||
case .desktopToggle: return "Mochi on / off desktop"
|
||||
case .desktopToggle: return "BroV on / off desktop"
|
||||
case .wardrobeToggle: return "Open / close wardrobe"
|
||||
}
|
||||
}
|
||||
@@ -159,7 +159,7 @@ enum ShortcutLogic {
|
||||
return c
|
||||
}
|
||||
|
||||
// MARK: - Conflict detection (within Coucou's own shortcuts)
|
||||
// MARK: - Conflict detection (within BroV's own shortcuts)
|
||||
|
||||
/// Returns the set of actions whose (keyCode, nsFlags) pair is shared with another action.
|
||||
static func duplicates(in specs: [ShortcutAction: ShortcutSpec]) -> Set<ShortcutAction> {
|
||||
|
||||
Reference in New Issue
Block a user