Download
(require 'shell)
(require 'ansi-color)
(require 'cl)
(defvar multi-shell-window-configuration-before-complete nil
"The window configuration will record before complete window popup first time,
and window configuration will revert when you type `multi-shell-revert-window-keystroke' next time.")
(defadvice comint-dynamic-complete (around record-window-configuration-before-popup-window)
"This advice record window configuration before popup complete window.
And just record when variable `multi-shell-window-configuration-before-complete' is nil."
(if (and (boundp 'multi-shell-window-configuration-before-complete)
multi-shell-window-configuration-before-complete)
ad-do-it
(let (window-configuration-before-do
window-configuration-after-do)
(setq window-configuration-before-do (current-window-configuration))
ad-do-it
(setq window-configuration-after-do (current-window-configuration))
(unless (equal window-configuration-before-do window-configuration-after-do)
(setq multi-shell-window-configuration-before-complete window-configuration-before-do)))))
(defgroup multi-shell nil
"Multi-Shell Manager"
:group 'shell)
(defcustom multi-shell-command nil
"The command that multi-shell will use.
If nil, will try to get value of
environment variable `SHELL' or `ESHELL'."
:type 'string
:group 'multi-shell)
(defcustom multi-shell-use-ansi-color t
"Whether translate ANSI escape sequences into faces.
And default is t."
:type 'boolean
:group 'multi-shell)
(defcustom multi-shell-try-create t
"Try to create a new shell buffer when switch.
When use `multi-shell-next' or `multi-shell-prev' to switch shell buffer,
try to create a new shell buffer if haven't any shell buffers exist."
:type 'boolean
:group 'multi-shell)
(defcustom multi-shell-default-dir "~/"
"The default directory for create shell buffer,
when current local directory is no exist."
:type 'string
:group 'multi-shell)
(defcustom multi-shell-buffer-name "multi-shell"
"The name of shell buffer."
:type 'string
:group 'multi-shell)
(defcustom multi-shell-bottom-window-height -13
"The height of bottom window create."
:type 'integer
:group 'multi-shell)
(defcustom multi-shell-revert-window-keystroke "SPC"
"The keystroke that revert window after `comint-dynamic-complete'.
Default is `SPACE."
:type 'string
:group 'multi-shell)
(defcustom multi-shell-revert-window-after-complete t
"Revert window Lat's after `comint-dynamic-complete'.
Default, type `TAB' (comint-dynamic-complete) in `shell-mode',
will popup a window complete, but when you type TAB many times,
`shell-mode' can't revert window layout before you type TAB.
So it's too bad use `comint-dynamic-complete' in complex window layout,
it can break your window layout and can't revert.
If you type `TAB' in `multi-shell' buffer, and window layout
will revert when you type keystroke `multi-shell-revert-window-keystroke' next time."
:type 'boolean
:set (lambda (symbol value)
(set symbol value)
(if value
(progn
(define-key shell-mode-map
(read-kbd-macro multi-shell-revert-window-keystroke)
'multi-shell-insert-or-revert-window-layout)
(ad-enable-advice 'comint-dynamic-complete 'around 'record-window-configuration-before-popup-window))
(define-key shell-mode-map
(read-kbd-macro multi-shell-revert-window-keystroke)
'self-insert-command)
(ad-disable-advice 'comint-dynamic-complete 'around 'record-window-configuration-before-popup-window))
(ad-activate 'comint-dynamic-complete))
:group 'multi-shell)
(defun multi-shell-new ()
"Create a new multi-shell buffer."
(interactive)
(let* ((shell-buffer (multi-shell-get-buffer)))
(set-buffer shell-buffer)
(if multi-shell-use-ansi-color
(add-hook 'shell-mode-hook 'ansi-color-for-comint-mode-on)
(remove-hook 'shell-mode-hook 'ansi-color-for-comint-mode-on))
(save-window-excursion
(shell (buffer-name shell-buffer)))
(multi-shell-handle-close)
(multi-shell-handle-revert-window-after-complete)
(switch-to-buffer shell-buffer)
(add-hook 'kill-buffer-hook 'multi-shell-handle-kill-buffer)
))
(defun multi-shell-get-buffer ()
"Get shell buffer."
(let* ((shell-list-length (length (multi-shell-list)))
(index (if shell-list-length (1+ shell-list-length) 1)))
(with-temp-buffer
(cd (or default-directory (expand-file-name multi-shell-default-dir)))
(while (buffer-live-p (get-buffer (format "*%s<%s>*" multi-shell-buffer-name index)))
(incf index))
(setq explicit-shell-file-name (or multi-shell-command
(getenv "SHELL")
(getenv "ESHELL")
"/bin/sh"))
(get-buffer-create (format "*%s<%s>*" multi-shell-buffer-name index))
)))
(defun multi-shell-handle-close ()
"This function for close current shell buffer.
When `exit' from shell buffer."
(when (ignore-errors (get-buffer-process (current-buffer)))
(set-process-sentinel (get-buffer-process (current-buffer))
(lambda (proc change)
(when (string-match "\\(finished\\|exited\\)" change)
(kill-buffer (process-buffer proc)))))))
(defun multi-shell-handle-kill-buffer ()
"Function that hook `kill-buffer-hook'."
(when (and (eq major-mode 'shell-mode)
(comint-check-proc (current-buffer)))
(comint-interrupt-subjob)))
(defun multi-shell-handle-revert-window-after-complete ()
"Handle window configuration after complete."
(if multi-shell-revert-window-after-complete
(progn
(define-key shell-mode-map
(read-kbd-macro multi-shell-revert-window-keystroke)
'multi-shell-insert-or-revert-window-layout)
(ad-enable-advice 'comint-dynamic-complete 'around 'record-window-configuration-before-popup-window))
(define-key shell-mode-map
(read-kbd-macro multi-shell-revert-window-keystroke)
'self-insert-command)
(ad-disable-advice 'comint-dynamic-complete 'around 'record-window-configuration-before-popup-window))
(ad-activate 'comint-dynamic-complete))
(defun multi-shell-insert-or-revert-window-layout (&optional arg)
"Handle insert self or window configuration revert."
(interactive "P")
(if multi-shell-window-configuration-before-complete
(progn
(set-window-configuration multi-shell-window-configuration-before-complete)
(setq multi-shell-window-configuration-before-complete nil)
(message "Have revert window layout."))
(self-insert-command (or arg 1))))
(defun multi-shell-list ()
"The shell buffers presently active."
(autoload 'remove-if-not "cl-seq")
(sort
(remove-if-not (lambda (b)
(setq case-fold-search t)
(string-match
(format "^\\\*%s<[0-9]+>\\\*$" multi-shell-buffer-name)
(buffer-name b)))
(buffer-list))
(lambda (a b)
(< (string-to-number
(cadr (split-string (buffer-name a) "[<>]")))
(string-to-number
(cadr (split-string (buffer-name b) "[<>]")))))))
(defun multi-shell-switch (direction offset)
"Switch to shell buffer.
If DIRECTION is `NEXT', switch to next shell buffer.
if DIRECTION is `PREVIOUS', switch to previous shell buffer.
Default OFFSET is 1.
If option `multi-shell-try-create' is non-nil, will create a new shell buffer
if have any shell buffer exist."
(let (shells this-buffer)
(setq shells (multi-shell-list))
(if (consp shells)
(progn
(setf (cdr (last shells)) shells)
(setq this-buffer (position (current-buffer) (multi-shell-list)))
(if this-buffer
(if (eql direction 'NEXT)
(switch-to-buffer (nth (+ this-buffer offset) shells))
(switch-to-buffer (nth (+ (- (length (multi-shell-list)) offset)
this-buffer) shells)))
(switch-to-buffer (car shells))))
(if multi-shell-try-create
(progn
(multi-shell-new)
(message "Create a new `multi-shell' buffer."))
(message "Haven't any `multi-shell' buffer exist.")))))
(defun multi-shell-next (&optional offset)
"Switch to next shell buffer."
(interactive "P")
(multi-shell-switch 'NEXT (or offset 1)))
(defun multi-shell-prev (&optional offset)
"Switch to previous shell buffer."
(interactive "P")
(multi-shell-switch 'PREVIOUS (or offset 1)))
(defun multi-shell-current-directory ()
"Open shell that start at current directory."
(interactive)
(split-window-vertically multi-shell-bottom-window-height)
(other-window 1)
(multi-shell-new))
(provide 'multi-shell)