Add set-color/clear-color workspace actions for tab color via CLI (#1873)

* Add set-color/clear-color workspace actions for tab color via CLI

Expose the existing tab color functionality through the workspace-action
CLI command, enabling programmatic tab color setting without the GUI
context menu.

Supports both named colors (Red, Blue, Amber, etc.) and hex values
(#RRGGBB). Named colors resolve against the built-in palette via
case-insensitive matching.

Usage:
  cmux workspace-action --action set-color --color blue
  cmux workspace-action --action set-color --color "#C0392B"
  cmux workspace-action set-color Amber
  cmux workspace-action clear-color

* Return explicit null color in clear_color JSON response

Restore "color": null in the clear_color response payload so JSON
consumers can distinguish "color was cleared" from "no color field".

---------

Co-authored-by: Ariel Tobiana <arieltobiana@gmail.com>
This commit is contained in:
Ariel Tobiana 2026-03-25 08:58:54 +02:00 committed by GitHub
parent 533699f98c
commit 7cbd07e8cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 41 additions and 27 deletions

View file

@ -3379,14 +3379,18 @@ struct CMUXCLI {
let workspaceArg = workspaceOpt ?? (windowOverride == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil)
let workspaceId = try normalizeWorkspaceHandle(workspaceArg, client: client, allowCurrent: true)
let inferredTitle = positional.joined(separator: " ").trimmingCharacters(in: .whitespacesAndNewlines)
let title = (titleOpt ?? (inferredTitle.isEmpty ? nil : inferredTitle))?.trimmingCharacters(in: .whitespacesAndNewlines)
let inferredPositional = positional.joined(separator: " ").trimmingCharacters(in: .whitespacesAndNewlines)
let title = (titleOpt ?? (action == "rename" && !inferredPositional.isEmpty ? inferredPositional : nil))?.trimmingCharacters(in: .whitespacesAndNewlines)
if action == "rename", (title?.isEmpty ?? true) {
throw CLIError(message: "workspace-action rename requires --title <text> (or a trailing title)")
}
if action == "set_color", (colorOpt?.isEmpty ?? true) {
throw CLIError(message: "workspace-action set-color requires --color <#hex|name>")
let color = (
colorOpt ?? (action == "set_color" ? (inferredPositional.isEmpty ? nil : inferredPositional) : nil)
)?.trimmingCharacters(in: .whitespacesAndNewlines)
if action == "set_color", (color?.isEmpty ?? true) {
throw CLIError(message: "workspace-action set-color requires --color <name|#hex> (or a trailing color)")
}
var params: [String: Any] = ["action": action]
@ -3396,7 +3400,7 @@ struct CMUXCLI {
if let title, !title.isEmpty {
params["title"] = title
}
if let color = colorOpt, !color.isEmpty {
if let color, !color.isEmpty {
params["color"] = color
}
@ -3414,6 +3418,9 @@ struct CMUXCLI {
if let index = payload["index"] {
summaryParts.append("index=\(index)")
}
if let color = payload["color"] as? String {
summaryParts.append("color=\(color)")
}
printV2Payload(payload, jsonOutput: jsonOutput, idFormat: idFormat, fallbackText: summaryParts.joined(separator: " "))
}
@ -6260,15 +6267,21 @@ struct CMUXCLI {
Flags:
--action <name> Action name (required if not positional)
--workspace <id|ref|index> Target workspace (default: current/$CMUX_WORKSPACE_ID)
--title <text> Title for rename (or pass trailing title text)
--color <#hex|name> Color for set-color (e.g. '#C0392B' or 'Red')
--title <text> Title for rename
--color <name|#hex> Color for set-color (name or #RRGGBB hex)
Named colors:
Red, Crimson, Orange, Amber, Olive, Green, Teal, Aqua,
Blue, Navy, Indigo, Purple, Magenta, Rose, Brown, Charcoal
Example:
cmux workspace-action --workspace workspace:2 --action pin
cmux workspace-action --action rename --title "infra"
cmux workspace-action close-others
cmux workspace-action --action set-color --workspace workspace:1 --color '#C0392B'
cmux workspace-action --action clear-color --workspace workspace:1
cmux workspace-action --action set-color --color blue
cmux workspace-action --action set-color --color "#C0392B"
cmux workspace-action set-color Amber
cmux workspace-action clear-color
"""
case "tab-action":
return """
@ -11981,7 +11994,7 @@ struct CMUXCLI {
close-window --window <id>
move-workspace-to-window --workspace <id|ref> --window <id|ref>
reorder-workspace --workspace <id|ref|index> (--index <n> | --before <id|ref|index> | --after <id|ref|index>) [--window <id|ref|index>]
workspace-action --action <name> [--workspace <id|ref|index>] [--title <text>] [--color <#hex|name>]
workspace-action --action <name> [--workspace <id|ref|index>] [--title <text>] [--color <name|#hex>]
list-workspaces
new-workspace [--cwd <path>] [--command <text>]
ssh <destination> [--name <title>] [--port <n>] [--identity <path>] [--ssh-option <opt>] [-- <remote-command-args>]

View file

@ -4085,29 +4085,30 @@ class TerminalController {
finish()
case "set_color":
guard let colorRaw = v2String(params, "color"), !colorRaw.isEmpty else {
result = .err(code: "invalid_params", message: "set-color requires --color", data: nil)
guard let colorRaw = v2String(params, "color"),
!colorRaw.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
result = .err(code: "invalid_params", message: "Missing or invalid color", data: nil)
return
}
// Resolve named color to hex via palette lookup
let resolved: String
if colorRaw.hasPrefix("#") {
guard let normalized = WorkspaceTabColorSettings.normalizedHex(colorRaw) else {
result = .err(code: "invalid_params", message: "Invalid hex color '\(colorRaw)'. Expected #RRGGBB", data: nil)
return
}
resolved = normalized
} else if let entry = WorkspaceTabColorSettings.defaultPalette.first(where: {
$0.name.lowercased() == colorRaw.lowercased()
let colorInput = colorRaw.trimmingCharacters(in: .whitespacesAndNewlines)
// Resolve named colors from effective palette (includes user overrides, excludes custom entries)
let effectivePalette = WorkspaceTabColorSettings.defaultPaletteWithOverrides()
let hex: String
if let entry = effectivePalette.first(where: {
$0.name.caseInsensitiveCompare(colorInput) == .orderedSame
}) {
resolved = entry.hex
hex = entry.hex
} else if let normalized = WorkspaceTabColorSettings.normalizedHex(colorInput) {
hex = normalized
} else {
let names = WorkspaceTabColorSettings.defaultPalette.map(\.name).joined(separator: ", ")
result = .err(code: "invalid_params", message: "Unknown color '\(colorRaw)'. Use #RRGGBB or: \(names)", data: nil)
let colorNames = effectivePalette.map(\.name)
result = .err(code: "invalid_params", message: "Invalid color. Use a hex value (#RRGGBB) or a named color.", data: [
"named_colors": colorNames
])
return
}
tabManager.setTabColor(tabId: workspace.id, color: resolved)
finish(["color": resolved])
tabManager.setTabColor(tabId: workspace.id, color: hex)
finish(["color": hex])
case "clear_color":
tabManager.setTabColor(tabId: workspace.id, color: nil)