Command palette caret uses white tint (#361)

* Set command palette caret tint to white

* Add command palette window actions and shortcut sync
This commit is contained in:
Lawrence Chen 2026-02-23 04:08:01 -08:00 committed by GitHub
parent 7b9f247aa8
commit 8d03657c94
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 380 additions and 31 deletions

View file

@ -2315,6 +2315,25 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
return true
}
if matchShortcut(event: event, shortcut: KeyboardShortcutSettings.shortcut(for: .closeWindow)) {
guard let targetWindow = event.window ?? NSApp.keyWindow ?? NSApp.mainWindow else {
NSSound.beep()
return true
}
targetWindow.performClose(nil)
return true
}
if matchShortcut(event: event, shortcut: KeyboardShortcutSettings.shortcut(for: .renameTab)) {
// Keep Cmd+R browser reload behavior when a browser panel is focused.
if tabManager?.focusedBrowserPanel != nil {
return false
}
let targetWindow = activeCommandPaletteWindow() ?? event.window ?? NSApp.keyWindow ?? NSApp.mainWindow
NotificationCenter.default.post(name: .commandPaletteRenameTabRequested, object: targetWindow)
return true
}
// Numeric shortcuts for specific sidebar tabs: Cmd+1-9 (9 = last workspace)
if flags == [.command],
let manager = tabManager,

View file

