Skip to content

Commit 7f403fa

Browse files
phasetrys-offisis
andauthored
fix: resolve tmux reattach workspace list paths (#66)
* chore: initialize tmux reattach workspace list fix * fix: resolve tmux reattach workspace list paths * fix: preserve tmux reattach paths across switches * fix: normalize tmux cwd paths to workspace aliases * fix: restore tmux reattach send targets * fix: preserve duplicate tmux project cwds * fix: harden tmux mirror target resolution * fix: expose imported tmux window aliases * docs: document tmux reattach aliases --------- Co-authored-by: Yoshitsugu Sekine <yoshitsugu.sekine@offisis.co.jp>
1 parent 7c7fbda commit 7f403fa

13 files changed

Lines changed: 1089 additions & 98 deletions

README.org

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,8 @@ and the cheat-sheet ensures you never miss useful functionality.
166166

167167
*** Prerequisites
168168
- Emacs 30.1 or later
169-
- [[https://github.com/akib/emacs-eat][eat]] (0.9.4 or later)
169+
- [[https://github.com/tmux/tmux][tmux]] for the default backend
170+
- [[https://github.com/akib/emacs-eat][eat]] (0.9.4 or later) only when using the legacy =eat= backend
170171

171172
*Note*: This package has been developed and tested exclusively on macOS with GNU Emacs 30.1.
172173
While it should work on other platforms and Emacs versions (30.1+),
@@ -263,7 +264,7 @@ Currently operates with a single fixed workspace (=ws:01=), with multi-workspace
263264
- =enkan-repl-workspace-delete= - Delete a workspace and stop all associated terminal resources. This is the only interactive workspace deletion command. In `enkan-repl-workspace-list-mode', delete the workspace at point; otherwise delete the current workspace. With prefix ARG, prompt for a workspace. Noninteractive string ARG deletes that workspace ID. All paths use `enkan-repl--delete-workspace-completely'.
264265

265266
*** Utilities
266-
- =enkan-repl-tmux-reattach= - Reconnect Emacs state to live tmux sessions. FILE defaults to `enkan-repl-state-file'. Live tmux sessions whose names start with `enkan-repl-tmux-session-prefix' define the workspaces to restore. When a matching persisted workspace exists, its saved state is reused. When no saved state exists for a live tmux session, a minimal workspace is imported from the tmux session's windows so reattach works after Emacs state was lost. This command is intentionally manual; enkan-repl does not reattach on load.
267+
- =enkan-repl-tmux-reattach= - Reconnect Emacs state to live tmux sessions. FILE defaults to `enkan-repl-state-file'. Live tmux sessions whose names start with `enkan-repl-tmux-session-prefix' define the workspaces to restore. When a matching persisted workspace exists, its saved state is reused. When no saved state exists for a live tmux session, a minimal workspace is imported from the tmux session's windows so reattach works after Emacs state was lost. Imported tmux window names become the workspace aliases used by send commands, workspace-list path display, and project-directory selection. This command is intentionally manual; enkan-repl does not reattach on load.
267268
- =enkan-repl-recenter-bottom= - Recenter all enkan terminal buffers at bottom.
268269
- =enkan-repl-toggle-global-mode= - Toggle enkan-repl global mode on/off.
269270
- =enkan-repl-workspace-switch= - Switch to another workspace. Uses `hmenu' if available to show workspace ID with its project.
@@ -328,8 +329,11 @@ same user-facing send / layout commands (=enkan-repl-send-line=,
328329
Live =enkan-*= tmux sessions define the workspaces to restore. When
329330
saved state exists for a live workspace it is reused; when it does not,
330331
enkan-repl imports a minimal workspace from the tmux windows so the
331-
session can be used again. Reattach ensures mirror buffers exist, but
332-
it avoids force-refreshing every restored workspace; use
332+
session can be used again. Imported tmux window names become workspace
333+
aliases, so =enkan-repl-send-line=, =enkan-repl-workspace-list=, and
334+
=enkan-repl-open-project-directory= can resolve every live window even
335+
when the Emacs state file was missing. Reattach ensures mirror buffers
336+
exist, but it avoids force-refreshing every restored workspace; use
333337
=M-x enkan-repl-tmux-refresh-workspace= when you explicitly want to
334338
refresh the current workspace's mirror contents.
335339

enkan-repl-constants.el

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
;;; Code:
1212

1313
(defconst enkan-repl-cheat-sheet-candidates
14-
'(("enkan-repl-tmux-reattach" . "Reconnect Emacs state to live tmux sessions. FILE defaults to `enkan-repl-state-file'. Live tmux sessions whose names start with `enkan-repl-tmux-session-prefix' define the workspaces to restore. When a matching persisted workspace exists, its saved state is reused. When no saved state exists for a live tmux session, a minimal workspace is imported from the tmux session's windows so reattach works after Emacs state was lost. This command is intentionally manual; enkan-repl does not reattach on load.")
14+
'(("enkan-repl-tmux-reattach" . "Reconnect Emacs state to live tmux sessions. FILE defaults to `enkan-repl-state-file'. Live tmux sessions whose names start with `enkan-repl-tmux-session-prefix' define the workspaces to restore. When a matching persisted workspace exists, its saved state is reused. When no saved state exists for a live tmux session, a minimal workspace is imported from the tmux session's windows so reattach works after Emacs state was lost. Imported tmux window names become the workspace aliases used by send commands, workspace-list path display, and project-directory selection. This command is intentionally manual; enkan-repl does not reattach on load.")
1515
("enkan-repl-send-region" . "Send region text (from START to END) to enkan session buffer with optional PFX. - From enkan buffer: Send to current buffer - From other buffer without prefix: Interactive buffer selection - With numeric prefix: Send to buffer at index (1-based) Uses unified backend with smart buffer detection. Category: Text Sender")
1616
("enkan-repl-send-line" . "Send current line to enkan session buffer with optional PFX. - From enkan buffer: Send to current buffer - From other buffer without prefix: Interactive buffer selection - With numeric prefix: Send to buffer at index (1-based) Uses unified backend with smart buffer detection. Category: Text Sender")
1717
("enkan-repl-send-enter" . "Send enter key to enkan session buffer with optional PFX. - From enkan buffer: Send to current buffer - From other buffer without prefix: Interactive buffer selection - With numeric prefix: Send to buffer at index (1-based) Uses unified backend with smart buffer detection. Category: Text Sender")
@@ -27,7 +27,7 @@
2727
("enkan-repl-cheat-sheet" . "Display interactive `cheat-sheet' for enkan-repl commands. Category: Command Palette")
2828
("enkan-repl-toggle-global-mode" . "Toggle enkan-repl global mode on/off.")
2929
("enkan-repl-send-escape" . "Send ESC key to enkan session buffer with optional PFX. - If called from enkan buffer: Send ESC to current buffer - If called from center file without prefix: Select from available enkan buffers - With numeric prefix: Send to buffer at that index (1-based) Category: Center File Multi-buffer Access")
30-
("enkan-repl-open-project-directory" . "Open project directory in Dired from enkan-repl-projects with optional PFX. With prefix argument (\\\\[universal-argument]), select from available buffers. Category: Center File Multi-buffer Access")
30+
("enkan-repl-open-project-directory" . "Open the current workspace's project directory in Dired with optional PFX. Directory choices are resolved from `enkan-repl-projects' plus the current workspace's aliases and target directories, including aliases imported by `enkan-repl-tmux-reattach'. With prefix argument (\\\\[universal-argument]), select from available buffers. Category: Center File Multi-buffer Access")
3131
("enkan-repl-open-center-file" . "Open or create the center file based on enkan-repl-center-file configuration. Category: Center File Operations")
3232
("enkan-repl-print-setup-to-buffer" . "Print current setup variables for debugging. Displays enkan-repl-projects, enkan-repl-target-directories, enkan-repl-project-aliases, and current session state. Category: Debugging")
3333
("enkan-repl-workspace-switch" . "Switch to another workspace. Uses `hmenu' if available to show workspace ID with its project.")

enkan-repl-terminal.el

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,14 @@ Returns 1 when ID has no explicit instance suffix."
185185

186186
;;;; eat backend
187187

188+
(defun enkan-repl--terminal-eat--live-process (buffer)
189+
"Return BUFFER's live eat process, or nil."
190+
(when (and buffer (buffer-live-p buffer))
191+
(with-current-buffer buffer
192+
(let ((proc (or (and (boundp 'eat--process) eat--process)
193+
(get-buffer-process buffer))))
194+
(and proc (process-live-p proc) proc)))))
195+
188196
(defun enkan-repl--terminal-eat-start (dir)
189197
"Eat backend: create eat session in DIR and rename to enkan buffer name.
190198
Return the eat buffer."
@@ -231,10 +239,7 @@ NEWLINE non-nil appends a CR after TEXT."
231239

232240
(defun enkan-repl--terminal-eat-alive-p (id)
233241
"Eat backend: t if buffer ID has a live process."
234-
(and id
235-
(buffer-live-p id)
236-
(let ((proc (get-buffer-process id)))
237-
(and proc (process-live-p proc)))))
242+
(and (enkan-repl--terminal-eat--live-process id) t))
238243

239244
(defun enkan-repl--terminal-eat-list ()
240245
"Eat backend: list of live enkan buffers in current workspace."
@@ -247,8 +252,7 @@ NEWLINE non-nil appends a CR after TEXT."
247252
(buffer-name buf)
248253
(enkan-repl--buffer-name-matches-workspace
249254
(buffer-name buf) current-ws)
250-
(let ((proc (get-buffer-process buf)))
251-
(and proc (process-live-p proc)))))
255+
(enkan-repl--terminal-eat--live-process buf)))
252256
(buffer-list)))))
253257

254258
(defun enkan-repl--terminal-eat-kill (id)
@@ -362,6 +366,22 @@ or nil on non-zero exit. Otherwise return t on zero exit, nil on non-zero."
362366
(split-string out "\n" t)
363367
nil)))
364368

