Skip to content

Add copy-on-select setting - #1326

Closed
lawrencecchen wants to merge 1 commit into
mainfrom
feat-copy-on-select-option
Closed

lawrencecchen wants to merge 1 commit into
mainfrom
feat-copy-on-select-option

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Mar 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • add a Preferences picker for Ghostty's upstream copy-on-select modes, with an Inherit Ghostty Config default so cmux preserves current Ghostty behavior unless the user overrides it
  • load the selected override after Ghostty config files so the cmux setting maps directly to upstream true, clipboard, and false values
  • add focused unit coverage for the mode-to-config mapping

Testing

  • jq empty Resources/Localizable.xcstrings
  • xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux-unit -destination 'platform=macOS' -derivedDataPath /tmp/cmux-feat-copy-on-select-option-unit -only-testing:cmuxTests/TerminalCopyOnSelectSettingsTests -only-testing:cmuxTests/GhosttyConfigTests test
  • ./scripts/reload.sh --tag feat-copy-on-select-option

Issues

  • Related task: Feature request: add copy-on-select option so double-click/selection automatically copies to clipboard, and compare the implementation with upstream Ghostty support

Summary by cubic

Add a Copy on Select preference that mirrors upstream Ghostty modes and preserves current behavior by default. The setting overrides Ghostty config only when changed and applies immediately.

  • New Features

    • Preferences picker for copy-on-select with modes: Inherit Ghostty Config, Selection Clipboard, Clipboard, Off.
    • Maps to Ghostty copy-on-select values: selection → true, clipboard → clipboard, off → false; inherit leaves config unchanged.
    • Applies override after loading Ghostty config and reloads on change for immediate effect.
    • Adds localized labels and descriptions (en, ja).
  • Refactors

    • Added a reusable helper to load inline Ghostty config snippets; reused for CJK font fallback loading.

Written for commit 618b1f6. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Added a "Copy on Select" setting in preferences with configurable modes: inherit, selection, clipboard, or disabled to control text copy behavior.
  • Localization

    • Added comprehensive multilingual support for the new "Copy on Select" feature across all supported languages.

@vercel

vercel Bot commented Mar 13, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Mar 13, 2026 9:41am

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai

coderabbitai Bot commented Mar 13, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR implements a copy-on-select feature across the application by adding localization strings, introducing a TerminalCopyOnSelectSettings configuration subsystem with multiple modes, integrating settings UI controls with persistent storage, refactoring config injection to use inline loading, and adding corresponding tests.

Changes

Cohort / File(s) Summary
Localization
Resources/Localizable.xcstrings
Added ~153 lines of localized strings for the "Copy on Select" feature, including labels, subtitles, and mode descriptions across multiple languages.
Config Loading & Injection
Sources/GhosttyTerminalView.swift
Introduced inline config loading via loadInlineGhosttyConfig helper; added loadCopyOnSelectOverrideIfNeeded() to apply copy-on-select overrides; refactored CJK font fallback to use inline loading instead of temporary files.
Settings UI & Storage
Sources/cmuxApp.swift
Added TerminalCopyOnSelectSettings enum with Mode variants (inherit, selection, clipboard, disabled); introduced terminalCopyOnSelectMode AppStorage binding; wired UI picker row in SettingsView with onChange handler to trigger config reload; integrated reset handling.
Tests
cmuxTests/GhosttyConfigTests.swift
Added TerminalCopyOnSelectSettingsTests suite validating default modes, ghosttyConfigLine mapping, and override resolution from UserDefaults (inserted twice as duplicate).

Sequence Diagram

sequenceDiagram
    actor User
    participant SettingsView
    participant AppStorage
    participant cmuxApp
    participant GhosttyApp
    participant GhosttyTerminalView

    User->>SettingsView: Select copy-on-select mode
    SettingsView->>AppStorage: Save terminalCopyOnSelectMode
    AppStorage-->>SettingsView: onChange triggered
    SettingsView->>cmuxApp: Normalize & trigger reload
    cmuxApp->>GhosttyApp: reloadConfiguration("settings.copy_on_select")
    GhosttyApp->>GhosttyTerminalView: Pass config to terminal
    GhosttyTerminalView->>GhosttyTerminalView: loadCopyOnSelectOverrideIfNeeded()
    GhosttyTerminalView->>GhosttyTerminalView: loadInlineGhosttyConfig() with override line
    GhosttyTerminalView-->>User: Apply selected copy-on-select behavior
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 A clipboard mode, now carefully set,
With selects on shifts—no regrets!
Through AppStorage's charm and settings so bright,
The copy-on-select hops into light! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add copy-on-select setting' directly and clearly describes the main change—adding a copy-on-select preference setting to the application.
Description check ✅ Passed The description covers the summary section explaining what changed and why, includes testing steps and verification methods, and mentions related issues. All key template sections are addressed.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat-copy-on-select-option
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 4 files

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
cmuxTests/GhosttyConfigTests.swift (1)

541-543: Use unique UserDefaults suite names per test to avoid cross-test coupling.

A single shared suite name in this class can make tests flaky when execution overlaps. Prefer generating a UUID-based suite per test instance.

