Use Ghostty theme picker for cmux themes
This commit is contained in:
parent
6584a01aef
commit
ae24dad89f
4 changed files with 173 additions and 3 deletions
145
CLI/cmux.swift
145
CLI/cmux.swift
|
|
@ -4281,8 +4281,11 @@ struct CMUXCLI {
|
|||
cmux themes set --dark <theme> [--light <theme>]
|
||||
cmux themes clear
|
||||
|
||||
List bundled and discovered Ghostty themes, show the current cmux light/dark
|
||||
theme defaults, and update the default theme override used by cmux.
|
||||
When run in a TTY, `cmux themes` opens an interactive theme picker with
|
||||
live app preview. Use `cmux themes list` for a plain listing.
|
||||
|
||||
The picker previews the selected theme across the running cmux app and
|
||||
lets you apply it to the light theme, dark theme, or both defaults.
|
||||
|
||||
Commands:
|
||||
list List available themes and mark the current light/dark defaults
|
||||
|
|
@ -4293,6 +4296,7 @@ struct CMUXCLI {
|
|||
|
||||
Examples:
|
||||
cmux themes
|
||||
cmux themes list
|
||||
cmux themes set "Catppuccin Mocha"
|
||||
cmux themes set --light "Catppuccin Latte" --dark "Catppuccin Mocha"
|
||||
cmux themes clear
|
||||
|
|
@ -5380,8 +5384,145 @@ struct CMUXCLI {
|
|||
let targetBundleIdentifier: String
|
||||
}
|
||||
|
||||
private enum ThemePickerTargetMode: String {
|
||||
case both
|
||||
case light
|
||||
case dark
|
||||
}
|
||||
|
||||
private func shouldUseInteractiveThemePicker(jsonOutput: Bool) -> Bool {
|
||||
guard !jsonOutput else { return false }
|
||||
return isatty(STDIN_FILENO) == 1 && isatty(STDOUT_FILENO) == 1
|
||||
}
|
||||
|
||||
private func runInteractiveThemes() throws {
|
||||
guard let helperURL = bundledHelperURL(named: "ghostty") else {
|
||||
throw CLIError(message: "Bundled Ghostty theme picker helper not found")
|
||||
}
|
||||
|
||||
let selection = currentThemeSelection()
|
||||
var environment = ProcessInfo.processInfo.environment
|
||||
environment["CMUX_THEME_PICKER_CONFIG"] = try cmuxThemeOverrideConfigURL().path
|
||||
environment["CMUX_THEME_PICKER_BUNDLE_ID"] = currentCmuxAppBundleIdentifier() ?? Self.cmuxThemeOverrideBundleIdentifier
|
||||
environment["CMUX_THEME_PICKER_TARGET"] = defaultThemePickerTargetMode(current: selection).rawValue
|
||||
if let light = selection.light {
|
||||
environment["CMUX_THEME_PICKER_INITIAL_LIGHT"] = light
|
||||
}
|
||||
if let dark = selection.dark {
|
||||
environment["CMUX_THEME_PICKER_INITIAL_DARK"] = dark
|
||||
}
|
||||
if let resourcesURL = bundledGhosttyResourcesURL() {
|
||||
environment["GHOSTTY_RESOURCES_DIR"] = resourcesURL.path
|
||||
}
|
||||
|
||||
let process = Process()
|
||||
process.executableURL = helperURL
|
||||
process.arguments = ["+list-themes"]
|
||||
process.environment = environment
|
||||
process.standardInput = FileHandle.standardInput
|
||||
process.standardOutput = FileHandle.standardOutput
|
||||
process.standardError = FileHandle.standardError
|
||||
try process.run()
|
||||
process.waitUntilExit()
|
||||
|
||||
guard process.terminationReason == .exit else {
|
||||
throw CLIError(message: "Interactive theme picker terminated unexpectedly")
|
||||
}
|
||||
guard process.terminationStatus == 0 else {
|
||||
throw CLIError(message: "Interactive theme picker failed with status \(process.terminationStatus)")
|
||||
}
|
||||
}
|
||||
|
||||
private func defaultThemePickerTargetMode(current: ThemeSelection) -> ThemePickerTargetMode {
|
||||
if let light = current.light,
|
||||
let dark = current.dark,
|
||||
light.caseInsensitiveCompare(dark) == .orderedSame {
|
||||
return .both
|
||||
}
|
||||
return defaultAppearancePrefersDarkThemes() ? .dark : .light
|
||||
}
|
||||
|
||||
private func defaultAppearancePrefersDarkThemes() -> Bool {
|
||||
let globalDefaults = UserDefaults.standard.persistentDomain(forName: UserDefaults.globalDomain)
|
||||
let interfaceStyle = (globalDefaults?["AppleInterfaceStyle"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return interfaceStyle?.caseInsensitiveCompare("Dark") == .orderedSame
|
||||
}
|
||||
|
||||
private func bundledHelperURL(named helperName: String) -> URL? {
|
||||
let fileManager = FileManager.default
|
||||
guard let executableURL = resolvedExecutableURL() else { return nil }
|
||||
|
||||
var candidates: [URL] = [
|
||||
executableURL.deletingLastPathComponent().appendingPathComponent(helperName, isDirectory: false)
|
||||
]
|
||||
|
||||
var current = executableURL.deletingLastPathComponent().standardizedFileURL
|
||||
while true {
|
||||
if current.lastPathComponent == "Contents" {
|
||||
candidates.append(
|
||||
current
|
||||
.appendingPathComponent("Resources", isDirectory: true)
|
||||
.appendingPathComponent("bin", isDirectory: true)
|
||||
.appendingPathComponent(helperName, isDirectory: false)
|
||||
)
|
||||
}
|
||||
|
||||
let projectMarker = current.appendingPathComponent("GhosttyTabs.xcodeproj/project.pbxproj", isDirectory: false)
|
||||
let repoHelper = current
|
||||
.appendingPathComponent("ghostty", isDirectory: true)
|
||||
.appendingPathComponent("zig-out", isDirectory: true)
|
||||
.appendingPathComponent("bin", isDirectory: true)
|
||||
.appendingPathComponent(helperName, isDirectory: false)
|
||||
if fileManager.fileExists(atPath: projectMarker.path),
|
||||
fileManager.isExecutableFile(atPath: repoHelper.path) {
|
||||
candidates.append(repoHelper)
|
||||
break
|
||||
}
|
||||
|
||||
guard let parent = parentSearchURL(for: current) else { break }
|
||||
current = parent
|
||||
}
|
||||
|
||||
return candidates.first(where: { fileManager.isExecutableFile(atPath: $0.path) })
|
||||
}
|
||||
|
||||
private func bundledGhosttyResourcesURL() -> URL? {
|
||||
let fileManager = FileManager.default
|
||||
guard let executableURL = resolvedExecutableURL() else { return nil }
|
||||
|
||||
var current = executableURL.deletingLastPathComponent().standardizedFileURL
|
||||
while true {
|
||||
if current.lastPathComponent == "Contents" {
|
||||
let candidate = current
|
||||
.appendingPathComponent("Resources", isDirectory: true)
|
||||
.appendingPathComponent("ghostty", isDirectory: true)
|
||||
if fileManager.fileExists(atPath: candidate.path) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
|
||||
let projectMarker = current.appendingPathComponent("GhosttyTabs.xcodeproj/project.pbxproj", isDirectory: false)
|
||||
let repoResources = current
|
||||
.appendingPathComponent("Resources", isDirectory: true)
|
||||
.appendingPathComponent("ghostty", isDirectory: true)
|
||||
if fileManager.fileExists(atPath: projectMarker.path),
|
||||
fileManager.fileExists(atPath: repoResources.path) {
|
||||
return repoResources
|
||||
}
|
||||
|
||||
guard let parent = parentSearchURL(for: current) else { break }
|
||||
current = parent
|
||||
}
|
||||
|
||||
return Bundle.main.resourceURL?.appendingPathComponent("ghostty", isDirectory: true)
|
||||
}
|
||||
|
||||
private func runThemes(commandArgs: [String], jsonOutput: Bool) throws {
|
||||
if commandArgs.isEmpty {
|
||||
if shouldUseInteractiveThemePicker(jsonOutput: jsonOutput) {
|
||||
try runInteractiveThemes()
|
||||
return
|
||||
}
|
||||
try printThemesList(jsonOutput: jsonOutput)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,6 +86,20 @@ touch the same stale-frame mitigation path and tend to conflict in the same file
|
|||
|
||||
The fork branch HEAD is now the section 6 zsh redraw commit.
|
||||
|
||||
### 7) cmux theme picker helper hooks
|
||||
|
||||
- Commit: `0c52c987b` (Add cmux theme picker helper hooks)
|
||||
- Files:
|
||||
- `build.zig`
|
||||
- `src/cli/list_themes.zig`
|
||||
- `src/main_ghostty.zig`
|
||||
- Summary:
|
||||
- Adds a `zig build cli-helper` step so cmux can bundle Ghostty's CLI helper binary on macOS.
|
||||
- Lets `+list-themes` switch into a cmux-managed mode via env vars, writing the cmux theme override file and posting the existing cmux reload notification for live app-wide preview.
|
||||
- Fixes the helper-only `app-runtime=none` stdout path so the Ghostty CLI binary builds with the current Zig toolchain.
|
||||
|
||||
The fork branch HEAD is now the section 7 cmux theme picker helper commit.
|
||||
|
||||
## Upstreamed fork changes
|
||||
|
||||
### cursor-click-to-move respects OSC 133 click-to-move
|
||||
|
|
@ -109,4 +123,9 @@ These files change frequently upstream; be careful when rebasing the fork:
|
|||
`OSC 133;A` vs `OSC 133;P` split intact for redraw-heavy themes. Pure-style `\n%{\r%}`
|
||||
prompt newlines should not get an extra explicit continuation marker after the hidden CR.
|
||||
|
||||
- `src/cli/list_themes.zig`
|
||||
- cmux now relies on the upstream picker UI plus local env-driven hooks for live preview and restore.
|
||||
If upstream reorganizes the preview loop or key handling, re-check the cmux mode path and keep the
|
||||
stock Ghostty behavior unchanged when the cmux env vars are absent.
|
||||
|
||||
If you resolve a conflict, update this doc with what changed.
|
||||
|
|
|
|||
2
ghostty
2
ghostty
|
|
@ -1 +1 @@
|
|||
Subproject commit 312c7b23a7c8dc0704431940d76ba5dc32a46afb
|
||||
Subproject commit 0c52c987be769f244c740b305033a2ec9d85b982
|
||||
|
|
@ -306,15 +306,25 @@ else
|
|||
fi
|
||||
sleep 0.3
|
||||
CMUXD_SRC="$PWD/cmuxd/zig-out/bin/cmuxd"
|
||||
GHOSTTY_HELPER_SRC="$PWD/ghostty/zig-out/bin/ghostty"
|
||||
if [[ -d "$PWD/cmuxd" ]]; then
|
||||
(cd "$PWD/cmuxd" && zig build -Doptimize=ReleaseFast)
|
||||
fi
|
||||
if [[ -d "$PWD/ghostty" ]]; then
|
||||
(cd "$PWD/ghostty" && zig build cli-helper -Dapp-runtime=none -Demit-macos-app=false -Demit-xcframework=false -Doptimize=ReleaseFast)
|
||||
fi
|
||||
if [[ -x "$CMUXD_SRC" ]]; then
|
||||
BIN_DIR="$APP_PATH/Contents/Resources/bin"
|
||||
mkdir -p "$BIN_DIR"
|
||||
cp "$CMUXD_SRC" "$BIN_DIR/cmuxd"
|
||||
chmod +x "$BIN_DIR/cmuxd"
|
||||
fi
|
||||
if [[ -x "$GHOSTTY_HELPER_SRC" ]]; then
|
||||
BIN_DIR="$APP_PATH/Contents/Resources/bin"
|
||||
mkdir -p "$BIN_DIR"
|
||||
cp "$GHOSTTY_HELPER_SRC" "$BIN_DIR/ghostty"
|
||||
chmod +x "$BIN_DIR/ghostty"
|
||||
fi
|
||||
CLI_PATH="$APP_PATH/Contents/Resources/bin/cmux"
|
||||
if [[ -x "$CLI_PATH" ]]; then
|
||||
echo "$CLI_PATH" > /tmp/cmux-last-cli-path || true
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue