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 likels -l
‘s. (This is withansi-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 like10
. emacsclient
insideshell-mode
works as expected. Woot! Setdisplay-buffer-reuse-frames
tot
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.
Pingback: John
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.
This is super cool! It finally makes my shell recognize my .bashrc!
Thank you very much for sharing!
Do you still run your shells in Emacs?
definitely!
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
the history messages thing is fixed on trunk: http://debbugs.gnu.org/cgi/bugreport.cgi?bug=13223
thank you thank you thank you!
Have you found a way to have the current path/working directory displayed somewhere?
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!Pingback: dicol
Great post! The only problem I faced was to closing emacs (remote machine) inside my term inside my emacs