♻️ Proposed isolation tweak
 final class TerminalCopyOnSelectSettingsTests: XCTestCase {
     func testCopyOnSelectSettingDefaultsToInheritWhenUnset() {
-        let defaults = makeCopyOnSelectDefaults()
-        defer { defaults.removePersistentDomain(forName: defaultsSuiteName) }
+        let (defaults, suiteName) = makeCopyOnSelectDefaults()
+        defer { defaults.removePersistentDomain(forName: suiteName) }

         XCTAssertEqual(TerminalCopyOnSelectSettings.mode(defaults: defaults), .inherit)
         XCTAssertNil(TerminalCopyOnSelectSettings.overrideConfigLine(defaults: defaults))
     }

@@
     func testCopyOnSelectSettingReadsStoredClipboardOverride() {
-        let defaults = makeCopyOnSelectDefaults()
-        defer { defaults.removePersistentDomain(forName: defaultsSuiteName) }
+        let (defaults, suiteName) = makeCopyOnSelectDefaults()
+        defer { defaults.removePersistentDomain(forName: suiteName) }
         defaults.set(
             TerminalCopyOnSelectSettings.Mode.clipboard.rawValue,
             forKey: TerminalCopyOnSelectSettings.modeKey
         )

@@
-    private let defaultsSuiteName = "TerminalCopyOnSelectSettingsTests"
-
-    private func makeCopyOnSelectDefaults() -> UserDefaults {
-        let defaults = UserDefaults(suiteName: defaultsSuiteName)!
-        defaults.removePersistentDomain(forName: defaultsSuiteName)
-        return defaults
+    private func makeCopyOnSelectDefaults() -> (UserDefaults, String) {
+        let suiteName = "cmux.tests.copy-on-select.\(UUID().uuidString)"
+        let defaults = UserDefaults(suiteName: suiteName)!
+        defaults.removePersistentDomain(forName: suiteName)
+        return (defaults, suiteName)
     }
 }

Also applies to: 565-567, 579-584

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxTests/GhosttyConfigTests.swift` around lines 541 - 543, The tests use a
shared defaultsSuiteName which can cause cross-test coupling; change each test
to create a unique suite name (e.g., UUID string) and pass it into
makeCopyOnSelectDefaults (or overload/extend makeCopyOnSelectDefaults to accept
a suite name) instead of using the global defaultsSuiteName, then update cleanup
to call defaults.removePersistentDomain(forName: generatedSuiteName); update all
occurrences around makeCopyOnSelectDefaults and the deferred
removePersistentDomain calls (including the other instances at the mentioned
ranges) to use the per-test generated suite identifier.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 1056-1058: Replace the DEBUG-only Self.initLog call with a dlog()
call: inside the existing `#if` DEBUG / `#endif` block where Self.initLog("failed to
write \(logLabel) config: \(error)") is invoked, call dlog(...) instead and pass
the same formatted message (including logLabel and error) so the debug event
uses the standardized dlog function; locate the call by searching for
Self.initLog in the GhosttyTerminalView initialization/config write code and
update that site accordingly.

---

Nitpick comments:
In `@cmuxTests/GhosttyConfigTests.swift`:
- Around line 541-543: The tests use a shared defaultsSuiteName which can cause
cross-test coupling; change each test to create a unique suite name (e.g., UUID
string) and pass it into makeCopyOnSelectDefaults (or overload/extend
makeCopyOnSelectDefaults to accept a suite name) instead of using the global
defaultsSuiteName, then update cleanup to call
defaults.removePersistentDomain(forName: generatedSuiteName); update all
occurrences around makeCopyOnSelectDefaults and the deferred
removePersistentDomain calls (including the other instances at the mentioned
ranges) to use the per-test generated suite identifier.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 62063f02-b93a-4e54-97a9-4c30a2b71bad

📥 Commits

Reviewing files that changed from the base of the PR and between c69342a and 618b1f6.

📒 Files selected for processing (4)
  • Resources/Localizable.xcstrings
  • Sources/GhosttyTerminalView.swift
  • Sources/cmuxApp.swift
  • cmuxTests/GhosttyConfigTests.swift

Comment on lines +1056 to +1058
#if DEBUG
Self.initLog("failed to write \(logLabel) config: \(error)")
#endif

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use dlog() for this DEBUG log call.

Line 1057 logs via Self.initLog(...); new debug call sites in Swift should use dlog().

Suggested patch
         } catch {
             `#if` DEBUG
-            Self.initLog("failed to write \(logLabel) config: \(error)")
+            dlog("failed to write \(logLabel) config: \(error)")
             `#endif`
         }

As per coding guidelines, “All debug events must be logged using the dlog() function … wrapped in #if DEBUG / #endif.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#if DEBUG
Self.initLog("failed to write \(logLabel) config: \(error)")
#endif
`#if` DEBUG
dlog("failed to write \(logLabel) config: \(error)")
`#endif`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 1056 - 1058, Replace the
DEBUG-only Self.initLog call with a dlog() call: inside the existing `#if` DEBUG /
`#endif` block where Self.initLog("failed to write \(logLabel) config: \(error)")
is invoked, call dlog(...) instead and pass the same formatted message
(including logLabel and error) so the debug event uses the standardized dlog
function; locate the call by searching for Self.initLog in the
GhosttyTerminalView initialization/config write code and update that site
accordingly.

@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026

This branch was successfully deployed

1 active deployment
Preview — 618b1f6d Deployed Mar 13, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants