Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 144 additions & 3 deletions CLI/cmux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2694,7 +2694,13 @@ struct CMUXCLI {
"bind-key",
"unbind-key",
"copy-mode",
"show-option",
"show-options",
"set",
"set-option",
"set-window-option",
"set-buffer",
"setw",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"paste-buffer",
"list-buffers",
"respawn-pane",
Expand Down Expand Up @@ -6871,6 +6877,37 @@ struct CMUXCLI {
cmux omc team 3:claude "implement feature"
cmux omc --watch
""")
case "show-option", "show-options":
return """
Usage: cmux show-options [-svq] [name]

Show tmux-compat option values. Without a name, list all known values.

Flags:
-s Server option scope compatibility flag
-v Print only the option value
-q Suppress errors for missing or unsupported options

Examples:
cmux show-options
cmux show-options -sv extended-keys
"""
case "set", "set-option", "set-window-option", "setw":
return """
Usage: cmux set-option [-aouq] <name> <value>

Persist a tmux-compat option value.

Flags:
-a Append to the existing value
-o Only set if the option is not already set
-u Unset the option
-q Suppress errors for unknown options

Examples:
cmux set-option extended-keys always
cmux set-option -u extended-keys
"""
case "identify":
return """
Usage: cmux identify [--workspace <id|ref|index>] [--surface <id|ref|index>] [--no-caller]
Expand Down Expand Up @@ -11626,7 +11663,7 @@ struct CMUXCLI {
print(buffer)
}

case "last-window", "next-window", "previous-window", "set-hook", "set-buffer", "list-buffers":
case "last-window", "next-window", "previous-window", "set-hook", "show-option", "show-options", "set", "set-option", "set-window-option", "set-buffer", "list-buffers", "setw":
try runTmuxCompatCommand(
command: command,
commandArgs: rawArgs,
Expand Down Expand Up @@ -11689,7 +11726,7 @@ struct CMUXCLI {
try tmuxPruneCompatWorkspaceState(workspaceId: workspaceId)
}

case "set-option", "set", "set-window-option", "setw", "source-file", "refresh-client", "attach-session", "detach-client":
case "source-file", "refresh-client", "attach-session", "detach-client":
return

case "-V", "-v":
Expand All @@ -11712,6 +11749,7 @@ struct CMUXCLI {
private struct TmuxCompatStore: Codable {
var buffers: [String: String] = [:]
var hooks: [String: String] = [:]
var options: [String: String] = [:]
/// Tracks main-vertical layout state per workspace, keyed by workspace ID.
var mainVerticalLayouts: [String: MainVerticalState] = [:]
/// Tracks the last surface created by split-window per workspace.
Expand All @@ -11720,19 +11758,41 @@ struct CMUXCLI {
var lastSplitSurface: [String: String] = [:]

/// Custom decoder so older store files missing newer keys
/// (mainVerticalLayouts, lastSplitSurface) decode gracefully
/// (options, mainVerticalLayouts, lastSplitSurface) decode gracefully
/// instead of throwing and resetting the entire store.
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
buffers = try container.decodeIfPresent([String: String].self, forKey: .buffers) ?? [:]
hooks = try container.decodeIfPresent([String: String].self, forKey: .hooks) ?? [:]
options = try container.decodeIfPresent([String: String].self, forKey: .options) ?? [:]
mainVerticalLayouts = try container.decodeIfPresent([String: MainVerticalState].self, forKey: .mainVerticalLayouts) ?? [:]
lastSplitSurface = try container.decodeIfPresent([String: String].self, forKey: .lastSplitSurface) ?? [:]
}

init() {}
}

private func tmuxCompatDefaultOptions() -> [String: String] {
[
// cmux is not a tmux server, but OMX expects the tmux compatibility
// shim to answer lease-management probes for extended-keys.
"extended-keys": "off"
]
}

private func tmuxCompatDefaultOptionValue(_ name: String) -> String? {
tmuxCompatDefaultOptions()[name.lowercased()]
}

private func tmuxCompatMergedOptions(store: TmuxCompatStore) -> [String: String] {
tmuxCompatDefaultOptions().merging(store.options) { _, stored in stored }
}

private func tmuxCompatOptionValue(_ name: String, store: TmuxCompatStore) -> String? {
let normalizedName = name.lowercased()
return tmuxCompatMergedOptions(store: store)[normalizedName]
}

private func tmuxCompatStoreURL() -> URL {
let homePath = ProcessInfo.processInfo.environment["HOME"]
?? NSString(string: "~").expandingTildeInPath
Expand Down Expand Up @@ -12204,6 +12264,85 @@ struct CMUXCLI {
try saveTmuxCompatStore(store)
print("OK")

case "show-option", "show-options":
let parsed = try parseTmuxArguments(
commandArgs,
valueFlags: [],
boolFlags: ["-g", "-p", "-q", "-s", "-v", "-w"]
)
let store = loadTmuxCompatStore()
let optionName = parsed.positional.first?
.trimmingCharacters(in: .whitespacesAndNewlines)

if let optionName, !optionName.isEmpty {
guard let value = tmuxCompatOptionValue(optionName, store: store) else {
if parsed.hasFlag("-q") {
return
}
throw CLIError(message: "Unsupported tmux compatibility option: \(optionName)")
}

if parsed.hasFlag("-v") {
print(value)
} else {
print("\(optionName) \(value)")
}
return
}

let allOptions = tmuxCompatMergedOptions(store: store)
if allOptions.isEmpty {
if parsed.hasFlag("-q") {
return
}
throw CLIError(message: "No tmux compatibility options are set")
}
for (name, value) in allOptions.sorted(by: { $0.key < $1.key }) {
if parsed.hasFlag("-v") {
print(value)
} else {
print("\(name) \(value)")
}
}

case "set-option", "set", "set-window-option", "setw":
let parsed = try parseTmuxArguments(
commandArgs,
valueFlags: [],
boolFlags: ["-F", "-a", "-g", "-o", "-p", "-q", "-s", "-u", "-w"]
)
guard let optionName = parsed.positional.first?
.trimmingCharacters(in: .whitespacesAndNewlines),
!optionName.isEmpty else {
throw CLIError(message: "\(command) requires an option name")
}

var store = loadTmuxCompatStore()
let normalizedName = optionName.lowercased()
if parsed.hasFlag("-u") {
store.options.removeValue(forKey: normalizedName)
try saveTmuxCompatStore(store)
return
}

let valueParts = Array(parsed.positional.dropFirst())
guard !valueParts.isEmpty else {
throw CLIError(message: "\(command) requires an option value")
}
let value = valueParts.joined(separator: " ")

if parsed.hasFlag("-o"), tmuxCompatOptionValue(normalizedName, store: store) != nil {
return
}

if parsed.hasFlag("-a") {
let existing = tmuxCompatOptionValue(normalizedName, store: store) ?? ""
store.options[normalizedName] = existing + value
} else {
store.options[normalizedName] = value
}
try saveTmuxCompatStore(store)

Comment on lines +12308 to +12345

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

Implement parsed -o / -a behavior in set-option.

Line 12261 parses -o and -a, but Lines 12283-12284 always overwrite the value. That diverges from tmux semantics and can break scripts expecting conditional set or append behavior.

🔧 Suggested patch
         case "set-option", "set", "set-window-option", "setw":
             let parsed = try parseTmuxArguments(
                 commandArgs,
                 valueFlags: [],
                 boolFlags: ["-F", "-a", "-g", "-o", "-p", "-q", "-s", "-u", "-w"]
             )
@@
             if parsed.hasFlag("-u") {
                 store.options.removeValue(forKey: normalizedName)
                 try saveTmuxCompatStore(store)
                 return
             }
@@
-            store.options[normalizedName] = value
+            if parsed.hasFlag("-o"), store.options[normalizedName] != nil {
+                return
+            }
+
+            if parsed.hasFlag("-a"), let existing = store.options[normalizedName] {
+                store.options[normalizedName] = existing + value
+            } else {
+                store.options[normalizedName] = value
+            }
             try saveTmuxCompatStore(store)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLI/cmux.swift` around lines 12257 - 12285, The current code always
overwrites store.options[normalizedName] but parseTmuxArguments included flags
for "-o" (only-set) and "-a" (append); update the set-option handling to honor
these flags: after computing normalizedName and value, if parsed.hasFlag("-o")
then only set store.options[normalizedName] if it does not already exist; if
parsed.hasFlag("-a") then read existing = store.options[normalizedName] and set
store.options[normalizedName] = existing.isEmpty ? value :
"\(existing),\(value)" (or similar list-append logic), otherwise perform the
current overwrite; finally call try saveTmuxCompatStore(store) as before.

case "popup":
throw CLIError(message: "popup is not supported yet in cmux CLI parity mode")

Expand Down Expand Up @@ -14393,6 +14532,8 @@ struct CMUXCLI {
last-pane [--workspace <id|ref>]
find-window [--content] [--select] <query>
clear-history [--workspace <id|ref>] [--surface <id|ref>]
show-options [-sv] [name]
set-option [-asq] <name> <value>
set-hook [--list] [--unset <event>] | <event> <command>
popup
bind-key | unbind-key | copy-mode
Expand Down
39 changes: 39 additions & 0 deletions tests_v2/test_tmux_compat_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,45 @@ def main() -> int:
c.send_surface(s1, f"echo {capture_token}\n")
_wait_for(lambda: _surface_has(c, ws, s1, capture_token))

show_extended_keys = _run_cli(cli, ["show-options", "-sv", "extended-keys"])
_must(show_extended_keys.stdout.strip() == "off", f"show-options should default extended-keys to off, got {show_extended_keys.stdout!r}")
_run_cli(cli, ["set-option", "-sq", "extended-keys", "always"])
show_extended_keys = _run_cli(cli, ["show-options", "-sv", "extended-keys"])
_must(show_extended_keys.stdout.strip() == "always", f"set-option should persist extended-keys, got {show_extended_keys.stdout!r}")
_run_cli(cli, ["set-option", "-sq", "extended-keys", "off"])

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.

P2 Use -u to unset instead of re-setting to the default value

The teardown sets extended-keys back to "off" via set-option, which persists the entry in the store. Using set-option -u extended-keys would cleanly remove the key, restoring the store to its pre-test state and matching OMX's actual lease-release behavior (which calls -u to relinquish ownership).

Suggested change
_run_cli(cli, ["set-option", "-sq", "extended-keys", "off"])
_run_cli(cli, ["set-option", "-u", "extended-keys"])

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

current OMX restores extended-keys with set-option -sq ..., not -u

show_all_options = _run_cli(cli, ["show-options"])
_must("extended-keys off" in show_all_options.stdout, f"show-options should list default options, got {show_all_options.stdout!r}")

custom_option = f"@compat_probe_{stamp}"
_run_cli(cli, ["set-option", custom_option, "alpha"])
_run_cli(cli, ["set-option", "-o", custom_option, "beta"])
custom_value = _run_cli(cli, ["show-options", "-v", custom_option])
_must(custom_value.stdout.strip() == "alpha", f"set-option -o should keep the existing value, got {custom_value.stdout!r}")
_run_cli(cli, ["set-option", "-a", custom_option, "gamma"])
custom_value = _run_cli(cli, ["show-options", "-v", custom_option])
_must(custom_value.stdout.strip() == "alphagamma", f"set-option -a should append to the existing value, got {custom_value.stdout!r}")
show_all_options = _run_cli(cli, ["show-options"])
_must(f"{custom_option} alphagamma" in show_all_options.stdout, f"show-options should list custom options, got {show_all_options.stdout!r}")
_run_cli(cli, ["set-option", "-u", custom_option])

show_help = _run_cli(cli, ["show-options", "--help"])
_must("Usage: cmux show-options" in show_help.stdout, f"show-options --help should print usage, got {show_help.stdout!r}")
set_help = _run_cli(cli, ["set-option", "--help"])
_must("Usage: cmux set-option" in set_help.stdout, f"set-option --help should print usage, got {set_help.stdout!r}")

empty_option = f"@empty_probe_{stamp}"
_run_cli(cli, ["set-option", empty_option, ""])
empty_value = _run_cli(cli, ["show-options", "-v", empty_option])
_must(empty_value.stdout == "\n", f"set-option should preserve explicit empty values, got {empty_value.stdout!r}")
_run_cli(cli, ["set-option", "-u", empty_option])

spaced_option = f"@space_probe_{stamp}"
spaced_value = " padded value "
_run_cli(cli, ["set-option", spaced_option, spaced_value])
shown_spaced_value = _run_cli(cli, ["show-options", "-v", spaced_option])
_must(shown_spaced_value.stdout == spaced_value + "\n", f"set-option should preserve leading/trailing whitespace, got {shown_spaced_value.stdout!r}")
_run_cli(cli, ["set-option", "-u", spaced_option])

cap = _run_cli(cli, ["capture-pane", "--workspace", ws, "--surface", s1, "--scrollback"])
_must(capture_token in cap.stdout, f"capture-pane missing token: {cap.stdout!r}")

Expand Down