Skip to content

Commit 67cdbe0

Browse files
authored
fix: stabilize tmux pane targeting and workspace layouts
Fix tmux pane targeting for dotted window names and automatically apply workspace layouts after setup and workspace switches.
1 parent eaa6887 commit 67cdbe0

10 files changed

Lines changed: 446 additions & 103 deletions

enkan-repl-terminal.el

Lines changed: 107 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -368,18 +368,32 @@ or nil on non-zero exit. Otherwise return t on zero exit, nil on non-zero."
368368

369369
(defun enkan-repl--terminal-tmux--list-window-cwds (session)
370370
"Return list of (WINDOW . CWD) pairs in tmux SESSION."
371+
(mapcar
372+
(lambda (info)
373+
(cons (plist-get info :window)
374+
(plist-get info :cwd)))
375+
(enkan-repl--terminal-tmux--list-window-info session)))
376+
377+
(defun enkan-repl--terminal-tmux--list-window-info (session)
378+
"Return plist entries describing tmux windows in SESSION.
379+
Each entry contains :window, :cwd, and :pane. :pane is the stable tmux
380+
pane id (for example, \"%12\") used for command targets."
371381
(let ((out (enkan-repl--terminal-tmux--call
372382
(list "list-windows" "-t" session "-F"
373-
"#{window_name}\t#{pane_current_path}")
383+
"#{window_name}\t#{pane_current_path}\t#{pane_id}")
374384
t)))
375385
(when (and out (not (string-empty-p out)))
376386
(delq
377387
nil
378388
(mapcar
379389
(lambda (line)
380-
(pcase-let ((`(,window ,cwd) (split-string line "\t")))
381-
(when (and window cwd (not (string-empty-p window)))
382-
(cons window (unless (string-empty-p cwd) cwd)))))
390+
(pcase-let ((`(,window ,cwd ,pane) (split-string line "\t")))
391+
(when (and window (not (string-empty-p window)))
392+
(list :window window
393+
:cwd (unless (string-empty-p cwd) cwd)
394+
:pane (and pane
395+
(not (string-empty-p pane))
396+
pane)))))
383397
(split-string out "\n" t))))))
384398

385399
(defun enkan-repl--terminal-tmux--derive-base-name (dir)
@@ -399,20 +413,38 @@ Tries BASE, then BASE-2, BASE-3, ... until an unused name is found."
399413
(setq candidate (format "%s-%d" base n)))
400414
candidate))
401415

402-
(defun enkan-repl--terminal-tmux--make-id (session window)
403-
"Return tmux target identifier for SESSION:WINDOW."
404-
(format "%s:%s" session window))
416+
(defun enkan-repl--terminal-tmux--make-id (session window &optional pane)
417+
"Return enkan tmux identifier for SESSION, WINDOW, and optional PANE.
418+
When PANE is available, the identifier keeps WINDOW for Emacs-side metadata
419+
but tmux commands target the stable PANE id. This avoids tmux parsing window
420+
names such as dr-remote.jp as pane selectors."
421+
(if (and (stringp pane) (not (string-empty-p pane)))
422+
(format "%s:%s|%s" session window pane)
423+
(format "%s:%s" session window)))
405424

406425
(defun enkan-repl--terminal-tmux--id-window (id)
407-
"Return the window component of tmux ID (after the colon)."
426+
"Return the window component of tmux ID."
408427
(when (and (stringp id) (string-match ":\\(.+\\)$" id))
428+
(let ((window (match-string 1 id)))
429+
(if (string-match "\\`\\(.*\\)|%[0-9]+\\'" window)
430+
(match-string 1 window)
431+
window))))
432+
433+
(defun enkan-repl--terminal-tmux--id-pane (id)
434+
"Return the pane id component of tmux ID, or nil for legacy IDs."
435+
(when (and (stringp id) (string-match "|\\(%[0-9]+\\)\\'" id))
409436
(match-string 1 id)))
410437

