M. Rincón Guided by asteriks

Eat Evil

2024-03-27

Eat is a terminal emulator for Emacs available in NonGNU ELpa. Eat is much slower than foot, but faster than term.el. It has some nice features like sixel support, and being able to send messages to Emacs. It also has some surprising idiosyncrasies which makes adding Evil key bindings and other modifications challenging.

I think that the default state, semi char mode, captures too many key combinations. This can be changed by modifying eat-semi-char-non-bound-keys and reloading eat. But doing that inside a whith-eval-after-load block creates an infinite loop. To prevent the loop, the reloading should be done inside a let block:

(with-eval-after-load 'eat
  (setq eat-semi-char-non-bound-keys (list [?\C-\\]
                                           [?\C-w]
                                           [?\C-h]
                                           [?\C-x]
                                           [?\e ?x]))
  (eat-update-semi-char-mode-map)
  (let ((after-load-alist nil)
        (after-load-functions nil))
    (eat-reload)))

Though eat doesn't work well with Evil, eat is a modal terminal emulator. And these modes can be leveraged to make it a bit more evil. This requires starting eat with a new function, my-eat-start:

(defun my-eat-end ()
  "Kill and close the eat buffer."
  (interactive)
  (eat-kill-process)
  (kill-this-buffer))

(defun my-eat-emacs-mode ()
  "Help create a normal state with `eat-emacs-mode`."
  (interactive)
  (eat-emacs-mode)
  (evil-define-key* 'normal eat-mode-map (kbd "[[") 'eat-previous-shell-prompt)
  (evil-define-key* 'normal eat-mode-map (kbd "]]") 'eat-next-shell-prompt)
  (evil-define-key* 'normal eat-mode-map (kbd "i") 'my-eat-semi-char-mode)
  (evil-define-key* 'normal eat-mode-map (kbd "gO") 'browse-url)
  (evil-define-key* 'normal eat-mode-map (kbd "q") 'my-eat-end)
  (evil-normal-state))

(defun my-eat-semi-char-mode ()
  "Create bindings for `eat-mode` and switch to Emacs state."
  (interactive)
  (eat-semi-char-mode)
  (keymap-set eat-mode-map "C-w h" 'evil-window-left)
  (keymap-set eat-mode-map "C-w l" 'evil-window-right)
  (keymap-set eat-mode-map "C-w k" 'evil-window-up)
  (keymap-set eat-mode-map "C-w j" 'evil-window-down)
  (keymap-set eat-mode-map "C-w +" 'evil-window-increase-height)
  (keymap-set eat-mode-map "C-w -" 'evil-window-decrease-height)
  (keymap-set eat-mode-map "C-w <" 'evil-window-decrease-width)
  (keymap-set eat-mode-map "C-w >" 'evil-window-increase-width)
  (keymap-set eat-mode-map "C-w =" 'balance-windows)
  (keymap-set eat-mode-map "C-w R" 'evil-window-rotate-upwards)
  (keymap-set eat-mode-map "C-w r" 'evil-window-rotate-downwards)
  (keymap-set eat-mode-map "C-w W" 'evil-window-prev)
  (keymap-set eat-mode-map "C-w w" 'evil-window-next)
  (keymap-set eat-mode-map "C-w s" 'evil-window-split)
  (keymap-set eat-mode-map "C-w v" 'evil-window-vsplit)
  (keymap-set eat-mode-map "C-w _" 'evil-window-set-height)
  (keymap-set eat-mode-map "C-w |" 'evil-window-set-width)
  (keymap-set eat-mode-map "C-w c" 'evil-window-delete)
  (keymap-set eat-mode-map "C-w n" 'evil-window-new)
  (keymap-set eat-mode-map "C-w o" 'delete-other-windows)
  (keymap-set eat-mode-map "C-w p" 'evil-window-mru)
  (keymap-set eat-mode-map "C-w q" 'evil-quit)
  (keymap-set eat-mode-map "C-w N" 'my-eat-emacs-mode)
  (keymap-set eat-mode-map "C-\\ C-n" 'my-eat-emacs-mode)
  (keymap-set eat-mode-map "C-\\ C-p" 'eat-send-password)
  (keymap-set eat-mode-map "C-w \"" 'eat-yank)
  (keymap-set eat-mode-map "C-x b" 'switch-to-buffer)
  (keymap-set eat-mode-map "C-x k" 'my-eat-end)
  (keymap-set eat-mode-map "C-x p" 'eat-send-password)
  (evil-emacs-state)
  (goto-char (point-max)))

(defun my-eat-start ()
  "Start Eat in the current project root directory in another window."
  (interactive)
  (require 'project)
  (require 'eat)
  (let* ((proj (project-current nil)))
    (if proj (let* ((default-directory (project-root proj))
                    (eat-buffer-name (project-prefixed-buffer-name "eat")))
               (eat-other-window)
               (switch-to-buffer eat-buffer-name)
               (my-eat-semi-char-mode))
      (progn (eat-other-window)
             (switch-to-buffer eat-buffer-name)
             (my-eat-semi-char-mode)))))

Eat also provides great color support; however, if .bashrc is making some color settings based on the terminal name, the file needs to be modified.

### Color support
case "${TERM}" in
    xterm-color|*-256color|foot|*-truecolor)
        echo Making color settings...
        ;;
    ,*)
        echo :(
        ;;
esac

if [ -n "$EAT_SHELL_INTEGRATION_DIR" ]; then
    if [ -r "$EAT_SHELL_INTEGRATION_DIR/bash" ]; then
        # shellcheck source=/dev/null
        source "$EAT_SHELL_INTEGRATION_DIR/bash"
    fi
fi

One nice thing about eat is that it can communicate with Emacs. With a few modifications, eat can open a file inside the host Emacs. With this change the command _eat_msg open will open a file in Emacs.

(defun my-eat-open (file)
  "Helper function to open files from eat terminal."
  (if (file-exists-p file)
      (find-file-other-window file t)
    (warn "File doesn't exist")))

(add-to-list 'eat-message-handler-alist (cons "open" 'my-eat-open))

And for ease of use, the command can be aliased toe emopen in .bashrc.

alias emopen='_eat_msg open'