1 ;;; multishell.el --- manage interaction with multiple local and remote shells
3 ;; Copyright (C) 1999-2016 Free Software Foundation, Inc. and Ken Manheimer
5 ;; Author: Ken Manheimer <ken dot manheimer at gmail...>
6 ;; Maintainer: Ken Manheimer <ken dot manheimer at gmail...>
7 ;; Created: 1999 -- first public release
9 ;; Website: https://github.com/kenmanheimer/EmacsUtils
13 ;; Get to a shell buffer with a keystroke, or to the input point for the
14 ;; current shell buffer. Use universal arguments to launch and choose
15 ;; between alternate shell buffers and to select which is default. Prepend
16 ;; a path to a new shell name to launch a shell in that directory, and use
17 ;; Emacs tramp path syntax to launch a remote shell. Fluidly articulate
20 ;; See the pop-to-shell docstring for details.
23 ;; * Change name to multishell.
24 ;; - Most functions will be prefixed, eg multishell:pop-to-shell
25 ;; * Preservable (savehist) history that associates names with paths
26 ;; - Using an association list between names and paths
27 ;; - Searched for search backwards/forwards on isearch-like M-r/M-s bindings
28 ;; - *Not* searched for regular completion
30 ;; - Using isearch keybinding M-e
32 ;; - New association overrides previous
33 ;; - Deleting path removes association and history entry
34 ;; * Customize activation of savehist
35 ;; - Customize entry has warning about activating savehist
36 ;; - Adds the name/path association list to savehist-additional-variables
37 ;; - Activates savehist, if inactive
39 (defvar non-interactive-process-buffers '("*compilation*" "*grep*"))
44 (defgroup multishell nil
45 "Allout extension that highlights outline structure graphically.
47 Customize `allout-widgets-auto-activation' to activate allout-widgets
51 (defcustom multishell:non-interactive-process-buffers
52 '("*compilation*" "*grep*")
53 "Names of buffers that have processes but are not for interaction.
54 Add names of buffers that you don't want pop-to-shell to stick around in."
55 :type '(repeat string)
57 (defcustom multishell:command-key "\M- "
58 "The key to use if `multishell:activate-command-key' is true.
60 You can instead bind `pop-to-shell` to your preferred key using emacs
61 lisp, eg: (global-set-key \"\\M- \" 'pop-to-shell)."
65 (defvar multishell:responsible-for-command-key nil
66 "Multishell internal.")
67 (defun multishell:activate-command-key-setter (symbol setting)
68 "Implement `multishell:activate-command-key' choice."
69 (set-default 'multishell:activate-command-key setting)
70 (when (or setting multishell:responsible-for-command-key)
71 (multishell:implement-command-key-choice (not setting))))
72 (defun multishell:implement-command-key-choice (&optional unbind)
73 "If settings dicate, implement binding of multishell command key.
75 If optional UNBIND is true, globally unbind the key.
77 * `multishell:activate-command-key' - Set this to get the binding or not.
78 * `multishell:command-key' - The key to use for the binding, if appropriate."
80 (when (and (boundp 'multishell:command-key) multishell:command-key)
81 (global-unset-key multishell:command-key)))
82 ((not (and (boundp 'multishell:activate-command-key)
83 (boundp 'multishell:command-key)))
85 ((and multishell:activate-command-key multishell:command-key)
86 (setq multishell:responsible-for-command-key t)
87 (global-set-key multishell:command-key 'pop-to-shell))))
89 (defcustom multishell:activate-command-key nil
90 "Set this to impose the `multishell:command-key' binding.
92 You can instead bind `pop-to-shell` to your preferred key using emacs
93 lisp, eg: (global-set-key \"\\M- \" 'pop-to-shell)."
95 :set 'multishell:activate-command-key-setter
98 ;; Assert the customizations whenever the package is loaded:
99 (with-eval-after-load "multishell"
100 (multishell:implement-command-key-choice))
102 (defcustom multishell:pop-to-frame nil
103 "*If non-nil, jump to a frame already showing the shell, if another is.
105 Otherwise, open a new window in the current frame.
107 \(Adjust `pop-up-windows' to change other-buffer vs current-buffer behavior.)"
111 ;; (defcustom multishell:persist-shell-names nil
112 ;; "Remember shell name/path associations across sessions. Note well:
113 ;; This will activate minibuffer history persistence, in general, if it's not
118 (defvar multishell:name-path-assoc nil
119 "Assoc list from name to path")
121 (defvar multishell:primary-name "*shell*"
122 "Shell name to use for un-modified pop-to-shell buffer target.")
123 (defvar multishell:buffer-name-history nil
124 "Distinct pop-to-shell completion history container.")
126 (defun pop-to-shell (&optional arg)
127 "Easily navigate to and within multiple shell buffers, local and remote.
129 Use universal arguments to launch and choose between alternate
130 shell buffers and to select which is default. Prepend a path to
131 a new shell name to launch a shell in that directory, and use
132 Emacs tramp syntax to launch a remote shell.
134 Customize-group `multishell' to set up a key binding and tweak behaviors.
136 ==== Basic operation:
138 - If the current buffer is associated with a subprocess (that is
139 not among those named on `non-interactive-process-buffers'),
140 then focus is moved to the process input point.
142 \(You can use a universal argument go to a different shell
143 buffer when already in a buffer that has a process - see
146 - If not in a shell buffer (or with universal argument), go to a
147 window that is already showing the (a) shell buffer, if any.
149 In this case, the cursor is left in its prior position in the
150 shell buffer. Repeating the command will then go to the
151 process input point, per the first item in this list.
153 We respect `pop-up-windows', so you can adjust it to set the
154 other-buffer/same-buffer behavior.
156 - Otherwise, start a new shell buffer, using the current
157 directory as the working directory.
159 If a buffer with the resulting name exists and its shell process
160 was disconnected or otherwise stopped, it's resumed.
162 ===== Universal arg to start and select between named shell buffers:
164 You can name alternate shell buffers to create or return to using
165 single or doubled universal arguments:
167 - With a single universal argument, prompt for the buffer name
168 to use (without the asterisks that shell mode will put around
169 the name), defaulting to 'shell'.
171 Completion is available.
173 This combination makes it easy to start and switch between
174 multiple shell buffers.
176 - A double universal argument will prompt for the name *and* set
177 the default to that name, so the target shell becomes the
180 ===== Select starting directory and remote host:
182 The shell buffer name you give to the prompt for a universal arg
183 can include a preceding path. That will be used for the startup
184 directory. You can use tramp remote syntax to specify a remote
185 shell. If there is an element after a final '/', that's used for
186 the buffer name. Otherwise, the host, domain, or path is used.
190 * Use '/ssh:example.net:/' for a shell buffer on example.net named
192 * '/ssh:example.net|sudo:root@example.net:/\#ex' for a root shell on
193 example.net named \"#ex\"."
195 ;; I'm leaving the following out of the docstring for now because just
196 ;; saving the buffer names, and not the paths, yields sometimes unwanted
199 ;; ===== Persisting your alternate shell buffer names and paths:
201 ;; You can use emacs builtin SaveHist to preserve your alternate
202 ;; shell buffer names and paths across emacs sessions. To do so,
203 ;; customize the `savehist' group, and:
205 ;; 1. Add `multishell:pop-to-shell-buffer-name-history' to Savehist Additional
207 ;; 2. Activate Savehist Mode, if not already activated.
212 (let* ((from-buffer (current-buffer))
213 (from-buffer-is-shell (eq major-mode 'shell-mode))
214 (doublearg (equal arg '(16)))
216 (multishell:read-bare-shell-buffer-name
217 (format "Shell buffer name [%s]%s "
218 (substring-no-properties
219 multishell:primary-name
220 1 (- (length multishell:primary-name) 1))
221 (if doublearg " <==" ":"))
222 multishell:primary-name)
223 multishell:primary-name))
225 (target-shell-buffer-name
226 ;; Derive target name, and default-dir if any, from temp.
227 (cond ((string= temp "") multishell:primary-name)
228 ((string-match "^\\*\\(/.*/\\)\\(.*\\)\\*" temp)
229 (setq use-default-dir (match-string 1 temp))
230 (multishell:bracket-asterisks
231 (if (string= (match-string 2 temp) "")
232 (let ((v (tramp-dissect-file-name
234 (or (tramp-file-name-host v)
235 (tramp-file-name-domain v)
236 (tramp-file-name-localname v)
238 (match-string 2 temp))))
239 (t (multishell:bracket-asterisks temp))))
240 (curr-buff-proc (get-buffer-process from-buffer))
241 (target-buffer (if (and (or curr-buff-proc from-buffer-is-shell)
242 (not (member (buffer-name from-buffer)
243 non-interactive-process-buffers)))
245 (get-buffer target-shell-buffer-name)))
250 (setq multishell:primary-name target-shell-buffer-name))
256 ((and (or curr-buff-proc from-buffer-is-shell)
258 (eq from-buffer target-buffer)
259 (not (eq target-shell-buffer-name (buffer-name from-buffer))))
260 ;; In a shell buffer, but not named - stay in buffer, but go to end.
261 (setq already-there t))
263 ((string= (buffer-name) target-shell-buffer-name)
264 ;; Already in the specified shell buffer:
265 (setq already-there t))
267 ((or (not target-buffer)
269 (multishell:get-visible-window-for-buffer target-buffer))))
270 ;; No preexisting shell buffer, or not in a visible window:
271 (pop-to-buffer target-shell-buffer-name pop-up-windows))
273 ;; Buffer exists and already has a window - jump to it:
274 (t (if (and multishell:pop-to-frame
276 (not (equal (window-frame (selected-window))
277 (window-frame inwin))))
278 (select-frame-set-input-focus (window-frame inwin)))
279 (if (not (string= (buffer-name (current-buffer))
280 target-shell-buffer-name))
281 (pop-to-buffer target-shell-buffer-name t))))
283 ;; We're in the buffer.
285 ;; If we have a use-default-dir, impose it:
286 (when use-default-dir
287 (cd use-default-dir))
291 (if (not (comint-check-proc (current-buffer)))
292 (multishell:start-shell-in-buffer (buffer-name (current-buffer))))
294 ;; If the destination buffer has a stopped process, resume it:
295 (let ((process (get-buffer-process (current-buffer))))
296 (if (and process (equal 'stop (process-status process)))
297 (continue-process process)))
298 (when (or already-there
299 (equal (current-buffer) from-buffer))
300 (goto-char (point-max))
301 (and (get-buffer-process from-buffer)
302 (goto-char (process-mark (get-buffer-process from-buffer)))))))
304 (defun multishell:get-visible-window-for-buffer (buffer)
305 "Return visible window containing buffer."
308 (function (lambda (win)
309 (if (and (eq (window-buffer win) buffer)
310 (equal (frame-parameter
311 (selected-frame) 'display)
313 (window-frame win) 'display)))
314 (throw 'got-a-vis win))))
318 (defun multishell:read-bare-shell-buffer-name (prompt default)
319 "PROMPT for shell buffer name, sans asterisks.
321 Return the supplied name bracketed with the asterisks, or specified DEFAULT
323 (let* ((candidates (append
325 (mapcar (lambda (buffer)
326 (let ((name (buffer-name buffer)))
327 (if (with-current-buffer buffer
328 (eq major-mode 'shell-mode))
329 ;; Shell mode buffers.
330 (if (> (length name) 2)
336 (got (completing-read prompt
337 candidates ; COLLECTION
339 'confirm ; REQUIRE-MATCH
341 'multishell:buffer-name-history ; HIST
343 (if (not (string= got "")) (multishell:bracket-asterisks got) default)))
345 (defun multishell:bracket-asterisks (name)
346 "Return a copy of name, ensuring it has an asterisk at the beginning and end."
347 (if (not (string= (substring name 0 1) "*"))
348 (setq name (concat "*" name)))
349 (if (not (string= (substring name -1) "*"))
350 (setq name (concat name "*")))
352 (defun multishell:unbracket-asterisks (name)
353 "Return a copy of name, removing asterisks, if any, at beginning and end."
354 (if (string= (substring name 0 1) "*")
355 (setq name (substring name 1)))
356 (if (string= (substring name -1) "*")
357 (setq name (substring name 0 -1)))
359 (defun multishell:start-shell-in-buffer (buffer-name)
360 "Ensure a shell is started, using whatever name we're passed."
361 ;; We work around shell-mode's bracketing of the buffer name, and do
362 ;; some tramp-mode hygiene for remote connections.
367 (let* ((buffer buffer-name)
368 (prog (or explicit-shell-file-name
372 (name (file-name-nondirectory prog))
373 (startfile (concat "~/.emacs_" name))
374 (xargs-name (intern-soft (concat "explicit-" name "-args"))))
375 (set-buffer buffer-name)
376 (when (and (file-remote-p default-directory)
377 (eq major-mode 'shell-mode)
378 (not (comint-check-proc (current-buffer))))
379 ;; We're returning to an already established but disconnected remote
381 (tramp-cleanup-connection
382 (tramp-dissect-file-name default-directory 'noexpand)
383 'keep-debug 'keep-password))
384 (setq buffer (set-buffer (apply 'make-comint
385 (multishell:unbracket-asterisks buffer-name)
387 (if (file-exists-p startfile)
391 (symbol-value xargs-name)
395 (provide 'multishell)