-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtmux.go
More file actions
155 lines (138 loc) · 6.14 KB
/
tmux.go
File metadata and controls
155 lines (138 loc) · 6.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
package main
import (
"fmt"
"os/exec"
"strings"
)
// tmuxSocket is the path to the dedicated tmux server socket for this repo.
// Set once at startup via initTmuxSocket. All tmux commands use this socket
// so tulip sessions are completely isolated from the user's own tmux.
var tmuxSocket string
// initTmuxSocket sets the socket path derived from the repo's .tulip directory.
func initTmuxSocket(tulipDir string) {
tmuxSocket = tulipDir + "/tmux.sock"
}
// tmuxArgs prepends the -S socket flag when a socket has been configured.
func tmuxArgs(args []string) []string {
if tmuxSocket == "" {
return args
}
return append([]string{"-S", tmuxSocket}, args...)
}
// tmuxRun runs a tmux command with the given arguments.
func tmuxRun(args ...string) error {
cmd := exec.Command("tmux", tmuxArgs(args)...)
return cmd.Run()
}
// tmuxHasSession returns true if a tmux session with the given name exists.
func tmuxHasSession(name string) bool {
cmd := exec.Command("tmux", tmuxArgs([]string{"has-session", "-t", name})...)
return cmd.Run() == nil
}
// tmuxApplyGlobalSettings applies all global tmux options and key bindings.
// It is called both when creating a new session and at tulip startup so that
// settings are always current even if the tmux server was started by an older
// version of tulip.
func tmuxApplyGlobalSettings() {
_ = tmuxRun("set-option", "-g", "status", "on")
_ = tmuxRun("set-option", "-g", "status-style", "bg=colour235,fg=colour245")
_ = tmuxRun("set-option", "-g", "status-left", "#[fg=colour6,bold] #{s|watch/.*|Graft Debug|:#{s|shell/.*|Shell|:#{s|claude|Claude Code|:#{window_name}}}} #[nobold,fg=colour8]— #[fg=colour245]#{@branch}#{?#{==:#{@grafting},active}, #[fg=colour2]graft: active,#{?#{==:#{@grafting},failed}, #[fg=colour1]graft: failed,}} #[bg=colour240,fg=colour255] #{?pane_in_mode,selecting…,copy} ")
_ = tmuxRun("set-option", "-g", "status-left-length", "80")
_ = tmuxRun("set-option", "-g", "status-right", "#[bg=colour240,fg=colour255] ← back to tulip #[default]")
_ = tmuxRun("set-option", "-g", "status-right-length", "22")
_ = tmuxRun("set-option", "-g", "window-status-format", "")
_ = tmuxRun("set-option", "-g", "window-status-current-format", "")
_ = tmuxRun("set-option", "-g", "window-status-separator", "")
_ = tmuxRun("set-option", "-g", "mouse", "on")
_ = tmuxRun("set-option", "-g", "set-clipboard", "off")
_ = tmuxRun("bind-key", "-T", "copy-mode", "MouseDragEnd1Pane", "send-keys", "-X", "copy-pipe-and-cancel", "pbcopy")
// Remove legacy root-table MouseDragEnd1Pane that older tulip versions set.
_ = tmuxRun("unbind-key", "-n", "MouseDragEnd1Pane")
_ = tmuxRun("bind-key", "-n", "MouseDown1StatusRight", "detach-client")
_ = tmuxRun("bind-key", "-n", "MouseUp1StatusRight", "detach-client")
_ = tmuxRun("bind-key", "-T", "copy-mode", "MouseDown1StatusRight", "detach-client")
_ = tmuxRun("bind-key", "-T", "copy-mode", "MouseUp1StatusRight", "detach-client")
_ = tmuxRun("bind-key", "-n", "MouseDown1StatusLeft", "copy-mode")
}
// tmuxNewSession creates a new detached tmux session with the given name, starting in startDir.
// branch is stored as a session variable so the status bar can display it.
func tmuxNewSession(name, branch, startDir string) error {
if err := tmuxRun("new-session", "-d", "-s", name, "-n", "claude", "-c", startDir); err != nil {
return err
}
_ = tmuxRun("set-option", "-t", name, "@branch", branch)
tmuxApplyGlobalSettings()
return nil
}
// tmuxSendKeys sends a command followed by Enter to the given tmux session.
func tmuxSendKeys(session, command string) error {
return tmuxRun("send-keys", "-t", session, command, "Enter")
}
// tmuxNewWindow creates a new detached window in the given session, starting in startDir.
// If command is non-empty it is passed directly to new-window so the window's lifetime
// is tied to the process — when the command exits, the window closes.
func tmuxNewWindow(session, name, startDir, command string) error {
args := []string{"new-window", "-d", "-t", session, "-n", name, "-c", startDir}
if command != "" {
args = append(args, command)
}
return tmuxRun(args...)
}
// tmuxHasWindow returns true if a window with the given name exists in the session.
func tmuxHasWindow(session, window string) bool {
cmd := exec.Command("tmux", tmuxArgs([]string{"list-windows", "-t", session, "-F", "#{window_name}"})...)
out, err := cmd.Output()
if err != nil {
return false
}
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
if strings.TrimSpace(line) == window {
return true
}
}
return false
}
// tmuxKillWindow kills a named window inside a session. No-op if it doesn't exist.
func tmuxKillWindow(session, window string) {
_ = tmuxRun("kill-window", "-t", session+":"+window)
}
// tmuxSetWindowOption sets a tmux window option on a specific window.
func tmuxSetWindowOption(session, window, option, value string) {
_ = tmuxRun("set-window-option", "-t", session+":"+window, option, value)
}
// tmuxIsWindowDead returns true if the window exists but its pane has exited.
func tmuxIsWindowDead(session, window string) bool {
cmd := exec.Command("tmux", tmuxArgs([]string{
"display-message", "-p", "-t", session + ":" + window, "#{pane_dead}",
})...)
out, err := cmd.Output()
if err != nil {
return false
}
return strings.TrimSpace(string(out)) == "1"
}
// tmuxWindowExitStatus returns the exit status of a dead pane, or -1 on error.
func tmuxWindowExitStatus(session, window string) int {
cmd := exec.Command("tmux", tmuxArgs([]string{
"display-message", "-p", "-t", session + ":" + window, "#{pane_dead_status}",
})...)
out, err := cmd.Output()
if err != nil {
return -1
}
var code int
fmt.Sscanf(strings.TrimSpace(string(out)), "%d", &code)
return code
}
// tmuxKillSession kills a tmux session by name. If the session doesn't exist, it's a no-op.
func tmuxKillSession(name string) error {
if !tmuxHasSession(name) {
return nil
}
return tmuxRun("kill-session", "-t", name)
}
// tmuxSetGraftStatus updates the @grafting session variable so the status bar
// reflects the current graft state: "", "active", or "failed".
func tmuxSetGraftStatus(session, status string) {
_ = tmuxRun("set-option", "-t", session, "@grafting", status)
}