Coverage report: /home/ellis/comp/core/lib/cli/tools/tmux.lisp

KindCoveredAll%
expression0182 0.0
branch08 0.0
Key
Not instrumented
Conditionalized out
Executed
Not executed
 
Both branches taken
One branch taken
Neither branch taken
1
 ;;; cli/tmux.lisp --- Tmux Tools
2
 
3
 ;; Control Tmux from Lisp
4
 
5
 ;;; Commentary:
6
 
7
 ;; ref: https://github.com/tmux/tmux/wiki/Getting-Started#getting-started
8
 
9
 ;; By default tmux tries to open a TTY and errors when it can't, so normally
10
 ;; you'd want to use SPAWN-TMUX to allocate a terminal first.
11
 
12
 ;; There is however a control-mode available which provides a text-based
13
 ;; channel without needing a TTY.
14
 
15
 ;; To use this mode call RUN-TMUX with the "-C" arg.
16
 
17
 ;; ref: https://github.com/tmux/tmux/wiki/Control-Mode#control-mode
18
 
19
 ;;; Code:
20
 (in-package :cli/tools/tmux)
21
 
22
 (defparameter *tmux-user-config-path* (merge-pathnames ".tmux.conf" (user-homedir-pathname)))
23
 (defparameter *tmux-system-config-path* (merge-pathnames "tmux.conf" "/etc/"))
24
 
25
 (defparameter *tmux* (find-exe "tmux"))
26
 
27
 (defparameter *default-tmux-tmpdir* (pathname (format nil "/tmp/tmux-~A/" (sb-posix:getuid))))
28
 (defparameter *default-tmux-socket* (merge-pathnames "default" *default-tmux-tmpdir*))
29
 
30
 ;;; Utils
31
 (define-cli-tool :tmux (&rest args)
32
   (let ((proc (sb-ext:run-program *tmux* (or args nil) :output t)))
33
     (unless (eq 0 (sb-ext:process-exit-code proc))
34
       (tmux-error "tmux command failed: ~A ~A" args))))
35
 
36
 (defun spawn-tmux (&rest args)
37
   (run-term (append (list "-e" "tmux") args)))
38
 
39
 ;;; Session > Window > Pane
40
 (defstruct tmux-session
41
   (id 0 :type fixnum)
42
   name
43
   (windows nil :type list))
44
 
45
 (defstruct tmux-window
46
   (id 0 :type fixnum)
47
   name
48
   (panes nil :type list)
49
   layout)
50
 
51
 (defstruct tmux-pane
52
   (id 0 :type fixnum)
53
   name)
54
 
55
 ;;; Controller
56
 (defstruct tmux-controller
57
   (input nil :type (or null sb-sys:fd-stream))
58
   (output nil :type (or null sb-sys:fd-stream))
59
   (silent nil :type boolean))
60
 
61
 (defun run-tmux-controller (&rest args)
62
   (sb-ext:run-program *tmux* (or args nil) :output :stream :input :stream))
63
 
64
 (defun init-tmux-controller (ctrl &rest args)
65
   (let ((proc (funcall
66
                #'run-tmux-controller
67
                (if (tmux-controller-silent ctrl) "-CC" "-C")
68
                args)))
69
     (setf (tmux-controller-output ctrl) (sb-ext:process-output proc)
70
           (tmux-controller-input ctrl) (sb-ext:process-input proc))
71
     ctrl))
72
 
73
 (defun write-tmux-line (ctrl string)
74
   (write-line string (tmux-controller-input ctrl)))
75
 
76
 (defun read-tmux-line (ctrl)
77
   (read-line (tmux-controller-output ctrl)))
78
 
79
 (defstruct tmux-command name flags args)
80
 
