Import Coucou 0.1.9 (Louis-CFM/coucou@83708fe), Mac app only

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
maksarsanjeev
2026-10-06 19:34:32 +03:00
commit 53b4f60da1
158 changed files with 29442 additions and 0 deletions
+236
View File
@@ -0,0 +1,236 @@
import Foundation
// MARK: - Shortcut Action
/// Every keyboard shortcut Coucou 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.
enum ShortcutAction: String, CaseIterable, Sendable {
// MARK: Global (Carbon RegisterEventHotKey)
case toggleIsland = "toggleIsland" // ⌘⇧N — open / close island (toggle)
case openChat = "openChat" // ⌃⌥Space — open island on chat, focus field
case goToAlert = "goToAlert" // ⌃⌥A — jump to pending alert
case jumpToTerminal = "jumpToTerminal" // ⌃⌥T — focus agent terminal
case attachFrontWindow = "attachFrontWindow" // ⌃⌥W — attach front window (non-AppStore)
case nextPill = "nextPill" // ⌃⌥] — next pill, open if closed
case prevPill = "prevPill" // ⌃⌥[ — previous pill, open if closed
case muteToggle = "muteToggle" // ⌃⌥M — mute / unmute sounds
case desktopToggle = "desktopToggle" // ⌃⌥D — send Mochi to desktop / bring back
case wardrobeToggle = "wardrobeToggle" // ⌃⌥G — open / close wardrobe
// MARK: UserDefaults keys
/// Key code storage key. `toggleIsland` reuses the legacy "hotkeyCode" key.
var udKeyCode: String {
self == .toggleIsland ? "hotkeyCode" : "shortcut.\(rawValue).keyCode"
}
/// Modifier flags storage key. `toggleIsland` reuses the legacy "hotkeyFlags" key.
var udFlags: String {
self == .toggleIsland ? "hotkeyFlags" : "shortcut.\(rawValue).flags"
}
/// Enabled flag storage key. `toggleIsland` reuses the legacy "hotkeyEnabled" key.
var udEnabled: String {
self == .toggleIsland ? "hotkeyEnabled" : "shortcut.\(rawValue).enabled"
}
// MARK: Metadata
var displayName: String {
switch self {
case .toggleIsland: return "Open / close island"
case .openChat: return "Open chat"
case .goToAlert: return "Go to alert"
case .jumpToTerminal: return "Jump to terminal"
case .attachFrontWindow: return "Attach front window"
case .nextPill: return "Next pill"
case .prevPill: return "Previous pill"
case .muteToggle: return "Mute / unmute sounds"
case .desktopToggle: return "Mochi on / off desktop"
case .wardrobeToggle: return "Open / close wardrobe"
}
}
/// Whether this action should be omitted from App Store builds.
var isAppStoreOnly: Bool { false }
var isNonAppStore: Bool { self == .attachFrontWindow }
/// Whether the shortcut is enabled by default (all new global shortcuts are on by default;
/// `toggleIsland` is off by default to match the pre-existing behaviour).
var enabledByDefault: Bool { self != .toggleIsland }
}
// MARK: - Shortcut Spec
/// A key + modifier combination stored as plain values (no AppKit dependency).
struct ShortcutSpec: Equatable, Hashable, Sendable {
/// Virtual key code (matching kVK_* constants and NSEvent.keyCode).
var keyCode: UInt16
/// NSEvent.ModifierFlags raw value — only the modifier bits (.command / .shift / .option / .control).
/// Stored and compared as a plain UInt so this file stays AppKit-free.
var nsFlags: UInt
// MARK: NSEvent modifier raw values (stable since macOS 10.x; see AppKit/NSEvent.h)
static let cmdBit: UInt = 1 << 20 // NSEvent.ModifierFlags.command = 1048576
static let shiftBit: UInt = 1 << 17 // NSEvent.ModifierFlags.shift = 131072
static let optBit: UInt = 1 << 19 // NSEvent.ModifierFlags.option = 524288
static let ctrlBit: UInt = 1 << 18 // NSEvent.ModifierFlags.control = 262144
static let modMask: UInt = cmdBit | shiftBit | optBit | ctrlBit
/// True when nsFlags contains at least one of ⌘ ⇧ ⌥ ⌃.
var hasModifier: Bool { nsFlags & ShortcutSpec.modMask != 0 }
// MARK: Convenience values for the default table
static let cmdShift: UInt = cmdBit | shiftBit // 1179648
static let ctrlOpt: UInt = ctrlBit | optBit // 786432
}
// MARK: - Pure logic
/// Stateless helpers for shortcut management — no AppKit, fully unit-testable standalone.
enum ShortcutLogic {
// MARK: - Default table
static let defaults: [ShortcutAction: ShortcutSpec] = [
.toggleIsland: ShortcutSpec(keyCode: 45, nsFlags: ShortcutSpec.cmdShift), // ⌘⇧N
.openChat: ShortcutSpec(keyCode: 49, nsFlags: ShortcutSpec.ctrlOpt), // ⌃⌥Space
.goToAlert: ShortcutSpec(keyCode: 0, nsFlags: ShortcutSpec.ctrlOpt), // ⌃⌥A
.jumpToTerminal: ShortcutSpec(keyCode: 17, nsFlags: ShortcutSpec.ctrlOpt), // ⌃⌥T
.attachFrontWindow: ShortcutSpec(keyCode: 13, nsFlags: ShortcutSpec.ctrlOpt), // ⌃⌥W
.nextPill: ShortcutSpec(keyCode: 30, nsFlags: ShortcutSpec.ctrlOpt), // ⌃⌥]
.prevPill: ShortcutSpec(keyCode: 33, nsFlags: ShortcutSpec.ctrlOpt), // ⌃⌥[
.muteToggle: ShortcutSpec(keyCode: 46, nsFlags: ShortcutSpec.ctrlOpt), // ⌃⌥M
.desktopToggle: ShortcutSpec(keyCode: 2, nsFlags: ShortcutSpec.ctrlOpt), // ⌃⌥D
.wardrobeToggle: ShortcutSpec(keyCode: 5, nsFlags: ShortcutSpec.ctrlOpt), // ⌃⌥G
]
// MARK: - Load / save (UserDefaults)
/// Current spec for `action`, falling back to the default if not stored.
static func hotKey(for action: ShortcutAction) -> ShortcutSpec {
let ud = UserDefaults.standard
let def = defaults[action]!
let kc = ud.object(forKey: action.udKeyCode) as? Int ?? Int(def.keyCode)
let fl = ud.object(forKey: action.udFlags) as? Int ?? Int(def.nsFlags)
return ShortcutSpec(keyCode: UInt16(kc), nsFlags: UInt(fl))
}
static func save(_ spec: ShortcutSpec, for action: ShortcutAction) {
UserDefaults.standard.set(Int(spec.keyCode), forKey: action.udKeyCode)
UserDefaults.standard.set(Int(spec.nsFlags), forKey: action.udFlags)
}
static func isEnabled(_ action: ShortcutAction) -> Bool {
UserDefaults.standard.object(forKey: action.udEnabled) as? Bool
?? action.enabledByDefault
}
static func setEnabled(_ enabled: Bool, for action: ShortcutAction) {
UserDefaults.standard.set(enabled, forKey: action.udEnabled)
}
static func resetToDefault(_ action: ShortcutAction) {
let def = defaults[action]!
save(def, for: action)
setEnabled(action.enabledByDefault, for: action)
}
// MARK: - Carbon modifier conversion
// Carbon modifier bit flags (CarbonCore/Events.h) — no import needed, just constants.
private static let cCmdKey: UInt32 = 1 << 8 // cmdKey = 256
private static let cShiftKey: UInt32 = 1 << 9 // shiftKey = 512
private static let cOptKey: UInt32 = 1 << 11 // optionKey = 2048
private static let cCtrlKey: UInt32 = 1 << 12 // controlKey = 4096
/// Convert NSEvent modifier flags (as raw UInt) to Carbon modifier bit mask.
static func carbonModifiers(fromNS nsFlags: UInt) -> UInt32 {
let cmd = ShortcutSpec.cmdBit
let shift = ShortcutSpec.shiftBit
let opt = ShortcutSpec.optBit
let ctrl = ShortcutSpec.ctrlBit
var c: UInt32 = 0
if nsFlags & cmd != 0 { c |= cCmdKey }
if nsFlags & shift != 0 { c |= cShiftKey }
if nsFlags & opt != 0 { c |= cOptKey }
if nsFlags & ctrl != 0 { c |= cCtrlKey }
return c
}
// MARK: - Conflict detection (within Coucou'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> {
var seen: [ShortcutSpec: ShortcutAction] = [:]
var dups: Set<ShortcutAction> = []
for (action, spec) in specs {
if let other = seen[spec] {
dups.insert(action)
dups.insert(other)
} else {
seen[spec] = action
}
}
return dups
}
// MARK: - Display string
/// Human-readable modifier+key string, e.g. "⌃⌥A" or "⌘⇧N".
static func displayString(for spec: ShortcutSpec) -> String {
let f = spec.nsFlags
var s = ""
if f & ShortcutSpec.ctrlBit != 0 { s += "⌃" }
if f & ShortcutSpec.optBit != 0 { s += "⌥" }
if f & ShortcutSpec.shiftBit != 0 { s += "⇧" }
if f & ShortcutSpec.cmdBit != 0 { s += "⌘" }
s += keyCodeToString(spec.keyCode)
return s
}
/// Printable name for the most common virtual key codes.
static func keyCodeToString(_ code: UInt16) -> String {
let map: [UInt16: String] = [
0:"A", 1:"S", 2:"D", 3:"F", 4:"H", 5:"G", 6:"Z", 7:"X",
8:"C", 9:"V", 11:"B", 12:"Q", 13:"W", 14:"E", 15:"R", 16:"Y",
17:"T", 31:"O", 32:"U", 33:"[", 34:"I", 35:"P", 37:"L", 38:"J",
40:"K", 45:"N", 46:"M", 30:"]", 49:"Space",
36:"↩", 51:"⌫", 53:"⎋",
123:"←", 124:"→", 125:"↓", 126:"↑",
18:"1", 19:"2", 20:"3", 21:"4", 23:"5", 22:"6", 26:"7", 28:"8", 25:"9",
]
return map[code] ?? "·"
}
// MARK: - Card navigation (pure, no AppKit dependency)
/// Compute the next card-selection index.
/// - Returns `nil` when `itemCount == 0`.
/// - Clamps to `0 ..< itemCount`.
/// - `nil` selection + `delta > 0` → first item; `delta < 0` → last item.
static func navigate(selection: Int?, delta: Int, itemCount: Int) -> Int? {
guard itemCount > 0 else { return nil }
if let cur = selection {
return max(0, min(itemCount - 1, cur + delta))
}
return delta > 0 ? 0 : itemCount - 1
}
// MARK: - Island-local shortcut table (read-only, shown in Settings)
/// Descriptive table of island-local shortcuts for the Settings view.
static let islandShortcuts: [(key: String, description: String)] = [
("⌘→ / ⌘←", "Next / previous pill"),
("⌘1 – ⌘9", "Switch to pill by number"),
("⌘↓ / ⌘↑", "Navigate list items"),
("⌘O", "Open selected item"),
("⌘E", "Open / close current diff"),
("⌘↩", "Send chat message"),
("⌘K", "New conversation"),
("⌘,", "Open Settings"),
("⌘P", "Pin / unpin island"),
("⎋", "Close island (if not pinned)"),
]
}