From 11b3cd5a5cc91b3a8e71a09ea22f43406e2a00a7 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Tue, 9 Jun 2026 15:39:50 -0400 Subject: [PATCH] feat(bar-app): legible tier chips, honest Icon label, dashboard auto-start Lift chip text toward white so small tier badges (max/pro) read clearly on the dark surface. Rename the icon toggle from Color/Mono to 'Icon' (it only swaps the menu-bar icon, not the bar theme) to remove the misunderstanding. Make the Dashboard button actually work when the server is down: probe reachability and, if not up, launch 'ccs config' (detached) which boots the web-server and opens the dashboard itself; degrade gracefully when the ccs binary isn't found. --- macos-bar/Sources/CCSBarApp/BarMenuView.swift | 24 ++++--- .../Sources/CCSBarApp/DashboardLauncher.swift | 67 +++++++++++++++++++ 2 files changed, 82 insertions(+), 9 deletions(-) create mode 100644 macos-bar/Sources/CCSBarApp/DashboardLauncher.swift diff --git a/macos-bar/Sources/CCSBarApp/BarMenuView.swift b/macos-bar/Sources/CCSBarApp/BarMenuView.swift index 4078fec0..83b6f6a5 100644 --- a/macos-bar/Sources/CCSBarApp/BarMenuView.swift +++ b/macos-bar/Sources/CCSBarApp/BarMenuView.swift @@ -208,11 +208,11 @@ struct BarMenuView: View { viewModel.toggleIconStyle() } label: { Label( - viewModel.iconStyle == .color ? "Color" : "Mono", + "Icon", systemImage: viewModel.iconStyle == .color ? "paintpalette" : "circle.lefthalf.filled" ) } - .help("Toggle the menu-bar icon between color and monochrome") + .help("Toggle the menu-bar icon between color and monochrome (does not change the bar theme)") Button { showingPrefs = true } label: { @@ -240,9 +240,8 @@ struct BarMenuView: View { } private func openDashboard() { - if case .success(let discovery) = BarDiscovery.load(), let url = discovery.resolvedURL { - NSWorkspace.shared.open(url) - } + // Open the dashboard if the server is up; otherwise start it via `ccs config`. + Task { await DashboardLauncher.openOrStart() } } } @@ -510,12 +509,19 @@ struct Chip: View { self.text = text self.tint = tint } + /// Lift the tint toward white so the small 9pt label stays legible on the dark + /// surface — the raw tint (e.g. the indigo subscription color) was too dim to read. + private var textColor: Color { + if tint == .secondary { return .secondary } + let lifted = NSColor(tint).blended(withFraction: 0.5, of: .white) ?? NSColor(tint) + return Color(nsColor: lifted) + } var body: some View { Text(text) - .font(.system(size: 9, weight: .semibold)) + .font(.system(size: 9.5, weight: .semibold)) .padding(.horizontal, 5) - .padding(.vertical, 1) - .background(tint.opacity(0.16), in: Capsule()) - .foregroundStyle(tint == .secondary ? Color.secondary : tint) + .padding(.vertical, 1.5) + .background(tint.opacity(0.22), in: Capsule()) + .foregroundStyle(textColor) } } diff --git a/macos-bar/Sources/CCSBarApp/DashboardLauncher.swift b/macos-bar/Sources/CCSBarApp/DashboardLauncher.swift new file mode 100644 index 00000000..bdaa0485 --- /dev/null +++ b/macos-bar/Sources/CCSBarApp/DashboardLauncher.swift @@ -0,0 +1,67 @@ +import AppKit +import CCSBarCore +import Foundation + +/// Opens the CCS dashboard, starting the local server when it isn't running. +/// +/// The dashboard is just a page served by the CCS web-server, so it can't load +/// if no server is up. When the discovered URL isn't reachable we launch +/// `ccs config`, which boots the server AND opens the dashboard in the browser +/// itself — so we must not also open it (that would double-open a tab). +enum DashboardLauncher { + /// A GUI app does not inherit the shell PATH, so probe the common install + /// locations for the `ccs` binary explicitly. First executable match wins. + private static var ccsCandidates: [String] { + let home = NSHomeDirectory() + return [ + "\(home)/.bun/bin/ccs", + "/opt/homebrew/bin/ccs", + "/usr/local/bin/ccs", + "\(home)/.local/bin/ccs", + "\(home)/.npm-global/bin/ccs", + "/usr/bin/ccs", + ] + } + + private static func ccsBinary() -> String? { + ccsCandidates.first { FileManager.default.isExecutableFile(atPath: $0) } + } + + private static func dashboardURL() -> URL? { + if case .success(let discovery) = BarDiscovery.load() { return discovery.resolvedURL } + return nil + } + + /// Quick reachability probe so a stale `bar.json` URL doesn't send the user to + /// a dead page. Short timeout — the dashboard is local. + private static func isReachable(_ url: URL) async -> Bool { + var request = URLRequest(url: url) + request.httpMethod = "HEAD" + request.timeoutInterval = 1.5 + return (try? await URLSession.shared.data(for: request)) != nil + } + + @MainActor + static func openOrStart() async { + if let url = dashboardURL(), await isReachable(url) { + NSWorkspace.shared.open(url) + return + } + + // Server isn't up. Start it via `ccs config`, fully detached (nohup) so it + // survives this app quitting; it opens the dashboard on its own. + if let bin = ccsBinary() { + let proc = Process() + proc.executableURL = URL(fileURLWithPath: "/bin/sh") + proc.arguments = ["-c", "nohup \"\(bin)\" config >/dev/null 2>&1 &"] + try? proc.run() + return + } + + // Can't find the ccs binary — best effort: open whatever URL we know so the + // user at least lands on the right place (or sees the connection is refused). + if let url = dashboardURL() { + NSWorkspace.shared.open(url) + } + } +}