Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6a7da54ca |
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user