@ -2380,7 +2380,7 @@ struct ContentView: View {
TextField(commandPaletteSearchPlaceholder, text: $commandPaletteQuery)
.textFieldStyle(.plain)
.font(.system(size: 13, weight: .regular))
.tint(.blue)
.tint(.white)
.focused($isCommandPaletteSearchFocused)
.onSubmit {
runSelectedCommandPaletteResult(visibleResults: visibleResults)
@ -2582,7 +2582,7 @@ struct ContentView: View {
TextField(target.placeholder, text: $commandPaletteRenameDraft)
.textFieldStyle(.plain)
.font(.system(size: 13, weight: .regular))
.tint(.blue)
.tint(.white)
.focused($isCommandPaletteRenameFocused)
.backport.onKeyPress(.delete) { modifiers in
handleCommandPaletteRenameDeleteBackward(modifiers: modifiers)
@ -2941,7 +2941,7 @@ struct ContentView: View {
rank: nextRank,
title: contribution.title(context),
subtitle: contribution.subtitle(context),
shortcutHint: commandPaletteShortcutHint(for: contribution),
shortcutHint: commandPaletteShortcutHint(for: contribution, context: context),
keywords: contribution.keywords,
dismissOnRun: contribution.dismissOnRun,
action: action
@ -2953,7 +2953,15 @@ struct ContentView: View {
return commands
}
private func commandPaletteShortcutHint(for contribution: CommandPaletteCommandContribution) -> String? {
private func commandPaletteShortcutHint(
for contribution: CommandPaletteCommandContribution,
context: CommandPaletteContextSnapshot
) -> String? {
// Preserve browser reload semantics for Cmd+R when a browser tab is focused.
if contribution.commandId == "palette.renameTab",
context.bool(CommandPaletteContextKeys.panelIsBrowser) {
return nil
}
if let action = commandPaletteShortcutAction(for: contribution.commandId) {
return KeyboardShortcutSettings.shortcut(for: action).displayString
}
@ -2967,16 +2975,22 @@ struct ContentView: View {
switch commandId {
case "palette.newWorkspace":
return .newTab
case "palette.newWindow":
return .newWindow
case "palette.newTerminalTab":
return .newSurface
case "palette.newBrowserTab":
return .openBrowser
case "palette.closeWindow":
return .closeWindow
case "palette.toggleSidebar":
return .toggleSidebar
case "palette.showNotifications":
return .showNotifications
case "palette.jumpUnread":
return .jumpToUnread
case "palette.renameTab":
return .renameTab
case "palette.renameWorkspace":
return .renameWorkspace
case "palette.nextWorkspace":
@ -3108,6 +3122,14 @@ struct ContentView: View {
keywords: ["create", "new", "workspace"]
)
)
contributions.append(
CommandPaletteCommandContribution(
commandId: "palette.newWindow",
title: constant("New Window"),
subtitle: constant("Window"),
keywords: ["create", "new", "window"]
)
)
contributions.append(
CommandPaletteCommandContribution(
commandId: "palette.newTerminalTab",
@ -3144,6 +3166,14 @@ struct ContentView: View {
keywords: ["close", "workspace"]
)
)
contributions.append(
CommandPaletteCommandContribution(
commandId: "palette.closeWindow",
title: constant("Close Window"),
subtitle: constant("Window"),
keywords: ["close", "window"]
)
)
contributions.append(
CommandPaletteCommandContribution(
commandId: "palette.reopenClosedBrowserTab",
@ -3543,6 +3573,9 @@ struct ContentView: View {
registry.register(commandId: "palette.newWorkspace") {
tabManager.addWorkspace()
}
registry.register(commandId: "palette.newWindow") {
AppDelegate.shared?.openNewMainWindow(nil)
}
registry.register(commandId: "palette.newTerminalTab") {
tabManager.newSurface()
}
@ -3555,6 +3588,13 @@ struct ContentView: View {
registry.register(commandId: "palette.closeWorkspace") {
tabManager.closeCurrentWorkspaceWithConfirmation()
}
registry.register(commandId: "palette.closeWindow") {
guard let window = observedWindow ?? NSApp.keyWindow ?? NSApp.mainWindow else {
NSSound.beep()
return
}
window.performClose(nil)
}
registry.register(commandId: "palette.reopenClosedBrowserTab") {
_ = tabManager.reopenMostRecentlyClosedBrowserPanel()
}

View file

@ -8,6 +8,7 @@ enum KeyboardShortcutSettings {
case toggleSidebar
case newTab
case newWindow
case closeWindow
case showNotifications
case jumpToUnread
case triggerFlash
@ -17,6 +18,7 @@ enum KeyboardShortcutSettings {
case prevSurface
case nextSidebarTab
case prevSidebarTab
case renameTab
case renameWorkspace
case closeWorkspace
case newSurface
@ -43,6 +45,7 @@ enum KeyboardShortcutSettings {
case .toggleSidebar: return "Toggle Sidebar"
case .newTab: return "New Workspace"
case .newWindow: return "New Window"
case .closeWindow: return "Close Window"
case .showNotifications: return "Show Notifications"
case .jumpToUnread: return "Jump to Latest Unread"
case .triggerFlash: return "Flash Focused Panel"
@ -50,6 +53,7 @@ enum KeyboardShortcutSettings {
case .prevSurface: return "Previous Surface"
case .nextSidebarTab: return "Next Workspace"
case .prevSidebarTab: return "Previous Workspace"
case .renameTab: return "Rename Tab"
case .renameWorkspace: return "Rename Workspace"
case .closeWorkspace: return "Close Workspace"
case .newSurface: return "New Surface"
@ -72,11 +76,13 @@ enum KeyboardShortcutSettings {
case .toggleSidebar: return "shortcut.toggleSidebar"
case .newTab: return "shortcut.newTab"
case .newWindow: return "shortcut.newWindow"
case .closeWindow: return "shortcut.closeWindow"
case .showNotifications: return "shortcut.showNotifications"
case .jumpToUnread: return "shortcut.jumpToUnread"
case .triggerFlash: return "shortcut.triggerFlash"
case .nextSidebarTab: return "shortcut.nextSidebarTab"
case .prevSidebarTab: return "shortcut.prevSidebarTab"
case .renameTab: return "shortcut.renameTab"
case .renameWorkspace: return "shortcut.renameWorkspace"
case .closeWorkspace: return "shortcut.closeWorkspace"
case .focusLeft: return "shortcut.focusLeft"
@ -104,6 +110,8 @@ enum KeyboardShortcutSettings {
return StoredShortcut(key: "n", command: true, shift: false, option: false, control: false)
case .newWindow:
return StoredShortcut(key: "n", command: true, shift: true, option: false, control: false)
case .closeWindow:
return StoredShortcut(key: "w", command: true, shift: false, option: false, control: true)
case .showNotifications:
return StoredShortcut(key: "i", command: true, shift: false, option: false, control: false)
case .jumpToUnread:
@ -114,6 +122,8 @@ enum KeyboardShortcutSettings {
return StoredShortcut(key: "]", command: true, shift: false, option: false, control: true)
case .prevSidebarTab:
return StoredShortcut(key: "[", command: true, shift: false, option: false, control: true)
case .renameTab:
return StoredShortcut(key: "r", command: true, shift: false, option: false, control: false)
case .renameWorkspace:
return StoredShortcut(key: "r", command: true, shift: true, option: false, control: false)
case .closeWorkspace:

View file

@ -8230,6 +8230,37 @@ class TerminalController {
}
#if DEBUG
private func debugShortcutName(for action: KeyboardShortcutSettings.Action) -> String {
let snakeCase = action.rawValue.replacingOccurrences(
of: "([a-z0-9])([A-Z])",
with: "$1_$2",
options: .regularExpression
)
return snakeCase.lowercased()
}
private func debugShortcutAction(named rawName: String) -> KeyboardShortcutSettings.Action? {
let normalized = rawName
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
.replacingOccurrences(of: "-", with: "_")
for action in KeyboardShortcutSettings.Action.allCases {
let snakeCaseName = debugShortcutName(for: action)
if normalized == snakeCaseName || normalized == snakeCaseName.replacingOccurrences(of: "_", with: "") {
return action
}
}
return nil
}
private func debugShortcutSupportedNames() -> String {
KeyboardShortcutSettings.Action.allCases
.map(debugShortcutName(for:))
.sorted()
.joined(separator: ", ")
}
private func setShortcut(_ args: String) -> String {
let trimmed = args.trimmingCharacters(in: .whitespacesAndNewlines)
let parts = trimmed.split(separator: " ", maxSplits: 1).map(String.init)
@ -8237,29 +8268,15 @@ class TerminalController {
return "ERROR: Usage: set_shortcut <name> <combo|clear>"
}
let name = parts[0].lowercased()
let name = parts[0]
let combo = parts[1].trimmingCharacters(in: .whitespacesAndNewlines)
let defaultsKey: String?
switch name {
case "focus_left", "focusleft":
defaultsKey = KeyboardShortcutSettings.focusLeftKey
case "focus_right", "focusright":
defaultsKey = KeyboardShortcutSettings.focusRightKey
case "focus_up", "focusup":
defaultsKey = KeyboardShortcutSettings.focusUpKey
case "focus_down", "focusdown":
defaultsKey = KeyboardShortcutSettings.focusDownKey
default:
defaultsKey = nil
}
guard let defaultsKey else {
return "ERROR: Unknown shortcut name. Supported: focus_left, focus_right, focus_up, focus_down"
guard let action = debugShortcutAction(named: name) else {
return "ERROR: Unknown shortcut name. Supported: \(debugShortcutSupportedNames())"
}
if combo.lowercased() == "clear" || combo.lowercased() == "default" || combo.lowercased() == "reset" {
UserDefaults.standard.removeObject(forKey: defaultsKey)
UserDefaults.standard.removeObject(forKey: action.defaultsKey)
return "OK"
}
@ -8277,7 +8294,7 @@ class TerminalController {
guard let data = try? JSONEncoder().encode(shortcut) else {
return "ERROR: Failed to encode shortcut"
}
UserDefaults.standard.set(data, forKey: defaultsKey)
UserDefaults.standard.set(data, forKey: action.defaultsKey)
return "OK"
}

View file

@ -555,6 +555,30 @@ final class BrowserDeveloperToolsShortcutDefaultsTests: XCTestCase {
}
final class WorkspaceRenameShortcutDefaultsTests: XCTestCase {
func testRenameTabShortcutDefaultsAndMetadata() {
XCTAssertEqual(KeyboardShortcutSettings.Action.renameTab.label, "Rename Tab")
XCTAssertEqual(KeyboardShortcutSettings.Action.renameTab.defaultsKey, "shortcut.renameTab")
let shortcut = KeyboardShortcutSettings.Action.renameTab.defaultShortcut
XCTAssertEqual(shortcut.key, "r")
XCTAssertTrue(shortcut.command)
XCTAssertFalse(shortcut.shift)
XCTAssertFalse(shortcut.option)
XCTAssertFalse(shortcut.control)
}
func testCloseWindowShortcutDefaultsAndMetadata() {
XCTAssertEqual(KeyboardShortcutSettings.Action.closeWindow.label, "Close Window")
XCTAssertEqual(KeyboardShortcutSettings.Action.closeWindow.defaultsKey, "shortcut.closeWindow")
let shortcut = KeyboardShortcutSettings.Action.closeWindow.defaultShortcut
XCTAssertEqual(shortcut.key, "w")
XCTAssertTrue(shortcut.command)
XCTAssertFalse(shortcut.shift)
XCTAssertFalse(shortcut.option)
XCTAssertTrue(shortcut.control)
}
func testRenameWorkspaceShortcutDefaultsAndMetadata() {
XCTAssertEqual(KeyboardShortcutSettings.Action.renameWorkspace.label, "Rename Workspace")
XCTAssertEqual(KeyboardShortcutSettings.Action.renameWorkspace.defaultsKey, "shortcut.renameWorkspace")

View file

@ -9,6 +9,7 @@ This test checks for:
from __future__ import annotations
import re
import subprocess
import sys
from pathlib import Path
@ -94,6 +95,48 @@ def check_autoupdating_text_styles(files: List[Path]) -> List[Tuple[Path, int, s
return violations
def check_command_palette_caret_tint(repo_root: Path) -> List[str]:
"""Ensure command palette text inputs keep a white caret tint."""
content_view = repo_root / "Sources" / "ContentView.swift"
if not content_view.exists():
return [f"Missing expected file: {content_view}"]
try:
content = content_view.read_text()
except Exception as e:
return [f"Could not read {content_view}: {e}"]
checks = [
(
"search input",
r"TextField\(commandPaletteSearchPlaceholder, text: \$commandPaletteQuery\)(?P<body>.*?)"
r"\.focused\(\$isCommandPaletteSearchFocused\)",
),
(
"rename input",
r"TextField\(target\.placeholder, text: \$commandPaletteRenameDraft\)(?P<body>.*?)"
r"\.focused\(\$isCommandPaletteRenameFocused\)",
),
]
violations: List[str] = []
for label, pattern in checks:
match = re.search(pattern, content, flags=re.DOTALL)
if not match:
violations.append(
f"Could not locate command palette {label} TextField block in Sources/ContentView.swift"
)
continue
body = match.group("body")
if ".tint(.white)" not in body:
violations.append(
f"Command palette {label} TextField must use `.tint(.white)` in Sources/ContentView.swift"
)
return violations
def main():
"""Run the lint checks."""
repo_root = get_repo_root()
@ -102,15 +145,18 @@ def main():
print(f"Checking {len(swift_files)} Swift files for performance issues...")
# Check for auto-updating Text styles
violations = check_autoupdating_text_styles(swift_files)
style_violations = check_autoupdating_text_styles(swift_files)
tint_violations = check_command_palette_caret_tint(repo_root)
has_failures = False
if violations:
if style_violations:
has_failures = True
print("\n❌ LINT FAILURES: Auto-updating Text styles found")
print("=" * 60)
print("These patterns cause continuous SwiftUI view updates and high CPU usage:")
print()
for file_path, line_num, line in violations:
for file_path, line_num, line in style_violations:
rel_path = file_path.relative_to(repo_root)
print(f" {rel_path}:{line_num}")
print(f" {line}")
@ -120,9 +166,23 @@ def main():
print(" Instead of: Text(date, style: .time)")
print(" Use: Text(date.formatted(date: .omitted, time: .shortened))")
print()
if tint_violations:
has_failures = True
print("\n❌ LINT FAILURES: Command palette caret tint drifted")
print("=" * 60)
print("The command palette search and rename text fields must keep a white caret:")
print()
for message in tint_violations:
print(f" {message}")
print()
print("FIX: Set command palette TextField tint modifiers to `.white`.")
print()
if has_failures:
return 1
print("✅ No auto-updating Text style patterns found")
print("✅ No linted SwiftUI pattern regressions found")
return 0

View file

@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""
Regression test: command-palette shortcut hints stay in sync with editable shortcuts.
Validates:
- New Window / Close Window / Rename Tab commands are present in command mode.
- Their displayed shortcut hints reflect the current KeyboardShortcutSettings values.
"""
import os
import sys
import time
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from cmux import cmux, cmuxError
SOCKET_PATH = os.environ.get("CMUX_SOCKET", "/tmp/cmux-debug.sock")
def _wait_until(predicate, timeout_s=4.0, interval_s=0.05, message="timeout"):
start = time.time()
while time.time() - start < timeout_s:
if predicate():
return
time.sleep(interval_s)
raise cmuxError(message)
def _palette_visible(client: cmux, window_id: str) -> bool:
payload = client._call("debug.command_palette.visible", {"window_id": window_id}) or {}
return bool(payload.get("visible"))
def _set_palette_visible(client: cmux, window_id: str, visible: bool) -> None:
if _palette_visible(client, window_id) == visible:
return
client._call("debug.command_palette.toggle", {"window_id": window_id})
_wait_until(
lambda: _palette_visible(client, window_id) == visible,
message=f"command palette did not become visible={visible}",
)
def _palette_results(client: cmux, window_id: str, limit=12) -> dict:
return client.command_palette_results(window_id=window_id, limit=limit)
def _open_palette_and_rows(client: cmux, window_id: str, limit: int = 80) -> list:
_set_palette_visible(client, window_id, False)
_set_palette_visible(client, window_id, True)
payload = _palette_results(client, window_id, limit=limit)
rows = payload.get("results") or []
if not rows:
raise cmuxError(f"command palette returned no rows: {payload}")
return rows
def _assert_shortcut_hint(rows: list, command_id: str, expected_hint: str) -> None:
row = next((row for row in rows if str((row or {}).get("command_id") or "") == command_id), None)
if row is None:
raise cmuxError(f"missing command palette row for {command_id!r}; rows={rows}")
shortcut_hint = str((row or {}).get("shortcut_hint") or "")
if shortcut_hint != expected_hint:
raise cmuxError(
f"unexpected shortcut hint for {command_id}: expected {expected_hint!r}, got {shortcut_hint!r} row={row}"
)
def main() -> int:
with cmux(SOCKET_PATH) as client:
client.activate_app()
time.sleep(0.2)
window_id = client.current_window()
for row in client.list_windows():
other_id = str(row.get("id") or "")
if other_id and other_id != window_id:
client.close_window(other_id)
time.sleep(0.2)
client.focus_window(window_id)
client.activate_app()
time.sleep(0.2)
workspace_id = client.new_workspace(window_id=window_id)
client.select_workspace(workspace_id)
time.sleep(0.2)
shortcut_names = ["new_window", "close_window", "rename_tab"]
try:
rows = _open_palette_and_rows(client, window_id)
_assert_shortcut_hint(rows, "palette.newWindow", "⇧⌘N")
_assert_shortcut_hint(rows, "palette.closeWindow", "⌃⌘W")
_assert_shortcut_hint(rows, "palette.renameTab", "⌘R")
client.set_shortcut("new_window", "cmd+opt+n")
client.set_shortcut("close_window", "cmd+opt+w")
client.set_shortcut("rename_tab", "cmd+ctrl+r")
rows = _open_palette_and_rows(client, window_id)
_assert_shortcut_hint(rows, "palette.newWindow", "⌥⌘N")
_assert_shortcut_hint(rows, "palette.closeWindow", "⌥⌘W")
_assert_shortcut_hint(rows, "palette.renameTab", "⌃⌘R")
finally:
for name in shortcut_names:
try:
client.set_shortcut(name, "clear")
except cmuxError:
pass
_set_palette_visible(client, window_id, False)
print("PASS: command-palette shortcut hints track editable shortcuts for new/close/rename window-tab actions")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -9,6 +9,7 @@ This test checks for:
from __future__ import annotations
import re
import subprocess
import sys
from pathlib import Path
@ -94,6 +95,48 @@ def check_autoupdating_text_styles(files: List[Path]) -> List[Tuple[Path, int, s
return violations
def check_command_palette_caret_tint(repo_root: Path) -> List[str]:
"""Ensure command palette text inputs keep a white caret tint."""
content_view = repo_root / "Sources" / "ContentView.swift"
if not content_view.exists():
return [f"Missing expected file: {content_view}"]
try:
content = content_view.read_text()
except Exception as e:
return [f"Could not read {content_view}: {e}"]
checks = [
(
"search input",
r"TextField\(commandPaletteSearchPlaceholder, text: \$commandPaletteQuery\)(?P<body>.*?)"
r"\.focused\(\$isCommandPaletteSearchFocused\)",
),
(
"rename input",
r"TextField\(target\.placeholder, text: \$commandPaletteRenameDraft\)(?P<body>.*?)"
r"\.focused\(\$isCommandPaletteRenameFocused\)",
),
]
violations: List[str] = []
for label, pattern in checks:
match = re.search(pattern, content, flags=re.DOTALL)
if not match:
violations.append(
f"Could not locate command palette {label} TextField block in Sources/ContentView.swift"
)
continue
body = match.group("body")
if ".tint(.white)" not in body:
violations.append(
f"Command palette {label} TextField must use `.tint(.white)` in Sources/ContentView.swift"
)
return violations
def main():
"""Run the lint checks."""
repo_root = get_repo_root()
@ -102,15 +145,18 @@ def main():
print(f"Checking {len(swift_files)} Swift files for performance issues...")
# Check for auto-updating Text styles
violations = check_autoupdating_text_styles(swift_files)
style_violations = check_autoupdating_text_styles(swift_files)
tint_violations = check_command_palette_caret_tint(repo_root)
has_failures = False
if violations:
if style_violations:
has_failures = True
print("\n❌ LINT FAILURES: Auto-updating Text styles found")
print("=" * 60)
print("These patterns cause continuous SwiftUI view updates and high CPU usage:")
print()
for file_path, line_num, line in violations:
for file_path, line_num, line in style_violations:
rel_path = file_path.relative_to(repo_root)
print(f" {rel_path}:{line_num}")
print(f" {line}")
@ -120,9 +166,23 @@ def main():
print(" Instead of: Text(date, style: .time)")
print(" Use: Text(date.formatted(date: .omitted, time: .shortened))")
print()
if tint_violations:
has_failures = True
print("\n❌ LINT FAILURES: Command palette caret tint drifted")
print("=" * 60)
print("The command palette search and rename text fields must keep a white caret:")
print()
for message in tint_violations:
print(f" {message}")
print()
print("FIX: Set command palette TextField tint modifiers to `.white`.")
print()
if has_failures:
return 1
print("✅ No auto-updating Text style patterns found")
print("✅ No linted SwiftUI pattern regressions found")
return 0