411438
(defun enkan-repl--terminal-tmux--id-session (id)
412439
"Return the session component of tmux ID (before the colon)."
413440
(when (and (stringp id) (string-match "^\\([^:]+\\):" id))
414441
(match-string 1 id)))
415442

443+
(defun enkan-repl--terminal-tmux--target (id)
444+
"Return the actual tmux target for enkan tmux ID."
445+
(or (enkan-repl--terminal-tmux--id-pane id)
446+
id))
447+
416448
(defun enkan-repl--terminal-tmux-start (dir)
417449
"Tmux backend: start a new session/window in DIR for current workspace.
418450
Ensures the workspace's tmux session exists (creating it on demand) and
@@ -426,27 +458,37 @@ target identifier (e.g. \"enkan-01:lat\")."
426458
(cond
427459
;; Session does not exist: create it with the first window in DIR.
428460
((not (enkan-repl--terminal-tmux--has-session session))
429-
(unless (enkan-repl--terminal-tmux--call
430-
(list "new-session" "-d" "-s" session "-c" cdir "-n" base))
431-
(user-error "Tmux new-session failed for %s" session))
432-
(enkan-repl--terminal-tmux--make-id session base))
461+
(let ((pane (enkan-repl--terminal-tmux--call
462+
(list "new-session" "-d" "-P" "-F" "#{pane_id}"
463+
"-s" session "-c" cdir "-n" base)
464+
t)))
465+
(unless (and pane (not (string-empty-p pane)))
466+
(user-error "Tmux new-session failed for %s" session))
467+
(enkan-repl--terminal-tmux--make-id session base pane)))
433468
;; Session exists: add a new window with a non-colliding name.
434469
(t
435470
(let ((win (enkan-repl--terminal-tmux--next-instance-name session base)))
436-
(unless (enkan-repl--terminal-tmux--call
437-
(list "new-window" "-t" session "-n" win "-c" cdir))
438-
(user-error "Tmux new-window failed for %s:%s" session win))
439-
(enkan-repl--terminal-tmux--make-id session win))))))
471+
(let ((pane (enkan-repl--terminal-tmux--call
472+
(list "new-window" "-P" "-F" "#{pane_id}"
473+
"-t" session "-n" win "-c" cdir)
474+
t)))
475+
(unless (and pane (not (string-empty-p pane)))
476+
(user-error "Tmux new-window failed for %s:%s" session win))
477+
(enkan-repl--terminal-tmux--make-id session win pane)))))))
440478

441479
(defun enkan-repl--terminal-tmux-send (id text &optional newline)
442480
"Tmux backend: send TEXT to ID via send-keys -l.
443481
When NEWLINE is non-nil, follow with an Enter key. Returns t on success."
444482
(when (and id text)
445483
(and (enkan-repl--terminal-tmux--call
446-
(list "send-keys" "-t" id "-l" text))
484+
(list "send-keys" "-t"
485+
(enkan-repl--terminal-tmux--target id)
486+
"-l" text))
447487
(or (not newline)
448488
(enkan-repl--terminal-tmux--call
449-
(list "send-keys" "-t" id "Enter"))))))
489+
(list "send-keys" "-t"
490+
(enkan-repl--terminal-tmux--target id)
491+
"Enter"))))))
450492

451493
(defun enkan-repl--terminal-tmux-send-key (id key)
452494
"Tmux backend: send special KEY (`escape', `enter', integer 1..9) to ID."
@@ -455,29 +497,42 @@ When NEWLINE is non-nil, follow with an Enter key. Returns t on success."
455497
('enter "Enter")
456498
((pred integerp) (number-to-string key))
457499
(_ (user-error "Unsupported terminal key: %S" key)))))
458-
(enkan-repl--terminal-tmux--call (list "send-keys" "-t" id arg))))
500+
(enkan-repl--terminal-tmux--call
501+
(list "send-keys" "-t" (enkan-repl--terminal-tmux--target id) arg))))
459502

