Add settings sidebar shell (#3244)

* Add settings sidebar shell

* Add settings shell prototype app

* Migrate settings to native shell layout

* Align settings shell with prototype

* Remove settings titlebar debug hooks

* Use SwiftUI scene for settings window

* Improve settings search results

* Remove settings detail title

* Tune settings window default size

* Highlight settings search results

* Make settings search highlight persistent

* Fade settings search highlight

* Expose settings json in settings

* Split settings navigation helpers

* Limit settings highlight to search clicks

* Make settings sidebar scroll latest-wins

* Use wand icon for automation settings

* Move workspace colors below shortcuts

* Address settings window review feedback

* Fix settings shortcut routing test cleanup

* Fix settings window activation warning

---------

Co-authored-by: Lawrence Chen <[email protected]>
This commit is contained in:
Lawrence Chen
2026-04-28 23:52:35 -07:00
committed by GitHub
co-authored by Lawrence Chen
parent af36cabe4e
commit 1ccde06843
18 changed files with 1750 additions and 602 deletions
+10 -6
View File
@@ -32,6 +32,7 @@
84E00D47E4584162AE53BC8D /* xterm-ghostty in Resources */ = {isa = PBXBuildFile; fileRef = B2E7294509CC42FE9191870E /* xterm-ghostty */; };
8C4BBF2DEF6DF93F395A9EE7 /* TerminalControllerSocketSecurityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491751CE2321474474F27DCF /* TerminalControllerSocketSecurityTests.swift */; };
A5001001 /* cmuxApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001011 /* cmuxApp.swift */; };
A50019A0 /* SettingsNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50019A1 /* SettingsNavigation.swift */; };
A5001002 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001012 /* ContentView.swift */; };
C0DE32470000000000000001 /* ContentViewIdentifierCopyCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE32470000000000000002 /* ContentViewIdentifierCopyCommands.swift */; };
619D509BC9B1EA946CBD6A8A /* WorkspaceRuntimeSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9960CC3992F3C44489E7627C /* WorkspaceRuntimeSettings.swift */; };
@@ -289,6 +290,7 @@
9CEC6EF35B71AE59FA45BA69 /* VMClient.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = VMClient.swift; sourceTree = "<group>"; };
A5001000 /* cmux.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = cmux.app; sourceTree = BUILT_PRODUCTS_DIR; };
A5001011 /* cmuxApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = cmuxApp.swift; sourceTree = "<group>"; };
A50019A1 /* SettingsNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsNavigation.swift; sourceTree = "<group>"; };
A5001012 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
C0DE32470000000000000002 /* ContentViewIdentifierCopyCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentViewIdentifierCopyCommands.swift; sourceTree = "<group>"; };
9960CC3992F3C44489E7627C /* WorkspaceRuntimeSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/WorkspaceRuntimeSettings.swift; sourceTree = "<group>"; };
@@ -583,12 +585,13 @@
A5001041 /* Sources */ = {
isa = PBXGroup;
children = (
3023B1003023B1003023B100 /* ConfigSource.swift */,
3023B1013023B1013023B101 /* ConfigSettingsView.swift */,
A5001011 /* cmuxApp.swift */,
A5001012 /* ContentView.swift */,
C0DE32470000000000000002 /* ContentViewIdentifierCopyCommands.swift */,
9960CC3992F3C44489E7627C /* WorkspaceRuntimeSettings.swift */,
3023B1003023B1003023B100 /* ConfigSource.swift */,
3023B1013023B1013023B101 /* ConfigSettingsView.swift */,
A5001011 /* cmuxApp.swift */,
A50019A1 /* SettingsNavigation.swift */,
A5001012 /* ContentView.swift */,
C0DE32470000000000000002 /* ContentViewIdentifierCopyCommands.swift */,
9960CC3992F3C44489E7627C /* WorkspaceRuntimeSettings.swift */,
243632BA1DBBA36FD46E0610 /* SidebarAppearanceSupport.swift */,
610008EA5E3744DDBE05C8E2 /* InternalTabDragConfiguration.swift */,
A080D1F6CCCBFBED8D95DD1D /* ShortcutHintPill.swift */,
@@ -980,6 +983,7 @@
3023A1003023A1003023A100 /* ConfigSource.swift in Sources */,
3023A1013023A1013023A101 /* ConfigSettingsView.swift in Sources */,
A5001001 /* cmuxApp.swift in Sources */,
A50019A0 /* SettingsNavigation.swift in Sources */,
A5001002 /* ContentView.swift in Sources */,
C0DE32470000000000000001 /* ContentViewIdentifierCopyCommands.swift in Sources */,
619D509BC9B1EA946CBD6A8A /* WorkspaceRuntimeSettings.swift in Sources */,
@@ -0,0 +1,11 @@
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
version = 1
name = "SettingsShellLab"
[setup]
script = ""
[[actions]]
name = "Run"
icon = "run"
command = "./script/build_and_run.sh"
+2
View File
@@ -0,0 +1,2 @@
.build/
dist/
+20
View File
@@ -0,0 +1,20 @@
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "SettingsShellLab",
platforms: [
.macOS(.v14),
],
products: [
.executable(name: "SettingsShellLab", targets: ["SettingsShellLab"]),
],
targets: [
.executableTarget(
name: "SettingsShellLab",
path: "Sources/SettingsShellLab",
resources: [.process("Resources")]
),
]
)
@@ -0,0 +1,24 @@
import AppKit
import SwiftUI
@main
struct SettingsShellLabApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
var body: some Scene {
WindowGroup(String(localized: "app.window.title", defaultValue: "Settings Shell Lab")) {
SettingsShellView()
}
.defaultSize(width: 980, height: 680)
.commands {
SidebarCommands()
}
}
}
final class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
NSApp.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: true)
}
}
@@ -0,0 +1,74 @@
import SwiftUI
enum SettingsSection: String, CaseIterable, Identifiable {
case general
case terminal
case sidebar
case browser
case keyboard
case automation
case advanced
var id: Self { self }
var title: String {
switch self {
case .general:
return String(localized: "section.general.title", defaultValue: "General")
case .terminal:
return String(localized: "section.terminal.title", defaultValue: "Terminal")
case .sidebar:
return String(localized: "section.sidebar.title", defaultValue: "Sidebar")
case .browser:
return String(localized: "section.browser.title", defaultValue: "Browser")
case .keyboard:
return String(localized: "section.keyboard.title", defaultValue: "Keyboard")
case .automation:
return String(localized: "section.automation.title", defaultValue: "Automation")
case .advanced:
return String(localized: "section.advanced.title", defaultValue: "Advanced")
}
}
var symbolName: String {
switch self {
case .general:
return "gearshape"
case .terminal:
return "terminal"
case .sidebar:
return "sidebar.left"
case .browser:
return "globe"
case .keyboard:
return "keyboard"
case .automation:
return "bolt.horizontal"
case .advanced:
return "wrench.and.screwdriver"
}
}
var detail: String {
switch self {
case .general:
return String(localized: "section.general.detail", defaultValue: "Language, appearance, updates")
case .terminal:
return String(localized: "section.terminal.detail", defaultValue: "Font, scrollback, bell")
case .sidebar:
return String(localized: "section.sidebar.detail", defaultValue: "Layout, badges, metadata")
case .browser:
return String(localized: "section.browser.detail", defaultValue: "Search, links, history")
case .keyboard:
return String(localized: "section.keyboard.detail", defaultValue: "Shortcuts and chords")
case .automation:
return String(localized: "section.automation.detail", defaultValue: "Socket, hooks, ports")
case .advanced:
return String(localized: "section.advanced.detail", defaultValue: "Diagnostics and reset")
}
}
var searchText: String {
"\(title) \(detail)"
}
}
@@ -0,0 +1,32 @@
{
"sourceLanguage": "en",
"version": "1.0",
"strings": {
"app.window.title": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Settings Shell" } }, "ja": { "stringUnit": { "state": "translated", "value": "設定シェル" } } } },
"detail.mode": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Mode" } }, "ja": { "stringUnit": { "state": "translated", "value": "モード" } } } },
"detail.primaryAction": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Open Related File" } }, "ja": { "stringUnit": { "state": "translated", "value": "関連ファイルを開く" } } } },
"detail.secondaryAction": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Restore Defaults" } }, "ja": { "stringUnit": { "state": "translated", "value": "デフォルトに戻す" } } } },
"detail.section": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Section" } }, "ja": { "stringUnit": { "state": "translated", "value": "セクション" } } } },
"detail.slider": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Amount" } }, "ja": { "stringUnit": { "state": "translated", "value": "量" } } } },
"detail.toggle": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Enable" } }, "ja": { "stringUnit": { "state": "translated", "value": "有効にする" } } } },
"mode.compact": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Compact" } }, "ja": { "stringUnit": { "state": "translated", "value": "コンパクト" } } } },
"mode.expanded": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Expanded" } }, "ja": { "stringUnit": { "state": "translated", "value": "拡張" } } } },
"mode.system": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "System" } }, "ja": { "stringUnit": { "state": "translated", "value": "システム" } } } },
"section.advanced.detail": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Advanced" } }, "ja": { "stringUnit": { "state": "translated", "value": "詳細" } } } },
"section.advanced.title": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Advanced" } }, "ja": { "stringUnit": { "state": "translated", "value": "詳細" } } } },
"section.automation.detail": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Automation" } }, "ja": { "stringUnit": { "state": "translated", "value": "自動化" } } } },
"section.automation.title": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Automation" } }, "ja": { "stringUnit": { "state": "translated", "value": "自動化" } } } },
"section.browser.detail": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Browser" } }, "ja": { "stringUnit": { "state": "translated", "value": "ブラウザ" } } } },
"section.browser.title": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Browser" } }, "ja": { "stringUnit": { "state": "translated", "value": "ブラウザ" } } } },
"section.general.detail": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "General" } }, "ja": { "stringUnit": { "state": "translated", "value": "一般" } } } },
"section.general.title": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "General" } }, "ja": { "stringUnit": { "state": "translated", "value": "一般" } } } },
"section.keyboard.detail": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Keyboard" } }, "ja": { "stringUnit": { "state": "translated", "value": "キーボード" } } } },
"section.keyboard.title": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Keyboard" } }, "ja": { "stringUnit": { "state": "translated", "value": "キーボード" } } } },
"section.sidebar.detail": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Sidebar" } }, "ja": { "stringUnit": { "state": "translated", "value": "サイドバー" } } } },
"section.sidebar.title": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Sidebar" } }, "ja": { "stringUnit": { "state": "translated", "value": "サイドバー" } } } },
"section.terminal.detail": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Terminal" } }, "ja": { "stringUnit": { "state": "translated", "value": "ターミナル" } } } },
"section.terminal.title": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Terminal" } }, "ja": { "stringUnit": { "state": "translated", "value": "ターミナル" } } } },
"settings.search.prompt": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Search" } }, "ja": { "stringUnit": { "state": "translated", "value": "検索" } } } },
"settings.sidebar.title": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Settings" } }, "ja": { "stringUnit": { "state": "translated", "value": "設定" } } } }
}
}
@@ -0,0 +1,72 @@
import SwiftUI
struct SettingsDetailView: View {
let section: SettingsSection
@State private var isEnabled = true
@State private var selectedMode = Mode.system
@State private var numericValue = 12.0
var body: some View {
Form {
Section {
LabeledContent(
String(localized: "detail.section", defaultValue: "Section"),
value: section.title
)
Toggle(
String(localized: "detail.toggle", defaultValue: "Enable"),
isOn: $isEnabled
)
Picker(String(localized: "detail.mode", defaultValue: "Mode"), selection: $selectedMode) {
ForEach(Mode.allCases) { mode in
Text(mode.title).tag(mode)
}
}
.pickerStyle(.segmented)
Slider(
value: $numericValue,
in: 0...24
) {
Text(String(localized: "detail.slider", defaultValue: "Amount"))
}
} header: {
Text(section.title)
}
Section {
Button(String(localized: "detail.primaryAction", defaultValue: "Open Related File")) {
}
.disabled(true)
Button(String(localized: "detail.secondaryAction", defaultValue: "Restore Defaults")) {
}
.disabled(true)
}
}
.formStyle(.grouped)
.padding()
}
}
private enum Mode: String, CaseIterable, Identifiable {
case system
case compact
case expanded
var id: Self { self }
var title: String {
switch self {
case .system:
return String(localized: "mode.system", defaultValue: "System")
case .compact:
return String(localized: "mode.compact", defaultValue: "Compact")
case .expanded:
return String(localized: "mode.expanded", defaultValue: "Expanded")
}
}
}
@@ -0,0 +1,41 @@
import SwiftUI
struct SettingsShellView: View {
@SceneStorage("selectedSettingsSection") private var selectedSectionRaw = SettingsSection.general.rawValue
@State private var columnVisibility: NavigationSplitViewVisibility = .all
@State private var searchText = ""
private var selectedSection: SettingsSection {
SettingsSection(rawValue: selectedSectionRaw) ?? .general
}
private var filteredSections: [SettingsSection] {
let query = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
guard !query.isEmpty else { return SettingsSection.allCases }
return SettingsSection.allCases.filter { section in
section.searchText.localizedStandardContains(query)
}
}
var body: some View {
NavigationSplitView(columnVisibility: $columnVisibility) {
List(selection: $selectedSectionRaw) {
ForEach(filteredSections) { section in
Label(section.title, systemImage: section.symbolName)
.tag(section.rawValue)
}
}
.listStyle(.sidebar)
.navigationTitle(String(localized: "settings.sidebar.title", defaultValue: "Settings"))
.searchable(
text: $searchText,
placement: .sidebar,
prompt: Text(String(localized: "settings.search.prompt", defaultValue: "Search"))
)
.navigationSplitViewColumnWidth(210)
} detail: {
SettingsDetailView(section: selectedSection)
}
.navigationSplitViewStyle(.balanced)
}
}
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env bash
set -euo pipefail
MODE="${1:-run}"
APP_NAME="SettingsShellLab"
BUNDLE_ID="com.cmux.prototype.SettingsShellLab"
MIN_SYSTEM_VERSION="14.0"
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DIST_DIR="$ROOT_DIR/dist"
APP_BUNDLE="$DIST_DIR/$APP_NAME.app"
APP_CONTENTS="$APP_BUNDLE/Contents"
APP_MACOS="$APP_CONTENTS/MacOS"
APP_BINARY="$APP_MACOS/$APP_NAME"
INFO_PLIST="$APP_CONTENTS/Info.plist"
cd "$ROOT_DIR"
pkill -x "$APP_NAME" >/dev/null 2>&1 || true
swift build
BUILD_BINARY="$(swift build --show-bin-path)/$APP_NAME"
rm -rf "$APP_BUNDLE"
mkdir -p "$APP_MACOS"
cp "$BUILD_BINARY" "$APP_BINARY"
chmod +x "$APP_BINARY"
cat >"$INFO_PLIST" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>$APP_NAME</string>
<key>CFBundleIdentifier</key>
<string>$BUNDLE_ID</string>
<key>CFBundleName</key>
<string>$APP_NAME</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>LSMinimumSystemVersion</key>
<string>$MIN_SYSTEM_VERSION</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>
PLIST
open_app() {
/usr/bin/open -n "$APP_BUNDLE"
}
case "$MODE" in
run)
open_app
;;
--debug|debug)
lldb -- "$APP_BINARY"
;;
--logs|logs)
open_app
/usr/bin/log stream --info --style compact --predicate "process == \"$APP_NAME\""
;;
--telemetry|telemetry)
open_app
/usr/bin/log stream --info --style compact --predicate "subsystem == \"$BUNDLE_ID\""
;;
--verify|verify)
open_app
sleep 1
pgrep -x "$APP_NAME" >/dev/null
;;
*)
echo "usage: $0 [run|--debug|--logs|--telemetry|--verify]" >&2
exit 2
;;
esac
+363
View File
@@ -2,6 +2,15 @@
"sourceLanguage": "en",
"version": "1.0",
"strings": {
"detail.mode": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Mode" } }, "ja": { "stringUnit": { "state": "translated", "value": "モード" } } } },
"detail.primaryAction": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Open Related File" } }, "ja": { "stringUnit": { "state": "translated", "value": "関連ファイルを開く" } } } },
"detail.secondaryAction": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Restore Defaults" } }, "ja": { "stringUnit": { "state": "translated", "value": "デフォルトに戻す" } } } },
"detail.section": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Section" } }, "ja": { "stringUnit": { "state": "translated", "value": "セクション" } } } },
"detail.slider": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Amount" } }, "ja": { "stringUnit": { "state": "translated", "value": "量" } } } },
"detail.toggle": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Enable" } }, "ja": { "stringUnit": { "state": "translated", "value": "有効にする" } } } },
"mode.compact": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Compact" } }, "ja": { "stringUnit": { "state": "translated", "value": "コンパクト" } } } },
"mode.expanded": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Expanded" } }, "ja": { "stringUnit": { "state": "translated", "value": "拡張" } } } },
"mode.system": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "System" } }, "ja": { "stringUnit": { "state": "translated", "value": "システム" } } } },
"about.appName": {
"extractionState": "manual",
"localizations": {
@@ -1090,6 +1099,57 @@
}
}
},
"debug.menu.aboutTitlebarDebug": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "About Titlebar Debug…"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "Aboutタイトルバーデバッグ…"
}
}
}
},
"debug.aboutTitlebarDebug.title": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "About Titlebar Debug"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "Aboutタイトルバーデバッグ"
}
}
}
},
"debug.aboutTitlebarDebug.resetAbout": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Reset About"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "Aboutをリセット"
}
}
}
},
"debug.menu.tabBarBackdropLab": {
"extractionState": "manual",
"localizations": {
@@ -49730,6 +49790,23 @@
}
}
},
"settings.app.appearance": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Appearance"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "外観"
}
}
}
},
"settings.app.appIcon.subtitle": {
"extractionState": "manual",
"localizations": {
@@ -51120,6 +51197,167 @@
}
}
},
"settings.settingsJSON.docsButton": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Open Docs"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ドキュメントを開く"
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": "문서 열기"
}
}
}
},
"settings.settingsJSON.documentation": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Documentation"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ドキュメント"
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": "문서"
}
}
}
},
"settings.settingsJSON.documentation.subtitle": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "View supported keys, file locations, schema, and reload behavior."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "サポートされているキー、ファイルの場所、スキーマ、再読み込み方法を確認します。"
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": "지원되는 키, 파일 위치, 스키마, 다시 불러오기 동작을 확인합니다."
}
}
}
},
"settings.settingsJSON.file": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "User settings file"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ユーザー設定ファイル"
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": "사용자 설정 파일"
}
}
}
},
"settings.settingsJSON.file.subtitle": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Edit cmux-owned app settings, shortcuts, automation, sidebar, notifications, and browser behavior."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "cmux 管理のアプリ設定、ショートカット、オートメーション、サイドバー、通知、ブラウザ動作を編集します。"
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": "cmux가 관리하는 앱 설정, 단축키, 자동화, 사이드바, 알림, 브라우저 동작을 편집합니다."
}
}
}
},
"settings.settingsJSON.openButton": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Open"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "開く"
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": "열기"
}
}
}
},
"settings.settingsJSON.openFile": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Open settings.json"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "settings.jsonを開く"
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": "settings.json 열기"
}
}
}
},
"settings.app.configWindow": {
"extractionState": "manual",
"localizations": {
@@ -56872,6 +57110,74 @@
}
}
},
"settings.notifications.desktop": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Desktop Notifications"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "デスクトップ通知"
}
}
}
},
"settings.notifications.command": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Notification Command"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "通知コマンド"
}
}
}
},
"settings.notifications.command.subtitle": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Run a shell command when a notification arrives. $CMUX_NOTIFICATION_TITLE, $CMUX_NOTIFICATION_SUBTITLE, $CMUX_NOTIFICATION_BODY are set."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "通知が届いたときにシェルコマンドを実行します。$CMUX_NOTIFICATION_TITLE、$CMUX_NOTIFICATION_SUBTITLE、$CMUX_NOTIFICATION_BODY が設定されます。"
}
}
}
},
"settings.notifications.command.placeholder": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "say \"done\""
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "say \"done\""
}
}
}
},
"settings.automation.claudeCode": {
"extractionState": "manual",
"localizations": {
@@ -67264,6 +67570,29 @@
}
}
},
"settings.section.settingsJSON": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "settings.json"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "settings.json"
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": "settings.json"
}
}
}
},
"settings.section.reset": {
"extractionState": "manual",
"localizations": {
@@ -68661,6 +68990,40 @@
}
}
},
"settings.search.prompt": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Search"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "検索"
}
}
}
},
"settings.search.noResults": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "No Results"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "結果なし"
}
}
}
},
"settings.workspaceColors.base": {
"extractionState": "manual",
"localizations": {
+2 -10
View File
@@ -6601,25 +6601,17 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
static func presentPreferencesWindow(
navigationTarget: SettingsNavigationTarget? = nil,
showFallbackSettingsWindow: @MainActor (SettingsNavigationTarget?) -> Void = { target in
SettingsWindowController.shared.show(navigationTarget: target)
SettingsWindowPresenter.show(navigationTarget: target)
},
activateApplication: @MainActor () -> Void = {
NSRunningApplication.current.activate(options: [.activateAllWindows, .activateIgnoringOtherApps])
}
) {
#if DEBUG
cmuxDebugLog("settings.open.present path=customWindowDirect")
cmuxDebugLog("settings.open.present path=swiftuiWindow")
#endif
showFallbackSettingsWindow(navigationTarget)
activateApplication()
if let window = SettingsWindowController.shared.window {
window.orderFrontRegardless()
window.makeKeyAndOrderFront(nil)
DispatchQueue.main.async {
window.orderFrontRegardless()
window.makeKeyAndOrderFront(nil)
}
}
#if DEBUG
cmuxDebugLog("settings.open.present activate=1")
#endif
+489
View File
@@ -0,0 +1,489 @@
import SwiftUI
enum SettingsNavigationTarget: String, CaseIterable, Identifiable {
case account
case app
case terminal
case sidebarAppearance
case automation
case browser
case browserImport
case globalHotkey
case keyboardShortcuts
case workspaceColors
case settingsJSON
case reset
var id: Self { self }
var title: String {
switch self {
case .account:
return String(localized: "settings.section.account", defaultValue: "Account")
case .app:
return String(localized: "settings.section.app", defaultValue: "App")
case .terminal:
return String(localized: "settings.section.terminal", defaultValue: "Terminal")
case .workspaceColors:
return String(localized: "settings.section.workspaceColors", defaultValue: "Workspace Colors")
case .sidebarAppearance:
return String(localized: "settings.section.sidebarAppearance", defaultValue: "Sidebar Appearance")
case .automation:
return String(localized: "settings.section.automation", defaultValue: "Automation")
case .browser:
return String(localized: "settings.section.browser", defaultValue: "Browser")
case .browserImport:
return String(localized: "settings.browser.import", defaultValue: "Import Browser Data")
case .globalHotkey:
return String(localized: "settings.section.globalHotkey", defaultValue: "Global Hotkey")
case .keyboardShortcuts:
return String(localized: "settings.section.keyboardShortcuts", defaultValue: "Keyboard Shortcuts")
case .settingsJSON:
return String(localized: "settings.section.settingsJSON", defaultValue: "settings.json")
case .reset:
return String(localized: "settings.section.reset", defaultValue: "Reset")
}
}
var symbolName: String {
switch self {
case .account:
return "person.crop.circle"
case .app:
return "gearshape"
case .terminal:
return "terminal"
case .workspaceColors:
return "paintpalette"
case .sidebarAppearance:
return "sidebar.left"
case .automation:
return "wand.and.sparkles"
case .browser:
return "globe"
case .browserImport:
return "square.and.arrow.down"
case .globalHotkey:
return "keyboard.badge.ellipsis"
case .keyboardShortcuts:
return "keyboard"
case .settingsJSON:
return "doc.text"
case .reset:
return "arrow.counterclockwise"
}
}
var searchText: String {
switch self {
case .account:
return "\(title) sign in team sync"
case .app:
return "\(title) appearance language workspace notifications menu bar telemetry"
case .terminal:
return "\(title) scrollbar"
case .workspaceColors:
return "\(title) palette tabs"
case .sidebarAppearance:
return "\(title) sidebar tint details branches badges"
case .automation:
return "\(title) socket integrations hooks ports claude cursor gemini"
case .browser:
return "\(title) search engine links history theme"
case .browserImport:
return "\(title) browser import data bookmarks history cookies"
case .globalHotkey:
return "\(title) system wide shortcut"
case .keyboardShortcuts:
return "\(title) keybindings commands chords"
case .settingsJSON:
return "\(title) config file preferences editor documentation schema jsonc reload"
case .reset:
return "\(title) defaults"
}
}
}
enum SettingsNavigationRequest {
static let notificationName = Notification.Name("cmux.settings.navigate")
private static let targetKey = "target"
private static let anchorKey = "anchor"
private static let highlightKey = "highlight"
static func post(_ target: SettingsNavigationTarget, anchorID: String? = nil, highlight: Bool = false) {
NotificationCenter.default.post(
name: notificationName,
object: nil,
userInfo: [
targetKey: target.rawValue,
anchorKey: anchorID ?? SettingsSearchIndex.sectionID(for: target),
highlightKey: highlight
]
)
}
static func target(from notification: Notification) -> SettingsNavigationTarget? {
destination(from: notification)?.target
}
static func destination(from notification: Notification) -> SettingsNavigationDestination? {
guard
let rawValue = notification.userInfo?[targetKey] as? String,
let target = SettingsNavigationTarget(rawValue: rawValue)
else {
return nil
}
let anchorID = notification.userInfo?[anchorKey] as? String
let shouldHighlight = notification.userInfo?[highlightKey] as? Bool ?? false
return SettingsNavigationDestination(
target: target,
anchorID: anchorID ?? SettingsSearchIndex.sectionID(for: target),
shouldHighlight: shouldHighlight
)
}
}
struct SettingsNavigationDestination {
let target: SettingsNavigationTarget
let anchorID: String
let shouldHighlight: Bool
}
struct SettingsSearchHighlightState: Equatable {
let anchorID: String?
let token: Int
let startedAt: Date?
}
private struct SettingsSearchHighlightStateKey: EnvironmentKey {
static let defaultValue = SettingsSearchHighlightState(anchorID: nil, token: 0, startedAt: nil)
}
extension EnvironmentValues {
var settingsSearchHighlightState: SettingsSearchHighlightState {
get { self[SettingsSearchHighlightStateKey.self] }
set { self[SettingsSearchHighlightStateKey.self] = newValue }
}
}
extension View {
@ViewBuilder
func settingsSearchAnchor(_ anchorID: String?) -> some View {
if let anchorID {
settingsSearchAnchors([anchorID])
} else {
self
}
}
@ViewBuilder
func settingsSearchAnchors(_ anchorIDs: [String]) -> some View {
let filteredAnchorIDs = anchorIDs.filter { !$0.isEmpty }
if let primaryAnchorID = filteredAnchorIDs.first {
self
.id(primaryAnchorID)
.modifier(SettingsSearchHighlightModifier(anchorIDs: filteredAnchorIDs))
} else {
self
}
}
}
private struct SettingsSearchHighlightModifier: ViewModifier {
@Environment(\.settingsSearchHighlightState) private var highlightState
let anchorIDs: [String]
private func matches(_ state: SettingsSearchHighlightState) -> Bool {
guard let anchorID = state.anchorID else { return false }
return anchorIDs.contains(anchorID)
}
func body(content: Content) -> some View {
content
.background {
if matches(highlightState) {
TimelineView(.animation) { context in
let opacity = highlightOpacity(at: context.date, for: highlightState)
RoundedRectangle(cornerRadius: 8, style: .continuous)
.fill(Color.accentColor.opacity(opacity * 0.24))
.overlay(
RoundedRectangle(cornerRadius: 8, style: .continuous)
.stroke(Color.accentColor.opacity(opacity), lineWidth: 2.5)
)
.shadow(color: Color.accentColor.opacity(opacity * 0.24), radius: 8, x: 0, y: 0)
}
}
}
}
private func highlightOpacity(at date: Date, for state: SettingsSearchHighlightState) -> Double {
guard matches(state), let startedAt = state.startedAt else { return 0 }
let elapsed = date.timeIntervalSince(startedAt)
if elapsed < 0.14 {
return max(0, min(1, elapsed / 0.14))
}
if elapsed < 5 {
return 1
}
if elapsed < 5.9 {
return max(0, 1 - ((elapsed - 5) / 0.9))
}
return 0
}
}
enum SettingsSearchEntryKind {
case section
case setting
}
struct SettingsSearchEntry: Identifiable {
let id: String
let kind: SettingsSearchEntryKind
let target: SettingsNavigationTarget
let title: String
let subtitle: String?
let symbolName: String
let normalizedSearchText: String
init(
id: String,
kind: SettingsSearchEntryKind,
target: SettingsNavigationTarget,
title: String,
subtitle: String?,
symbolName: String,
searchText: String
) {
self.id = id
self.kind = kind
self.target = target
self.title = title
self.subtitle = subtitle
self.symbolName = symbolName
normalizedSearchText = SettingsSearchIndex.normalized("\(title) \(subtitle ?? "") \(searchText)")
}
}
enum SettingsSearchIndex {
static let defaultSelectionID = sectionID(for: .account)
private static let sectionEntries: [SettingsSearchEntry] = SettingsNavigationTarget.allCases.map { target in
SettingsSearchEntry(
id: sectionID(for: target),
kind: .section,
target: target,
title: target.title,
subtitle: nil,
symbolName: target.symbolName,
searchText: target.searchText
)
}
private static let settingEntries: [SettingsSearchEntry] = [
setting(.account, "account", String(localized: "settings.section.account", defaultValue: "Account"), "sign in login team sync user profile"),
setting(.app, "language", String(localized: "settings.app.language", defaultValue: "Language"), "locale translation japanese english restart"),
setting(.app, "appearance", String(localized: "settings.app.appearance", defaultValue: "Appearance"), "theme light dark system"),
setting(.app, "app-icon", String(localized: "settings.app.appIcon", defaultValue: "App Icon"), "dock icon alternate"),
setting(.app, "new-workspace-placement", String(localized: "settings.app.newWorkspacePlacement", defaultValue: "New Workspace Placement"), "workspace order position"),
setting(.app, "minimal-mode", String(localized: "settings.app.minimalMode", defaultValue: "Minimal Mode"), "presentation compact chrome"),
setting(.app, "keep-workspace-open", String(localized: "settings.app.closeWorkspaceOnLastSurfaceShortcut", defaultValue: "Keep Workspace Open When Closing Last Surface"), "close last surface shortcut cmd w"),
setting(.app, "focus-pane-first-click", String(localized: "settings.app.paneFirstClickFocus", defaultValue: "Focus Pane on First Click"), "mouse click focus"),
setting(.app, "preferred-editor", String(localized: "settings.app.preferredEditor", defaultValue: "Open Files With"), "editor code zed subl cmd click file"),
setting(.app, "terminal-config", String(localized: "settings.app.configWindow", defaultValue: "Terminal Config"), "ghostty config merged preview"),
setting(.app, "markdown-viewer", String(localized: "settings.app.openMarkdownInCmuxViewer", defaultValue: "Open Markdown in cmux Viewer"), "md markdown viewer"),
setting(.app, "reorder-notification", String(localized: "settings.app.reorderOnNotification", defaultValue: "Reorder on Notification"), "workspace notification order"),
setting(.app, "dock-badge", String(localized: "settings.app.dockBadge", defaultValue: "Dock Badge"), "unread count app icon"),
setting(.app, "menu-bar-only", String(localized: "settings.app.menuBarOnly", defaultValue: "Menu Bar Only"), "dock icon cmd tab"),
setting(.app, "show-menu-bar", String(localized: "settings.app.showInMenuBar", defaultValue: "Show in Menu Bar"), "menu extra status item"),
setting(.app, "unread-pane-ring", String(localized: "settings.notifications.paneRing.title", defaultValue: "Unread Pane Ring"), "notification blue ring pane"),
setting(.app, "pane-flash", String(localized: "settings.notifications.paneFlash.title", defaultValue: "Pane Flash"), "notification flash highlight"),
setting(.app, "desktop-notifications", String(localized: "settings.notifications.desktop", defaultValue: "Desktop Notifications"), "permission alerts test notification"),
setting(.app, "notification-sound", String(localized: "settings.notifications.sound.title", defaultValue: "Notification Sound"), "custom sound alert audio"),
setting(.app, "notification-command", String(localized: "settings.notifications.command", defaultValue: "Notification Command"), "shell command environment variables"),
setting(.app, "telemetry", String(localized: "settings.app.telemetry", defaultValue: "Send anonymous telemetry"), "analytics crash usage"),
setting(.app, "warn-before-quit", String(localized: "settings.app.warnBeforeQuit", defaultValue: "Warn Before Quit"), "cmd q confirmation"),
setting(.app, "rename-selects-name", String(localized: "settings.app.renameSelectsName", defaultValue: "Rename Selects Existing Name"), "command palette rename text selection"),
setting(.app, "palette-search-all", String(localized: "settings.app.commandPaletteSearchAllSurfaces", defaultValue: "Command Palette Searches All Surfaces"), "cmd p search terminal browser markdown"),
setting(.app, "hide-sidebar-details", String(localized: "settings.app.hideAllSidebarDetails", defaultValue: "Hide All Sidebar Details"), "workspace sidebar compact"),
setting(.app, "sidebar-branch-layout", String(localized: "settings.app.sidebarBranchLayout", defaultValue: "Sidebar Branch Layout"), "branch directory vertical inline"),
setting(.app, "show-notification-message", String(localized: "settings.app.showNotificationMessage", defaultValue: "Show Notification Message in Sidebar"), "workspace latest notification"),
setting(.app, "show-branch-directory", String(localized: "settings.app.showBranchDirectory", defaultValue: "Show Branch + Directory in Sidebar"), "git cwd path"),
setting(.app, "show-pull-requests", String(localized: "settings.app.showPullRequests", defaultValue: "Show Pull Requests in Sidebar"), "review pr mr link"),
setting(.app, "open-pr-links", String(localized: "settings.app.openSidebarPRLinks", defaultValue: "Open Sidebar PR Links in cmux Browser"), "pull request link browser"),
setting(.app, "open-port-links", String(localized: "settings.app.openSidebarPortLinks", defaultValue: "Open Sidebar Port Links in cmux Browser"), "port link browser"),
setting(.app, "show-ssh", String(localized: "settings.app.showSSH", defaultValue: "Show SSH in Sidebar"), "remote target"),
setting(.app, "show-ports", String(localized: "settings.app.showPorts", defaultValue: "Show Listening Ports in Sidebar"), "localhost port"),
setting(.app, "show-log", String(localized: "settings.app.showLog", defaultValue: "Show Latest Log in Sidebar"), "status message"),
setting(.app, "show-progress", String(localized: "settings.app.showProgress", defaultValue: "Show Progress in Sidebar"), "progress bar"),
setting(.app, "show-metadata", String(localized: "settings.app.showMetadata", defaultValue: "Show Custom Metadata in Sidebar"), "report meta status block"),
setting(.terminal, "scrollbar", String(localized: "settings.terminal.scrollBar", defaultValue: "Show Terminal Scroll Bar"), "terminal shell scrollback"),
setting(.sidebarAppearance, "match-terminal", String(localized: "settings.sidebarAppearance.matchTerminalBackground", defaultValue: "Match Terminal Background"), "sidebar material transparency"),
setting(.sidebarAppearance, "light-tint", String(localized: "settings.sidebarAppearance.tintColorLight", defaultValue: "Light Mode Tint"), "sidebar color light"),
setting(.sidebarAppearance, "dark-tint", String(localized: "settings.sidebarAppearance.tintColorDark", defaultValue: "Dark Mode Tint"), "sidebar color dark"),
setting(.sidebarAppearance, "tint-opacity", String(localized: "settings.sidebarAppearance.tintOpacity", defaultValue: "Tint Opacity"), "sidebar color opacity"),
setting(.sidebarAppearance, "reset-tint", String(localized: "settings.sidebarAppearance.reset", defaultValue: "Reset Sidebar Tint"), "restore default sidebar appearance"),
setting(.automation, "socket-mode", String(localized: "settings.automation.socketMode", defaultValue: "Socket Control Mode"), "unix socket api access password auth"),
setting(.automation, "socket-password", String(localized: "settings.automation.socketPassword", defaultValue: "Socket Password"), "socket auth credential"),
setting(.automation, "claude-code", String(localized: "settings.automation.claudeCode", defaultValue: "Claude Code Integration"), "agent hooks notifications"),
setting(.automation, "claude-path", String(localized: "settings.automation.claudeCode.customPath", defaultValue: "Claude Binary Path"), "custom claude executable"),
setting(.automation, "cursor", String(localized: "settings.automation.cursor", defaultValue: "Cursor Integration"), "agent hooks notifications"),
setting(.automation, "gemini", String(localized: "settings.automation.gemini", defaultValue: "Gemini CLI Integration"), "agent hooks notifications"),
setting(.automation, "port-base", String(localized: "settings.automation.portBase", defaultValue: "Port Base"), "CMUX_PORT start"),
setting(.automation, "port-range", String(localized: "settings.automation.portRange", defaultValue: "Port Range Size"), "CMUX_PORT_END workspace ports"),
setting(.browser, "search-engine", String(localized: "settings.browser.searchEngine", defaultValue: "Default Search Engine"), "address bar query google duckduckgo"),
setting(.browser, "search-suggestions", String(localized: "settings.browser.searchSuggestions", defaultValue: "Show Search Suggestions"), "browser address bar suggestions"),
setting(.browser, "theme", String(localized: "settings.browser.theme", defaultValue: "Browser Theme"), "web appearance light dark system"),
setting(.browser, "terminal-links", String(localized: "settings.browser.openTerminalLinks", defaultValue: "Open Terminal Links in cmux Browser"), "click links browser"),
setting(.browser, "intercept-open", String(localized: "settings.browser.interceptOpen", defaultValue: "Intercept open http(s) in Terminal"), "open command urls"),
setting(.browser, "host-whitelist", String(localized: "settings.browser.hostWhitelist", defaultValue: "Hosts to Open in Embedded Browser"), "hosts wildcard terminal links"),
setting(.browser, "external-patterns", String(localized: "settings.browser.externalPatterns", defaultValue: "URLs to Always Open Externally"), "regex url rules default browser"),
setting(.browser, "http-allowlist", String(localized: "settings.browser.httpAllowlist", defaultValue: "HTTP Hosts Allowed in Embedded Browser"), "localhost non https warning"),
setting(.browserImport, "import-data", String(localized: "settings.browser.import", defaultValue: "Import Browser Data"), "bookmarks history cookies profiles"),
setting(.browserImport, "import-hint", String(localized: "settings.browser.import.hint.show", defaultValue: "Show import hint on blank browser tabs"), "blank tab browser import"),
setting(.browser, "react-grab", String(localized: "settings.browser.reactGrabVersion", defaultValue: "React Grab Version"), "npm react grab toolbar"),
setting(.browser, "history", String(localized: "settings.browser.history", defaultValue: "Browsing History"), "clear visited suggestions"),
setting(.globalHotkey, "enable-hotkey", String(localized: "settings.globalHotkey.enable", defaultValue: "Enable System-Wide Hotkey"), "global shortcut show hide windows"),
setting(.globalHotkey, "shortcut", String(localized: "settings.section.globalHotkey", defaultValue: "Global Hotkey"), "keyboard recorder command option control"),
setting(.keyboardShortcuts, "shortcut-chords", String(localized: "settings.shortcuts.chords", defaultValue: "Shortcut Chords"), "tmux multi step keybindings"),
setting(.keyboardShortcuts, "show-hints", String(localized: "settings.shortcuts.showHints", defaultValue: "Show Cmd/Ctrl-Hold Shortcut Hints"), "modifier hold hints keyboard"),
setting(.keyboardShortcuts, "shortcuts", String(localized: "settings.section.keyboardShortcuts", defaultValue: "Keyboard Shortcuts"), "keybindings commands"),
setting(.workspaceColors, "indicator", String(localized: "settings.workspaceColors.indicator", defaultValue: "Workspace Color Indicator"), "tab color indicator"),
setting(.workspaceColors, "selection", String(localized: "settings.workspaceColors.selectionColor", defaultValue: "Selection Highlight"), "selected workspace background"),
setting(.workspaceColors, "badge", String(localized: "settings.workspaceColors.notificationBadgeColor", defaultValue: "Notification Badge"), "unread notification color"),
setting(.workspaceColors, "palette", String(localized: "settings.workspaceColors.resetPalette", defaultValue: "Reset Palette"), "named colors palette"),
setting(.settingsJSON, "open-file", String(localized: "settings.settingsJSON.openFile", defaultValue: "Open settings.json"), "config json file editor dotfiles"),
setting(.settingsJSON, "documentation", String(localized: "settings.settingsJSON.documentation", defaultValue: "Documentation"), "settings json schema reference docs"),
setting(.reset, "reset-all", String(localized: "settings.reset.resetAll", defaultValue: "Reset All Settings"), "restore defaults")
]
private static let allEntries = sectionEntries + settingEntries
private static let entriesByID: [String: SettingsSearchEntry] = Dictionary(
uniqueKeysWithValues: allEntries.map { ($0.id, $0) }
)
private static let settingsPathAnchorIDs: [String: String] = [
"app.language": settingID(for: .app, idSuffix: "language"),
"app.appearance": settingID(for: .app, idSuffix: "appearance"),
"app.appIcon": settingID(for: .app, idSuffix: "app-icon"),
"app.newWorkspacePlacement": settingID(for: .app, idSuffix: "new-workspace-placement"),
"app.minimalMode": settingID(for: .app, idSuffix: "minimal-mode"),
"app.keepWorkspaceOpenWhenClosingLastSurface": settingID(for: .app, idSuffix: "keep-workspace-open"),
"app.focusPaneOnFirstClick": settingID(for: .app, idSuffix: "focus-pane-first-click"),
"app.preferredEditor": settingID(for: .app, idSuffix: "preferred-editor"),
"app.openMarkdownInCmuxViewer": settingID(for: .app, idSuffix: "markdown-viewer"),
"app.reorderOnNotification": settingID(for: .app, idSuffix: "reorder-notification"),
"notifications.dockBadge": settingID(for: .app, idSuffix: "dock-badge"),
"app.menuBarOnly": settingID(for: .app, idSuffix: "menu-bar-only"),
"notifications.showInMenuBar": settingID(for: .app, idSuffix: "show-menu-bar"),
"notifications.unreadPaneRing": settingID(for: .app, idSuffix: "unread-pane-ring"),
"notifications.paneFlash": settingID(for: .app, idSuffix: "pane-flash"),
"notifications.sound": settingID(for: .app, idSuffix: "notification-sound"),
"notifications.customSoundFilePath": settingID(for: .app, idSuffix: "notification-sound"),
"notifications.command": settingID(for: .app, idSuffix: "notification-command"),
"app.sendAnonymousTelemetry": settingID(for: .app, idSuffix: "telemetry"),
"app.warnBeforeQuit": settingID(for: .app, idSuffix: "warn-before-quit"),
"app.renameSelectsExistingName": settingID(for: .app, idSuffix: "rename-selects-name"),
"app.commandPaletteSearchesAllSurfaces": settingID(for: .app, idSuffix: "palette-search-all"),
"sidebar.hideAllDetails": settingID(for: .app, idSuffix: "hide-sidebar-details"),
"sidebar.branchLayout": settingID(for: .app, idSuffix: "sidebar-branch-layout"),
"sidebar.showNotificationMessage": settingID(for: .app, idSuffix: "show-notification-message"),
"sidebar.showBranchDirectory": settingID(for: .app, idSuffix: "show-branch-directory"),
"sidebar.showPullRequests": settingID(for: .app, idSuffix: "show-pull-requests"),
"sidebar.openPullRequestLinksInCmuxBrowser": settingID(for: .app, idSuffix: "open-pr-links"),
"sidebar.openPortLinksInCmuxBrowser": settingID(for: .app, idSuffix: "open-port-links"),
"sidebar.showSSH": settingID(for: .app, idSuffix: "show-ssh"),
"sidebar.showPorts": settingID(for: .app, idSuffix: "show-ports"),
"sidebar.showLog": settingID(for: .app, idSuffix: "show-log"),
"sidebar.showProgress": settingID(for: .app, idSuffix: "show-progress"),
"sidebar.showCustomMetadata": settingID(for: .app, idSuffix: "show-metadata"),
"terminal.showScrollBar": settingID(for: .terminal, idSuffix: "scrollbar"),
"workspaceColors.indicatorStyle": settingID(for: .workspaceColors, idSuffix: "indicator"),
"workspaceColors.selectionColor": settingID(for: .workspaceColors, idSuffix: "selection"),
"workspaceColors.notificationBadgeColor": settingID(for: .workspaceColors, idSuffix: "badge"),
"sidebarAppearance.matchTerminalBackground": settingID(for: .sidebarAppearance, idSuffix: "match-terminal"),
"sidebarAppearance.lightModeTintColor": settingID(for: .sidebarAppearance, idSuffix: "light-tint"),
"sidebarAppearance.darkModeTintColor": settingID(for: .sidebarAppearance, idSuffix: "dark-tint"),
"sidebarAppearance.tintOpacity": settingID(for: .sidebarAppearance, idSuffix: "tint-opacity"),
"automation.socketControlMode": settingID(for: .automation, idSuffix: "socket-mode"),
"automation.socketPassword": settingID(for: .automation, idSuffix: "socket-password"),
"automation.claudeCodeIntegration": settingID(for: .automation, idSuffix: "claude-code"),
"automation.claudeBinaryPath": settingID(for: .automation, idSuffix: "claude-path"),
"automation.cursorIntegration": settingID(for: .automation, idSuffix: "cursor"),
"automation.geminiIntegration": settingID(for: .automation, idSuffix: "gemini"),
"automation.portBase": settingID(for: .automation, idSuffix: "port-base"),
"automation.portRange": settingID(for: .automation, idSuffix: "port-range"),
"browser.defaultSearchEngine": settingID(for: .browser, idSuffix: "search-engine"),
"browser.showSearchSuggestions": settingID(for: .browser, idSuffix: "search-suggestions"),
"browser.theme": settingID(for: .browser, idSuffix: "theme"),
"browser.openTerminalLinksInCmuxBrowser": settingID(for: .browser, idSuffix: "terminal-links"),
"browser.interceptTerminalOpenCommandInCmuxBrowser": settingID(for: .browser, idSuffix: "intercept-open"),
"browser.hostsToOpenInEmbeddedBrowser": settingID(for: .browser, idSuffix: "host-whitelist"),
"browser.urlsToAlwaysOpenExternally": settingID(for: .browser, idSuffix: "external-patterns"),
"browser.insecureHttpHostsAllowedInEmbeddedBrowser": settingID(for: .browser, idSuffix: "http-allowlist"),
"browser.showImportHintOnBlankTabs": settingID(for: .browserImport, idSuffix: "import-hint"),
"browser.reactGrabVersion": settingID(for: .browser, idSuffix: "react-grab"),
"shortcuts.showModifierHoldHints": settingID(for: .keyboardShortcuts, idSuffix: "show-hints")
]
static func entries(matching query: String) -> [SettingsSearchEntry] {
let tokens = normalizedTokens(for: query)
guard !tokens.isEmpty else { return sectionEntries }
return allEntries.filter { entry in
tokens.allSatisfy { token in entry.normalizedSearchText.contains(token) }
}
}
static func entry(withID id: String) -> SettingsSearchEntry? {
entriesByID[id]
}
static func sectionEntry(for target: SettingsNavigationTarget) -> SettingsSearchEntry {
entriesByID[sectionID(for: target)] ?? sectionEntries[0]
}
static func sectionID(for target: SettingsNavigationTarget) -> String {
"section:\(target.rawValue)"
}
static func settingID(for target: SettingsNavigationTarget, idSuffix: String) -> String {
"setting:\(target.rawValue):\(idSuffix)"
}
static func anchorID(forSettingsPath path: String) -> String? {
settingsPathAnchorIDs[path]
}
static func normalized(_ text: String) -> String {
text.folding(options: [.caseInsensitive, .diacriticInsensitive], locale: .current)
}
private static func setting(
_ target: SettingsNavigationTarget,
_ idSuffix: String,
_ title: String,
_ searchText: String
) -> SettingsSearchEntry {
SettingsSearchEntry(
id: settingID(for: target, idSuffix: idSuffix),
kind: .setting,
target: target,
title: title,
subtitle: target.title,
symbolName: target.symbolName,
searchText: "\(target.searchText) \(searchText)"
)
}
private static func normalizedTokens(for query: String) -> [String] {
normalized(query)
.split { character in
character.unicodeScalars.allSatisfy { scalar in
CharacterSet.whitespacesAndNewlines.contains(scalar)
|| CharacterSet.punctuationCharacters.contains(scalar)
}
}
.map(String.init)
}
}
+7 -7
View File
@@ -7366,20 +7366,20 @@ class TerminalController {
let shouldActivate = v2FocusAllowed(requested: v2Bool(params, "activate") ?? true)
let navigationTarget: SettingsNavigationTarget?
switch targetRaw {
case nil:
if let targetRaw {
guard let target = SettingsNavigationTarget(rawValue: targetRaw) else {
return .err(code: "invalid_params", message: "Unknown settings target", data: ["target": targetRaw])
}
navigationTarget = target
} else {
navigationTarget = nil
case SettingsNavigationTarget.keyboardShortcuts.rawValue:
navigationTarget = .keyboardShortcuts
default:
return .err(code: "invalid_params", message: "Unknown settings target", data: ["target": targetRaw ?? ""])
}
DispatchQueue.main.async {
if shouldActivate {
AppDelegate.presentPreferencesWindow(navigationTarget: navigationTarget)
} else {
SettingsWindowController.shared.show(navigationTarget: navigationTarget)
SettingsWindowPresenter.show(navigationTarget: navigationTarget)
}
}
return .ok([
+1 -3
View File
@@ -69,9 +69,7 @@ final class WindowDecorationsController {
}
private func trafficLightOffset(for window: NSWindow) -> NSPoint {
guard window.identifier?.rawValue == "cmux.settings" else { return .zero }
// Nudge controls slightly right/down to align with the custom Settings title row.
return NSPoint(x: 7, y: -4)
return .zero
}
private func shouldHideTrafficLights(for window: NSWindow) -> Bool {
+519 -571
View File
File diff suppressed because it is too large Load Diff
@@ -357,7 +357,6 @@ final class AppDelegateShortcutRoutingTests: XCTestCase {
let windowId = appDelegate.createMainWindow()
defer {
SettingsWindowController.shared.close()
closeWindow(withId: windowId)
}
+5 -4
View File
@@ -222,9 +222,6 @@ export default function ConfigurationPage() {
schemaVersion: schemaProperties.schemaVersion,
} satisfies Record<string, SchemaProperty | undefined>;
const shortcutsSection = schemaProperties.shortcuts;
const shortcutProperties = shortcutsSection?.properties ?? {};
return (
<>
<h1>{t("title")}</h1>
@@ -252,12 +249,16 @@ scrollback-limit = 50000000
split-divider-color = #3e4451
working-directory = ~/code`}</CodeBlock>
<h2>cmux settings.json</h2>
<h2 id="settings-json" className="scroll-mt-24">cmux settings.json</h2>
<p>
cmux keeps app-owned settings in a separate user file instead of mixing them into Ghostty
config. On launch, if neither settings location exists, cmux writes a commented template to{" "}
<code>~/.config/cmux/settings.json</code>.
</p>
<p>
Open cmux Settings, then use the <code>settings.json</code> section to open the active file
in your preferred text editor.
</p>
<ol>
<li>
<code>~/.config/cmux/settings.json</code>