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
\line continuations between them.)
- If I start my shells in my
.emacs, they don’t parse the
^[[KANSI color codes in output like
ls -l's. (This is with
ansi-color-for-comint-modeon). 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
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
~/.ssh/configand you're good to go!
ansi-color-for-comint-mode-onto 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-conservativelyto something like
shell-modeworks as expected. Woot! Set
tto 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.
protect-buffer-from-kill-modeto 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-sendto 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) )) ; == 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.