diff --git a/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_128x128.png b/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_128x128.png index 80358b5..5828316 100644 Binary files a/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_128x128.png and b/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_128x128.png differ diff --git a/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png b/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png index 4aaea44..4faf1c2 100644 Binary files a/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png and b/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png differ diff --git a/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_16x16.png b/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_16x16.png index 4ab622d..04a8a40 100644 Binary files a/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_16x16.png and b/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_16x16.png differ diff --git a/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png b/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png index f678cb2..8cd4521 100644 Binary files a/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png and b/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png differ diff --git a/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_256x256.png b/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_256x256.png index 78f8ba1..4faf1c2 100644 Binary files a/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_256x256.png and b/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_256x256.png differ diff --git a/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png b/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png index 5bd5779..e3294f3 100644 Binary files a/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png and b/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png differ diff --git a/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_32x32.png b/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_32x32.png index 49d72b9..8cd4521 100644 Binary files a/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_32x32.png and b/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_32x32.png differ diff --git a/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png b/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png index c618f66..6cf8d89 100644 Binary files a/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png and b/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png differ diff --git a/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_512x512.png b/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_512x512.png index 0594e4b..e3294f3 100644 Binary files a/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_512x512.png and b/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_512x512.png differ diff --git a/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png b/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png index af79fdc..ee96d6c 100644 Binary files a/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png and b/NotchBuddy/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png differ diff --git a/NotchBuddy/Assets.xcassets/MenuBarIcon.imageset/menubar.png b/NotchBuddy/Assets.xcassets/MenuBarIcon.imageset/menubar.png index b89ab1d..817ff17 100644 Binary files a/NotchBuddy/Assets.xcassets/MenuBarIcon.imageset/menubar.png and b/NotchBuddy/Assets.xcassets/MenuBarIcon.imageset/menubar.png differ diff --git a/NotchBuddy/Assets.xcassets/MenuBarIcon.imageset/menubar@2x.png b/NotchBuddy/Assets.xcassets/MenuBarIcon.imageset/menubar@2x.png index b2ffb99..a26ecc9 100644 Binary files a/NotchBuddy/Assets.xcassets/MenuBarIcon.imageset/menubar@2x.png and b/NotchBuddy/Assets.xcassets/MenuBarIcon.imageset/menubar@2x.png differ diff --git a/NotchBuddy/Assets.xcassets/MenuBarIcon.imageset/menubar@3x.png b/NotchBuddy/Assets.xcassets/MenuBarIcon.imageset/menubar@3x.png index 12c49ec..9772a15 100644 Binary files a/NotchBuddy/Assets.xcassets/MenuBarIcon.imageset/menubar@3x.png and b/NotchBuddy/Assets.xcassets/MenuBarIcon.imageset/menubar@3x.png differ diff --git a/NotchBuddy/Sources/App/AppDelegate.swift b/NotchBuddy/Sources/App/AppDelegate.swift index bca82b9..b610dd6 100644 --- a/NotchBuddy/Sources/App/AppDelegate.swift +++ b/NotchBuddy/Sources/App/AppDelegate.swift @@ -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 diff --git a/NotchBuddy/Sources/App/ClaudeService.swift b/NotchBuddy/Sources/App/ClaudeService.swift index 405c3f2..3a71cd9 100644 --- a/NotchBuddy/Sources/App/ClaudeService.swift +++ b/NotchBuddy/Sources/App/ClaudeService.swift @@ -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) \ diff --git a/NotchBuddy/Sources/App/HookServer.swift b/NotchBuddy/Sources/App/HookServer.swift index 26dde14..69c5f39 100644 --- a/NotchBuddy/Sources/App/HookServer.swift +++ b/NotchBuddy/Sources/App/HookServer.swift @@ -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) diff --git a/NotchBuddy/Sources/App/IslandViewContent.swift b/NotchBuddy/Sources/App/IslandViewContent.swift index 7eaa27a..a8d31fd 100644 --- a/NotchBuddy/Sources/App/IslandViewContent.swift +++ b/NotchBuddy/Sources/App/IslandViewContent.swift @@ -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) diff --git a/NotchBuddy/Sources/App/SettingsView.swift b/NotchBuddy/Sources/App/SettingsView.swift index b2920b6..dbe70f4 100644 --- a/NotchBuddy/Sources/App/SettingsView.swift +++ b/NotchBuddy/Sources/App/SettingsView.swift @@ -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)" diff --git a/NotchBuddy/Sources/App/ShortcutLogic.swift b/NotchBuddy/Sources/App/ShortcutLogic.swift index 7862174..07f630a 100644 --- a/NotchBuddy/Sources/App/ShortcutLogic.swift +++ b/NotchBuddy/Sources/App/ShortcutLogic.swift @@ -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 { diff --git a/NotchBuddy/Sources/CoucouKit/BotEngine.swift b/NotchBuddy/Sources/CoucouKit/BotEngine.swift index 0443ef9..53f9793 100644 --- a/NotchBuddy/Sources/CoucouKit/BotEngine.swift +++ b/NotchBuddy/Sources/CoucouKit/BotEngine.swift @@ -79,9 +79,10 @@ enum MochiConst { static let eyeH: CGFloat = 0.27 static let eyeSp: CGFloat = 0.37 static let eyeP: CGFloat = -0.12 - static let baseTop = CGColor(red: 0.929, green: 0.929, blue: 0.937, alpha: 1) // #EDEDEF - static let baseBottom = CGColor(red: 0.769, green: 0.773, blue: 0.792, alpha: 1) // #C4C5CA - static let ink = CGColor(red: 0.102, green: 0.082, blue: 0.071, alpha: 1) // #1A1412 + // BroV: swamp-zombie skin and dried-blood ink. + static let baseTop = CGColor(red: 0.659, green: 0.749, blue: 0.416, alpha: 1) // #A8BF6A + static let baseBottom = CGColor(red: 0.357, green: 0.451, blue: 0.251, alpha: 1) // #5B7340 + static let ink = CGColor(red: 0.165, green: 0.039, blue: 0.039, alpha: 1) // #2A0A0A static let miniInk = CGColor(red: 0.063, green: 0.075, blue: 0.102, alpha: 1) // #10131A } @@ -916,9 +917,13 @@ final class BotEngine: ObservableObject { drawBlush(ctx: &ctx, path: bodyPath, rx: rx, ry: ry, R: R, blush: blushVal) } + if showsBroVFace, R > 11 { drawBroVSkin(ctx: ctx, path: bodyPath, R: R, rx: rx, ry: ry) } + // Eyes drawEyes(ctx: &ctx, path: bodyPath, R: R, rx: rx, ry: ry) + if showsBroVFace, R > 11 { drawBroVFace(ctx: ctx, path: bodyPath, R: R, rx: rx, ry: ry) } + // Mouth hole — dark pill cutout inside the box face // Spec: left/right margins 0.10R, top margin 0.08R from box top (-0.94R) if morph > 0.05 { @@ -1135,12 +1140,14 @@ final class BotEngine: ObservableObject { let tw = R * 1.0 let th = R * 0.94 let tr = R * 0.42 + let lumpTime = CGFloat(CACurrentMediaTime()) var path = Path() for i in 0...n { let a = CGFloat(i) / CGFloat(n) * .pi * 2 let ca = cos(a), sa = sin(a) - let px0 = rx * (ca >= 0 ? pow(ca, expN) : -pow(-ca, expN)) - let py0 = ry * (sa >= 0 ? pow(sa, expN) : -pow(-sa, expN)) + let lump = broVLump(angle: a, time: lumpTime) + let px0 = rx * lump * (ca >= 0 ? pow(ca, expN) : -pow(-ca, expN)) + let py0 = ry * lump * (sa >= 0 ? pow(sa, expN) : -pow(-sa, expN)) let px: CGFloat let py: CGFloat if morph < 0.005 { @@ -1205,8 +1212,8 @@ final class BotEngine: ObservableObject { ctx.fill(path, with: .color(Color(cgColor: bc))) } else { // Main bot: linear gradient body - let top: Color = pumpkinColors ? Color(hex: "#FFA94D") : Color(red: 0.929, green: 0.929, blue: 0.937) - let bot: Color = pumpkinColors ? Color(hex: "#E8590C") : Color(red: 0.769, green: 0.773, blue: 0.792) + let top: Color = pumpkinColors ? Color(hex: "#FFA94D") : Color(cgColor: MochiConst.baseTop) + let bot: Color = pumpkinColors ? Color(hex: "#E8590C") : Color(cgColor: MochiConst.baseBottom) ctx.fill(path, with: .linearGradient( Gradient(colors: [top, bot]), startPoint: CGPoint(x: rx*0.7, y: -ry*0.85), @@ -1307,6 +1314,9 @@ final class BotEngine: ObservableObject { case .wide: drawEyeShape(ctx: &ctx, shape: .pill, w: w*1.16, h: h*1.12, open: open, sd: sd, R: R) + case .pill where showsBroVFace: + drawBroVEye(ctx: &ctx, w: w, h: h, open: open, sd: sd, ink: ink) + case .pill: let hh = max(h * open, w * 0.3) var p = Path() @@ -1657,3 +1667,201 @@ extension BadgeType: Equatable { } } } + + +// MARK: - BroV face +// +// BroV ("бровь") is a lumpy swamp-zombie blob: one huge hairy unibrow, a bloodshot +// mismatched pair of eyes, a toothy grin with missing teeth, drool, warts and a +// stitched scar. Everything rides on the same engine (blink, gaze, emotes, states); +// mini bots and the upload box keep the plain look. + +extension BotEngine { + /// Main bot only, and not while morphing into the upload box. + var showsBroVFace: Bool { !isMini && bodyColor == nil && morph < 0.3 && outfit != .pumpkin } + + /// Slow, breathing bumps on the outline (main bot only). + func broVLump(angle a: CGFloat, time t: CGFloat) -> CGFloat { + guard !isMini, bodyColor == nil else { return 1 } + let k = 1 - min(1, morph * 2) + let bumps = 0.045 * sin(3 * a + t * 1.1) + + 0.030 * sin(5 * a - t * 0.7 + 1.3) + + 0.018 * sin(9 * a + 2.1) + return 1 + bumps * k + } + + /// Warts and the stitched scar (under the eyes). + func drawBroVSkin(ctx: GraphicsContext, path: Path, R: CGFloat, rx: CGFloat, ry: CGFloat) { + var c = ctx + c.clip(to: path) + let shift = sin(yaw) * rx * 0.8 + let a = 1 - morph / 0.3 + + // Warts: darker bumps with a tiny highlight. + let warts: [(CGFloat, CGFloat, CGFloat)] = [(-0.78, 0.30, 0.085), (0.70, 0.52, 0.065), (-0.30, 0.68, 0.05), (0.85, -0.10, 0.045)] + for (wx, wy, wr) in warts { + let p = CGPoint(x: wx * rx + shift, y: wy * ry) + var e = Path(); e.addEllipse(in: CGRect(x: p.x - wr * R, y: p.y - wr * R, width: wr * 2 * R, height: wr * 2 * R)) + c.fill(e, with: .color(Color(hex: "#3F5229").opacity(0.85 * Double(a)))) + var h = Path(); h.addEllipse(in: CGRect(x: p.x - wr * R * 0.35, y: p.y - wr * R * 0.6, width: wr * R * 0.5, height: wr * R * 0.4)) + c.fill(h, with: .color(Color.white.opacity(0.25 * Double(a)))) + } + + // Stitched scar across the upper right of the head. + let s0 = CGPoint(x: rx * 0.20 + shift, y: -ry * 0.78) + let s1 = CGPoint(x: rx * 0.72 + shift, y: -ry * 0.40) + var scar = Path(); scar.move(to: s0) + scar.addQuadCurve(to: s1, control: CGPoint(x: rx * 0.55 + shift, y: -ry * 0.75)) + c.stroke(scar, with: .color(Color(hex: "#6B2B2B").opacity(Double(a))), style: StrokeStyle(lineWidth: R * 0.05, lineCap: .round)) + for i in 1...4 { + let t = CGFloat(i) / 5 + let mx = (1 - t) * (1 - t) * s0.x + 2 * (1 - t) * t * (rx * 0.55 + shift) + t * t * s1.x + let my = (1 - t) * (1 - t) * s0.y + 2 * (1 - t) * t * (-ry * 0.75) + t * t * s1.y + var st = Path() + st.move(to: CGPoint(x: mx - R * 0.06, y: my - R * 0.07)) + st.addLine(to: CGPoint(x: mx + R * 0.06, y: my + R * 0.07)) + c.stroke(st, with: .color(Color(hex: "#2A0A0A").opacity(0.8 * Double(a))), style: StrokeStyle(lineWidth: R * 0.025, lineCap: .round)) + } + } + + /// Bloodshot sclera + small twitchy pupil. The left eye is bigger. + func drawBroVEye(ctx: inout GraphicsContext, w: CGFloat, h: CGFloat, open: CGFloat, sd: CGFloat, ink: Color) { + let big: CGFloat = sd < 0 ? 1.28 : 0.92 + let sw = w * 1.75 * big + let sh = max(h * 1.45 * big * open, w * 0.25) + let sclera = Path(ellipseIn: CGRect(x: -sw / 2, y: -sh / 2, width: sw, height: sh)) + ctx.fill(sclera, with: .radialGradient( + Gradient(colors: [Color(hex: "#F4EFC8"), Color(hex: "#E2C98E"), Color(hex: "#C9785F")]), + center: .zero, startRadius: 0, endRadius: sw * 0.6)) + guard open > 0.25 else { + ctx.stroke(sclera, with: .color(ink), lineWidth: w * 0.12) + return + } + var inner = ctx + inner.clip(to: sclera) + // Veins from the edge toward the pupil. + for i in 0..<6 { + let ang = CGFloat(i) * 1.05 + sd * 0.4 + var v = Path() + let r0 = sw * 0.52, r1 = sw * 0.2 + v.move(to: CGPoint(x: cos(ang) * r0, y: sin(ang) * r0 * sh / sw)) + v.addQuadCurve(to: CGPoint(x: cos(ang + 0.15) * r1, y: sin(ang + 0.15) * r1 * sh / sw), + control: CGPoint(x: cos(ang - 0.25) * sw * 0.36, y: sin(ang - 0.25) * sw * 0.36 * sh / sw)) + inner.stroke(v, with: .color(Color(hex: "#C2182B").opacity(0.75)), lineWidth: max(0.6, w * 0.06)) + } + // Pupil with a jittery twitch. + let t = CGFloat(CACurrentMediaTime()) + let jx = sin(t * 23 + sd * 3) * w * 0.04 + let jy = cos(t * 19 + sd) * w * 0.03 + let pr = w * 0.42 * (sd < 0 ? 0.8 : 1.15) + inner.fill(Path(ellipseIn: CGRect(x: -pr + jx, y: -pr + jy, width: pr * 2, height: pr * 2)), with: .color(ink)) + inner.fill(Path(ellipseIn: CGRect(x: -pr * 0.3 + jx + pr * 0.35, y: -pr * 0.3 + jy - pr * 0.35, width: pr * 0.5, height: pr * 0.5)), + with: .color(Color.white.opacity(0.85))) + ctx.stroke(sclera, with: .color(Color(hex: "#3A1010").opacity(0.7)), lineWidth: max(0.8, w * 0.07)) + } + + /// Unibrow above the eyes and the toothy mouth below. + func drawBroVFace(ctx: GraphicsContext, path: Path, R: CGFloat, rx: CGFloat, ry: CGFloat) { + var c = ctx + c.clip(to: path) + let alpha = Double(1 - morph / 0.3) + let shift = sin(yaw) * rx * 0.8 + let t = CGFloat(CACurrentMediaTime()) + let eyeY = -sin(MochiConst.eyeP + pitch) * ry + let ink = Color(cgColor: MochiConst.ink).opacity(alpha) + + // ── Unibrow ─────────────────────────────────────────────── + // Shape by mood: angry V, worried ∧, raised on questions, droopy asleep. + let mid: CGFloat // center offset (positive = lower) + let tilt: CGFloat // extra raise of the right end + switch state { + case .approval, .error: mid = 0.10; tilt = 0 + case .ratelimit: mid = -0.08; tilt = 0 + case .question, .searching: mid = 0; tilt = -0.14 + case .sleeping: mid = -0.02; tilt = 0.06 + case .dizzy: mid = sin(t * 6) * 0.08; tilt = cos(t * 5) * 0.08 + default: mid = 0.02 + sin(t * 1.7) * 0.015; tilt = 0 + } + let by = eyeY - R * 0.42 + let half = R * 0.78 + let L = CGPoint(x: -half + shift, y: by - R * 0.04) + let M = CGPoint(x: shift, y: by + mid * R) + let Rt = CGPoint(x: half + shift, y: by - R * 0.04 + tilt * R) + var brow = Path() + brow.move(to: L) + brow.addQuadCurve(to: M, control: CGPoint(x: (L.x + M.x) / 2, y: min(L.y, M.y) - R * 0.07)) + brow.addQuadCurve(to: Rt, control: CGPoint(x: (M.x + Rt.x) / 2, y: min(M.y, Rt.y) - R * 0.07)) + c.stroke(brow, with: .color(ink), style: StrokeStyle(lineWidth: R * 0.17, lineCap: .round, lineJoin: .round)) + // Hairs sticking out. + for i in 0..<22 { + let u = CGFloat(i) / 21 + let p = u < 0.5 + ? quad(L, CGPoint(x: (L.x + M.x) / 2, y: min(L.y, M.y) - R * 0.07), M, u * 2) + : quad(M, CGPoint(x: (M.x + Rt.x) / 2, y: min(M.y, Rt.y) - R * 0.07), Rt, (u - 0.5) * 2) + let ang = -CGFloat.pi / 2 + sin(CGFloat(i) * 2.7) * 0.7 + let len = R * (0.10 + 0.05 * abs(sin(CGFloat(i) * 1.9))) + var hair = Path() + hair.move(to: p) + hair.addLine(to: CGPoint(x: p.x + cos(ang) * len, y: p.y + sin(ang) * len)) + c.stroke(hair, with: .color(ink), style: StrokeStyle(lineWidth: max(0.7, R * 0.025), lineCap: .round)) + } + + // ── Mouth ───────────────────────────────────────────────── + let my = eyeY + R * 0.42 + let mw: CGFloat + let mh: CGFloat + switch state { + case .approval: mw = R * 0.95; mh = R * 0.34 // screaming + case .error: mw = R * 0.70; mh = R * 0.10 + case .finished: mw = R * 1.00; mh = R * 0.26 + case .sleeping: mw = R * 0.30; mh = R * 0.14 + case .working, .thinking, .searching: + mw = R * 0.80; mh = R * (0.12 + 0.05 * abs(sin(t * 7))) // chattering + default: mw = R * 0.88; mh = R * 0.20 + } + let mx = shift + var mouth = Path() + mouth.move(to: CGPoint(x: mx - mw / 2, y: my - mh * 0.2)) + mouth.addQuadCurve(to: CGPoint(x: mx + mw / 2, y: my - mh * 0.2), control: CGPoint(x: mx, y: my + mh * 0.25)) + mouth.addQuadCurve(to: CGPoint(x: mx - mw / 2, y: my - mh * 0.2), control: CGPoint(x: mx, y: my + mh * 1.6)) + mouth.closeSubpath() + c.fill(mouth, with: .color(Color(hex: "#3A0505").opacity(alpha))) + var teethCtx = c + teethCtx.clip(to: mouth) + let teeth = 7 + for i in 0.. CGPoint { + let u = 1 - t + return CGPoint(x: u * u * a.x + 2 * u * t * c.x + t * t * b.x, + y: u * u * a.y + 2 * u * t * c.y + t * t * b.y) + } +} diff --git a/scripts/RenderBroV.swift b/scripts/RenderBroV.swift new file mode 100644 index 0000000..68bcbdd --- /dev/null +++ b/scripts/RenderBroV.swift @@ -0,0 +1,150 @@ +// RenderBroV.swift (pass any argument to render the app + menu bar icons instead) — renders the real BotEngine in every state to a PNG. +// Run: bash scripts/render-brov.sh → /tmp/brov-states.png +import Foundation +import SwiftUI +import AppKit + +// MARK: - Stubs (substitutes for app-only types) + +@MainActor +final class SoundEngine { + static let shared = SoundEngine() + var enabled: Bool = false + func play(_ name: String) {} +} + +extension Notification.Name { + static let botDizzy = Notification.Name("notchBuddy.botDizzy") + static let botGreet = Notification.Name("notchBuddy.botGreet") + static let botBlink = Notification.Name("notchBuddy.botBlink") + static let botSetTgEs = Notification.Name("notchBuddy.botSetTgEs") + static let botGulp = Notification.Name("notchBuddy.botGulp") + static let botMorphTo = Notification.Name("notchBuddy.botMorphTo") + static let triggerEmote = Notification.Name("notchBuddy.triggerEmote") + static let triggerSlap = Notification.Name("notchBuddy.triggerSlap") + static let greetComplete = Notification.Name("notchBuddy.greetComplete") + static let greetingHover = Notification.Name("notchBuddy.greetingHover") + static let greetingInterrupt = Notification.Name("notchBuddy.greetingInterrupt") + static let islandAction = Notification.Name("notchBuddy.islandAction") + static let islandCollapse = Notification.Name("notchBuddy.islandCollapse") + static let hookReveal = Notification.Name("notchBuddy.hookReveal") + static let musicReveal = Notification.Name("notchBuddy.musicReveal") + static let openFullSettings = Notification.Name("notchBuddy.openFullSettings") +} + +extension Color { + init(hex: String) { + let h = hex.trimmingCharacters(in: CharacterSet(charactersIn: "#")) + let val = UInt64(h, radix: 16) ?? 0 + let r = Double((val >> 16) & 0xFF) / 255 + let g = Double((val >> 8) & 0xFF) / 255 + let b = Double( val & 0xFF) / 255 + self.init(red: r, green: g, blue: b) + } +} + + +struct StateCell: View { + let engine: BotEngine + let label: String + let size: CGFloat + var body: some View { + VStack(spacing: 2) { + Canvas { ctx, sz in + engine.drawHandsBehind(context: ctx, size: sz) + engine.draw(context: ctx, size: sz) + engine.drawHandsAndExtras(context: ctx, size: sz) + } + .frame(width: size, height: size) + Text(label).font(.system(size: 10, design: .monospaced)).foregroundColor(.gray) + } + } +} + +@main +struct RenderBroV { + @MainActor static func main() { + if CommandLine.arguments.count > 1 { renderIcons(); return } + var cells: [(BotEngine, String, CGFloat)] = [] + for st in BotState.allCases { + let e = BotEngine() + e.setState(st, force: true) + for _ in 0..<90 { e.update(dt: 1.0 / 60) } + cells.append((e, st.rawValue, 170)) + } + let small = BotEngine() + for _ in 0..<60 { small.update(dt: 1.0 / 60) } + cells.append((small, "notch-size", 48)) + let view = VStack(alignment: .leading, spacing: 8) { + ForEach(0..<3, id: \.self) { row in + HStack(spacing: 8) { + ForEach(Array(cells.enumerated()).filter { $0.offset / 4 == row }, id: \.offset) { _, c in + StateCell(engine: c.0, label: c.1, size: c.2) + } + } + } + } + .padding(16) + .background(Color(red: 0.04, green: 0.04, blue: 0.05)) + let r = ImageRenderer(content: view) + r.scale = 2 + guard let img = r.nsImage, let tiff = img.tiffRepresentation, + let rep = NSBitmapImageRep(data: tiff), let png = rep.representation(using: .png, properties: [:]) else { + print("render failed"); return + } + try! png.write(to: URL(fileURLWithPath: "/tmp/brov-states.png")) + print("wrote /tmp/brov-states.png") + } +} + +@MainActor +func savePNG(_ view: V, scale: CGFloat, to path: String) { + let r = ImageRenderer(content: view) + r.scale = scale + r.isOpaque = false + guard let cg = r.cgImage else { print("render failed"); return } + let rep = NSBitmapImageRep(cgImage: cg) + try! rep.representation(using: .png, properties: [:])!.write(to: URL(fileURLWithPath: path)) + print("wrote", path) +} + +/// /tmp/brov-icon-1024.png (app icon) and /tmp/brov-menubar-{1,2,3}x.png (template unibrow). +@MainActor +func renderIcons() { + let e = BotEngine() + e.setState(.approval, force: true) + for _ in 0..<90 { e.update(dt: 1.0 / 60) } + let icon = ZStack { + RoundedRectangle(cornerRadius: 185, style: .continuous) + .fill(LinearGradient(colors: [Color(hex: "#2B1B2E"), Color(hex: "#0B0A10")], startPoint: .top, endPoint: .bottom)) + .frame(width: 824, height: 824) + Canvas { ctx, sz in + e.draw(context: ctx, size: sz) + } + .frame(width: 900, height: 900) + .offset(y: -20) + } + .frame(width: 1024, height: 1024) + savePNG(icon, scale: 1, to: "/tmp/brov-icon-1024.png") + + // Menu bar: a hairy unibrow over two eyes, black on clear (used as a template image). + let bar = Canvas { ctx, sz in + let w = sz.width, h = sz.height + var brow = Path() + brow.move(to: CGPoint(x: w * 0.06, y: h * 0.42)) + brow.addQuadCurve(to: CGPoint(x: w * 0.5, y: h * 0.40), control: CGPoint(x: w * 0.28, y: h * 0.12)) + brow.addQuadCurve(to: CGPoint(x: w * 0.94, y: h * 0.42), control: CGPoint(x: w * 0.72, y: h * 0.12)) + ctx.stroke(brow, with: .color(.black), style: StrokeStyle(lineWidth: h * 0.2, lineCap: .round)) + for i in 0..<9 { + let x = w * (0.12 + 0.76 * CGFloat(i) / 8) + var hair = Path() + hair.move(to: CGPoint(x: x, y: h * 0.28)) + hair.addLine(to: CGPoint(x: x + w * 0.02, y: h * 0.10)) + ctx.stroke(hair, with: .color(.black), style: StrokeStyle(lineWidth: h * 0.07, lineCap: .round)) + } + ctx.fill(Path(ellipseIn: CGRect(x: w * 0.20, y: h * 0.56, width: w * 0.20, height: h * 0.34)), with: .color(.black)) + ctx.fill(Path(ellipseIn: CGRect(x: w * 0.60, y: h * 0.60, width: w * 0.15, height: h * 0.26)), with: .color(.black)) + } + .frame(width: 22, height: 16) + for s in [1, 2, 3] { savePNG(bar, scale: CGFloat(s), to: "/tmp/brov-menubar-\(s)x.png") } +} diff --git a/scripts/render-brov.sh b/scripts/render-brov.sh new file mode 100755 index 0000000..87f357d --- /dev/null +++ b/scripts/render-brov.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# Render BroV in every state → /tmp/brov-states.png +set -e +cd "$(dirname "$0")/.." +K=NotchBuddy/Sources/CoucouKit +swiftc -parse-as-library -target arm64-apple-macosx15.0 \ + $K/IslandScreenGeometry.swift $K/IslandTypes.swift $K/MochiWardrobe.swift \ + $K/BotEngine.swift $K/MochiOutfitDrawing.swift scripts/RenderBroV.swift \ + -framework AppKit -framework SwiftUI -o /tmp/brov-render +/tmp/brov-render "$@"