From 68e74fcbbb899b34c4622bc8a3ca97e4274d97a0 Mon Sep 17 00:00:00 2001 From: bdeshi Date: Tue, 4 Aug 2026 22:44:21 +0600 Subject: [PATCH] Show profile paths, report the build version, and colour each profile - Selecting a row in the Profiles tab shows its Claude Desktop and Claude Code dirs inline and selectable, instead of requiring a right-click "Reveal in Finder" to find out where a profile actually lives. - build.sh stamps Contents/Resources/COMMIT with the short HEAD SHA, left empty when HEAD sits exactly on a tag (how a real release is built), since the version number alone is unambiguous there. About appends it when present, so two dev builds off the same VERSION are distinguishable. The update checker still compares plain semver. - Profile dot colours are picked per NSApp.effectiveAppearance at render time: same hue either way, but full saturation with brightness split per background (1.0 dark, 0.55 light), so they read as bold hues rather than washing out on white or muddying against a dark menu. Co-Authored-By: Claude Opus 5 --- Sources/AppDelegate.swift | 2 +- Sources/ManageWindow.swift | 58 ++++++++++++++++++++++++++++++++----- Sources/ProfileColor.swift | 11 ++++++- Sources/UpdateChecker.swift | 15 ++++++++++ VERSION | 2 +- build.sh | 12 ++++++++ 6 files changed, 90 insertions(+), 10 deletions(-) diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 45418f6..f62eacf 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -274,7 +274,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, LiveSt Author: \(author) Source: \(homepage) - Version: \(UpdateChecker.currentVersion) + Version: \(UpdateChecker.displayVersion) """ NSApp.orderFrontStandardAboutPanel(options: [ .applicationName: "shannoncoat", diff --git a/Sources/ManageWindow.swift b/Sources/ManageWindow.swift index 20fbd6a..0b2df70 100644 --- a/Sources/ManageWindow.swift +++ b/Sources/ManageWindow.swift @@ -58,6 +58,9 @@ private final class ProfilesViewController: NSViewController, NSTableViewDataSou private var infos: [ProfileInfo] = [] private let tableView = NSTableView() private let removeButton = NSButton(title: "\u{2212}", target: nil, action: nil) + private let detailStack = NSStackView() + private let detailAppValue = NSTextField(labelWithString: "") + private let detailCodeValue = NSTextField(labelWithString: "") private let addForm = NSStackView() private let nameField = NSTextField() private let codeField = NSTextField() @@ -91,11 +94,15 @@ private final class ProfilesViewController: NSViewController, NSTableViewDataSou buttonRow.spacing = 4 buttonRow.translatesAutoresizingMaskIntoConstraints = false + buildDetailStack() + detailStack.isHidden = true + detailStack.translatesAutoresizingMaskIntoConstraints = false + buildAddForm() addForm.isHidden = true addForm.translatesAutoresizingMaskIntoConstraints = false - let stack = NSStackView(views: [scroll, buttonRow, addForm]) + let stack = NSStackView(views: [scroll, detailStack, buttonRow, addForm]) stack.orientation = .vertical stack.spacing = 8 stack.edgeInsets = NSEdgeInsets(top: 12, left: 12, bottom: 12, right: 12) @@ -108,9 +115,36 @@ private final class ProfilesViewController: NSViewController, NSTableViewDataSou stack.trailingAnchor.constraint(equalTo: view.trailingAnchor), stack.bottomAnchor.constraint(equalTo: view.bottomAnchor), scroll.heightAnchor.constraint(greaterThanOrEqualToConstant: 160), + detailStack.widthAnchor.constraint(equalTo: stack.widthAnchor), ]) } + // Shows the selected profile's two dirs — the thing you'd otherwise + // have to right-click "Reveal" to find out. + private func buildDetailStack() { + for value in [detailAppValue, detailCodeValue] { + value.isSelectable = true + value.textColor = .secondaryLabelColor + value.font = .systemFont(ofSize: NSFont.smallSystemFontSize) + value.lineBreakMode = .byTruncatingMiddle + value.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + } + + let appRow = NSStackView(views: [labeled("Claude Desktop:"), detailAppValue]) + appRow.orientation = .horizontal + appRow.spacing = 6 + let codeRow = NSStackView(views: [labeled("Claude Code:"), detailCodeValue]) + codeRow.orientation = .horizontal + codeRow.spacing = 6 + + detailStack.orientation = .vertical + detailStack.alignment = .leading + detailStack.spacing = 2 + [appRow, codeRow].forEach(detailStack.addArrangedSubview) + appRow.widthAnchor.constraint(equalTo: detailStack.widthAnchor).isActive = true + codeRow.widthAnchor.constraint(equalTo: detailStack.widthAnchor).isActive = true + } + private func buildAddForm() { nameField.placeholderString = "Profile name" nameField.delegate = self @@ -156,9 +190,21 @@ private final class ProfilesViewController: NSViewController, NSTableViewDataSou func update(_ infos: [ProfileInfo]) { self.infos = infos tableView.reloadData() - removeButton.isEnabled = tableView.selectedRow >= 0 - && infos.indices.contains(tableView.selectedRow) - && infos[tableView.selectedRow].profile.name != "default" + refreshForSelection() + } + + private func refreshForSelection() { + let row = tableView.selectedRow + guard infos.indices.contains(row) else { + removeButton.isEnabled = false + detailStack.isHidden = true + return + } + let profile = infos[row].profile + removeButton.isEnabled = profile.name != "default" + detailAppValue.stringValue = profile.appDir + detailCodeValue.stringValue = profile.codeDir + detailStack.isHidden = false } // MARK: NSTableViewDataSource / Delegate @@ -198,9 +244,7 @@ private final class ProfilesViewController: NSViewController, NSTableViewDataSou } func tableViewSelectionDidChange(_ notification: Notification) { - removeButton.isEnabled = tableView.selectedRow >= 0 - && infos.indices.contains(tableView.selectedRow) - && infos[tableView.selectedRow].profile.name != "default" + refreshForSelection() } // MARK: NSTextFieldDelegate — live default-path preview as the name is typed diff --git a/Sources/ProfileColor.swift b/Sources/ProfileColor.swift index e8e63a5..b60c4b6 100644 --- a/Sources/ProfileColor.swift +++ b/Sources/ProfileColor.swift @@ -18,9 +18,18 @@ enum ProfileColor { return hash } + // A brightness/saturation pair that reads clearly on its own + // background — a color bright enough to pop on a dark menu would wash + // out on a light one, and vice versa, so this picks per current + // appearance rather than using one fixed value for both. + private static func isDarkAppearance() -> Bool { + NSApp.effectiveAppearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua + } + 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) + let (saturation, brightness): (CGFloat, CGFloat) = isDarkAppearance() ? (0.90, 1.0) : (1.0, 0.55) + return NSColor(calibratedHue: hue, saturation: saturation, brightness: brightness, alpha: 1.0) } // `dimmed` marks a profile that isn't actually running right now — the diff --git a/Sources/UpdateChecker.swift b/Sources/UpdateChecker.swift index 8258fe9..4716e24 100644 --- a/Sources/UpdateChecker.swift +++ b/Sources/UpdateChecker.swift @@ -33,6 +33,21 @@ enum UpdateChecker { return contents.trimmingCharacters(in: .whitespacesAndNewlines) } + // What the About panel shows: the version alone for a real release + // build (COMMIT is written empty when build.sh finds HEAD sitting + // exactly on a tag), or version + short SHA for a dev build, so two + // local builds off the same version number are distinguishable. + static var displayVersion: String { + commitSHA.isEmpty ? currentVersion : "\(currentVersion) (\(commitSHA))" + } + + private static var commitSHA: String { + guard let resourceURL = Bundle.main.resourceURL, + let contents = try? String(contentsOf: resourceURL.appendingPathComponent("COMMIT"), encoding: .utf8) + else { return "" } + return contents.trimmingCharacters(in: .whitespacesAndNewlines) + } + static func checkManually(completion: @escaping (Outcome) -> Void) { fetchLatestVersion { latest in guard let latest else { completion(.failed); return } diff --git a/VERSION b/VERSION index 8acdd82..4e379d2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.1 +0.0.2 diff --git a/build.sh b/build.sh index 9c91167..4204218 100755 --- a/build.sh +++ b/build.sh @@ -53,6 +53,18 @@ command -v swiftc >/dev/null || { echo "error: swiftc not found (install Xcode C # panel and the update checker read the same file this script stamped into # Info.plist above. cp "$VERSION_FILE" "$APP/Contents/Resources/VERSION" + +# COMMIT is the short SHA the About panel appends to the version, so a dev +# build is distinguishable from another — left empty for a real release +# build (HEAD sitting exactly on a tag, which is how release.yml checks +# this out), since the version number alone is unambiguous there. +GIT_SHA="" +if git -C "$SCRIPT_DIR" rev-parse --git-dir >/dev/null 2>&1 \ + && ! git -C "$SCRIPT_DIR" describe --tags --exact-match >/dev/null 2>&1; then + GIT_SHA="$(git -C "$SCRIPT_DIR" rev-parse --short HEAD 2>/dev/null || true)" +fi +printf '%s' "$GIT_SHA" >"$APP/Contents/Resources/COMMIT" + swiftc -O "$SOURCES_DIR"/*.swift -o "$APP/Contents/MacOS/launcher" touch "$APP"