I spend a lot of time in the shell and I’ve never found a terminal emulator I really like. Konsole is okay, but the cut and paste is rubbish. PuTTY isn’t too bad either, but it doesn’t have tabs. Wouldn’t it be nice if there was something like PuTTY that gave me something similar to buffers in emacs?
You might be thinking that screen + PuTTY would work but that replaces some of the shortcuts I use all the time (C-a for example). Does
eshell give me what I want? What about
ansi-term is pretty close to what I want. It also reconfigures some of the keys1 but I can easily fix that. So let me make a shortlist of the features I want:
- fix the key bindings
- get the mouse cut and paste working properly
- Have a frame just for the shell
- Have some kind of alias mapping to shells
- Have multiple shells in different buffers
Let’s get started.
(require 'cl) (require 'term)
Connecting to a remote host and a reasonable shell-like font.
(defconst sw-ssh "ssh -q -o StrictHostKeyChecking=no") (defconst sw-shell-font "-*-courier new-*-r-*-*-18-*-*-*-*-*-iso8859-1")
Somewhere to store the frame for the shell and the buffers.
(defvar sw-frame nil) (defvar sw-buffers nil)
Configuration variables: does the shell open in a name frame, and if the shell has exited for some reason do we keep it in the list?
(defvar sw-open-in-new-frame nil) (defvar sw-remove-dead-terms t)
I like to build my software in layers. The lowest layer is the most primitive and provides functions to get processes if they exist and to kill buffers that don’t have a process.
(defun sw-shell-get-process (buffer-name) (let ((buffer (get-buffer (concat "*" buffer-name "*")))) (and (buffer-live-p buffer) (get-buffer-process buffer)))) (defun sw-get-process-if-live (buffer-name) (let ((proc (sw-shell-get-process buffer-name))) (and (processp proc) (equal (process-status proc) 'run) proc))) (defun sw-kill-buffer-if-no-process (buffer-name) (let* ((buffer (get-buffer (concat "*" buffer-name "*"))) (proc (sw-get-process-if-live buffer-name))) (when (and (not proc) (buffer-live-p buffer)) (kill-buffer buffer)))) (defalias 'sw-shell-exists-p 'sw-get-process-if-live)
The next layer provides two functions for wrapping basic buffer selection/creation. The plan is to ensure that there is a shell running in the buffer when the function finishes, but we don’t kill it if it exists already. Then there is another function that allows us to create or select a shell and send commands to it.
(defun sw-basic-shell (buffer-name) (sw-kill-buffer-if-no-process buffer-name) ;; If there is a process running, leave it, otherwise ;; create the new buffer (if (sw-shell-exists-p buffer-name) (message "Buffer already exists") (ansi-term "bash" buffer-name)) (switch-to-buffer (concat "*" buffer-name "*"))) (defun sw-shell/commands (buffer-name &rest commands) (sw-basic-shell buffer-name) (let ((proc (sw-shell-get-process buffer-name))) (dolist (cmd commands) (term-simple-send proc cmd))))
This layer has methods for creating my standard shell locally (
sw-standard-shell) and remotely (
sw-server-login). I generally need to
exec bash -l and
su - after I’ve logged in. I could have done this in the
.bashrc of course. You can adapt these start-up commands for your own usage.
(defun sw-standard-shell (buffer-name) (if (sw-shell-exists-p buffer-name) (switch-to-buffer (concat "*" buffer-name "*")) (sw-shell/commands buffer-name "exec bash -l" "su -"))) (defun sw-server-login (host &optional buffer-name) (setq buffer-name (or buffer-name host)) (if (sw-shell-exists-p buffer-name) (switch-to-buffer (concat "*" buffer-name "*")) (sw-shell/commands buffer-name (concat sw-ssh " " host) "exec bash -l" "su -")))
Set up my preferred colors and fonts.
(defun sw-set-display () (interactive) (set-background-color "black") (set-foreground-color "orange") (set-frame-font sw-shell-font))
I need to override a few keys. This should all be fairly obvious.
(defun sw-set-keymap () (term-set-escape-char ?\C-z) (define-key term-raw-map "\C-c" 'term-interrupt-subjob) (define-key term-raw-map "\C-y" 'yank) (define-key term-raw-map (kbd "\M-x") 'execute-extended-command) (define-key term-raw-map "\e" 'term-send-raw) (define-key term-raw-map (kbd "") 'scroll-down) (define-key term-raw-map (kbd "") 'scroll-up)) (sw-set-keymap)
I like highlighted regions to be automatically copied and to paste with the right-hand mouse button. This more-or-less works although it would need some tweaking to get it to send exactly the command you see on the screen.
;; Functions to get the mouse working more-or-less as I like it (defun sw-mouse-paste-clipboard (click arg) (interactive "e\nP") (let ((proc (get-buffer-process (current-buffer)))) (term-send-string proc (current-kill 0))) (setq deactivate-mark t)) (defun sw-mouse-copy-region-to-clipboard (click) (interactive "e") (mouse-set-region click) (let ((transient-mark-mode nil)) (copy-region-as-kill (region-beginning) (region-end)))) (define-key term-raw-map [drag-mouse-1] 'sw-mouse-copy-region-to-clipboard) (define-key term-raw-map [mouse-3] 'sw-mouse-paste-clipboard)
Functions to read a buffer name from the user and select the shell-specific frame if we’ve set
(defun sw-read-buffer-name () (when sw-remove-dead-terms ;; Remove dead terms before offering them to the user (setq sw-buffers (delete-if-not 'sw-shell-exists-p sw-buffers))) (let ((buffer-name (ido-completing-read "Choose buffer name: " sw-buffers))) (if (stringp buffer-name) buffer-name (error "Invalid buffer name")))) (defun sw-get-buffer-proc () (sw-get-process-if-live (sw-read-buffer-name))) (defun sw-select-frame () (if (not sw-open-in-new-frame) (sw-set-display) (unless (frame-live-p sw-frame) (setq sw-frame (make-frame)) (select-frame-set-input-focus sw-frame) (sw-set-display)) (select-frame-set-input-focus sw-frame)))
This actually selects the buffer, reading the buffer-name from the user if necessary.
(defun sw-choose-buffer (&optional buffer-name) (sw-select-frame) (unless (stringp buffer-name) (setq buffer-name (sw-read-buffer-name))) (unless (sw-shell-exists-p buffer-name) (sw-kill-buffer-if-no-process buffer-name) (setq sw-buffers (delete buffer-name sw-buffers))) (let ((already-existed t)) (if (member buffer-name sw-buffers) (switch-to-buffer (concat "*" buffer-name "*")) (setq already-existed nil) (push buffer-name sw-buffers)) (list buffer-name already-existed)))
And these are the top level commands –
sw-open-remote-shell. They build on
sw-server-login respectively but read a buffer name and remote server name as appropriate.
(defun sw-open-shell (&optional buffer-name) (interactive) (multiple-value-bind (buffer-name already-existed) (sw-choose-buffer buffer-name) (unless already-existed (sw-standard-shell buffer-name)))) (defun sw-open-remote-shell (&optional buffer-name server-name) (interactive) (multiple-value-bind (buffer-name already-existed) (sw-choose-buffer buffer-name) (unless already-existed (unless (stringp server-name) (setq server-name (read-string "Server: " buffer-name))) (sw-server-login server-name buffer-name)))) (provide 'shell-wrappers)
And there we go.
sw-shell/commands is generally useful for firing up shells and executing arbitrary commands. I use this all over the place.
Let me know if you have any tips or suggestions for improvement.
If you liked this post, why not subscribe to my RSS feed.
1.For example changing C-c to C-c C-c is very bad. One day I might need to kill that runaway
rm -rf / command very urgently and maybe I’ll have forgotten that C-c is broken.