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:
co-authored by
Lawrence Chen
parent
af36cabe4e
commit
1ccde06843
@@ -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"
|
||||
@@ -0,0 +1,2 @@
|
||||
.build/
|
||||
dist/
|
||||
@@ -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
@@ -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
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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([
|
||||
|
||||
@@ -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
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user