First attempt

You can skip this and scroll down to the solution or read it to see some of the problems I was having with term.el.

Problems with term.el

Any minor mode which is enabled while term is running will override bindings

Therefore, if you can, make any such bindings that may interfere with term into global mappings instead.

1
2
3
4
5
6
7
8
;; Comment this out
;; (define-key my-mode-map (kbd "M-k") 'avy-goto-char)

;; Unload binding
(define-key my-mode-map (kbd "M-k") nil)

;; Replace with this
(define-key global-map (kbd "M-k") 'avy-goto-char)

gud-mode stole C-c C-a

C-a is important in many programs for going to the start of the line.

gud-mode creates a global prefix before we have time to prevent that from happening

Load this early before gud is required

That way it will set its global binding to this prefix instead.

1
2
3
4
5
6
7
8
;; This prefix must never be accessible
(define-prefix-command 'my-prefix-tick)
;; This is not a real key. Therefore it won't be pressed.
(global-set-key (kbd "✓") 'my-prefix-tick)

(setq gud-key-prefix (kbd "✓"))

(provide 'my-gud)

If you can, disable modes that interfere with term using manage-minor-mode

1
2
(term-mode
 (off my-mode))

Give C-a to term

This is needed so we can use C-a in whatever application needs it. Bash recognises C-a as the command to go to the start of the line, for instance.

1
2
3
4
(define-key term-raw-map (kbd "C-a") #'term-send-raw)

;; We also need C-x. term-send-raw sends the last char typed, so this should send C-x
(define-key term-raw-map (kbd "C-c C-x") #'term-send-raw)

Obstacles to fixing term.el

It does not play well with manage-minor-mode.el

Also, manage-minor-mode is not all-powerful: you can’t enable a minor mode globally but disable it for a single major mode, for instance.

1
2
3
4
5
6
7
8
;; This does not work sadly
(term-mode
 (off yas-minor-mode)
 (off org-indent-mode)
 (off persp-mode)
 (off which-key-mode)
 (off company-mode)
 (off gud-minor-mode))

Ensure line-numbers-mode, linum-mode etc. are disabled

It messes up the term.

My line-numbers-mode was globalised.

Therefore, disabling it with manage-minor-mode wasn’t an option for me.

1
2
3
(setq manage-minor-mode-default
      '((term-mode
         (off linum-mode))))

I tried using hooks but, due to the way term is started, the modes were not disabled until after term had exit.

term-load-hook did nothing at all.

1
2
(add-hook 'term-mode-hook #'disable-modes-for-term)
(add-hook 'term-load-hook #'disable-modes-for-term)

Therefore, I used advice to turn off my globalised modes. That worked for me.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
(defun disable-modes-for-term ()
  (interactive)
  (progn
    (global-display-line-numbers-mode -1)
    (global-hide-mode-line-mode 1)
    (visual-line-mode -1)
    (fringe-mode -1))

  (my-mode -1))

(defun term-around-advice (proc &rest args)
  (let ((res (apply proc args)))
    (disable-modes-for-term)
    res))
(advice-add 'term :around #'term-around-advice)

The term-raw-mode escape char is a problem. Reassign it to a key that doesn’t exist

Unfortunately, the C-c prefix is populated with all manor of things.

There is no way to avoid it.

Reassign this key.

1
(term-set-escape-char ?✓)

The solution: It requires a fair bit of emacs lisp

I can’t abolish C-c from all the minor modes, but at least I can control what happens when I press it!

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
(setq explicit-shell-file-name "/bin/bash")

;; These prefixes are required for my-term-set-raw-map
(define-prefix-command 'my-term-c-c)
(define-prefix-command 'my-term-c-c-esc)

;; When the program ends, use C-d to kill the buffer
(defun term-raw-or-kill ()
  (interactive)
  (if (not (term-check-proc (current-buffer)))
      (kill-buffer)
    (term-send-raw)))

(defun my-term-set-raw-map ()
  (interactive)
        (let* ((map (make-keymap))
               (esc-map (make-keymap))
               (i 0))

          (define-key map (kbd "C-c") #'my-term-c-c)
          (define-key map (kbd "C-c ESC") #'my-term-c-c-esc)

          (while (< i 128)
            ;; if not C-c
            (if (not (equalp i 3))
                (define-key map (make-string 1 i) #'term-send-raw))

            (define-key map (concat "\C-c" (make-string 1 i)) 'term-send-raw)
            ;; Avoid O and [. They are used in escape sequences for various keys.
            (unless (or (eq i ?O) (eq i 91))
              (define-key esc-map (make-string 1 i) 'term-send-raw-meta))
            (setq i (1+ i)))
          (define-key map [remap self-insert-command] 'term-send-raw)
          (define-key map "\e" esc-map)

          ;; Added nearly all the 'gray keys' -mm

          (if (featurep 'xemacs)
              (define-key map [button2] 'term-mouse-paste)
            (define-key map [mouse-2] 'term-mouse-paste))
          (define-key map (kbd "C-p") 'term-send-raw)
          (define-key map [up] 'term-send-up)
          (define-key map [down] 'term-send-down)
          (define-key map [right] 'term-send-right)
          (define-key map [left] 'term-send-left)
          (define-key map [C-up] 'term-send-ctrl-up)
          (define-key map [C-down] 'term-send-ctrl-down)
          (define-key map [C-right] 'term-send-ctrl-right)
          (define-key map [C-left] 'term-send-ctrl-left)
          (define-key map [delete] 'term-send-del)
          (define-key map [deletechar] 'term-send-del)
          (define-key map [backspace] 'term-send-backspace)
          (define-key map [home] 'term-send-home)
          (define-key map [end] 'term-send-end)
          (define-key map [insert] 'term-send-insert)
          (define-key map [S-prior] 'scroll-down)
          (define-key map [S-next] 'scroll-up)
          (define-key map [S-insert] 'term-paste)
          (define-key map [prior] 'term-send-prior)
          (define-key map [next] 'term-send-next)
          (define-key map [xterm-paste] #'term--xterm-paste)
          (define-key map (kbd "C-d") 'term-raw-or-kill)
          (setq term-raw-map map)
          (setq term-raw-escape-map esc-map))
  ;; This means that M-l will not pass though. I have it set to a hydra menu
  (define-key term-raw-map (kbd "M-l") nil)
  (define-key term-raw-map (kbd "C-c ESC") #'my-term-c-c-esc)
  ;; To send M-l, you do C-c M-l
  (define-key term-raw-map (kbd "C-c M-l") #'term-send-raw-meta)
  ;; To bring up the eval repl, press C-c M-:
  (define-key term-raw-map (kbd "C-c M-:") #'pp-eval-expression)
  ;; (define-key term-raw-map (kbd "C-s") #'term-line-mode)
  (define-key term-raw-map (kbd "C-c C-j") #'term-line-mode)
  (define-key term-raw-map (kbd "C-c C-h") #'describe-mode)
  (define-key term-raw-map (kbd "C-d") 'term-raw-or-kill))

;; This is required because something keeps resetting the map.
;; I think (require 'term), for one, will redefine the value of the map
(defun term-around-advice (proc &rest args)
  (my-term-set-raw-map)
  (let ((res (apply proc args)))
    res))
(advice-add 'term :around #'term-around-advice)

;; Not sure why this is not sufficient
(my-term-set-raw-map)

;; We make the term escape char something we can never hit. Not unless you have a tick key on your keyboard, that is
(term-set-escape-char ?✓)

;; We don't want to move the terminal around as we are trying to use it
(defun my-term-set-scroll-margin ()
  (make-local-variable 'hscroll-margin)
  (setq hscroll-margin 0)
  (make-local-variable 'scroll-margin)
  (setq scroll-margin 0)
  (make-local-variable 'scroll-conservatively)
  (setq scroll-conservatively 10000))

(add-hook 'term-mode-hook #'my-term-set-scroll-margin)

;; Redefining this is required to prevent C-s from stopping programs in the terminal
(defun term-exec-1 (name buffer command switches)
  ;; We need to do an extra (fork-less) exec to run stty.
  ;; (This would not be needed if we had suitable Emacs primitives.)
  ;; The 'if ...; then shift; fi' hack is because Bourne shell
  ;; loses one arg when called with -c, and newer shells (bash,  ksh) don't.
  ;; Thus we add an extra dummy argument "..", and then remove it.
  (let ((process-environment
       (nconc
        (list
         (format "TERM=%s" term-term-name)
         (format "TERMINFO=%s" data-directory)
         (format term-termcap-format "TERMCAP="
                 term-term-name term-height term-width)

         (format "INSIDE_EMACS=%s,term:%s" emacs-version term-protocol-version)
         (format "LINES=%d" term-height)
         (format "COLUMNS=%d" term-width))
        process-environment))
      (process-connection-type t)
      ;; We should suppress conversion of end-of-line format.
      (inhibit-eol-conversion t)
      ;; The process's output contains not just chars but also binary
      ;; escape codes, so we need to see the raw output.  We will have to
      ;; do the decoding by hand on the parts that are made of chars.
      (coding-system-for-read 'binary))
    (when (term--bash-needs-EMACSp)
      (push (format "EMACS=%s (term:%s)" emacs-version term-protocol-version)
            process-environment))
    (apply 'start-process name buffer
         "/bin/sh" "-c"
         (format "stty stop undef; stty start undef; stty -nl echo rows %d columns %d sane 2>/dev/null;\
if [ $1 = .. ]; then shift; fi; exec \"$@\""
                 term-height term-width)
         ".."
         command switches)))

(provide 'my-term)

Another issue is that you can’t have multiple terms unless you rename the buffer

1
(advice-add 'term :after 'unique-buffer-generic-after-advice)

See one of my other blog posts to see how this is done.

Uniqifying emacs apps // Bodacious Blog

One last issue is that sometimes it’s necessary to resize the program within term

term-mode appears to only downsize the program but never upsize.

I will try to figure that out and return to this blog post.

Demonstration

asciinema recording

enable 256 colors

https://github.com/dieggsy/eterm-256color

enable function keys

Use my function key branch of eterm-256color

Neither the terminfo file eterm-color.ti nor eterm-256color.ti had definitions for the function keys.

https://github.com/mullikine/eterm-256color/tree/add-function-keys

Add the following loop to term-send-function-key

This creates C-c <f1> to C-c <f4> for sending those function keys through.

You can extend this to include more keys if you wish.

1
2
3
4
5
;; Send the function key to terminal
(dolist (spec term-function-key-alist)
  (define-key map
    (read-kbd-macro (message (format "C-c <%s>" (car spec))))
    'term-send-function-key))

Finish writing the code. Just use the following

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
(defun term-send-function-key ()
  (interactive)
  (let* ((char last-input-event)
         (output (cdr (assoc (str char) term-function-key-alist))))
    (term-send-raw-string output)))

(defconst term-function-key-alist `(("f1" . ,(read-kbd-macro "<ESC> OP"))
                                    ("80" . ,(read-kbd-macro "<ESC> OP"))
                                    ("f2" . ,(read-kbd-macro "<ESC> OQ"))
                                    ("81" . ,(read-kbd-macro "<ESC> OQ"))
                                    ("f3" . ,(read-kbd-macro "<ESC> OR"))
                                    ("82" . ,(read-kbd-macro "<ESC> OR"))
                                    ("f4" . ,(read-kbd-macro "<ESC> OS"))
                                    ("83" . ,(read-kbd-macro "<ESC> OS"))
                                    ;;("f2" . "\eOQ")
                                    ;;("f3" . "\eOR")
                                    ;;("f4" . "\eOS")
                                    ))

(defun my-term-set-raw-map ()
  (interactive)
  (let* ((map (make-keymap))
         (esc-map (make-keymap))
         (i 0))

    (define-key map (kbd "C-c") #'my-term-c-c)
    (define-key map (kbd "C-c ESC") #'my-term-c-c-esc)

    ;; Send the function key to terminal
    (dolist (spec term-function-key-alist)
      (define-key map
        (read-kbd-macro (message (format "C-c <%s>" (car spec))))
        'term-send-function-key))

    (while (< i 128)
      ;; if not C-c
      (if (not (equalp i 3))
          (define-key map (make-string 1 i) #'term-send-raw))

      (define-key map (concat "\C-c" (make-string 1 i)) 'term-send-raw)
      ;; Avoid O and [. They are used in escape sequences for various keys.
      (unless (or (eq i ?O) (eq i 91))
        (define-key esc-map (make-string 1 i) 'term-send-raw-meta))
      (setq i (1+ i)))
    (define-key map [remap self-insert-command] 'term-send-raw)
    (define-key map "\e" esc-map)

    ;; Added nearly all the 'gray keys' -mm

    (if (featurep 'xemacs)
        (define-key map [button2] 'term-mouse-paste)
      (define-key map [mouse-2] 'term-mouse-paste))
    (define-key map (kbd "C-p") 'term-send-raw)
    (define-key map [up] 'term-send-up)
    (define-key map [down] 'term-send-down)
    (define-key map [right] 'term-send-right)
    (define-key map [left] 'term-send-left)
    (define-key map [C-up] 'term-send-ctrl-up)
    (define-key map [C-down] 'term-send-ctrl-down)
    (define-key map [C-right] 'term-send-ctrl-right)
    (define-key map [C-left] 'term-send-ctrl-left)
    (define-key map [delete] 'term-send-del)
    (define-key map [deletechar] 'term-send-del)
    (define-key map [backspace] 'term-send-backspace)
    (define-key map [home] 'term-send-home)
    (define-key map [end] 'term-send-end)
    (define-key map [insert] 'term-send-insert)
    (define-key map [S-prior] 'scroll-down)
    (define-key map [S-next] 'scroll-up)
    (define-key map [S-insert] 'term-paste)
    (define-key map [prior] 'term-send-prior)
    (define-key map [next] 'term-send-next)
    (define-key map [xterm-paste] #'term--xterm-paste)
    (define-key map (kbd "C-d") 'term-raw-or-kill)
    (setq term-raw-map map)
    (setq term-raw-escape-map esc-map))
  (define-key term-raw-map (kbd "M-l") nil)
  ;; I want M-x to transmit through
  ;; (define-key term-raw-map (kbd "M-x") nil)
  (define-key term-raw-map (kbd "C-c ESC") #'my-term-c-c-esc)
  (define-key term-raw-map (kbd "C-c M-l") #'term-send-raw-meta)
  (define-key term-raw-map (kbd "C-c M-:") #'pp-eval-expression)
  (define-key term-raw-map (kbd "C-c M-x") #'helm-M-x)
  ;; (define-key term-raw-map (kbd "C-s") #'term-line-mode)
  (define-key term-raw-map (kbd "C-c C-j") #'term-line-mode)
  (define-key term-raw-map (kbd "C-c C-h") #'describe-mode)
  (define-key term-raw-map (kbd "C-d") 'term-raw-or-kill))

Adding a sentinel to tell me when the process closes

This means I can create shell scripts which start processes inside emacs and close the emacs frame when done.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
;; This remembers the last frame which was created
(defvar termframe nil)

(defun set-termframe (frame)
  (message (concat "setting " (str frame)))
  (with-current-buffer "*scratch*"
    (setq termframe frame))
  )

(add-hook 'after-make-frame-functions 'set-termframe)

;; This sets a buffer-local variable with the frame which started when this process started
(defun my/term (program &optional closeframe)
  (interactive)
  (with-current-buffer (term program)
    (message (concat "term " (str termframe)))
    (if closeframe
        ;; (defset-local termframe-local (with-current-buffer "*scratch*" termframe))
        (defset-local termframe-local termframe))
    (if (string-equal program "midnight-commander")
        (mc-minor-mode)))
  ;; (add-hook 'after-make-frame-functions 'set-termframe)
  )

;; This sets the process sentinel to close the frame
(defun oleh-term-exec-hook ()
  (message "oleh-term-exec-hook()")
  (let* ((buff (current-buffer))
         (proc (get-buffer-process buff)))
    (message (concat "setting " (str buff) " " (str proc)))
    (set-process-sentinel
     proc
     `(lambda (process event)
        (if (string-match "\\(finished\\|exited\\)" event)
            ;; (string= event "finished\n")
            (progn
              (message "finished")
              (if (variable-p 'termframe-local)
                  (delete-frame termframe-local t))
              (with-current-buffer ,buff (ignore-errors (kill-buffer-and-window)))))))

    (message (concat "setting " (str buff) " " (str proc) " " (str (process-sentinel (get-buffer-process (current-buffer))))))))

(add-hook 'term-exec-hook 'oleh-term-exec-hook)

Demonstration

asciinema recording

Spacemacs needed me to remove this. It ignored the sentinel i made to force the way term-closes

1
vim +/";; Perhaps there is something in spacemacs force the ignore" "$EMACSD/config/my-term.el"

Found it!

1
(add-hook 'term-mode-hook 'ansi-term-handle-close)
1
vim +/"(add-hook 'term-mode-hook 'ansi-term-handle-close)" "$MYGIT/spacemacs/layers/+tools/shell/packages.el"

Solution

1
2
(if (cl-search "SPACEMACS" my-daemon-name)
    (remove-hook 'term-mode-hook 'ansi-term-handle-close))

Preventing window margins from interfering with term

The problem makes term almost unusable.

Therefore, fixing it is important.

This appears to solve the problem

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
(defun term-move-columns (delta)
  (setq term-current-column (max 0 (+ (term-current-column) delta)))
  (let ((point-at-eol (line-end-position)))
    (move-to-column term-current-column t)
    ;; If move-to-column extends the current line it will use the face
    ;; from the last character on the line, set the face for the chars
    ;; to default.
    (when (> (point) point-at-eol)
      (put-text-property point-at-eol (point) 'font-lock-face 'default)))

  ;; This is almost a fix -- it might even be a fix
  (set-window-margins (selected-window) 0 0)
  (set-window-hscroll (selected-window) 0)
  (set-window-vscroll (selected-window) 0)
  )