Repository navigation
Add Ghostty theme picker to the command palette #1149
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
b7a7501
faeb59f
76468ea
60a52b5
3f37356
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1332,6 +1332,11 @@ struct ContentView: View { | |
| @State private var commandPalettePendingActivation: CommandPalettePendingActivation? | ||
| @State private var commandPaletteResultsRevision: UInt64 = 0 | ||
| @State private var commandPaletteUsageHistoryByCommandId: [String: CommandPaletteUsageEntry] = [:] | ||
| @State private var commandPaletteCommandSubmenu: CommandPaletteCommandSubmenu? | ||
| @State private var commandPaletteGhosttyThemeNames: [String] = [] | ||
| @State private var commandPaletteGhosttyResolvedCurrentTheme: String? | ||
| @State private var commandPaletteGhosttyThemesAreLoading = false | ||
| @State private var commandPaletteGhosttyThemeLoadTask: Task<Void, Never>? | ||
| @State private var isFeedbackComposerPresented = false | ||
| @AppStorage(CommandPaletteRenameSelectionSettings.selectAllOnFocusKey) | ||
| private var commandPaletteRenameSelectAllOnFocus = CommandPaletteRenameSelectionSettings.defaultSelectAllOnFocus | ||
|
|
@@ -1340,6 +1345,10 @@ struct ContentView: View { | |
| @FocusState private var isCommandPaletteSearchFocused: Bool | ||
| @FocusState private var isCommandPaletteRenameFocused: Bool | ||
|
|
||
| private enum CommandPaletteCommandSubmenu { | ||
| case ghosttyThemes | ||
| } | ||
|
|
||
| private enum CommandPaletteMode { | ||
| case commands | ||
| case renameInput(CommandPaletteRenameTarget) | ||
|
|
@@ -3097,6 +3106,14 @@ struct ContentView: View { | |
| resetCommandPaletteSearchFocus() | ||
| } | ||
| .onChange(of: commandPaletteQuery) { _ in | ||
| if !commandPaletteQuery.hasPrefix(Self.commandPaletteCommandsPrefix) { | ||
| commandPaletteCommandSubmenu = nil | ||
| commandPaletteGhosttyThemeLoadTask?.cancel() | ||
| commandPaletteGhosttyThemeLoadTask = nil | ||
| commandPaletteGhosttyThemeNames = [] | ||
| commandPaletteGhosttyResolvedCurrentTheme = nil | ||
| commandPaletteGhosttyThemesAreLoading = false | ||
| } | ||
| commandPaletteSelectedResultIndex = 0 | ||
| commandPaletteSelectionAnchorCommandID = nil | ||
| commandPaletteHoveredResultIndex = nil | ||
|
|
@@ -3247,6 +3264,9 @@ struct ContentView: View { | |
| private var commandPaletteSearchPlaceholder: String { | ||
| switch commandPaletteListScope { | ||
| case .commands: | ||
| if commandPaletteCommandSubmenu == .ghosttyThemes { | ||
| return String(localized: "commandPalette.search.ghosttyThemesPlaceholder", defaultValue: "Search Ghostty themes") | ||
| } | ||
| return String(localized: "commandPalette.search.commandsPlaceholder", defaultValue: "Type a command") | ||
| case .switcher: | ||
| return String(localized: "commandPalette.search.switcherPlaceholder", defaultValue: "Search workspaces") | ||
|
|
@@ -3256,6 +3276,9 @@ struct ContentView: View { | |
| private var commandPaletteEmptyStateText: String { | ||
| switch commandPaletteListScope { | ||
| case .commands: | ||
| if commandPaletteCommandSubmenu == .ghosttyThemes { | ||
| return String(localized: "commandPalette.search.ghosttyThemesEmpty", defaultValue: "No Ghostty themes match your search.") | ||
| } | ||
| return String(localized: "commandPalette.search.commandsEmpty", defaultValue: "No commands match your search.") | ||
| case .switcher: | ||
| return String(localized: "commandPalette.search.switcherEmpty", defaultValue: "No workspaces match your search.") | ||
|
|
@@ -3837,6 +3860,10 @@ struct ContentView: View { | |
| } | ||
|
|
||
| private func commandPaletteCommands() -> [CommandPaletteCommand] { | ||
| if commandPaletteCommandSubmenu == .ghosttyThemes { | ||
| return commandPaletteGhosttyThemeCommands() | ||
| } | ||
|
|
||
| let context = commandPaletteContextSnapshot() | ||
| let contributions = commandPaletteCommandContributions() | ||
| var handlerRegistry = CommandPaletteHandlerRegistry() | ||
|
|
@@ -3870,6 +3897,146 @@ struct ContentView: View { | |
| return commands | ||
| } | ||
|
|
||
| private func commandPaletteGhosttyThemeCommands() -> [CommandPaletteCommand] { | ||
| let currentTheme = commandPaletteGhosttyResolvedCurrentTheme | ||
| let themes = commandPaletteGhosttyThemeNames | ||
| var commands: [CommandPaletteCommand] = [] | ||
| commands.reserveCapacity(max(2, themes.count + 1)) | ||
|
|
||
| commands.append( | ||
| CommandPaletteCommand( | ||
| id: "palette.ghosttyTheme.back", | ||
| rank: 0, | ||
| title: String(localized: "command.ghosttyTheme.back.title", defaultValue: "Back"), | ||
| subtitle: String(localized: "command.ghosttyTheme.back.subtitle", defaultValue: "Ghostty Themes"), | ||
| shortcutHint: nil, | ||
| keywords: ["back", "theme", "ghostty"], | ||
| dismissOnRun: false, | ||
| action: { exitCommandPaletteGhosttyThemeMenu() } | ||
| ) | ||
| ) | ||
|
|
||
| if commandPaletteGhosttyThemesAreLoading && themes.isEmpty { | ||
| commands.append( | ||
| CommandPaletteCommand( | ||
| id: "palette.ghosttyTheme.loading", | ||
| rank: 1, | ||
| title: String(localized: "command.ghosttyTheme.loading.title", defaultValue: "Loading Ghostty themes…"), | ||
| subtitle: String(localized: "command.ghosttyTheme.loading.subtitle", defaultValue: "Ghostty Theme"), | ||
| shortcutHint: nil, | ||
| keywords: ["loading", "theme", "ghostty"], | ||
| dismissOnRun: false, | ||
| action: {} | ||
| ) | ||
| ) | ||
| return commands | ||
| } | ||
|
|
||
| for (index, themeName) in themes.enumerated() { | ||
| let isCurrentTheme = currentTheme?.localizedCaseInsensitiveCompare(themeName) == .orderedSame | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✓ indicator never fires for light/dark variant themes
The fix is to resolve the raw value through let resolvedTheme = currentTheme.flatMap { raw in
GhosttyConfig.resolveThemeName(
from: raw,
preferredColorScheme: GhosttyConfig.currentColorSchemePreference()
)
}
let isCurrentTheme = resolvedTheme?.localizedCaseInsensitiveCompare(themeName) == .orderedSame
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
|
||
| commands.append( | ||
| CommandPaletteCommand( | ||
| id: commandPaletteGhosttyThemeCommandID(for: themeName), | ||
| rank: index + 1, | ||
| title: isCurrentTheme | ||
| ? String(localized: "command.ghosttyTheme.current.title", defaultValue: "\(themeName) ✓") | ||
| : themeName, | ||
| subtitle: String(localized: "command.ghosttyTheme.subtitle", defaultValue: "Ghostty Theme"), | ||
| shortcutHint: nil, | ||
| keywords: ["theme", "ghostty", "appearance", themeName], | ||
| dismissOnRun: true, | ||
| action: { applyGhosttyThemeFromPalette(themeName) } | ||
| ) | ||
| ) | ||
| } | ||
|
|
||
| return commands | ||
| } | ||
|
|
||
| private func enterCommandPaletteGhosttyThemeMenu() { | ||
| commandPaletteCommandSubmenu = .ghosttyThemes | ||
| commandPaletteQuery = Self.commandPaletteCommandsPrefix | ||
| commandPaletteSelectedResultIndex = 0 | ||
| commandPaletteSelectionAnchorCommandID = nil | ||
| refreshCommandPaletteGhosttyThemeData(force: true) | ||
| scheduleCommandPaletteResultsRefresh(forceSearchCorpusRefresh: true) | ||
|
Comment on lines
+3977
to
+3978
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Honor These forced Ghostty submenu refreshes still stay in the 💡 Minimal fix private func scheduleCommandPaletteResultsRefresh(forceSearchCorpusRefresh: Bool = false) {
refreshCommandPaletteSearchCorpus(force: forceSearchCorpusRefresh)
@@
commandPalettePendingActivation = nil
cancelCommandPaletteSearch()
- if Self.commandPaletteShouldSynchronouslySeedResults(
+ if forceSearchCorpusRefresh || Self.commandPaletteShouldSynchronouslySeedResults(
hasVisibleResultsForScope: commandPaletteVisibleResultsScope == scope
) {
let matches = Self.commandPaletteResolvedSearchMatches(
searchCorpus: searchCorpus,
query: query,Also applies to: 4058-4060 🤖 Prompt for AI Agents
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch on the UX flash — this is a valid observation. However, I'm intentionally not addressing this in this PR for the following reasons:
Will track this as a follow-up UX polish item. Thanks for the thorough analysis. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If helpful, I can open a follow‑up issue that scopes a global, low‑risk improvement across all submenus. Proposed acceptance criteria:
Optionally, the solution could be:
Would you like me to file that issue and assign it to you? ✏️ Learnings added
🧠 Learnings used |
||
| resetCommandPaletteSearchFocus() | ||
| syncCommandPaletteDebugStateForObservedWindow() | ||
| } | ||
|
|
||
| private func exitCommandPaletteGhosttyThemeMenu() { | ||
| commandPaletteCommandSubmenu = nil | ||
| commandPaletteGhosttyThemeLoadTask?.cancel() | ||
| commandPaletteGhosttyThemeLoadTask = nil | ||
| commandPaletteGhosttyThemeNames = [] | ||
| commandPaletteGhosttyResolvedCurrentTheme = nil | ||
| commandPaletteGhosttyThemesAreLoading = false | ||
| commandPaletteQuery = Self.commandPaletteCommandsPrefix | ||
| commandPaletteSelectedResultIndex = 0 | ||
| commandPaletteSelectionAnchorCommandID = nil | ||
| scheduleCommandPaletteResultsRefresh(forceSearchCorpusRefresh: true) | ||
| resetCommandPaletteSearchFocus() | ||
| syncCommandPaletteDebugStateForObservedWindow() | ||
| } | ||
|
|
||
| private func applyGhosttyThemeFromPalette(_ themeName: String) { | ||
| do { | ||
| try GhosttyConfig.applyTheme( | ||
| themeName, | ||
| preferredColorScheme: GhosttyConfig.currentColorSchemePreference() | ||
| ) | ||
| GhosttyConfig.invalidateLoadCache() | ||
| commandPaletteGhosttyResolvedCurrentTheme = themeName | ||
| GhosttyApp.shared.reloadConfiguration(source: "commandPalette.ghosttyTheme") | ||
| } catch { | ||
| NSSound.beep() | ||
| NSLog( | ||
| "commandPalette.ghosttyTheme.apply failed theme=%@ error=%@", | ||
| themeName, | ||
| error.localizedDescription | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| private func refreshCommandPaletteGhosttyThemeData(force: Bool = false) { | ||
| if !force, | ||
| (!commandPaletteGhosttyThemeNames.isEmpty || commandPaletteGhosttyThemesAreLoading) { | ||
| return | ||
| } | ||
|
|
||
| commandPaletteGhosttyThemeLoadTask?.cancel() | ||
| commandPaletteGhosttyThemesAreLoading = true | ||
| commandPaletteGhosttyThemeLoadTask = Task.detached(priority: .userInitiated) { | ||
| let preferredColorScheme = GhosttyConfig.currentColorSchemePreference() | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
|
||
| let resolvedCurrentTheme = ( | ||
| GhosttyConfig.load(useCache: false).theme.map { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This detached background task calls Useful? React with 👍 / 👎. |
||
| GhosttyConfig.resolveThemeName( | ||
| from: $0, | ||
| preferredColorScheme: preferredColorScheme | ||
| ) | ||
| } | ||
| )?.trimmingCharacters(in: .whitespacesAndNewlines) | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| let themes = GhosttyConfig.discoverThemeNames() | ||
|
|
||
| guard !Task.isCancelled else { return } | ||
|
|
||
| await MainActor.run { | ||
| guard commandPaletteCommandSubmenu == .ghosttyThemes else { return } | ||
| commandPaletteGhosttyThemeNames = themes | ||
| commandPaletteGhosttyResolvedCurrentTheme = resolvedCurrentTheme | ||
| commandPaletteGhosttyThemesAreLoading = false | ||
| commandPaletteGhosttyThemeLoadTask = nil | ||
| scheduleCommandPaletteResultsRefresh(forceSearchCorpusRefresh: true) | ||
| syncCommandPaletteDebugStateForObservedWindow() | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| } | ||
| } | ||
| } | ||
|
|
||
| private func commandPaletteGhosttyThemeCommandID(for themeName: String) -> String { | ||
| let encoded = themeName.utf8.map { String(format: "%02x", $0) }.joined() | ||
| return "palette.ghosttyTheme.\(encoded)" | ||
| } | ||
|
|
||
| private func commandPaletteShortcutHint( | ||
| for contribution: CommandPaletteCommandContribution, | ||
| context: CommandPaletteContextSnapshot | ||
|
|
@@ -4355,6 +4522,15 @@ struct ContentView: View { | |
| ) | ||
| ) | ||
|
|
||
| contributions.append( | ||
| CommandPaletteCommandContribution( | ||
| commandId: "palette.ghosttyThemeMenu", | ||
| title: constant(String(localized: "command.ghosttyThemeMenu.title", defaultValue: "Ghostty Theme…")), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Per Useful? React with 👍 / 👎. |
||
| subtitle: constant(String(localized: "command.ghosttyThemeMenu.subtitle", defaultValue: "Appearance")), | ||
| keywords: ["theme", "ghostty", "appearance", "color", "terminal"], | ||
| dismissOnRun: false | ||
| ) | ||
| ) | ||
| contributions.append( | ||
| CommandPaletteCommandContribution( | ||
| commandId: "palette.openWorkspacePullRequests", | ||
|
|
@@ -4647,6 +4823,9 @@ struct ContentView: View { | |
| } | ||
|
|
||
| private func registerCommandPaletteHandlers(_ registry: inout CommandPaletteHandlerRegistry) { | ||
| registry.register(commandId: "palette.ghosttyThemeMenu") { | ||
| enterCommandPaletteGhosttyThemeMenu() | ||
| } | ||
| registry.register(commandId: "palette.newWorkspace") { | ||
| tabManager.addWorkspace() | ||
| } | ||
|
|
@@ -5403,6 +5582,12 @@ struct ContentView: View { | |
|
|
||
| private func resetCommandPaletteListState(initialQuery: String) { | ||
| commandPaletteMode = .commands | ||
| commandPaletteCommandSubmenu = nil | ||
| commandPaletteGhosttyThemeLoadTask?.cancel() | ||
| commandPaletteGhosttyThemeLoadTask = nil | ||
| commandPaletteGhosttyThemeNames = [] | ||
| commandPaletteGhosttyResolvedCurrentTheme = nil | ||
| commandPaletteGhosttyThemesAreLoading = false | ||
| commandPaletteQuery = initialQuery | ||
| commandPaletteRenameDraft = "" | ||
| commandPaletteSelectedResultIndex = 0 | ||
|
|
@@ -5428,6 +5613,12 @@ struct ContentView: View { | |
| commandPaletteSearchRequestID &+= 1 | ||
| isCommandPalettePresented = false | ||
| commandPaletteMode = .commands | ||
| commandPaletteCommandSubmenu = nil | ||
| commandPaletteGhosttyThemeLoadTask?.cancel() | ||
| commandPaletteGhosttyThemeLoadTask = nil | ||
| commandPaletteGhosttyThemeNames = [] | ||
| commandPaletteGhosttyResolvedCurrentTheme = nil | ||
| commandPaletteGhosttyThemesAreLoading = false | ||
| commandPaletteQuery = "" | ||
| commandPaletteRenameDraft = "" | ||
| commandPaletteSelectedResultIndex = 0 | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keep submenu chrome out of the searchable result set.
Because
Backand the loading sentinel are emitted as normal commands, typing a non-matching query during discovery can hide both rows and render the Ghostty empty state even though loading is still in progress. Pin those rows outside fuzzy search, or suppress the empty state whilecommandPaletteGhosttyThemesAreLoadingis true.🤖 Prompt for AI Agents