369+
(defun enkan-repl--terminal-tmux--list-window-cwds (session)
370+
"Return list of (WINDOW . CWD) pairs in tmux SESSION."
371+
(let ((out (enkan-repl--terminal-tmux--call
372+
(list "list-windows" "-t" session "-F"
373+
"#{window_name}\t#{pane_current_path}")
374+
t)))
375+
(when (and out (not (string-empty-p out)))
376+
(delq
377+
nil
378+
(mapcar
379+
(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)))))
383+
(split-string out "\n" t))))))
384+
365385
(defun enkan-repl--terminal-tmux--derive-base-name (dir)
366386
"Derive a base window name from DIR (file-name-nondirectory of cleaned dir)."
367387
(let* ((clean (directory-file-name (expand-file-name dir)))
@@ -1026,7 +1046,7 @@ buffer-list-based layout and send-target lookup can find tmux mirrors."
10261046
enkan-repl--current-workspace)))
10271047
(enkan-repl--path->buffer-name
10281048
(file-name-as-directory path)
1029-
(enkan-repl--terminal-tmux-id-instance id))))
1049+
(enkan-repl--terminal-tmux-id-instance-for-path id path))))
10301050

10311051
(defun enkan-repl--terminal-tmux--mirror-buffer-name (id &optional path)
10321052
"Return mirror buffer name for tmux ID without calling tmux.
@@ -1436,6 +1456,8 @@ the asynchronous lookup returns."
14361456
(unless (derived-mode-p 'enkan-repl-tmux-mirror-mode)
14371457
(enkan-repl-tmux-mirror-mode))
14381458
(setq enkan-repl--tmux-mirror-id id)
1459+
(when path
1460+
(enkan-repl--terminal-tmux--mirror-rename-for-cwd buf id path))
14391461
(enkan-repl--terminal-tmux--mirror-update-status)
14401462
(unless path
14411463
(enkan-repl--terminal-tmux--mirror-start-cwd-lookup buf id))
@@ -1542,6 +1564,23 @@ Returns 1 when no \"-N\" suffix is present."
15421564
(string-to-number (match-string 1 win)))
15431565
(t 1))))
15441566