460503
(defun enkan-repl--terminal-tmux-alive-p (id)
461504
"Tmux backend: t if SESSION:WINDOW described by ID still exists."
462505
(when id
463506
(let ((session (enkan-repl--terminal-tmux--id-session id))
507+
(pane (enkan-repl--terminal-tmux--id-pane id))
464508
(window (enkan-repl--terminal-tmux--id-window id)))
465-
(and session window
509+
(and session
466510
(enkan-repl--terminal-tmux--has-session session)
467-
(member window (enkan-repl--terminal-tmux--list-windows session))
511+
(if pane
512+
(enkan-repl--terminal-tmux--call
513+
(list "display-message" "-p" "-t" pane "#{pane_id}")
514+
t)
515+
(and window
516+
(member window
517+
(enkan-repl--terminal-tmux--list-windows session))))
468518
t))))
469519

470520
(defun enkan-repl--terminal-tmux-list ()
471521
"Tmux backend: list all window identifiers in the current workspace's session."
472522
(let ((session (enkan-repl--terminal-tmux--workspace-session)))
473523
(when (and session (enkan-repl--terminal-tmux--has-session session))
474-
(mapcar (lambda (w) (enkan-repl--terminal-tmux--make-id session w))
475-
(enkan-repl--terminal-tmux--list-windows session)))))
524+
(mapcar (lambda (info)
525+
(enkan-repl--terminal-tmux--make-id
526+
session
527+
(plist-get info :window)
528+
(plist-get info :pane)))
529+
(enkan-repl--terminal-tmux--list-window-info session)))))
476530

477531
(defun enkan-repl--terminal-tmux-kill (id)
478532
"Tmux backend: kill the window described by ID."
479533
(when id
480-
(enkan-repl--terminal-tmux--call (list "kill-window" "-t" id))))
534+
(enkan-repl--terminal-tmux--call
535+
(list "kill-window" "-t" (enkan-repl--terminal-tmux--target id)))))
481536

482537
;;;;; tmux mirror buffer
483538

