Add copy-on-select preference (#2282)

This commit is contained in:
Austin Wang 2026-03-29 18:16:05 -07:00 committed by GitHub
parent 94cc865e83
commit 35cb42fbc8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 178 additions and 14 deletions

View file

@ -47815,6 +47815,57 @@
}
}
},
"settings.app.copyOnSelect": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Copy on Select"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "選択時にコピー"
}
}
}
},
"settings.app.copyOnSelect.subtitleOff": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Selecting terminal text does not copy it to the system clipboard."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ターミナルテキストを選択してもシステムのクリップボードにはコピーしません。"
}
}
}
},
"settings.app.copyOnSelect.subtitleOn": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Automatically copy selected terminal text to the system clipboard."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "選択したターミナルテキストをシステムのクリップボードに自動でコピーします。"
}
}
}
},
"settings.app.paneFirstClickFocus": {
"extractionState": "manual",
"localizations": {

View file

@ -1349,11 +1349,45 @@ class GhosttyApp {
#endif
}
private func loadInlineGhosttyConfig(
_ contents: String,
into config: ghostty_config_t,
prefix: String,
logLabel: String
) {
let trimmed = contents.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return }
let tmpURL = FileManager.default.temporaryDirectory
.appendingPathComponent("\(prefix)-\(UUID().uuidString).conf")
do {
try trimmed.write(to: tmpURL, atomically: true, encoding: .utf8)
defer { try? FileManager.default.removeItem(at: tmpURL) }
tmpURL.path.withCString { path in
ghostty_config_load_file(config, path)
}
} catch {
#if DEBUG
dlog("ghostty.config.inlineLoad.failed label=\(logLabel) error=\(error.localizedDescription)")
#endif
}
}
private func loadCopyOnSelectOverride(_ config: ghostty_config_t) {
loadInlineGhosttyConfig(
TerminalCopyOnSelectSettings.overrideConfigLine(),
into: config,
prefix: "cmux-copy-on-select",
logLabel: "copy-on-select override"
)
}
private func loadDefaultConfigFilesWithLegacyFallback(_ config: ghostty_config_t) {
ghostty_config_load_default_files(config)
loadLegacyGhosttyConfigIfNeeded(config)
ghostty_config_load_recursive_files(config)
loadCmuxAppSupportGhosttyConfigIfNeeded(config)
loadCopyOnSelectOverride(config)
loadCJKFontFallbackIfNeeded(config)
ghostty_config_finalize(config)
}
@ -1376,20 +1410,12 @@ class GhosttyApp {
let lines = mappings.map { range, font in
"font-codepoint-map = \(range)=\(font)"
}.joined(separator: "\n")
let tmpURL = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-cjk-font-fallback-\(UUID().uuidString).conf")
do {
try lines.write(to: tmpURL, atomically: true, encoding: .utf8)
defer { try? FileManager.default.removeItem(at: tmpURL) }
tmpURL.path.withCString { path in
ghostty_config_load_file(config, path)
}
} catch {
#if DEBUG
Self.initLog("failed to write CJK font fallback config: \(error)")
#endif
}
loadInlineGhosttyConfig(
lines,
into: config,
prefix: "cmux-cjk-font-fallback",
logLabel: "CJK font fallback"
)
}
/// Unicode ranges shared by all CJK languages (Han ideographs, symbols, fullwidth forms).

View file

@ -3785,6 +3785,22 @@ enum CommandPaletteRenameSelectionSettings {
}
}
enum TerminalCopyOnSelectSettings {
static let enabledKey = "terminalCopyOnSelectEnabled"
static let defaultEnabled = false
static func isEnabled(defaults: UserDefaults = .standard) -> Bool {
if defaults.object(forKey: enabledKey) == nil {
return defaultEnabled
}
return defaults.bool(forKey: enabledKey)
}
static func overrideConfigLine(defaults: UserDefaults = .standard) -> String {
isEnabled(defaults: defaults) ? "copy-on-select = clipboard" : "copy-on-select = false"
}
}
enum CommandPaletteSwitcherSearchSettings {
static let searchAllSurfacesKey = "commandPalette.switcherSearchAllSurfaces"
static let defaultSearchAllSurfaces = false
@ -3869,6 +3885,8 @@ struct SettingsView: View {
@AppStorage(QuitWarningSettings.warnBeforeQuitKey) private var warnBeforeQuitShortcut = QuitWarningSettings.defaultWarnBeforeQuit
@AppStorage(CommandPaletteRenameSelectionSettings.selectAllOnFocusKey)
private var commandPaletteRenameSelectAllOnFocus = CommandPaletteRenameSelectionSettings.defaultSelectAllOnFocus
@AppStorage(TerminalCopyOnSelectSettings.enabledKey)
private var terminalCopyOnSelectEnabled = TerminalCopyOnSelectSettings.defaultEnabled
@AppStorage(CommandPaletteSwitcherSearchSettings.searchAllSurfacesKey)
private var commandPaletteSearchAllSurfaces = CommandPaletteSwitcherSearchSettings.defaultSearchAllSurfaces
@AppStorage(ShortcutHintDebugSettings.alwaysShowHintsKey)
@ -3990,6 +4008,19 @@ struct SettingsView: View {
)
}
private var terminalCopyOnSelectSubtitle: String {
if terminalCopyOnSelectEnabled {
return String(
localized: "settings.app.copyOnSelect.subtitleOn",
defaultValue: "Automatically copy selected terminal text to the system clipboard."
)
}
return String(
localized: "settings.app.copyOnSelect.subtitleOff",
defaultValue: "Selecting terminal text does not copy it to the system clipboard."
)
}
private var selectedSidebarActiveTabIndicatorStyle: SidebarActiveTabIndicatorStyle {
SidebarActiveTabIndicatorSettings.resolvedStyle(rawValue: sidebarActiveTabIndicatorStyle)
}
@ -4511,6 +4542,20 @@ struct SettingsView: View {
SettingsCardDivider()
SettingsCardRow(
String(localized: "settings.app.copyOnSelect", defaultValue: "Copy on Select"),
subtitle: terminalCopyOnSelectSubtitle
) {
Toggle("", isOn: $terminalCopyOnSelectEnabled)
.labelsHidden()
.controlSize(.small)
.accessibilityLabel(
String(localized: "settings.app.copyOnSelect", defaultValue: "Copy on Select")
)
}
SettingsCardDivider()
SettingsCardRow(
String(localized: "settings.app.reorderOnNotification", defaultValue: "Reorder on Notification"),
subtitle: String(localized: "settings.app.reorderOnNotification.subtitle", defaultValue: "Move workspaces to the top when they receive a notification. Disable for stable shortcut positions.")
@ -5654,6 +5699,9 @@ struct SettingsView: View {
.onChange(of: notificationSoundCustomFilePath) { _, _ in
refreshNotificationCustomSoundStatus()
}
.onChange(of: terminalCopyOnSelectEnabled) { _, _ in
GhosttyApp.shared.reloadConfiguration(source: "settings.copy_on_select")
}
.onChange(of: browserInsecureHTTPAllowlist) { oldValue, newValue in
// Keep draft in sync with external changes unless the user has local unsaved edits.
if browserInsecureHTTPAllowlistDraft == oldValue {
@ -5777,6 +5825,7 @@ struct SettingsView: View {
showMenuBarExtra = MenuBarExtraSettings.defaultShowInMenuBar
warnBeforeQuitShortcut = QuitWarningSettings.defaultWarnBeforeQuit
commandPaletteRenameSelectAllOnFocus = CommandPaletteRenameSelectionSettings.defaultSelectAllOnFocus
terminalCopyOnSelectEnabled = TerminalCopyOnSelectSettings.defaultEnabled
commandPaletteSearchAllSurfaces = CommandPaletteSwitcherSearchSettings.defaultSearchAllSurfaces
ShortcutHintDebugSettings.resetVisibilityDefaults()
alwaysShowShortcutHints = ShortcutHintDebugSettings.defaultAlwaysShowHints

View file

@ -584,6 +584,44 @@ final class GhosttyConfigTests: XCTestCase {
XCTAssertFalse(TelemetrySettings.isEnabled(defaults: defaults))
}
func testTerminalCopyOnSelectDefaultsToDisabledWhenUnset() {
let suiteName = "cmux.tests.copy-on-select.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated user defaults suite")
return
}
defer {
defaults.removePersistentDomain(forName: suiteName)
}
defaults.removeObject(forKey: TerminalCopyOnSelectSettings.enabledKey)
XCTAssertFalse(TerminalCopyOnSelectSettings.isEnabled(defaults: defaults))
XCTAssertEqual(
TerminalCopyOnSelectSettings.overrideConfigLine(defaults: defaults),
"copy-on-select = false"
)
}
func testTerminalCopyOnSelectUsesClipboardOverrideWhenEnabled() {
let suiteName = "cmux.tests.copy-on-select.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated user defaults suite")
return
}
defer {
defaults.removePersistentDomain(forName: suiteName)
}
defaults.set(true, forKey: TerminalCopyOnSelectSettings.enabledKey)
XCTAssertTrue(TerminalCopyOnSelectSettings.isEnabled(defaults: defaults))
XCTAssertEqual(
TerminalCopyOnSelectSettings.overrideConfigLine(defaults: defaults),
"copy-on-select = clipboard"
)
}
private func rgb255(_ color: NSColor) -> RGB {
let srgb = color.usingColorSpace(.sRGB)!
var red: CGFloat = 0