1567+
(defun enkan-repl--terminal-tmux-id-instance-for-path (id path)
1568+
"Return tmux ID's instance index relative to PATH.
1569+
A numeric suffix is treated as an instance only when the tmux window name is
1570+
the PATH basename followed by -N. This keeps projects whose real names end in
1571+
digits, such as foo-2, at instance 1."
1572+
(let* ((win (enkan-repl--terminal-tmux--id-window id))
1573+
(base (and (stringp path)
1574+
(enkan-repl--terminal-tmux--derive-base-name path))))
1575+
(cond
1576+
((and win base (string= win base)) 1)
1577+
((and win base
1578+
(string-match
1579+
(format "\\`%s-\\([0-9]+\\)\\'" (regexp-quote base))
1580+
win))
1581+
(string-to-number (match-string 1 win)))
1582+
(t 1))))
1583+
15451584
;;;###autoload
15461585
(defun enkan-repl-tmux-kill-session (&optional session)
15471586
"Kill a tmux SESSION (defaults to the current workspace's enkan session).

enkan-repl-utils.el

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,10 @@ enkan buffer name."
6464

6565
(defun enkan-repl--path->buffer-name (path &optional instance)
6666
"Generate buffer name from PATH, optionally with INSTANCE suffix.
67-
Returns buffer name in format *ws:<id> enkan:<expanded-path>* (instance 1
68-
or unspecified) or *ws:<id> enkan:<expanded-path>*<N> (instance >= 2).
67+
PATH is normalized as a directory, so `/p' and `/p/' produce the same buffer
68+
name. Returns buffer name in format *ws:<id> enkan:<expanded-directory>*
69+
\(instance 1 or unspecified) or *ws:<id> enkan:<expanded-directory>*<N>
70+
\(instance >= 2).
6971
Workspace ID is taken from `enkan-repl--current-workspace' when available;
7072
otherwise falls back to \"01\"."
7173
(let* ((ws (when (boundp 'enkan-repl--current-workspace)
@@ -74,7 +76,8 @@ otherwise falls back to \"01\"."
7476
(string-match-p "^[0-9][0-9]$" ws))
7577
ws
7678
"01"))
77-
(base (format "*ws:%s enkan:%s*" ws-id (expand-file-name path))))
79+
(directory (file-name-as-directory (expand-file-name path)))
80+
(base (format "*ws:%s enkan:%s*" ws-id directory)))
7881
(if (and instance (integerp instance) (> instance 1))
7982
(format "%s<%d>" base instance)
8083
base)))
@@ -424,7 +427,7 @@ Returns a list of plists, each containing :name, :args, :docstring,
424427
(in-docstring nil)
425428
(docstring-lines '()))
426429
(while (and (< next-line-idx (length lines))
427-
(< next-line-idx (+ current-line 10))) ; reasonable limit
430+
(< next-line-idx (+ current-line 40))) ; reasonable limit
428431
(let ((next-line (nth next-line-idx lines)))
429432
(cond
430433
;; Start of docstring

enkan-repl-workspace-list.el

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
(declare-function enkan-repl--save-workspace-state "enkan-repl" ())
2525
(declare-function enkan-repl--load-workspace-state "enkan-repl" (workspace-id))
2626
(declare-function enkan-repl--get-project-paths-for-current "enkan-repl" (current-project projects target-directories))
27+
(declare-function enkan-repl--projects-with-current-aliases "enkan-repl" (projects current-project project-aliases))
28+
(declare-function enkan-repl--get-project-info-from-directories "enkan-repl-utils" (alias target-directories))
2729
(declare-function enkan-repl-workspace-delete "enkan-repl" (&optional arg))
2830
(declare-function enkan-repl-setup "enkan-repl" ())
2931

@@ -50,6 +52,40 @@
5052
(setq buffer-read-only t)
5153
(setq truncate-lines t))
5254

55+
(defun enkan-repl-workspace-list--project-alias-names (aliases)
56+
"Return alias names from ALIASES.
57+
ALIASES may be a list of strings or an alist of (alias . project-name)."
58+
(delq nil
59+
(mapcar (lambda (entry)
60+
(cond
61+
((stringp entry) entry)
62+
((consp entry) (car entry))))
63+
aliases)))
64+
65+
(defun enkan-repl-workspace-list--project-paths (state target-directories)
66+
"Return displayable project paths for workspace STATE.
67+
TARGET-DIRECTORIES is the fallback global directory registry."
68+
(let* ((current-project (plist-get state :current-project))
69+
(workspace-dirs (or (plist-get state :target-directories)
70+
target-directories))
71+
(paths (when current-project
72+
(enkan-repl--get-project-paths-for-current
73+
current-project
74+
(enkan-repl--projects-with-current-aliases
75+
(bound-and-true-p enkan-repl-projects)
76+
current-project
77+
(plist-get state :project-aliases))
78+
workspace-dirs))))
79+
(or paths
80+
(cl-loop for alias in (enkan-repl-workspace-list--project-alias-names
81+
(or (plist-get state :project-aliases)
82+
(and current-project
83+
(list current-project))))
84+
for info = (enkan-repl--get-project-info-from-directories
85+
alias workspace-dirs)
86+
when info
87+
collect (cons alias (cdr info))))))
88+
5389
(defun enkan-repl-workspace-list--format-workspace-info (workspace-id workspaces current-workspace target-directories)
5490
"Format workspace information for display.
5591
WORKSPACE-ID is the workspace identifier.
@@ -59,12 +95,8 @@ TARGET-DIRECTORIES is the list of target directories."
5995
(let* ((state (enkan-repl--get-workspace-state workspaces workspace-id))
6096
(current-project (plist-get state :current-project))
6197
(is-current (string= workspace-id current-workspace))
62-
;; Get project paths using existing function
63-
(project-paths (when current-project
64-
(enkan-repl--get-project-paths-for-current
65-
current-project
66-
(bound-and-true-p enkan-repl-projects)
67-
target-directories)))
98+
(project-paths
99+
(enkan-repl-workspace-list--project-paths state target-directories))
68100
;; Format all paths when multiple targets exist
69101
(project-dirs (if project-paths
70102
(mapconcat (lambda (pair) (cdr pair))

0 commit comments

Comments
 (0)