@@ -741,24 +796,31 @@ tmux capture process."
741796
0.2))
742797
(out (enkan-repl--terminal-tmux--call
743798
(list "list-windows" "-t" session "-F"
744-
"#{window_name}\t#{window_bell_flag}")
799+
"#{window_name}\t#{pane_id}\t#{window_bell_flag}")
745800
t)))
746801
(when (stringp out)
747802
(delq
748803
nil
749804
(mapcar
750805
(lambda (line)
751-
(pcase-let ((`(,window ,flag) (split-string line "\t")))
752-
(when (and window (string= flag "1"))
753-
(enkan-repl--terminal-tmux--make-id session window))))
806+
(let ((fields (split-string line "\t")))
807+
(pcase fields
808+
(`(,window ,pane ,flag)
809+
(when (and window (string= flag "1"))
810+
(enkan-repl--terminal-tmux--make-id session window pane)))
811+
(`(,window ,flag)
812+
(when (and window (string= flag "1"))
813+
(enkan-repl--terminal-tmux--make-id session window))))))
754814
(split-string out "\n" t))))))
755815

756816
(defun enkan-repl--terminal-tmux--all-targets (session)
757817
"Return all tmux target ids in SESSION."
758-
(let ((windows (enkan-repl--terminal-tmux--list-windows session)))
759-
(mapcar (lambda (window)
760-
(enkan-repl--terminal-tmux--make-id session window))
761-
windows)))
818+
(mapcar (lambda (info)
819+
(enkan-repl--terminal-tmux--make-id
820+
session
821+
(plist-get info :window)
822+
(plist-get info :pane)))
823+
(enkan-repl--terminal-tmux--list-window-info session)))
762824

763825
(defun enkan-repl--terminal-tmux--alert-capture (target)
764826
"Return a small bounded capture from TARGET for alert detection."
@@ -777,7 +839,7 @@ tmux capture process."
777839
(out (enkan-repl--terminal-tmux--call
778840
(list "capture-pane" "-p" "-J" "-S"
779841
(format "-%d" lines)
780-
"-t" target)
842+
"-t" (enkan-repl--terminal-tmux--target target))
781843
t)))
782844
(when (stringp out)
783845
(if (and max-chars (> (length out) max-chars))
@@ -1112,7 +1174,8 @@ CALLBACK receives the cwd string, or nil on failure."
11121174
(user-error "Tmux executable not found: %s" enkan-repl-tmux-executable))
11131175
(let* ((output-buffer (generate-new-buffer " *enkan-repl tmux cwd*"))
11141176
(command (list enkan-repl-tmux-executable
1115-
"display-message" "-p" "-t" id
1177+
"display-message" "-p" "-t"
1178+
(enkan-repl--terminal-tmux--target id)
11161179
"#{pane_current_path}")))
11171180
(make-process
11181181
:name (format "enkan-tmux-cwd %s" id)
@@ -1285,7 +1348,7 @@ arguments: CONTENT, or nil on failure, and the tmux process exit status."
12851348
(let* ((command (list enkan-repl-tmux-executable
12861349
"capture-pane" "-p" "-J" "-S"
12871350
(format "-%d" (max 0 lines))
1288-
"-t" id))
1351+
"-t" (enkan-repl--terminal-tmux--target id)))
12891352
(max-chars (and (integerp enkan-repl-tmux-mirror-max-chars)
12901353
(> enkan-repl-tmux-mirror-max-chars 0)
12911354
enkan-repl-tmux-mirror-max-chars))
@@ -1295,15 +1358,15 @@ arguments: CONTENT, or nil on failure, and the tmux process exit status."
12951358
process)
12961359
(cl-labels
12971360
((finish
1298-
(status)
1299-
(unless done
1300-
(setq done t)
1301-
(when timer
1302-
(cancel-timer timer)
1303-
(setq timer nil))
1304-
(funcall callback
1305-
(and (integerp status) (zerop status) content)
1306-
status))))
1361+
(status)
1362+
(unless done
1363+
(setq done t)
1364+
(when timer
1365+
(cancel-timer timer)
1366+
(setq timer nil))
1367+
(funcall callback
1368+
(and (integerp status) (zerop status) content)
1369+
status))))
13071370
(setq process
13081371
(make-process
13091372
:name (format "enkan-tmux-capture %s" id)

enkan-repl-workspace-list.el

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
(declare-function enkan-repl--get-workspace-state "enkan-repl-workspace" (workspaces workspace-id))
2424
(declare-function enkan-repl--save-workspace-state "enkan-repl" ())
2525
(declare-function enkan-repl--load-workspace-state "enkan-repl" (workspace-id))
26+
(declare-function enkan-repl--maybe-setup-current-project-layout "enkan-repl"
27+
(&optional context))
2628
(declare-function enkan-repl--get-project-paths-for-current "enkan-repl" (current-project projects target-directories))
2729
(declare-function enkan-repl--projects-with-current-aliases "enkan-repl" (projects current-project project-aliases))
2830
(declare-function enkan-repl--get-project-info-from-directories "enkan-repl-utils" (alias target-directories))
@@ -133,6 +135,7 @@ TARGET-DIRECTORIES is the list of target directories."
133135
(setq enkan-repl--current-workspace workspace-id)
134136
;; Load the new workspace state
135137
(enkan-repl--load-workspace-state workspace-id)
138+
(enkan-repl--maybe-setup-current-project-layout "workspace list switch")
136139
(message "Switched to workspace %s" workspace-id))))))
137140

138141
(defun enkan-repl-workspace-list-refresh ()

0 commit comments

Comments
 (0)