mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 18:18:43 +00:00
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.
This commit is contained in:
@@ -208,11 +208,11 @@ struct BarMenuView: View {
|
|||||||
viewModel.toggleIconStyle()
|
viewModel.toggleIconStyle()
|
||||||
} label: {
|
} label: {
|
||||||
Label(
|
Label(
|
||||||
viewModel.iconStyle == .color ? "Color" : "Mono",
|
"Icon",
|
||||||
systemImage: viewModel.iconStyle == .color ? "paintpalette" : "circle.lefthalf.filled"
|
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 {
|
Button {
|
||||||
showingPrefs = true
|
showingPrefs = true
|
||||||
} label: {
|
} label: {
|
||||||
@@ -240,9 +240,8 @@ struct BarMenuView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func openDashboard() {
|
private func openDashboard() {
|
||||||
if case .success(let discovery) = BarDiscovery.load(), let url = discovery.resolvedURL {
|
// Open the dashboard if the server is up; otherwise start it via `ccs config`.
|
||||||
NSWorkspace.shared.open(url)
|
Task { await DashboardLauncher.openOrStart() }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -510,12 +509,19 @@ struct Chip: View {
|
|||||||
self.text = text
|
self.text = text
|
||||||
self.tint = tint
|
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 {
|
var body: some View {
|
||||||
Text(text)
|
Text(text)
|
||||||
.font(.system(size: 9, weight: .semibold))
|
.font(.system(size: 9.5, weight: .semibold))
|
||||||
.padding(.horizontal, 5)
|
.padding(.horizontal, 5)
|
||||||
.padding(.vertical, 1)
|
.padding(.vertical, 1.5)
|
||||||
.background(tint.opacity(0.16), in: Capsule())
|
.background(tint.opacity(0.22), in: Capsule())
|
||||||
.foregroundStyle(tint == .secondary ? Color.secondary : tint)
|
.foregroundStyle(textColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user