The Complete Computing Environment

Evil, EXWM, and Firefox walk in to a bar



(provide 'cce/exwm-evil-firefox)

One of the things which was nearly lost during Firefox's Quantum migration1 were complete-makeover plugins like Pentadactyl and Vimperator. Relying on a single set of movement and control idioms across all of my systems allows myself a little bit more "working memory" in which to solve the problem I actually opened up the computer to solve. It's small, perhaps imperceptible, and on shorter timescales it's almost definitely not worth the time invested in achieving it. But I believe I am Developing Software for the Decades and so there is value in persuing things that provide mental- and time-space on longer scales.

Floating around my head at the same time, I had a thought that Emacs Nemesis System is probably worth it. I had a realization after using the Emacs macro subsystem to automate some web forms that if I could use get Emacs to intercept and rewrite commands to EXWM-managed windows, I could implement custom keybindings or perhaps even get Evil-Mode working with it.

And so, when Firefox Quantum came out and I had to use a normal Firefox, without any aide of Emacs or a macro system, it was tough, I felt out of sync with my keyboard, and I went looking for a solution to this that would fit in to the CCE. I'd found that someone already had implemented this EXWM/Firefox/Evil frankensteinian monster: walseb/exwm-firefox-evil. This, along with exwm-firefox-core provide a simple API for performing key-bound actions in Firefox from with emacs-lisp. From there, it's fairly straightforward to get to a set of Evil Mode bindings for those actions, and it's easy too to extend this.

It's a surprisingly effective solution, since it allows for you to have easy access to keybindings which I have otherwise had obscured by not being in Evil Normal State. Firefox additionally allows you to set keybindings for extensions' actions, which can be moved to more memorable single-character locations in this manner.

Having things which go in to insert state for a short period of time, rather than stay there is nice. I have this hack to accomplish this, in two parts: a function which can be advice'd around one of the exwm-firefox-FOO functions which will set a buffer-local variable to true. Then, I have <return> re-bound in insert mode to a function which sends a return key event to the Firefox window, and then, if that variable was set to true, set it to false and enter normal mode. It's a cheeky solution to a problem I invented.

(defun exwm-firefox-intercept-next-ret ()
  (setq-local exwm-firefox-next-ret-normal t))

(defun exwm-firefox-intercept-return ()
  (exwm-input--fake-key (aref (kbd "<return>") 0))
  (when (and (boundp 'exwm-firefox-next-ret-normal)
    (setq-local exwm-firefox-next-ret-normal nil)))
(evil-define-key 'insert exwm-mode-map (kbd "<return>") 'exwm-firefox-intercept-return)
(evil-define-key 'emacs exwm-mode-map (kbd "<return>") 'exwm-firefox-intercept-return)

(push (aref (kbd "<return>") 0) exwm-input-prefix-keys)

have defined here a helper macro which translates a set of mapped keys in to an interactive function which plumbs the key through, and then optionally dumps in to insert mode. This little macro will make the integrations I have far more legible.

(defmacro define-evil-firefox-key (command-name input-key mapped-key insert-after doc)
  (let ((fname (intern (format "exwm-firefox-%s" command-name))))
       (defun ,fname ()
         (exwm-input--fake-key (aref ,mapped-key 0))
         ,(when insert-after
       (evil-define-key 'normal exwm-firefox-evil-mode-map ,input-key #',fname)
       ,(when insert-after
          `(advice-add #',fname :after #'exwm-firefox-intercept-next-ret)))))

Wire f up to showing link-hints using this Firefox add-on.

(define-evil-firefox-key show-link-hints
  (kbd "f") (kbd "C-m") true
  "Show link hints using")

Wire C-k through to the client, but turn to insert mode, return to normal mode after hitting enter. Chat clients use this as the "room switcher" keybinding a lot of the time and everyone loves chatting with their friends in a web browser.

(define-evil-firefox-key show-room-switcher
  (kbd "C-k") (kbd "C-k") true
  "Chat clients generally intercept C-k to show a room/chat
  switcher. This does that and moves to insert mode")

Wire up A to toggling Dark Reader extension

(define-evil-firefox-key toggle-dark-reader
  (kbd "A") (kbd "M-A") nil
  "Toggle between dark CSS and light CSS.")

Wire e up to switching in to Firefox reader view.

(define-evil-firefox-key toggle-reader-mode
  (kbd "e") (kbd "C-M-r") nil
  "Toggle between firefox Reader Mode view.")

Wire U up to restoring a closed tab.

(evil-define-key 'normal exwm-firefox-evil-mode-map (kbd "U") 'exwm-firefox-core-tab-close-undo)

Wire T up to toggling tree-style-tabs. If the sidebar fails to toggle but other keybindings work, check that the extension's toggle shortcut is set to C-M-t; exwm's fake-input-key function doesnt like when i feed it (kbd "F1")

(define-evil-firefox-key toggle-tree-tabs
  (kbd "T") (kbd "C-M-t") nil
  "toggle visibility of tree-style-tabs sidebar")
(define-evil-firefox-key toggle-tree-tabs
  (kbd "st") (kbd "C-M-t") nil
  "toggle visibility of tree-style-tabs sidebar")

Wire s up to Simple Tab Groups. This is changed in the Extension Shortcuts setting which is synced between my devices.

(define-evil-firefox-key simple-tab-sidebar
  (kbd "sg") (kbd "C-8") nil
  "show the simple tab group sidebar")
(define-evil-firefox-key simple-tab-popup
  (kbd "sp") (kbd "C-M-8") t
  "show the simple tab group popup")
(define-evil-firefox-key simple-tab-popup-manage
  (kbd "sm") (kbd "M-Y") nil
  "show the simple tab group manager")

(define-evil-firefox-key simple-tab-prev-group
  (kbd "M-j") (kbd "C-M-u") nil
  "show the simple tab group popup")
(define-evil-firefox-key simple-tab-next-group
  (kbd "M-k") (kbd "C-M-y") nil
  "show the simple tab group popup")

Wire P up to a save Action in the Wallabagger extension. This will save a URL and a snapshot of its content to an application which I can read while I am offline. If this doesn't work, validate the token in the plugin-options. Validate as well that on "Manage Extensions Shortcut" page (found inside of gear dropdown on about:addons) the Wallabagger extension has either of its actions bound to Shift-Alt-W.

(define-evil-firefox-key wallabag-it
  (kbd "P") (kbd "M-w") nil
  "Save a page to a Wallabag instance.")

When I'm in insert mode, I don't have arrow keys bound any more, so use C-NPBF in insert mode.

(evil-define-key 'insert exwm-firefox-evil-mode-map (kbd "C-n") 'exwm-firefox-core-down)
(evil-define-key 'insert exwm-firefox-evil-mode-map (kbd "C-p") 'exwm-firefox-core-up)
(evil-define-key 'insert exwm-firefox-evil-mode-map (kbd "C-b") 'exwm-firefox-core-left)
(evil-define-key 'insert exwm-firefox-evil-mode-map (kbd "C-f") 'exwm-firefox-core-right)

(evil-define-key 'insert exwm-firefox-evil-mode-map (kbd "C-a") 'exwm-firefox-core-top)
(evil-define-key 'insert exwm-firefox-evil-mode-map (kbd "C-e") 'exwm-firefox-core-bottom)

This sort of thing is what's so incredible to me about working inside of Emacs. Even my GUI browser becomes programmable in this way and in ways that the software itself doesn't allow. Firefox doesn't allow extensions to intercept or rebind certain keys (C-n and C-q, I'm looking at you), other things (like the Reader Mode and Pocket shortcuts) aren't even exposed as bindable or externally callable. Having a programmable windowing manager between you and the browser lets you do crazy shit like this.

This is the sort of pattern which could be applied to any GUI application, too, I just don't really use that many other than Firefox. And putting this together isn't even difficult. The basic setup is an afternoon's work for anyone who cares to find it.

(use-package exwm-firefox-core)
(use-package exwm-firefox-evil
  :after exwm
  :hook (exwm-manage-finish . exwm-firefox-evil-activate-if-firefox)
  :commands (exwm-firefox-evil-activate-if-firefox exwm-firefox-evil-mode)
  (push (aref (kbd "<escape>") 0) exwm-input-prefix-keys)

  ;; This may have to be required for smooth operation
  (defun exwm-firefox-core-cancel ()
    "General cancel action."
    (exwm-input--fake-key 'escape))

  ;; Functionality for advising certain exwm-firefox commands
  ;; to only enter insert mode until return is hit.
  ;; Macro for aiding in rebind creation

  ;; Rebinds