cmux/daemon/remote/cmd/cmuxd-remote/tmux_compat.go
Lawrence Chen 27fa3873be
Add claude-teams, omo, and __tmux-compat to Go relay CLI for SSH sessions (#2238)
* Add claude-teams, omo, and __tmux-compat to Go relay CLI

These commands previously only existed in the Swift CLI which uses Unix
domain sockets and can't connect over TCP relay. The Go relay CLI already
handles TCP connections, so adding the commands here makes them work
inside `cmux ssh` sessions.

- `cmux claude-teams`: creates tmux shim scripts, configures environment
  (fake TMUX/TMUX_PANE, socket path, workspace/surface IDs), and execs
  into `claude --teammate-mode auto`
- `cmux omo`: same pattern for OpenCode with terminal-notifier shim
- `cmux __tmux-compat`: translates tmux commands (split-window,
  send-keys, capture-pane, display-message, list-panes, etc.) into cmux
  JSON-RPC calls over the relay socket. Includes main-vertical layout
  tracking, wait-for signaling, and format string rendering.

* Fix: search original PATH before environment modification

findExecutable was called after configureAgentEnvironment prepended the
shim directory to PATH. The Swift CLI searches the original PATH before
modification. Renamed to findExecutableInPath with explicit PATH arg and
moved the search before configureAgentEnvironment.

* Fix cmux omo hang and port oh-my-opencode plugin setup

Root cause: socketRoundTripV2 had no read timeout. When connecting to a
stale relay port (accepted TCP but never responded), the read blocked
forever. This caused getFocusedContext to hang, blocking agent launch.

Fixes:
- Add 15s read deadline to socketRoundTripV2 (affects all v2 RPC calls)
- Add 5s timeout to getFocusedContext so agent launch proceeds even if
  system.identify is slow
- Port omoEnsurePlugin from Swift: creates shadow config dir, adds
  oh-my-opencode to plugin list, symlinks node_modules/package.json,
  installs plugin via bun/npm if missing, configures tmux settings
  (enabled=true, lower min widths), sets OPENCODE_CONFIG_DIR

* Fix: use bun as runtime for node-script opencode when node is missing

opencode is installed via bun as a #!/usr/bin/env node script, but on
some systems (like the macmini) bun is installed without a standalone
node binary. Detect node scripts and fall back to bun as the runtime
since bun is node-compatible.

* Fix subagent pane theme: preserve COLORTERM, keep TERM_PROGRAM

The cmux ssh bootstrap exports COLORTERM=truecolor and TERM_PROGRAM=ghostty.
Our configureAgentEnvironment was unsetting TERM_PROGRAM and not setting
COLORTERM, causing subagent panes (created via split-window) to lose
truecolor detection and render with wrong theme colors.

* Restore TERM_PROGRAM unset, keep COLORTERM=truecolor

* Force dark colorScheme in opencode shadow config for SSH

* Remove hardcoded dark colorScheme, let opencode detect naturally

* Detect system color scheme for opencode over SSH

* Remove color scheme detection workaround, let opencode handle natively

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
2026-03-27 21:04:24 -07:00

1680 lines
42 KiB
Go

package main
import (
"encoding/json"
"fmt"
"math"
"os"
"path/filepath"
"regexp"
"strings"
"time"
)
// runTmuxCompat handles `cmux __tmux-compat <args...>`, translating tmux
// commands into cmux JSON-RPC calls over the relay socket.
func runTmuxCompat(socketPath string, args []string, refreshAddr func() string) int {
command, cmdArgs, err := splitTmuxCmd(args)
if err != nil {
fmt.Fprintf(os.Stderr, "cmux __tmux-compat: %v\n", err)
return 1
}
rc := &rpcContext{socketPath: socketPath, refreshAddr: refreshAddr}
if err := dispatchTmuxCommand(rc, command, cmdArgs); err != nil {
fmt.Fprintf(os.Stderr, "cmux __tmux-compat: %v\n", err)
return 1
}
return 0
}
// rpcContext holds connection info for making JSON-RPC calls.
type rpcContext struct {
socketPath string
refreshAddr func() string
}
// call makes a JSON-RPC call and returns the parsed result.
func (rc *rpcContext) call(method string, params map[string]any) (map[string]any, error) {
resp, err := socketRoundTripV2(rc.socketPath, method, params, rc.refreshAddr)
if err != nil {
return nil, err
}
var result map[string]any
if err := json.Unmarshal([]byte(resp), &result); err != nil {
// Some responses are bare values (string, null)
return nil, nil
}
return result, nil
}
// --- Tmux argument parsing ---
type tmuxParsed struct {
flags map[string]bool // boolean flags like -d, -P
options map[string][]string // value flags like -t <target>
positional []string
}
func (p *tmuxParsed) hasFlag(f string) bool {
return p.flags[f]
}
func (p *tmuxParsed) value(f string) string {
vals := p.options[f]
if len(vals) == 0 {
return ""
}
return vals[len(vals)-1]
}
func splitTmuxCmd(args []string) (string, []string, error) {
globalValueFlags := map[string]bool{"-L": true, "-S": true, "-f": true}
globalBoolFlags := map[string]bool{"-V": true, "-v": true}
i := 0
for i < len(args) {
arg := args[i]
if !strings.HasPrefix(arg, "-") || arg == "-" {
return strings.ToLower(arg), args[i+1:], nil
}
if arg == "--" {
break
}
if globalBoolFlags[arg] {
return arg, nil, nil
}
if globalValueFlags[arg] {
// Skip the value
i++
}
i++
}
return "", nil, fmt.Errorf("tmux shim requires a command")
}
func parseTmuxArgs(args []string, valueFlags, boolFlags []string) *tmuxParsed {
vSet := make(map[string]bool, len(valueFlags))
for _, f := range valueFlags {
vSet[f] = true
}
bSet := make(map[string]bool, len(boolFlags))
for _, f := range boolFlags {
bSet[f] = true
}
p := &tmuxParsed{
flags: make(map[string]bool),
options: make(map[string][]string),
}
pastTerminator := false
for i := 0; i < len(args); i++ {
arg := args[i]
if pastTerminator {
p.positional = append(p.positional, arg)
continue
}
if arg == "--" {
pastTerminator = true
continue
}
if !strings.HasPrefix(arg, "-") || arg == "-" {
p.positional = append(p.positional, arg)
continue
}
if strings.HasPrefix(arg, "--") {
p.positional = append(p.positional, arg)
continue
}
// Cluster parsing: -dPh etc.
cluster := []rune(arg[1:])
cursor := 0
recognized := false
for cursor < len(cluster) {
flag := "-" + string(cluster[cursor])
if bSet[flag] {
p.flags[flag] = true
cursor++
recognized = true
continue
}
if vSet[flag] {
remainder := string(cluster[cursor+1:])
var value string
if remainder != "" {
value = remainder
} else if i+1 < len(args) {
i++
value = args[i]
}
p.options[flag] = append(p.options[flag], value)
recognized = true
cursor = len(cluster)
continue
}
recognized = false
break
}
if !recognized {
p.positional = append(p.positional, arg)
}
}
return p
}
// --- Format string rendering ---
var tmuxFormatVarRe = regexp.MustCompile(`#\{[^}]+\}`)
func tmuxRenderFormat(format string, context map[string]string, fallback string) string {
if format == "" {
return fallback
}
rendered := format
for key, value := range context {
rendered = strings.ReplaceAll(rendered, "#{"+key+"}", value)
}
// Remove any remaining unresolved #{...} variables
rendered = tmuxFormatVarRe.ReplaceAllString(rendered, "")
rendered = strings.TrimSpace(rendered)
if rendered == "" {
return fallback
}
return rendered
}
// --- Format context building ---
func tmuxFormatContext(rc *rpcContext, workspaceId string, paneId string, surfaceId string) (map[string]string, error) {
canonicalWsId, err := tmuxResolveWorkspaceId(rc, workspaceId)
if err != nil {
return nil, err
}
ctx := map[string]string{
"session_name": "cmux",
"session_id": "$0",
"window_id": "@" + canonicalWsId,
"window_uuid": canonicalWsId,
"window_active": "1",
"window_flags": "*",
"pane_active": "1",
}
// Get workspace list for index/title
workspaces, err := tmuxWorkspaceItems(rc)
if err == nil {
for _, ws := range workspaces {
wsId, _ := ws["id"].(string)
wsRef, _ := ws["ref"].(string)
if wsId == canonicalWsId || wsRef == workspaceId {
if idx := intFromAnyGo(ws["index"]); idx >= 0 {
ctx["window_index"] = fmt.Sprintf("%d", idx)
}
if title, _ := ws["title"].(string); strings.TrimSpace(title) != "" {
ctx["window_name"] = strings.TrimSpace(title)
}
if paneCount := intFromAnyGo(ws["pane_count"]); paneCount >= 0 {
ctx["window_panes"] = fmt.Sprintf("%d", paneCount)
}
break
}
}
}
// Get current surface info
currentPayload, err := rc.call("surface.current", map[string]any{"workspace_id": canonicalWsId})
if err != nil {
return ctx, nil
}
resolvedPaneId := paneId
if resolvedPaneId == "" {
if pid, ok := currentPayload["pane_id"].(string); ok {
resolvedPaneId = pid
} else if pref, ok := currentPayload["pane_ref"].(string); ok {
resolvedPaneId = pref
}
}
resolvedSurfaceId := surfaceId
if resolvedSurfaceId == "" && resolvedPaneId != "" {
if sid, err := tmuxSelectedSurfaceId(rc, canonicalWsId, resolvedPaneId); err == nil {
resolvedSurfaceId = sid
}
}
if resolvedSurfaceId == "" {
if sid, ok := currentPayload["surface_id"].(string); ok {
resolvedSurfaceId = sid
}
}
if resolvedPaneId != "" {
ctx["pane_id"] = "%" + resolvedPaneId
ctx["pane_uuid"] = resolvedPaneId
panePayload, err := rc.call("pane.list", map[string]any{"workspace_id": canonicalWsId})
if err == nil {
panes, _ := panePayload["panes"].([]any)
for _, p := range panes {
pane, _ := p.(map[string]any)
if pane == nil {
continue
}
if pid, _ := pane["id"].(string); pid == resolvedPaneId {
if idx := intFromAnyGo(pane["index"]); idx >= 0 {
ctx["pane_index"] = fmt.Sprintf("%d", idx)
}
break
}
}
}
}
if resolvedSurfaceId != "" {
ctx["surface_id"] = resolvedSurfaceId
surfacePayload, err := rc.call("surface.list", map[string]any{"workspace_id": canonicalWsId})
if err == nil {
surfaces, _ := surfacePayload["surfaces"].([]any)
for _, s := range surfaces {
surface, _ := s.(map[string]any)
if surface == nil {
continue
}
if sid, _ := surface["id"].(string); sid == resolvedSurfaceId {
if title, _ := surface["title"].(string); strings.TrimSpace(title) != "" {
ctx["pane_title"] = strings.TrimSpace(title)
if _, ok := ctx["window_name"]; !ok {
ctx["window_name"] = strings.TrimSpace(title)
}
}
break
}
}
}
}
return ctx, nil
}
func tmuxEnrichContextWithGeometry(ctx map[string]string, pane map[string]any, containerFrame map[string]any) {
isFocused, _ := pane["focused"].(bool)
if isFocused {
ctx["pane_active"] = "1"
} else {
ctx["pane_active"] = "0"
}
columns := intFromAnyGo(pane["columns"])
rows := intFromAnyGo(pane["rows"])
if columns < 0 || rows < 0 {
return
}
ctx["pane_width"] = fmt.Sprintf("%d", columns)
ctx["pane_height"] = fmt.Sprintf("%d", rows)
cellW := intFromAnyGo(pane["cell_width_px"])
cellH := intFromAnyGo(pane["cell_height_px"])
if cellW <= 0 || cellH <= 0 {
return
}
if frame, ok := pane["pixel_frame"].(map[string]any); ok {
px := floatFromAny(frame["x"])
py := floatFromAny(frame["y"])
ctx["pane_left"] = fmt.Sprintf("%d", int(px)/cellW)
ctx["pane_top"] = fmt.Sprintf("%d", int(py)/cellH)
}
if containerFrame != nil {
cw := floatFromAny(containerFrame["width"])
ch := floatFromAny(containerFrame["height"])
ww := int(cw) / cellW
wh := int(ch) / cellH
if ww < 1 {
ww = 1
}
if wh < 1 {
wh = 1
}
ctx["window_width"] = fmt.Sprintf("%d", ww)
ctx["window_height"] = fmt.Sprintf("%d", wh)
}
}
func floatFromAny(v any) float64 {
switch t := v.(type) {
case float64:
return t
case int:
return float64(t)
case json.Number:
f, _ := t.Float64()
return f
}
return 0
}
func intFromAnyGo(v any) int {
switch t := v.(type) {
case float64:
return int(t)
case int:
return t
case json.Number:
i, err := t.Int64()
if err != nil {
return -1
}
return int(i)
}
return -1
}
// --- Target resolution ---
func tmuxCallerWorkspaceHandle() string {
return strings.TrimSpace(os.Getenv("CMUX_WORKSPACE_ID"))
}
func tmuxCallerSurfaceHandle() string {
return strings.TrimSpace(os.Getenv("CMUX_SURFACE_ID"))
}
func tmuxCallerPaneHandle() string {
for _, key := range []string{"TMUX_PANE", "CMUX_PANE_ID"} {
v := strings.TrimSpace(os.Getenv(key))
if v != "" {
return strings.TrimPrefix(v, "%")
}
}
return ""
}
func tmuxWorkspaceItems(rc *rpcContext) ([]map[string]any, error) {
payload, err := rc.call("workspace.list", nil)
if err != nil {
return nil, err
}
items, _ := payload["workspaces"].([]any)
var result []map[string]any
for _, item := range items {
if m, ok := item.(map[string]any); ok {
result = append(result, m)
}
}
return result, nil
}
func isUUIDish(s string) bool {
// Simple UUID check: 8-4-4-4-12 hex
if len(s) != 36 {
return false
}
for i, c := range s {
if i == 8 || i == 13 || i == 18 || i == 23 {
if c != '-' {
return false
}
} else if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
return false
}
}
return true
}
func tmuxResolveWorkspaceId(rc *rpcContext, raw string) (string, error) {
if raw == "" || raw == "current" {
if caller := tmuxCallerWorkspaceHandle(); caller != "" {
if isUUIDish(caller) {
return caller, nil
}
// Resolve ref
return tmuxResolveWorkspaceId(rc, caller)
}
payload, err := rc.call("workspace.current", nil)
if err != nil {
return "", fmt.Errorf("no workspace selected: %w", err)
}
if wsId, ok := payload["workspace_id"].(string); ok {
return wsId, nil
}
return "", fmt.Errorf("no workspace selected")
}
if isUUIDish(raw) {
return raw, nil
}
// Try to resolve as ref or index
items, err := tmuxWorkspaceItems(rc)
if err != nil {
return "", err
}
for _, item := range items {
if ref, _ := item["ref"].(string); ref == raw {
if id, _ := item["id"].(string); id != "" {
return id, nil
}
}
}
// Try name match
needle := strings.TrimSpace(raw)
for _, item := range items {
title, _ := item["title"].(string)
if strings.TrimSpace(title) == needle {
if id, _ := item["id"].(string); id != "" {
return id, nil
}
}
}
return "", fmt.Errorf("workspace not found: %s", raw)
}
func tmuxResolveWorkspaceTarget(rc *rpcContext, raw string) (string, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
if caller := tmuxCallerWorkspaceHandle(); caller != "" {
return tmuxResolveWorkspaceId(rc, caller)
}
return tmuxResolveWorkspaceId(rc, "")
}
if raw == "!" || raw == "^" || raw == "-" {
payload, err := rc.call("workspace.last", nil)
if err != nil {
return "", fmt.Errorf("previous workspace not found: %w", err)
}
if wsId, ok := payload["workspace_id"].(string); ok {
return wsId, nil
}
return "", fmt.Errorf("previous workspace not found")
}
// Strip session:window.pane format
token := raw
if dot := strings.LastIndex(token, "."); dot >= 0 {
token = token[:dot]
}
if colon := strings.LastIndex(token, ":"); colon >= 0 {
suffix := token[colon+1:]
if suffix != "" {
token = suffix
} else {
token = token[:colon]
}
}
token = strings.TrimPrefix(token, "@")
return tmuxResolveWorkspaceId(rc, token)
}
func tmuxPaneSelector(raw string) string {
raw = strings.TrimSpace(raw)
if raw == "" {
return ""
}
if strings.HasPrefix(raw, "%") {
return raw[1:]
}
if strings.HasPrefix(raw, "pane:") {
return raw
}
if dot := strings.LastIndex(raw, "."); dot >= 0 {
return raw[dot+1:]
}
return ""
}
func tmuxWindowSelector(raw string) string {
raw = strings.TrimSpace(raw)
if raw == "" {
return ""
}
if strings.HasPrefix(raw, "%") || strings.HasPrefix(raw, "pane:") {
return ""
}
if dot := strings.LastIndex(raw, "."); dot >= 0 {
return raw[:dot]
}
return raw
}
func tmuxCanonicalPaneId(rc *rpcContext, handle string, workspaceId string) (string, error) {
if isUUIDish(handle) {
return handle, nil
}
payload, err := rc.call("pane.list", map[string]any{"workspace_id": workspaceId})
if err != nil {
return "", err
}
panes, _ := payload["panes"].([]any)
for _, p := range panes {
pane, _ := p.(map[string]any)
if pane == nil {
continue
}
if ref, _ := pane["ref"].(string); ref == handle {
if id, _ := pane["id"].(string); id != "" {
return id, nil
}
}
if id, _ := pane["id"].(string); id == handle {
return id, nil
}
}
return "", fmt.Errorf("pane not found: %s", handle)
}
func tmuxFocusedPaneId(rc *rpcContext, workspaceId string) (string, error) {
payload, err := rc.call("surface.current", map[string]any{"workspace_id": workspaceId})
if err != nil {
return "", err
}
if pid, ok := payload["pane_id"].(string); ok {
return pid, nil
}
if pref, ok := payload["pane_ref"].(string); ok {
return tmuxCanonicalPaneId(rc, pref, workspaceId)
}
return "", fmt.Errorf("pane not found")
}
func tmuxWorkspaceIdForPaneHandle(rc *rpcContext, handle string) (string, error) {
if !isUUIDish(handle) {
return "", fmt.Errorf("not a UUID")
}
workspaces, err := tmuxWorkspaceItems(rc)
if err != nil {
return "", err
}
for _, ws := range workspaces {
wsId, _ := ws["id"].(string)
if wsId == "" {
continue
}
payload, err := rc.call("pane.list", map[string]any{"workspace_id": wsId})
if err != nil {
continue
}
panes, _ := payload["panes"].([]any)
for _, p := range panes {
pane, _ := p.(map[string]any)
if pane == nil {
continue
}
if pid, _ := pane["id"].(string); pid == handle {
return wsId, nil
}
if pref, _ := pane["ref"].(string); pref == handle {
return wsId, nil
}
}
}
return "", fmt.Errorf("pane not found in any workspace")
}
func tmuxResolvePaneTarget(rc *rpcContext, raw string) (workspaceId string, paneId string, err error) {
raw = strings.TrimSpace(raw)
paneSelector := tmuxPaneSelector(raw)
windowSelector := tmuxWindowSelector(raw)
if windowSelector != "" {
workspaceId, err = tmuxResolveWorkspaceTarget(rc, windowSelector)
if err != nil {
return "", "", err
}
} else if paneSelector != "" {
workspaceId, err = tmuxWorkspaceIdForPaneHandle(rc, paneSelector)
if err != nil {
workspaceId, err = tmuxResolveWorkspaceTarget(rc, "")
if err != nil {
return "", "", err
}
}
} else {
workspaceId, err = tmuxResolveWorkspaceTarget(rc, "")
if err != nil {
return "", "", err
}
}
if paneSelector != "" {
paneId, err = tmuxCanonicalPaneId(rc, paneSelector, workspaceId)
if err != nil {
return "", "", err
}
} else if callerWs := tmuxCallerWorkspaceHandle(); callerWs == workspaceId {
if callerPane := tmuxCallerPaneHandle(); callerPane != "" {
if pid, err2 := tmuxCanonicalPaneId(rc, callerPane, workspaceId); err2 == nil {
paneId = pid
}
}
}
if paneId == "" {
paneId, err = tmuxFocusedPaneId(rc, workspaceId)
if err != nil {
return "", "", err
}
}
return workspaceId, paneId, nil
}
func tmuxSelectedSurfaceId(rc *rpcContext, workspaceId string, paneId string) (string, error) {
payload, err := rc.call("pane.surfaces", map[string]any{"workspace_id": workspaceId, "pane_id": paneId})
if err != nil {
return "", err
}
surfaces, _ := payload["surfaces"].([]any)
for _, s := range surfaces {
surface, _ := s.(map[string]any)
if surface == nil {
continue
}
if sel, _ := surface["selected"].(bool); sel {
if id, _ := surface["id"].(string); id != "" {
return id, nil
}
}
}
// Fall back to first surface
if len(surfaces) > 0 {
if surface, ok := surfaces[0].(map[string]any); ok {
if id, _ := surface["id"].(string); id != "" {
return id, nil
}
}
}
return "", fmt.Errorf("pane has no surface")
}
func tmuxResolveSurfaceTarget(rc *rpcContext, raw string) (workspaceId string, paneId string, surfaceId string, err error) {
raw = strings.TrimSpace(raw)
if tmuxPaneSelector(raw) != "" {
workspaceId, paneId, err = tmuxResolvePaneTarget(rc, raw)
if err != nil {
return "", "", "", err
}
// When target pane matches caller's pane, prefer caller's surface
callerPane := tmuxCallerPaneHandle()
callerSurface := tmuxCallerSurfaceHandle()
if callerPane != "" && callerSurface != "" {
canonicalCallerPane, _ := tmuxCanonicalPaneId(rc, callerPane, workspaceId)
if paneId == callerPane || paneId == canonicalCallerPane {
surfaceId = callerSurface
return
}
}
surfaceId, err = tmuxSelectedSurfaceId(rc, workspaceId, paneId)
return
}
winSel := tmuxWindowSelector(raw)
workspaceId, err = tmuxResolveWorkspaceTarget(rc, winSel)
if err != nil {
return "", "", "", err
}
// When no explicit target and caller workspace matches, use caller's surface
if winSel == "" {
if callerWs := tmuxCallerWorkspaceHandle(); callerWs == workspaceId {
if callerSurface := tmuxCallerSurfaceHandle(); callerSurface != "" {
surfaceId = callerSurface
return
}
}
}
// Fall back to focused surface
payload, err := rc.call("surface.current", map[string]any{"workspace_id": workspaceId})
if err == nil {
if sid, ok := payload["surface_id"].(string); ok {
surfaceId = sid
return
}
}
// Last resort: first surface in the workspace
surfPayload, err := rc.call("surface.list", map[string]any{"workspace_id": workspaceId})
if err == nil {
surfs, _ := surfPayload["surfaces"].([]any)
for _, s := range surfs {
surf, _ := s.(map[string]any)
if surf == nil {
continue
}
if focused, _ := surf["focused"].(bool); focused {
if id, _ := surf["id"].(string); id != "" {
surfaceId = id
return workspaceId, "", surfaceId, nil
}
}
}
if len(surfs) > 0 {
if surf, ok := surfs[0].(map[string]any); ok {
if id, _ := surf["id"].(string); id != "" {
surfaceId = id
return workspaceId, "", surfaceId, nil
}
}
}
}
return "", "", "", fmt.Errorf("unable to resolve surface")
}
// --- TmuxCompatStore (local JSON state) ---
type mainVerticalState struct {
MainSurfaceId string `json:"mainSurfaceId"`
LastColumnSurfaceId string `json:"lastColumnSurfaceId,omitempty"`
}
type tmuxCompatStore struct {
Buffers map[string]string `json:"buffers,omitempty"`
Hooks map[string]string `json:"hooks,omitempty"`
MainVerticalLayouts map[string]mainVerticalState `json:"mainVerticalLayouts,omitempty"`
LastSplitSurface map[string]string `json:"lastSplitSurface,omitempty"`
}
func tmuxCompatStoreURL() string {
home, _ := os.UserHomeDir()
return filepath.Join(home, ".cmuxterm", "tmux-compat-store.json")
}
func loadTmuxCompatStore() tmuxCompatStore {
data, err := os.ReadFile(tmuxCompatStoreURL())
if err != nil {
return tmuxCompatStore{
Buffers: make(map[string]string),
Hooks: make(map[string]string),
MainVerticalLayouts: make(map[string]mainVerticalState),
LastSplitSurface: make(map[string]string),
}
}
var store tmuxCompatStore
if err := json.Unmarshal(data, &store); err != nil {
return tmuxCompatStore{
Buffers: make(map[string]string),
Hooks: make(map[string]string),
MainVerticalLayouts: make(map[string]mainVerticalState),
LastSplitSurface: make(map[string]string),
}
}
if store.Buffers == nil {
store.Buffers = make(map[string]string)
}
if store.Hooks == nil {
store.Hooks = make(map[string]string)
}
if store.MainVerticalLayouts == nil {
store.MainVerticalLayouts = make(map[string]mainVerticalState)
}
if store.LastSplitSurface == nil {
store.LastSplitSurface = make(map[string]string)
}
return store
}
func saveTmuxCompatStore(store tmuxCompatStore) error {
path := tmuxCompatStoreURL()
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}
data, err := json.Marshal(store)
if err != nil {
return err
}
return os.WriteFile(path, data, 0644)
}
// --- Special key translation ---
func tmuxSpecialKeyText(token string) string {
switch strings.ToLower(token) {
case "enter", "c-m", "kpenter":
return "\r"
case "tab", "c-i":
return "\t"
case "space":
return " "
case "bspace", "backspace":
return "\x7f"
case "escape", "esc", "c-[":
return "\x1b"
case "c-c":
return "\x03"
case "c-d":
return "\x04"
case "c-z":
return "\x1a"
case "c-l":
return "\x0c"
default:
return ""
}
}
func tmuxSendKeysText(tokens []string, literal bool) string {
if literal {
return strings.Join(tokens, " ")
}
var result strings.Builder
pendingSpace := false
for _, token := range tokens {
if special := tmuxSpecialKeyText(token); special != "" {
result.WriteString(special)
pendingSpace = false
continue
}
if pendingSpace {
result.WriteByte(' ')
}
result.WriteString(token)
pendingSpace = true
}
return result.String()
}
func tmuxShellQuote(value string) string {
return "'" + strings.ReplaceAll(value, "'", "'\"'\"'") + "'"
}
func tmuxShellCommandText(positional []string, cwd string) string {
cwd = strings.TrimSpace(cwd)
cmd := strings.TrimSpace(strings.Join(positional, " "))
if cwd == "" && cmd == "" {
return ""
}
var pieces []string
if cwd != "" {
pieces = append(pieces, "cd -- "+tmuxShellQuote(cwd))
}
if cmd != "" {
pieces = append(pieces, cmd)
}
return strings.Join(pieces, " && ") + "\r"
}
// --- Wait-for (filesystem-based signaling) ---
func tmuxWaitForSignalPath(name string) string {
var sanitized strings.Builder
for _, c := range name {
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') ||
c == '.' || c == '_' || c == '-' {
sanitized.WriteRune(c)
} else {
sanitized.WriteByte('_')
}
}
return fmt.Sprintf("/tmp/cmux-wait-for-%s.sig", sanitized.String())
}
// --- Main dispatch ---
func dispatchTmuxCommand(rc *rpcContext, command string, args []string) error {
switch command {
case "-v", "-V":
fmt.Println("tmux 3.4")
return nil
case "new-session", "new":
return tmuxNewSession(rc, args)
case "new-window", "neww":
return tmuxNewWindow(rc, args)
case "split-window", "splitw":
return tmuxSplitWindow(rc, args)
case "select-window", "selectw":
return tmuxSelectWindow(rc, args)
case "select-pane", "selectp":
return tmuxSelectPane(rc, args)
case "kill-window", "killw":
return tmuxKillWindow(rc, args)
case "kill-pane", "killp":
return tmuxKillPane(rc, args)
case "send-keys", "send":
return tmuxSendKeys(rc, args)
case "capture-pane", "capturep":
return tmuxCapturePane(rc, args)
case "display-message", "display", "displayp":
return tmuxDisplayMessage(rc, args)
case "list-windows", "lsw":
return tmuxListWindows(rc, args)
case "list-panes", "lsp":
return tmuxListPanes(rc, args)
case "rename-window", "renamew":
return tmuxRenameWindow(rc, args)
case "resize-pane", "resizep":
return tmuxResizePane(rc, args)
case "wait-for":
return tmuxWaitFor(rc, args)
case "last-pane":
return tmuxLastPane(rc, args)
case "has-session", "has":
return tmuxHasSession(rc, args)
case "select-layout":
return tmuxSelectLayout(rc, args)
case "show-buffer", "showb":
return tmuxShowBuffer(args)
case "save-buffer", "saveb":
return tmuxSaveBuffer(args)
// No-ops
case "set-option", "set", "set-window-option", "setw", "source-file",
"refresh-client", "attach-session", "detach-client",
"last-window", "next-window", "previous-window",
"set-hook", "set-buffer", "list-buffers":
return nil
default:
return fmt.Errorf("unsupported tmux command: %s", command)
}
}
// --- Command implementations ---
func tmuxNewSession(rc *rpcContext, args []string) error {
p := parseTmuxArgs(args, []string{"-c", "-F", "-n", "-s"}, []string{"-A", "-d", "-P"})
if p.hasFlag("-A") {
return fmt.Errorf("new-session -A is not supported")
}
params := map[string]any{"focus": false}
if cwd := p.value("-c"); cwd != "" {
params["cwd"] = cwd
}
created, err := rc.call("workspace.create", params)
if err != nil {
return err
}
wsId, _ := created["workspace_id"].(string)
if wsId == "" {
return fmt.Errorf("workspace.create did not return workspace_id")
}
if title := firstNonEmpty(p.value("-n"), p.value("-s")); strings.TrimSpace(title) != "" {
rc.call("workspace.rename", map[string]any{"workspace_id": wsId, "title": title})
}
if text := tmuxShellCommandText(p.positional, p.value("-c")); text != "" {
surfaceId, err := tmuxGetFirstSurface(rc, wsId)
if err == nil {
rc.call("surface.send_text", map[string]any{"workspace_id": wsId, "surface_id": surfaceId, "text": text})
}
}
if p.hasFlag("-P") {
ctx, err := tmuxFormatContext(rc, wsId, "", "")
if err != nil {
fmt.Printf("@%s\n", wsId)
return nil
}
fmt.Println(tmuxRenderFormat(p.value("-F"), ctx, "@"+wsId))
}
return nil
}
func tmuxNewWindow(rc *rpcContext, args []string) error {
p := parseTmuxArgs(args, []string{"-c", "-F", "-n", "-t"}, []string{"-d", "-P"})
params := map[string]any{"focus": false}
if cwd := p.value("-c"); cwd != "" {
params["cwd"] = cwd
}
created, err := rc.call("workspace.create", params)
if err != nil {
return err
}
wsId, _ := created["workspace_id"].(string)
if wsId == "" {
return fmt.Errorf("workspace.create did not return workspace_id")
}
if title := p.value("-n"); strings.TrimSpace(title) != "" {
rc.call("workspace.rename", map[string]any{"workspace_id": wsId, "title": title})
}
if text := tmuxShellCommandText(p.positional, p.value("-c")); text != "" {
surfaceId, err := tmuxGetFirstSurface(rc, wsId)
if err == nil {
rc.call("surface.send_text", map[string]any{"workspace_id": wsId, "surface_id": surfaceId, "text": text})
}
}
if p.hasFlag("-P") {
ctx, err := tmuxFormatContext(rc, wsId, "", "")
if err != nil {
fmt.Printf("@%s\n", wsId)
return nil
}
fmt.Println(tmuxRenderFormat(p.value("-F"), ctx, "@"+wsId))
}
return nil
}
func tmuxSplitWindow(rc *rpcContext, args []string) error {
p := parseTmuxArgs(args, []string{"-c", "-F", "-l", "-t"}, []string{"-P", "-b", "-d", "-h", "-v"})
targetWs, _, targetSurface, err := tmuxResolveSurfaceTarget(rc, p.value("-t"))
if err != nil {
return err
}
direction := "down"
if p.hasFlag("-h") {
direction = "right"
if p.hasFlag("-b") {
direction = "left"
}
} else if p.hasFlag("-b") {
direction = "up"
}
// Anchor splits to the leader surface for agent teams
callerSurface := tmuxCallerSurfaceHandle()
callerWorkspace := tmuxCallerWorkspaceHandle()
if callerSurface != "" && callerWorkspace != "" {
if wsId, err := tmuxResolveWorkspaceId(rc, callerWorkspace); err == nil {
store := loadTmuxCompatStore()
if mvState, ok := store.MainVerticalLayouts[wsId]; ok && mvState.LastColumnSurfaceId != "" {
targetWs = wsId
targetSurface = mvState.LastColumnSurfaceId
direction = "down"
} else {
targetWs = wsId
targetSurface = callerSurface
direction = "right"
}
}
}
focusNewPane := !p.hasFlag("-d")
created, err := rc.call("surface.split", map[string]any{
"workspace_id": targetWs,
"surface_id": targetSurface,
"direction": direction,
"focus": focusNewPane,
})
if err != nil {
return err
}
surfaceId, _ := created["surface_id"].(string)
if surfaceId == "" {
return fmt.Errorf("surface.split did not return surface_id")
}
newPaneId, _ := created["pane_id"].(string)
// Track for main-vertical layout
store := loadTmuxCompatStore()
store.LastSplitSurface[targetWs] = surfaceId
if _, ok := store.MainVerticalLayouts[targetWs]; ok {
mvs := store.MainVerticalLayouts[targetWs]
mvs.LastColumnSurfaceId = surfaceId
store.MainVerticalLayouts[targetWs] = mvs
} else if direction == "right" && callerSurface != "" {
store.MainVerticalLayouts[targetWs] = mainVerticalState{
MainSurfaceId: callerSurface,
LastColumnSurfaceId: surfaceId,
}
}
saveTmuxCompatStore(store)
// Equalize vertical splits
rc.call("workspace.equalize_splits", map[string]any{
"workspace_id": targetWs,
"orientation": "vertical",
})
if text := tmuxShellCommandText(p.positional, p.value("-c")); text != "" {
rc.call("surface.send_text", map[string]any{
"workspace_id": targetWs,
"surface_id": surfaceId,
"text": text,
})
}
if p.hasFlag("-P") {
ctx, err := tmuxFormatContext(rc, targetWs, newPaneId, surfaceId)
if err != nil {
fmt.Println(surfaceId)
return nil
}
fallback := surfaceId
if pid, ok := ctx["pane_id"]; ok {
fallback = pid
}
fmt.Println(tmuxRenderFormat(p.value("-F"), ctx, fallback))
}
return nil
}
func tmuxSelectWindow(rc *rpcContext, args []string) error {
p := parseTmuxArgs(args, []string{"-t"}, nil)
wsId, err := tmuxResolveWorkspaceTarget(rc, p.value("-t"))
if err != nil {
return err
}
_, err = rc.call("workspace.select", map[string]any{"workspace_id": wsId})
return err
}
func tmuxSelectPane(rc *rpcContext, args []string) error {
p := parseTmuxArgs(args, []string{"-P", "-T", "-t"}, nil)
// -P (style) and -T (title) are no-ops
if p.value("-P") != "" || p.value("-T") != "" {
return nil
}
wsId, paneId, err := tmuxResolvePaneTarget(rc, p.value("-t"))
if err != nil {
return err
}
_, err = rc.call("pane.focus", map[string]any{"workspace_id": wsId, "pane_id": paneId})
return err
}
func tmuxKillWindow(rc *rpcContext, args []string) error {
p := parseTmuxArgs(args, []string{"-t"}, nil)
wsId, err := tmuxResolveWorkspaceTarget(rc, p.value("-t"))
if err != nil {
return err
}
_, err = rc.call("workspace.close", map[string]any{"workspace_id": wsId})
return err
}
func tmuxKillPane(rc *rpcContext, args []string) error {
p := parseTmuxArgs(args, []string{"-t"}, nil)
wsId, _, surfId, err := tmuxResolveSurfaceTarget(rc, p.value("-t"))
if err != nil {
return err
}
_, err = rc.call("surface.close", map[string]any{"workspace_id": wsId, "surface_id": surfId})
if err != nil {
return err
}
// Re-equalize after removal
rc.call("workspace.equalize_splits", map[string]any{"workspace_id": wsId, "orientation": "vertical"})
return nil
}
func tmuxSendKeys(rc *rpcContext, args []string) error {
p := parseTmuxArgs(args, []string{"-t"}, []string{"-l"})
wsId, _, surfId, err := tmuxResolveSurfaceTarget(rc, p.value("-t"))
if err != nil {
return err
}
text := tmuxSendKeysText(p.positional, p.hasFlag("-l"))
if text != "" {
_, err = rc.call("surface.send_text", map[string]any{
"workspace_id": wsId,
"surface_id": surfId,
"text": text,
})
}
return err
}
func tmuxCapturePane(rc *rpcContext, args []string) error {
p := parseTmuxArgs(args, []string{"-E", "-S", "-t"}, []string{"-J", "-N", "-p"})
wsId, _, surfId, err := tmuxResolveSurfaceTarget(rc, p.value("-t"))
if err != nil {
return err
}
params := map[string]any{
"workspace_id": wsId,
"surface_id": surfId,
"scrollback": true,
}
if start := p.value("-S"); start != "" {
if lines := parseInt(start); lines < 0 {
params["lines"] = int(math.Abs(float64(lines)))
}
}
payload, err := rc.call("surface.read_text", params)
if err != nil {
return err
}
text, _ := payload["text"].(string)
if p.hasFlag("-p") {
fmt.Print(text)
} else {
store := loadTmuxCompatStore()
store.Buffers["default"] = text
saveTmuxCompatStore(store)
}
return nil
}
func tmuxDisplayMessage(rc *rpcContext, args []string) error {
p := parseTmuxArgs(args, []string{"-F", "-t"}, []string{"-p"})
wsId, paneId, surfId, err := tmuxResolveSurfaceTarget(rc, p.value("-t"))
if err != nil {
return err
}
ctx, err := tmuxFormatContext(rc, wsId, paneId, surfId)
if err != nil {
ctx = map[string]string{}
}
// Enrich with geometry
panePayload, err := rc.call("pane.list", map[string]any{"workspace_id": wsId})
if err == nil {
panes, _ := panePayload["panes"].([]any)
containerFrame, _ := panePayload["container_frame"].(map[string]any)
var matchingPane map[string]any
if paneId != "" {
for _, p := range panes {
pn, _ := p.(map[string]any)
if pid, _ := pn["id"].(string); pid == paneId {
matchingPane = pn
break
}
}
}
if matchingPane == nil {
for _, p := range panes {
pn, _ := p.(map[string]any)
if focused, _ := pn["focused"].(bool); focused {
matchingPane = pn
break
}
}
}
if matchingPane == nil && len(panes) > 0 {
matchingPane, _ = panes[0].(map[string]any)
}
if matchingPane != nil {
tmuxEnrichContextWithGeometry(ctx, matchingPane, containerFrame)
}
}
format := p.value("-F")
if len(p.positional) > 0 {
format = strings.Join(p.positional, " ")
}
rendered := tmuxRenderFormat(format, ctx, "")
if p.hasFlag("-p") || rendered != "" {
fmt.Println(rendered)
}
return nil
}
func tmuxListWindows(rc *rpcContext, args []string) error {
p := parseTmuxArgs(args, []string{"-F", "-t"}, nil)
items, err := tmuxWorkspaceItems(rc)
if err != nil {
return err
}
for _, item := range items {
wsId, _ := item["id"].(string)
if wsId == "" {
continue
}
ctx, err := tmuxFormatContext(rc, wsId, "", "")
if err != nil {
continue
}
fallback := ""
if idx, ok := ctx["window_index"]; ok {
fallback = idx
} else {
fallback = "?"
}
if name, ok := ctx["window_name"]; ok {
fallback += " " + name
} else {
fallback += " " + wsId
}
fmt.Println(tmuxRenderFormat(p.value("-F"), ctx, fallback))
}
return nil
}
func tmuxListPanes(rc *rpcContext, args []string) error {
p := parseTmuxArgs(args, []string{"-F", "-t"}, nil)
target := p.value("-t")
var wsId string
var err error
if target != "" && tmuxPaneSelector(target) != "" {
wsId, _, err = tmuxResolvePaneTarget(rc, target)
} else {
wsId, err = tmuxResolveWorkspaceTarget(rc, target)
}
if err != nil {
return err
}
payload, err := rc.call("pane.list", map[string]any{"workspace_id": wsId})
if err != nil {
return err
}
panes, _ := payload["panes"].([]any)
containerFrame, _ := payload["container_frame"].(map[string]any)
for _, p2 := range panes {
pane, _ := p2.(map[string]any)
if pane == nil {
continue
}
paneId, _ := pane["id"].(string)
if paneId == "" {
continue
}
ctx, err := tmuxFormatContext(rc, wsId, paneId, "")
if err != nil {
continue
}
tmuxEnrichContextWithGeometry(ctx, pane, containerFrame)
fallback := "%" + paneId
if pid, ok := ctx["pane_id"]; ok {
fallback = pid
}
fmt.Println(tmuxRenderFormat(p.value("-F"), ctx, fallback))
}
return nil
}
func tmuxRenameWindow(rc *rpcContext, args []string) error {
p := parseTmuxArgs(args, []string{"-t"}, nil)
title := strings.TrimSpace(strings.Join(p.positional, " "))
if title == "" {
return fmt.Errorf("rename-window requires a title")
}
wsId, err := tmuxResolveWorkspaceTarget(rc, p.value("-t"))
if err != nil {
return err
}
_, err = rc.call("workspace.rename", map[string]any{"workspace_id": wsId, "title": title})
return err
}
func tmuxResizePane(rc *rpcContext, args []string) error {
p := parseTmuxArgs(args, []string{"-t", "-x", "-y"}, []string{"-D", "-L", "-R", "-U"})
wsId, paneId, err := tmuxResolvePaneTarget(rc, p.value("-t"))
if err != nil {
return err
}
hasDirectional := p.hasFlag("-L") || p.hasFlag("-R") || p.hasFlag("-U") || p.hasFlag("-D")
if !hasDirectional {
if absWidthStr := p.value("-x"); absWidthStr != "" {
absWidth := parseInt(strings.ReplaceAll(absWidthStr, "%", ""))
// Get current width to compute delta
panePayload, err := rc.call("pane.list", map[string]any{"workspace_id": wsId})
if err != nil {
return err
}
panes, _ := panePayload["panes"].([]any)
for _, pp := range panes {
pane, _ := pp.(map[string]any)
if pane == nil {
continue
}
if pid, _ := pane["id"].(string); pid == paneId {
cellW := intFromAnyGo(pane["cell_width_px"])
currentCols := intFromAnyGo(pane["columns"])
if cellW > 0 && currentCols >= 0 {
delta := absWidth - currentCols
if delta != 0 {
dir := "right"
if delta < 0 {
dir = "left"
delta = -delta
}
rc.call("pane.resize", map[string]any{
"workspace_id": wsId,
"pane_id": paneId,
"direction": dir,
"amount": delta * cellW,
})
}
}
break
}
}
return nil
}
}
if hasDirectional {
dir := "right"
if p.hasFlag("-L") {
dir = "left"
} else if p.hasFlag("-U") {
dir = "up"
} else if p.hasFlag("-D") {
dir = "down"
}
rawAmount := firstNonEmpty(p.value("-x"), p.value("-y"), "5")
rawAmount = strings.ReplaceAll(rawAmount, "%", "")
amount := parseInt(rawAmount)
if amount <= 0 {
amount = 5
}
_, err := rc.call("pane.resize", map[string]any{
"workspace_id": wsId,
"pane_id": paneId,
"direction": dir,
"amount": amount,
})
return err
}
return nil
}
func tmuxWaitFor(_ *rpcContext, args []string) error {
p := parseTmuxArgs(args, []string{"--timeout"}, []string{"-S"})
name := ""
for _, pos := range p.positional {
if !strings.HasPrefix(pos, "-") {
name = pos
break
}
}
if name == "" {
return fmt.Errorf("wait-for requires a name")
}
signalPath := tmuxWaitForSignalPath(name)
if p.hasFlag("-S") {
// Signal mode: create the file
os.WriteFile(signalPath, []byte{}, 0644)
fmt.Println("OK")
return nil
}
// Wait mode: poll for the file
timeoutStr := p.value("--timeout")
timeout := 30.0
if timeoutStr != "" {
if t := parseFloat(timeoutStr); t > 0 {
timeout = t
}
}
deadline := time.Now().Add(time.Duration(timeout * float64(time.Second)))
for time.Now().Before(deadline) {
if _, err := os.Stat(signalPath); err == nil {
os.Remove(signalPath)
return nil
}
time.Sleep(50 * time.Millisecond)
}
return fmt.Errorf("wait-for timeout: %s", name)
}
func tmuxLastPane(rc *rpcContext, args []string) error {
p := parseTmuxArgs(args, []string{"-t"}, nil)
wsId, err := tmuxResolveWorkspaceTarget(rc, p.value("-t"))
if err != nil {
return err
}
_, err = rc.call("pane.last", map[string]any{"workspace_id": wsId})
return err
}
func tmuxHasSession(rc *rpcContext, args []string) error {
p := parseTmuxArgs(args, []string{"-t"}, nil)
_, err := tmuxResolveWorkspaceTarget(rc, p.value("-t"))
return err
}
func tmuxSelectLayout(rc *rpcContext, args []string) error {
p := parseTmuxArgs(args, []string{"-t"}, nil)
layoutName := ""
if len(p.positional) > 0 {
layoutName = p.positional[0]
}
// Resolve workspace from target (may be a pane reference)
var wsId string
var err error
if target := p.value("-t"); target != "" {
if tmuxPaneSelector(target) != "" {
wsId, _, err = tmuxResolvePaneTarget(rc, target)
} else {
wsId, err = tmuxResolveWorkspaceTarget(rc, target)
}
} else {
wsId, err = tmuxResolveWorkspaceTarget(rc, "")
}
if err != nil {
return err
}
if layoutName == "main-vertical" || layoutName == "main-horizontal" {
orientation := "vertical"
if layoutName == "main-horizontal" {
orientation = "horizontal"
}
rc.call("workspace.equalize_splits", map[string]any{
"workspace_id": wsId,
"orientation": orientation,
})
} else {
rc.call("workspace.equalize_splits", map[string]any{"workspace_id": wsId})
}
if layoutName == "main-vertical" {
if callerSurface := tmuxCallerSurfaceHandle(); callerSurface != "" {
store := loadTmuxCompatStore()
existingColumn := ""
if existing, ok := store.MainVerticalLayouts[wsId]; ok {
existingColumn = existing.LastColumnSurfaceId
}
seedColumn := existingColumn
if seedColumn == "" {
seedColumn = store.LastSplitSurface[wsId]
}
store.MainVerticalLayouts[wsId] = mainVerticalState{
MainSurfaceId: callerSurface,
LastColumnSurfaceId: seedColumn,
}
saveTmuxCompatStore(store)
}
} else if layoutName != "" {
store := loadTmuxCompatStore()
changed := false
if _, ok := store.MainVerticalLayouts[wsId]; ok {
delete(store.MainVerticalLayouts, wsId)
changed = true
}
if _, ok := store.LastSplitSurface[wsId]; ok {
delete(store.LastSplitSurface, wsId)
changed = true
}
if changed {
saveTmuxCompatStore(store)
}
}
return nil
}
func tmuxShowBuffer(args []string) error {
p := parseTmuxArgs(args, []string{"-b"}, nil)
name := p.value("-b")
if name == "" {
name = "default"
}
store := loadTmuxCompatStore()
if buf, ok := store.Buffers[name]; ok {
fmt.Print(buf)
}
return nil
}
func tmuxSaveBuffer(args []string) error {
p := parseTmuxArgs(args, []string{"-b"}, nil)
name := p.value("-b")
if name == "" {
name = "default"
}
store := loadTmuxCompatStore()
buf, ok := store.Buffers[name]
if !ok {
return fmt.Errorf("buffer not found: %s", name)
}
if len(p.positional) > 0 {
outputPath := strings.TrimSpace(p.positional[len(p.positional)-1])
if outputPath != "" {
return os.WriteFile(outputPath, []byte(buf), 0644)
}
}
fmt.Print(buf)
return nil
}
// --- Helpers ---
func tmuxGetFirstSurface(rc *rpcContext, workspaceId string) (string, error) {
payload, err := rc.call("surface.list", map[string]any{"workspace_id": workspaceId})
if err != nil {
return "", err
}
surfaces, _ := payload["surfaces"].([]any)
if len(surfaces) == 0 {
return "", fmt.Errorf("workspace has no surfaces")
}
// Prefer focused surface
for _, s := range surfaces {
surf, _ := s.(map[string]any)
if focused, _ := surf["focused"].(bool); focused {
if id, _ := surf["id"].(string); id != "" {
return id, nil
}
}
}
if surf, ok := surfaces[0].(map[string]any); ok {
if id, _ := surf["id"].(string); id != "" {
return id, nil
}
}
return "", fmt.Errorf("workspace has no surfaces")
}
func firstNonEmpty(values ...string) string {
for _, v := range values {
if v != "" {
return v
}
}
return ""
}
func parseInt(s string) int {
s = strings.TrimSpace(s)
var n int
fmt.Sscanf(s, "%d", &n)
return n
}
func parseFloat(s string) float64 {
s = strings.TrimSpace(s)
var f float64
fmt.Sscanf(s, "%f", &f)
return f
}