81
 (defun parse-tmux-command (str)
82
   "Parse a single TMUX-COMMAND from a string."
83
   (let ((words (split-sequence #\space str)))
84
     ;; TODO 2024-08-06: parse for real
85
     (make-tmux-command :name (car words) :args (cdr words))))
86
 
87
 (defconfig tmux-config ()
88
   ((commands :initform nil)
89
    (server-options :type hash-table)
90
    (session-options :type hash-table)
91
    (window-options :type hash-table)
92
    (keys :type hash-table))
93
   (:documentation "A CONFIG object containing the parsed content of a tmux configuration file."))
94
 
95
 (defmethod make-config ((obj (eql :tmux)) &key commands server session window keys)
96
   (let ((config (make-instance 'tmux-config)))
97
     (when commands (setf (slot-value config 'commands) commands))
98
     (when server (setf (slot-value config 'server-options) server))
99
     (when session (setf (slot-value config 'session-options) session))
100
     (when window (setf (slot-value config 'window-options) window))
101
     (when keys (setf (slot-value config 'keys) keys))
102
     config))
103
 
104
 (defmethod find-config ((obj (eql :tmux)) &key system user)
105
   "Find a tmux configuration and load it.
106
 
107
 When SYSTEM is non-nil, skip check for user config.
108
 
109
 When USER is non-nil it should be the name of a user whose config will be loaded
110
 from /home/USER/.tmux.conf."
111
   (let ((path (cond
112
                 (system (probe-file *tmux-system-config-path*))
113
                 (user (probe-file (format nil "/home/~A/.tmux.conf" user)))
114
                 (t (or (probe-file *tmux-user-config-path*) (probe-file *tmux-system-config-path*)))))
115
         (obj (make-config :tmux :commands nil)))
116
     (with-open-file (file path)
117
       (with-output-to-string (str)
118
         (loop for l = (read-line file nil nil)
119
               while l 
120
               unless (or (zerop (length l)) (equal (char l 0) #\#))
121
               do (push (parse-tmux-command l) (slot-value obj 'commands)))))
122
     obj))
123
 
124
 ;; (describe (find-config :tmux))
125
 
126
 ;;; Format Strings
127
 (defun format-tmux-string (dst fmt &rest args)
128
   (apply #'format dst fmt (mapcar (lambda (a) (format nil "#{~A}" a)) args)))
129
 
130
 (defvar *tmux-var-table* (make-hash-table))
131
 
132
 (defmacro tmux-format (dst fmt &rest args)
133
   "Format a tmux string, replacing symbols in ARGS that match a member of
134
 *TMUX-VARIABLES* with their corresponding lower-case name."
135
   `(format-tmux-string ,dst ,fmt
136
                        ,@(mapcar (lambda (a)
137
                                    (gethash (symbolicate a) *tmux-var-table* a))
138
                                  args)))
139
 (declaim ((vector symbol) *tmux-variables*))
140
 (defvar *tmux-variables*
141
   #(:active-window-index ;; Index of active window in session
142
     :alternate-on ;; 1 if pane is in alternate screen
143
     :alternate-saved-x ;; Saved cursor X in alternate screen
144
     :alternate-saved-y ;; Saved cursor Y in alternate screen
145
     :buffer-created ;; Time buffer created
146
     :buffer-name ;; Name of buffer
147
     :buffer-sample ;; Sample of start of buffer
148
     :buffer-size ;; Size of the specified buffer in bytes
149
     :client-activity ;; Time client last had activity
150
     :client-cell-height ;; Height of each client cell in pixels
151
     :client-cell-width ;; Width of each client cell in pixels
152
     :client-control-mode ;; 1 if client is in control mode
153
     :client-created ;; Time client created
154
     :client-discarded ;; Bytes discarded when client behind
155
     :client-flags ;; List of client flags
156
     :client-height ;; Height of client
157
     :client-key-table ;; Current key table
158
     :client-last-session ;; Name of the client's last session
159
     :client-name ;; Name of client
160
     :client-pid ;; PID of client process
161
     :client-prefix ;; 1 if prefix key has been pressed
162
     :client-readonly ;; 1 if client is read-only
163
     :client-session ;; Name of the client's session
164
     :client-termfeatures ;; Terminal features of client, if any
165
     :client-termname ;; Terminal name of client
166
     :client-termtype ;; Terminal type of client, if available
167
     :client-tty ;; Pseudo terminal of client
168
     :client-uid ;; UID of client process
169
     :client-user ;; User of client process
170
     :client-utf8 ;; 1 if client supports UTF-8
171
     :client-width ;; Width of client
172
     :client-written ;; Bytes written to client
173
     :command ;; Name of command in use, if any
174
     :command-list-alias ;; Command alias if listing commands
175
     :command-list-name ;; Command name if listing commands
176
     :command-list-usage ;; Command usage if listing commands
177
     :config-files ;; List of configuration files loaded
178
     :copy-cursor-line ;; Line the cursor is on in copy mode
179
     :copy-cursor-word ;; Word under cursor in copy mode
180
     :copy-cursor-x ;; Cursor X position in copy mode
181
     :copy-cursor-y ;; Cursor Y position in copy mode
182
     :current-file ;; Current configuration file
183
     :cursor-character ;; Character at cursor in pane
184
     :cursor-flag ;; Pane cursor flag
185
     :cursor-x ;; Cursor X position in pane
186
     :cursor-y ;; Cursor Y position in pane
187
     :history-bytes ;; Number of bytes in window history
188
     :history-limit ;; Maximum window history lines
189
     :history-size ;; Size of history in lines
190
     :hook ;; Name of running hook, if any
191
     :hook-client ;; Name of client where hook was run, if any
192
     :hook-pane ;; ID of pane where hook was run, if any
193
     :hook-session ;; ID of session where hook was run, if any
194
     :hook-session-name ;; Name of session where hook was run, if any
195
     :hook-window ;; ID of window where hook was run, if any
196
     :hook-window-name ;; Name of window where hook was run, if any
197
     :host ;; H  Hostname of local host
198
     :host-short ;; h    Hostname of local host (no domain name)
199
     :insert-flag ;; Pane insert flag
200
     :keypad-cursor-flag ;; Pane keypad cursor flag
201
     :keypad-flag ;; Pane keypad flag
202
     :last-window-index ;; Index of last window in session
203
     :line ;; Line number in the list
204
     :mouse-all-flag ;; Pane mouse all flag
205
     :mouse-any-flag ;; Pane mouse any flag
206
     :mouse-button-flag ;; Pane mouse button flag
207
     :mouse-hyperlink ;; Hyperlink under mouse, if any
208
     :mouse-line ;; Line under mouse, if any
209
     :mouse-sgr-flag ;; Pane mouse SGR flag
210
     :mouse-standard-flag ;; Pane mouse standard flag
211
     :mouse-status-line ;; Status line on which mouse event took place
212
     :mouse-status-range ;; Range type or argument of mouse event on status line
213
     :mouse-utf8-flag ;; Pane mouse UTF-8 flag
214
     :mouse-word ;; Word under mouse, if any
215
     :mouse-x ;; Mouse X position, if any
216
     :mouse-y ;; Mouse Y position, if any
217
     :next-session-id ;; Unique session ID for next new session
218
     :origin-flag ;; Pane origin flag
219
     :pane-active ;; 1 if active pane
220
     :pane-at-bottom ;; 1 if pane is at the bottom of window
221
     :pane-at-left ;; 1 if pane is at the left of window
222
     :pane-at-right ;; 1 if pane is at the right of window
223
     :pane-at-top ;; 1 if pane is at the top of window
224
     :pane-bg ;; Pane background colour
225
     :pane-bottom ;; Bottom of pane
226
     :pane-current-command ;; Current command if available
227
     :pane-current-path ;; Current path if available
228
     :pane-dead ;; 1 if pane is dead
229
     :pane-dead-signal ;; Exit signal of process in dead pane
230
     :pane-dead-status ;; Exit status of process in dead pane
231
     :pane-dead-time ;; Exit time of process in dead pane
232
     :pane-fg ;; Pane foreground colour
233
     :pane-format ;; 1 if format is for a pane
234
     :pane-height ;; Height of pane
235
     :pane-id ;; D       Unique pane ID
236
     :pane-in-mode ;; 1 if pane is in a mode
237
     :pane-index ;; P    Index of pane
238
     :pane-input-off ;; 1 if input to pane is disabled
239
     :pane-last ;; 1 if last pane
240
     :pane-left ;; Left of pane
241
     :pane-marked ;; 1 if this is the marked pane
242
     :pane-marked-set ;; 1 if a marked pane is set
243
     :pane-mode ;; Name of pane mode, if any
244
     :pane-path ;; Path of pane (can be set by application)
245
     :pane-pid ;; PID of first process in pane
246
     :pane-pipe ;; 1 if pane is being piped
247
     :pane-right ;; Right of pane
248
     :pane-search-string ;; Last search string in copy mode
249
     :pane-start-command ;; Command pane started with
250
     :pane-start-path ;; Path pane started with
251
     :pane-synchronized ;; 1 if pane is synchronized
252
     :pane-tabs ;; Pane tab positions
253
     :pane-title ;; T    Title of pane (can be set by application)
254
     :pane-top ;; Top of pane
255
     :pane-tty ;; Pseudo terminal of pane
256
     :pane-unseen-changes ;; 1 if there were changes in pane while in mode
257
     :pane-width ;; Width of pane
258
     :pid ;; Server PID
259
     :rectangle-toggle ;; 1 if rectangle selection is activated
260
     :scroll-position ;; Scroll position in copy mode
261
     :scroll-region-lower ;; Bottom of scroll region in pane
262
     :scroll-region-upper ;; Top of scroll region in pane
263
     :search-match ;; Search match if any
264
     :search-present ;; 1 if search started in copy mode
265
     :selection-active ;; 1 if selection started and changes with the cursor in copy mode
266
     :selection-end-x ;; X position of the end of the selection
267
     :selection-end-y ;; Y position of the end of the selection
268
     :selection-present ;; 1 if selection started in copy mode
269
     :selection-start-x ;; X position of the start of the selection
270
     :selection-start-y ;; Y position of the start of the selection
271
     :server-sessions ;; Number of sessions
272
     :session-activity ;; Time of session last activity
273
     :session-alerts ;; List of window indexes with alerts
274
     :session-attached ;; Number of clients session is attached to
275
     :session-attached-list ;; List of clients session is attached to
276
     :session-created ;; Time session created
277
     :session-format ;; 1 if format is for a session
278
     :session-group ;; Name of session group
279
     :session-group-attached ;; Number of clients sessions in group are attached to
280
     :session-group-attached-list ;; List of clients sessions in group are attached to
281
     :session-group-list ;; List of sessions in group
282
     :session-group-many-attached ;; 1 if multiple clients attached to sessions in group
283
     :session-group-size ;; Size of session group
284
     :session-grouped ;; 1 if session in a group
285
     :session-id ;; Unique session ID
286
     :session-last-attached ;; Time session last attached
287
     :session-many-attached ;; 1 if multiple clients attached
288
     :session-marked ;; 1 if this session contains the marked pane
289
     :session-name ;; S  Name of session
290
     :session-path ;; Working directory of session
291
     :session-stack ;; Window indexes in most recent order
292
     :session-windows ;; Number of windows in session
293
     :socket-path ;; Server socket path
294
     :start-time ;; Server start time
295
     :uid ;; Server UID
296
     :user ;; Server user
297
     :version ;; Server version
298
     :window-active ;; 1 if window active
299
     :window-active-clients ;; Number of clients viewing this window
300
     :window-active-clients-list ;; List of clients viewing this window
301
     :window-active-sessions ;; Number of sessions on which this window is active
302
     :window-active-sessions-list ;; List of sessions on which this window is active
303
     :window-activity ;; Time of window last activity
304
     :window-activity-flag ;; 1 if window has activity
305
     :window-bell-flag ;; 1 if window has bell
306
     :window-bigger ;; 1 if window is larger than client
307
     :window-cell-height ;; Height of each cell in pixels
308
     :window-cell-width ;; Width of each cell in pixels
309
     :window-end-flag ;; 1 if window has the highest index
310
     :window-flags ;; F  Window flags with # escaped as ##
311
     :window-format ;; 1 if format is for a window
312
     :window-height ;; Height of window
313
     :window-id ;; Unique window ID
314
     :window-index ;; I  Index of window
315
     :window-last-flag ;; 1 if window is the last used
316
     :window-layout ;; Window layout description, ignoring zoomed window panes
317
     :window-linked ;; 1 if window is linked across sessions
318
     :window-linked-sessions ;; Number of sessions this window is linked to
319
     :window-linked-sessions-list ;; List of sessions this window is linked to
320
     :window-marked-flag ;; 1 if window contains the marked pane
321
     :window-name ;; W   Name of window
322
     :window-offset-x ;; X offset into window if larger than client
323
     :window-offset-y ;; Y offset into window if larger than client
324
     :window-panes ;; Number of panes in window
325
     :window-raw-flags ;; Window flags with nothing escaped
326
     :window-silence-flag ;; 1 if window has silence alert
327
     :window-stack-index ;; Index in session most recent stack
328
     :window-start-flag ;; 1 if window has the lowest index
329
     :window-visible-layout ;; Window layout description, respecting zoomed window panes
330
     :window-width ;; Width of window
331
     :window-zoomed-flag ;; 1 if window is zoomed
332
     :wrap-flag ;; Pane wrap flag
333
     ;; display-menu vars
334
     :popup-centre-x ;; Centered in the client
335
     :popup-centre-y ;; entered in the client
336
     :popup-height ;; eight of menu or popup
337
     :popup-mouse-bottom ;; ottom of at the mouse
338
     :popup-mouse-centre-x ;; orizontal centre at the mouse
339
     :popup-mouse-centre-y ;; ertical centre at the mouse
340
     :popup-mouse-top ;; op at the mouse
341
     :popup-mouse-x ;; ouse X position
342
     :popup-mouse-y ;; ouse Y position
343
     :popup-pane-bottom ;; ottom of the pane
344
     :popup-pane-left ;; eft of the pane
345
     :popup-pane-right ;; ight of the pane
346
     :popup-pane-top ;; op of the pane
347
     :popup-status-line-y ;; bove or below the status line
348
     :popup-width ;; idth of menu or popup
349
     :popup-window-status-line-x ;; t the window position in status line
350
     :popup-window-status-line-y)) ;; t the status line showing the window
351
 
352
 (defvar *tmux-variable-names*
353
   (coerce 
354
    (loop for v across *tmux-variables*
355
          collect (string-downcase (substitute #\_ #\- (symbol-name v))))
356
    '(vector string)))