-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathmultishell-list.el
314 lines (274 loc) · 12.4 KB
/
multishell-list.el
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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
;;; multishell-list.el --- tabulated-list-mode for multishell shell buffers -*- lexical-binding:t -*-
;; Copyright (C) 2016-2022 Free Software Foundation, Inc. and Ken Manheimer
;; Author: Ken Manheimer <[email protected]>
;; Version: 1.1.8
;; Created: 2016 -- first public availability
;; Keywords: processes
;; URL: https://github.com/kenmanheimer/EmacsMultishell
;; See multishell.el for commentary, change log, etc.
(require 'tabulated-list)
(require 'multishell)
(eval-when-compile (require 'cl-lib))
(defgroup multishell-list nil
"Show a menu of all shell buffers in a buffer."
:group 'multishell)
(defface multishell-list-name
'((t (:weight bold)))
"Face for shell names in the Multishell List."
:group 'multishell-list)
(defun multishell-list-open-pop (&optional arg)
"Pop to current entry's shell in separate window.
The shell is started if it's not already going, unless this is
invoked with optional `universal-argument'. In that case we
pop to the buffer but don't change its run state."
(interactive "P")
(let ((list-buffer (current-buffer))
(entry (tabulated-list-get-id)))
(if arg
(pop-to-buffer
(multishell-bracket (multishell-name-from-entry entry)))
(multishell-list-dispatch-selected entry t))
(with-current-buffer list-buffer
(revert-buffer)
(multishell-list-goto-item-by-entry entry))))
(defun multishell-list-open-as-default ()
"Pop to current entry's shell, and set as the default shell."
(interactive)
(let ((list-buffer (current-buffer))
(entry (tabulated-list-get-id)))
(message "%s <==" (multishell-name-from-entry entry))
(multishell-list-dispatch-selected entry t t)
(with-current-buffer list-buffer
(revert-buffer)
(multishell-list-goto-item-by-entry entry))))
(defun multishell-list-open-here (&optional arg)
"Switch to current entry's shell buffer.
The shell is started if it's not already going, unless this is
invoked with optional `universal-argument'. In that case we
switch to the buffer but don't activate (or deactivate) it it."
(interactive "P")
(let* ((list-buffer (current-buffer))
(entry (tabulated-list-get-id)))
(if arg
(switch-to-buffer
(multishell-bracket (multishell-name-from-entry entry)))
(multishell-list-dispatch-selected entry nil))
(with-current-buffer list-buffer
(revert-buffer))))
(defun multishell-list-delete (&optional _arg)
"Remove current shell entry, and prompt for buffer-removal if present."
(interactive)
(let* ((entry (tabulated-list-get-id))
(name (multishell-name-from-entry entry))
(name-bracketed (multishell-bracket name))
(buffer (get-buffer name-bracketed)))
(when (multishell-delete-history-name name)
(and buffer
;; If the process is live, let shell-mode get confirmation:
(or (comint-check-proc (current-buffer))
(y-or-n-p (format "Kill buffer %s? " name-bracketed)))
(kill-buffer name-bracketed)))
(tabulated-list-delete-entry)))
(defun multishell-list-edit-entry (&optional arg)
"Edit the value of current shell entry.
Submitting the change will not launch the entry, unless this is
invoked with optional `universal-argument'. In the latter case,
submitting the entry will pop to the shell in a new window,
starting it if it's not already going."
(interactive "P")
(let* ((list-buffer (current-buffer))
(entry (tabulated-list-get-id))
(name (multishell-name-from-entry entry))
(revised (multishell-read-unbracketed-entry
(format "Edit shell spec for %s: " name)
entry
'no-record))
(revised-name (multishell-name-from-entry revised))
buffer)
(when (not (string= revised entry))
(multishell-replace-entry entry revised)
(when (and (not (string= name revised-name))
(setq buffer (get-buffer (multishell-bracket name))))
(with-current-buffer buffer
(rename-buffer (multishell-bracket revised-name)))))
(when arg
(multishell-list-dispatch-selected revised-name t))
(with-current-buffer list-buffer
(revert-buffer)
(multishell-list-goto-item-by-entry revised))))
(defun multishell-list-clone-entry (&optional arg)
"Create a new list entry, edited from the current one, ready to launch.
If you provide an optional `universal-argument', the new entry
will be launched when it's created.
The already existing original entry is left untouched."
(interactive "P")
(let* ((prototype (tabulated-list-get-id))
(name (multishell-name-from-entry prototype))
(new (multishell-read-unbracketed-entry
(format "Clone new shell spec from %s: " name)
prototype
'no-record))
(new-name (multishell-name-from-entry new))
(new-path (cadr (multishell-split-entry new))))
(when (not (string= new prototype))
(multishell-register-name-to-path new-name new-path)
(revert-buffer)
(multishell-list-goto-item-by-entry new)
(when arg
(multishell-list-dispatch-selected new-name t)))))
(defun multishell-list-mouse-select (event)
"Select the shell whose line is clicked."
(interactive "e")
(select-window (posn-window (event-end event)))
(let ((entry (tabulated-list-get-id (posn-point (event-end event)))))
(multishell-list-dispatch-selected entry nil)))
(defun multishell-list-dispatch-selected (entry pop &optional set-primary)
"Go to multishell ENTRY, popping to window if POP is non-nil.
Optional arg SET-PRIMARY non-nil sets `multishell-primary-name' to entry.
Provide for concluding minibuffer interaction if we're in completing mode."
(let ((set-primary-as-arg (and set-primary '(16))))
(if multishell-completing-read
;; In multishell completing-read, arrange to conclude minibuffer input:
(throw 'multishell-minibuffer-exit (list entry pop set-primary-as-arg))
(multishell-pop-to-shell set-primary-as-arg entry (not pop)))))
(defun multishell-list-placeholder (value default)
"Return VALUE if non-empty string, else DEFAULT."
(if (or (not value) (string= value ""))
default
value))
(defconst multishell-list-active-flag "+")
(defconst multishell-list-inactive-flag ".")
(defconst multishell-list-absent-flag "x")
(defun multishell-list-entries ()
"Generate multishell name/path-spec entries list for tabulated-list."
(let ((recency 0))
(mapcar #'(lambda (entry)
(setq recency (1+ recency))
(let* ((splat (multishell-split-entry entry))
(name (car splat))
(buffer (and name
(get-buffer
(multishell-bracket name))))
(status (cond ((not buffer)
multishell-list-absent-flag)
((comint-check-proc buffer)
multishell-list-active-flag)
(t multishell-list-inactive-flag)))
(rest (cadr splat))
(dir (and rest (or (file-remote-p rest 'localname)
rest)))
(hops (and dir
(file-remote-p rest 'localname)
(substring
rest 0 (- (length rest) (length dir))))))
(when (not name)
(setq name (multishell-name-from-entry entry)))
(list entry
(vector (format "%d" recency)
status
(multishell-list--decorate-name name)
(multishell-list-placeholder hops "-")
(multishell-list-placeholder dir "~")))))
(multishell-all-entries))))
(defun multishell-list-goto-item-by-entry (entry)
"Position at beginning of line of tabulated list item for ENTRY."
(goto-char (point-min))
(while (and (not (eobp))
(not (string= (tabulated-list-get-id) entry)))
(forward-line 1)))
(define-obsolete-function-alias 'multishell-collate-row-strings-as-numbers
#'multishell-list--collate-row-strings-as-numbers "multishell 1.1.6")
(defun multishell-list--collate-row-strings-as-numbers (a b)
(let ((a (aref (cadr a) 0))
(b (aref (cadr b) 0)))
(> (string-to-number a) (string-to-number b))))
(defun multishell-list--decorate-name (name)
(propertize name
'font-lock-face 'multishell-list-name
'mouse-face 'highlight))
(defvar multishell-list-mode-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map tabulated-list-mode-map)
(define-key map (kbd "c") 'multishell-list-clone-entry)
(define-key map (kbd "d") 'multishell-list-delete)
(define-key map (kbd "\C-k") 'multishell-list-delete)
(define-key map (kbd "k") 'multishell-list-delete)
(define-key map (kbd "e") 'multishell-list-edit-entry)
(define-key map (kbd "o") 'multishell-list-open-pop)
(define-key map (kbd " ") 'multishell-list-open-pop)
(define-key map (kbd "O") 'multishell-list-open-as-default)
(define-key map (kbd "RET") 'multishell-list-open-here)
(define-key map [mouse-2] 'multishell-list-mouse-select)
(define-key map [follow-link] 'mouse-face)
map))
(define-derived-mode multishell-list-mode
tabulated-list-mode "Shells"
"Major mode for listing current and historically registered shells.
Initial sort is from most to least recently used:
- First active shells, flagged with `+' a plus sign
- Then, inactive shells, flagged with `.' a period
- Then historical shells that currently have no buffer, flagged with `x' an ex
\\{multishell-list-mode-map\}"
(setq tabulated-list-format
[;; (name width sort '(:right-align nil :pad-right nil))
("#" 0 multishell-list--collate-row-strings-as-numbers :pad-right 1)
("! " 1 t :pad-right 1)
("Name" 15 t)
("Hops" 30 t)
("Directory" 30 t)]
tabulated-list-sort-key '("#" . t)
tabulated-list-entries #'multishell-list-entries)
(tabulated-list-init-header))
(defun multishell-list-cull-dups (entries)
"Return list of multishell ENTRIES sans ones with duplicate names.
For duplicates, we prefer the ones that have paths."
(let ((tally (make-hash-table :test #'equal))
got name name-order-reversed already)
(dolist (entry entries)
(setq name (multishell-name-from-entry entry)
already (gethash name tally nil))
(when (not already)
(push name name-order-reversed))
(when (or (not already) (< (length already) (length entry)))
;; Add new or replace shorter prior entry for name:
(puthash name entry tally)))
(dolist (name name-order-reversed)
(push (gethash name tally) got))
got))
;;;###autoload
(defun multishell-list (&optional completing)
"Edit your current and historic list of shell buffers.
If optional COMPLETING is nil, we present the full
`multishell-history' list in a popped buffer named `*Shells*'.
In the buffer, hit ? or h for a list of commands.
When optional COMPLETING is non-nil, it must be a list of
multishell-history completion candidate entries, as provided by
`completing-read'. Then we present the list as a part of
minibuffer completion.
You can get to the shells listing by recursively invoking
\\[multishell-pop-to-shell] at the `multishell-pop-to-shell'
`universal-argument' prompts."
(interactive)
(let ((from-entry (car (multishell-history-entries
(multishell-unbracket (buffer-name
(current-buffer))))))
(buffer (get-buffer-create (if completing
"*Completions*"
"*Shells*"))))
(if completing
(set-buffer buffer)
(pop-to-buffer buffer))
(multishell-list-mode)
(cl-progv
;; Temporarily assign multishell-history only when completing:
(when completing '(multishell-history))
(when completing
(list (multishell-list-cull-dups (mapcar #'substring-no-properties
completing))))
(tabulated-list-print))
(when completing
)
(when from-entry
(multishell-list-goto-item-by-entry from-entry))))
(provide 'multishell-list)
;;; multishell-list.el ends here