Rewrite shannoncoat as a native Swift menu-bar app
Replaces the original shannoncoat.sh entirely with in-process Swift. The script stopped earning its keep once the rest went native, and being a persistent process rather than a one-shot CLI is what later makes live window tracking possible at all. - ProfileStore: JSON config at ~/.shannoncoat/<name>.json (was hand-rolled YAML), with ~ expansion, paths relative to the config dir, and name/dir-collision validation in one place. - ProcessInspector: sysctl(KERN_PROC_ALL/KERN_PROCARGS2) enumeration instead of shelling out to pgrep. - ClaudeControl: launch via Process, focus via direct Accessibility calls (unhide, un-minimize, poll-for-window, AXRaise) instead of AppleScript, and a quit that respects Claude's own termination handling rather than unconditionally SIGKILLing after a flat 5s timeout. - LiveState: NSWorkspace notification-driven state with no polling timer, so the menu bar reflects reality within about a second — including changes made outside the app entirely. - ManageWindow: one non-modal window replacing what would otherwise be a string of separate popup alerts, with inline add/remove and directory pickers. - LaunchAtLogin / UpdateChecker: SMAppService login-item toggle, and a minimal VERSION-file update check that alerts on a manual check and posts a quiet notification on the automatic one. Also drops the standalone CLI in favour of GUI-only, and adds a placeholder app icon drawn from vector shapes rather than composited on top of Claude's own icon assets. Claude Desktop is located at runtime rather than assumed: a path the user picked previously, then /Applications, then a per-user ~/Applications install, each accepted only if it actually contains the executable. If none match, launching a profile asks the user to locate it once and remembers the answer — a launch that silently does nothing gives them no way to work out what's wrong.
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
// A stable, well-distributed color per profile name, shared by the menu
|
||||
// bar dots and the Manage window's profile list so the same name always
|
||||
// reads as the same color everywhere.
|
||||
import AppKit
|
||||
|
||||
enum ProfileColor {
|
||||
// FNV-1a: a proper avalanching hash, not a preset-palette lookup — the
|
||||
// full hue wheel is available, and single-character name edits (e.g.
|
||||
// "personal" vs "personal2") land on unrelated hues instead of an
|
||||
// adjacent bucket the way a plain sum-of-scalars hash mod'd into a
|
||||
// short color list would.
|
||||
private static func fnv1aHash(_ s: String) -> UInt32 {
|
||||
var hash: UInt32 = 0x811c_9dc5
|
||||
for byte in s.utf8 {
|
||||
hash ^= UInt32(byte)
|
||||
hash = hash &* 0x0100_0193
|
||||
}
|
||||
return hash
|
||||
}
|
||||
|
||||
static func dotColor(for name: String) -> NSColor {
|
||||
let hue = CGFloat(fnv1aHash(name) % 360) / 360.0
|
||||
return NSColor(calibratedHue: hue, saturation: 0.65, brightness: 0.9, alpha: 1.0)
|
||||
}
|
||||
|
||||
// `dimmed` marks a profile that isn't actually running right now — the
|
||||
// colored dot still identifies it, but faded, so "open" vs "known but
|
||||
// closed" is visible without reading a tooltip.
|
||||
static func dotImage(for name: String, dimmed: Bool = false) -> NSImage {
|
||||
let size = NSSize(width: 10, height: 10)
|
||||
let image = NSImage(size: size)
|
||||
image.lockFocus()
|
||||
dotColor(for: name).withAlphaComponent(dimmed ? 0.3 : 1.0).setFill()
|
||||
NSBezierPath(ovalIn: NSRect(origin: .zero, size: size)).fill()
|
||||
image.unlockFocus()
|
||||
image.isTemplate = false
|
||||
return image
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user