why I run shells inside Emacs

Mental context switches are evil. Some people handle them better than others, but not me. I handle them worse. I feel it, almost physically, when context switches whittle down my precious flow piece by piece. It hurts!

Even something as small as switching between Emacs and the shell can hurt, especially when I do it thousands of times a day. The physical movement isn’t so bad; it’s that they have different key bindings, different clipboards, and myriad deeper differences in behavior. What’s worse, only some parts are different, so my muscle memory is constantly seduced into a false sense of familiarity, only to have the rug yanked out from under it seconds later.

I put together various hacks to bring Emacs and my shell closer together, with some success, but it always felt like a chronic disease. Curing it completely was never on the table; I could only hope to manage it.

The holy grail, of course, was running shells inside Emacs. I had high hopes when I tried it eight years ago, but it didn’t work out. I tried it again recently though, and it’s still early days, but we’re pretty happy together this time. I think we might go the distance!

Here are a few remaining rough edges:

  • If I enter multiple commands with newlines between them, the prompt is reprinted as soon as the first command finishes. This is misleading, since the rest are still running. (This still happens if I put ;s or \ line continuations between them.)
  • If I start my shells in my .emacs, they don’t parse the ^[[K ANSI color codes in output like ls -l‘s. (This is with ansi-color-for-comint-mode on). However, if I start them after emacs has booted, they parse fine. Huh?
  • Optimize TRAMP. It’s decently fast right now, but I’m sure I could make it even faster!

Here are some things I did manage to tweak:

  • Make shell buffer contents read only by setting the read-only text property.
  • Use dirtrack-mode, which keeps track of the current directory and handles completion better than the default mechanism.
  • …but don’t use it in ssh shells to remote machines, since it doesn’t work. It complains e.g. Warning (emacs): Directory ~/gallery/ does not exist.
  • Instead, run long-lived ssh shells via TRAMP, and add the hook described in this thread to make completion work on remote hosts.
  • Speaking of TRAMP, I use OpenSSH 4.0’s awesome ControlMaster feature to optimize its ssh connections. Just add ControlMaster auto in ~/.ssh/config and you’re good to go!
  • Use ansi-color-for-comint-mode-on to render ANSI color codes.
  • When I type or paste a newline at the prompt, it scrolls halfway up the screen. Prevent this by setting scroll-conservatively to something like 10.
  • emacsclient inside shell-mode works as expected. Woot! Set display-buffer-reuse-frames to t to make sure Emacs doesn’t bury (switch away from) shell buffers when I run it.
  • Extend the SSH shells so that emacsclient opens remote files in the local Emacs via TRAMP.
  • Use protect-buffer-from-kill-mode to prevent accidentally closing shell buffers.
  • Make the completions buffer automatically close when I’m done with it.
  • When I search over command history and press the Enter key, run the selected command immediately. Also, suppress the annoying “History item: X” messages.
  • When I press enter in the middle of multi-line input, include every line of the input, not just up to the line point is on. (Requires comint-eol-on-send to be non-nil.)

Here’s the code in my .emacs:

(defvar my-local-shells
  '("*shell0*" "*shell1*" "*shell2*" "*shell3*" "*music*"))
(defvar my-remote-shells
  '("*snarfed*" "*heaven0*" "*heaven1*" "*heaven2*" "*heaven3*"))
(defvar my-shells (append my-local-shells my-remote-shells))

(require 'tramp)

(custom-set-variables
 '(tramp-default-method "ssh")          ; uses ControlMaster
 '(comint-scroll-to-bottom-on-input t)  ; always insert at the bottom
 '(comint-scroll-to-bottom-on-output nil) ; always add output at the bottom
 '(comint-scroll-show-maximum-output t) ; scroll to show max possible output
 ;; '(comint-completion-autolist t)     ; show completion list when ambiguous
 '(comint-input-ignoredups t)           ; no duplicates in command history
 '(comint-completion-addsuffix t)       ; insert space/slash after file completion
 '(comint-buffer-maximum-size 20000)    ; max length of the buffer in lines
 '(comint-prompt-read-only nil)         ; if this is t, it breaks shell-command
 '(comint-get-old-input (lambda () "")) ; what to run when i press enter on a
                                        ; line above the current prompt
 '(comint-input-ring-size 5000)         ; max shell history size
 '(protect-buffer-bury-p nil)
)

(setenv "PAGER" "cat")

;; truncate buffers continuously
(add-hook 'comint-output-filter-functions 'comint-truncate-buffer)

(defun make-my-shell-output-read-only (text)
  "Add to comint-output-filter-functions to make stdout read only in my shells."
  (if (member (buffer-name) my-shells)
      (let ((inhibit-read-only t)
            (output-end (process-mark (get-buffer-process (current-buffer)))))
        (put-text-property comint-last-output-start output-end 'read-only t))))
(add-hook 'comint-output-filter-functions 'make-my-shell-output-read-only)

(defun my-dirtrack-mode ()
  "Add to shell-mode-hook to use dirtrack mode in my shell buffers."
  (when (member (buffer-name) my-shells)
    (shell-dirtrack-mode 0)
    (set-variable 'dirtrack-list '("^.*[^ ]+:\\(.*\\)>" 1 nil))
    (dirtrack-mode 1)))
(add-hook 'shell-mode-hook 'my-dirtrack-mode)

; interpret and use ansi color codes in shell output windows
(add-hook 'shell-mode-hook 'ansi-color-for-comint-mode-on)

(defun set-scroll-conservatively ()
  "Add to shell-mode-hook to prevent jump-scrolling on newlines in shell buffers."
  (set (make-local-variable 'scroll-conservatively) 10))
(add-hook 'shell-mode-hook 'set-scroll-conservatively)

;; i think this is wrong, and it buries the shell when you run emacsclient from
;; it. temporarily removing.
;; (defun unset-display-buffer-reuse-frames ()
;;   "Add to shell-mode-hook to prevent switching away from the shell buffer
;; when emacsclient opens a new buffer."
;;   (set (make-local-variable 'display-buffer-reuse-frames) t))
;; (add-hook 'shell-mode-hook 'unset-display-buffer-reuse-frames)

;; make it harder to kill my shell buffers
(require 'protbuf)
(add-hook 'shell-mode-hook 'protect-process-buffer-from-kill-mode)

(defun make-comint-directory-tracking-work-remotely ()
  "Add this to comint-mode-hook to make directory tracking work
while sshed into a remote host, e.g. for remote shell buffers
started in tramp. (This is a bug fix backported from Emacs 24:
http://comments.gmane.org/gmane.emacs.bugs/39082"
  (set (make-local-variable 'comint-file-name-prefix)
       (or (file-remote-p default-directory) "")))
(add-hook 'comint-mode-hook 'make-comint-directory-tracking-work-remotely)

(defun enter-again-if-enter ()
  "Make the return key select the current item in minibuf and shell history isearch.
An alternate approach would be after-advice on isearch-other-meta-char."
  (when (and (not isearch-mode-end-hook-quit)
             (equal (this-command-keys-vector) [13])) ; == return
    (cond ((active-minibuffer-window) (minibuffer-complete-and-exit))
          ((member (buffer-name) my-shells) (comint-send-input)))))
(add-hook 'isearch-mode-end-hook 'enter-again-if-enter)

(defadvice comint-previous-matching-input
    (around suppress-history-item-messages activate)
  "Suppress the annoying 'History item : NNN' messages from shell history isearch.
If this isn't enough, try the same thing with
comint-replace-by-expanded-history-before-point."
  (let ((old-message (symbol-function 'message)))
    (unwind-protect
      (progn (fset 'message 'ignore) ad-do-it)
    (fset 'message old-message))))

(defadvice comint-send-input (around go-to-end-of-multiline activate)
  "When I press enter, jump to the end of the *buffer*, instead of the end of
the line, to capture multiline input. (This only has effect if
`comint-eol-on-send' is non-nil."
  (flet ((end-of-line () (end-of-buffer)))
    ad-do-it))

;; not sure why, but comint needs to be reloaded from the source (*not*
;; compiled) elisp to make the above advise stick.
(load "comint.el.gz")

;; for other code, e.g. emacsclient in TRAMP ssh shells and automatically
;; closing completions buffers, see the links above.

13 thoughts on “why I run shells inside Emacs

  1. Pingback: John

  2. This is great, I’ll give it a shot! I’ve been avoiding using the terminal inside emacs because of a lot of small quirks I couldn’t quite get used to.

  3. Thanks for you post.

    I have know so many knowledge.

    I like sh-mode in Emacs 24..

    But, if use alias to jump in sh-mode,

    will cause TAB auto-complete is stop work.

    Must reeval some function , so cont work.

    just this point is agonized

  4. Have you found a way to have the current path/working directory displayed somewhere?

  5. hmm. i’ve always done it in my shell (tcsh) with set prompt="%m:%~%# ", so i’ve never needed (or tried) to do it via emacs. definitely post back here if you find a way!

  6. Pingback: dicol

  7. Great post! The only problem I faced was to closing emacs (remote machine) inside my term inside my emacs

Leave a Reply

Your email address will not be published. Required fields are marked *