complete-computing-environment/nemesis.org

265 lines
11 KiB
Org Mode

:PROPERTIES:
:ID: cce/nemesis
:ROAM_ALIASES: "Writing Evil Modes for X11 Apps"
:END:
#+TITLE: Emacs Nemesis System
#+filetags: :CCE:
,#+ARROYO_MODULE_WANTS: cce/evil_mode.org
,#+ARROYO_MODULE_WANTS: cce/exwm_evil_firefox.org
,#+ARROYO_MODULE_WANTS: cce/exwm.org
,#+ARROYO_EMACS_MODULE: nemesis
#+ARCOLOGY_KEY: cce/nemesis
#+PROPERTY: header-args:emacs-lisp :tangle nemesis.el
No, not [[https://www.ign.com/wikis/shadow-of-war/Nemesis_System][that Nemesis System]]. This Nemesis System brings more Evil in to my [[id:cce/emacs][Emacs]] world. It's a simple method to build [[id:cce/evil_mode][Evil Mode]] bindings for my X11 applications running in [[id:cce/exwm][EXWM]], inspired by [[id:cce/exwm_evil_firefox][exwm-evil-firefox]]. I really like this paradigm so I am spending some time generally trying to adapt this to most of my applications. A [[id:465acf41-b758-4059-a425-b53c2f525c32][Hypergoal]] is an OS where all of my GUI applications, and web apps have customised overlay bindings that I can maintain. Right now the goal is not to build fully functional bindings but to make normal mode *useful for navigating* these applications. When I am in a focused text box or an interactive page, it's fine and rational to drop in to insert mode.
Why couldn't I eventually also have smarter modes for certain web-sites, as well? (need [[id:cce/tabfs][TabFS]] or fix [[id:cce/plasma_browser_integration][Plasma Browser Integration]])
#+begin_src emacs-lisp
(provide 'cce/nemesis)
#+end_src
* Nemesis: a library which can generate EXWM+Evil minor modes at runtime
Nemesis exists in three parts:
** A macro which can be used to generate interactive fake-key functions
:LOGBOOK:
- State "DONE" from "NEXT" [2021-06-25 Fri 13:58]
:END:
this is taken from my macro =define-evil-firefox-key= in [[id:cce/exwm_evil_firefox][exwm-evil-firefox]]
Putting =ESC= in =exwm-input-prefix-keys= will make all other EXWM applications pretty sad, but I fnorded that a while ago, and this will help me fnord it the rest of the way.
#+begin_src emacs-lisp
(defun nemesis-intercept-next-ret ()
(interactive)
(setq-local nemesis-next-ret-goto-normal t))
(defun nemesis-intercept-return ()
(interactive)
(exwm-input--fake-key (aref (kbd "<return>") 0))
(when (and (boundp 'nemesis-next-ret-goto-normal)
nemesis-next-ret-goto-normal)
(nemesis-normal-state)
(setq-local nemesis-next-ret-goto-normal nil)))
(evil-define-key 'insert exwm-mode-map (kbd "<return>") 'nemesis-intercept-return)
(evil-define-key 'normal exwm-mode-map (kbd "<return>") 'nemesis-intercept-return)
(push (aref (kbd "<return>") 0) exwm-input-prefix-keys)
(defun nemesis-fake-key (&rest args)
(eval (macroexpand (apply #'nemesis--fake-key args))))
(defun nemesis--fake-key (app-mode state command-name input-key mapped-key insert-after &optional docstring)
(let ((map-name (intern (format "nemesis-%s-mode-map" app-mode)))
(fname (intern (format "nemesis-%s-%s" app-mode command-name))))
`(progn
(defun ,fname ()
,docstring
(interactive)
(exwm-input--fake-key (aref ,mapped-key 0))
,(when insert-after
'(nemesis-insert-state)))
(evil-define-key* 'normal ,map-name ,input-key #',fname)
,(when insert-after
`(advice-add #',fname :after #'nemesis-intercept-next-ret)))))
(defun nemesis-normal-state ()
"Pass every key directly to Emacs."
(interactive)
(setq-local exwm-input-line-mode-passthrough t)
(evil-normal-state))
(defun nemesis-insert-state ()
"Pass every key to firefox."
(interactive)
(setq-local exwm-input-line-mode-passthrough nil)
(evil-insert-state))
(defun nemesis-exit-visual ()
"Exit visual state properly."
(interactive)
;; Unmark any selection
(exwm-firefox-core-left)
(exwm-firefox-core-right)
(exwm-firefox-evil-normal))
(defun nemesis-visual-change ()
"Change text in visual mode."
(interactive)
(exwm-firefox-core-cut)
(exwm-firefox-evil-insert))
#+end_src
** A minor-mode definition which has keybindings defined within
:LOGBOOK:
- State "DONE" from "NEXT" [2021-06-25 Fri 13:58]
:END:
This is embedded in the macro itself below, but the definition looks something like this
#+begin_src emacs-lisp :exports both :tangle no
(pp (macroexpand
'(nemesis-make-mode-form "element" '("Element")
'((:cmd "paste" :state normal :user-key (kbd "p") :mapped-key (kbd "C-v"))
(:cmd "room-list" :state normal :user-key (kbd "C-k") :mapped-key (kbd "C-k") :insert-after t)))))
#+end_src
#+results:
#+begin_example
(progn
(progn
(setq nemesis-element-mode-map
(make-sparse-keymap))
(define-minor-mode nemesis-element-map nil nil nil nemesis-element-mode-map
(if nemesis-element-map
(progn
(nemesis-normal-state))))
(setq nemesis-element-class-name
'("Element"))
(defun nemesis-element-evil-activate-maybe nil
(interactive)
(if
(member exwm-class-name nemesis-element-class-name)
(nemesis-element-map 1))))
(seq-map
(lambda
(defi)
(let
((cmd-name
(plist-get defi :cmd))
(state
(plist-get defi :state))
(user-key
(eval
(plist-get defi :user-key)))
(mapped-key
(eval
(plist-get defi :mapped-key)))
(insert-after\?
(plist-get defi :insert-after)))
(nemesis-fake-key "element" state cmd-name user-key mapped-key insert-after\?)))
'((:cmd "paste" :state normal :user-key
(kbd "p")
:mapped-key
(kbd "C-v"))
(:cmd "room-list" :state normal :user-key
(kbd "C-k")
:mapped-key
(kbd "C-k")
:insert-after t)))
(progn
(define-key nemesis-element-mode-map
[remap evil-exit-visual-state]
'nemesis-element-exit-visual)
(define-key nemesis-element-mode-map
[remap evil-normal-state]
'nemesis-normal-state)
(define-key nemesis-element-mode-map
[remap evil-force-normal-state]
'nemesis-normal-state)
(define-key nemesis-element-mode-map
[remap evil-insert-state]
'nemesis-insert-state)
(define-key nemesis-element-mode-map
[remap evil-insert]
'nemesis-insert-state)
(define-key nemesis-element-mode-map
[remap evil-substitute]
'nemesis-insert-state)
(define-key nemesis-element-mode-map
[remap evil-append]
'nemesis-insert-state)))
#+end_example
** A macro which can generate the minor-mode
:LOGBOOK:
- State "DONE" from "NEXT" [2021-06-25 Fri 13:59]
:END:
This thing returns a =progn= which will be evaluated by Emacs to set up the bindings.
#+begin_src emacs-lisp
(defmacro nemesis-make-mode-form (app-name window-classes bindings)
(let ((mode-name (intern (format"nemesis-%s-mode" app-name)))
(mode-map-name (intern (format "nemesis-%s-mode-map" app-name)))
(activate-name (intern (format "nemesis-%s-evil-activate-maybe" app-name)))
(class-var (intern (format "nemesis-%s-class-name" app-name))))
(list 'progn
`(progn
(setq ,mode-map-name (make-sparse-keymap))
(define-minor-mode ,mode-name ""
:keymap ,mode-map-name
:lighter ,app-name
(if ,mode-name
(progn
(nemesis-normal-state))))
(setq ,class-var ,window-classes)
(defun ,activate-name ()
(interactive)
(if (member exwm-class-name ,class-var)
(,mode-name 1)))
(add-hook 'exwm-manage-finish-hook #',activate-name))
`(seq-map (lambda (defi)
(let ((cmd-name (plist-get defi :cmd))
(state (plist-get defi :state))
(user-key (eval (plist-get defi :user-key)))
(mapped-key (eval (plist-get defi :mapped-key)))
(insert-after? (plist-get defi :insert-after)))
(nemesis-fake-key ,app-name state cmd-name user-key mapped-key insert-after?)))
,bindings)
`(progn
;; Bind normal
(define-key ,mode-map-name [remap evil-exit-visual-state] 'nemesis-element-exit-visual)
(define-key ,mode-map-name [remap evil-normal-state] 'nemesis-normal-state)
(define-key ,mode-map-name [remap evil-force-normal-state] 'nemesis-normal-state)
;; ,mode-map-name insert
(define-key ,mode-map-name [remap evil-insert-state] 'nemesis-insert-state)
(define-key ,mode-map-name [remap evil-insert] 'nemesis-insert-state)
(define-key ,mode-map-name [remap evil-substitute] 'nemesis-insert-state)
(define-key ,mode-map-name [remap evil-append] 'nemesis-insert-state)))))
#+end_src
* X11 mode bindings
** [[id:matrix_org_ecosystem][Matrix.org]]'s Element client
More keybindings can be seen/added using =C-/=.
#+begin_src emacs-lisp
(nemesis-make-mode-form "element" '("Element")
'((:cmd "paste" :state normal :user-key (kbd "p") :mapped-key (kbd "C-v"))
(:cmd "cut" :state normal :user-key (kbd "x") :mapped-key (kbd "C-x"))
(:cmd "up" :state normal :user-key (kbd "k") :mapped-key (kbd "<up>"))
(:cmd "down" :state normal :user-key (kbd "j") :mapped-key (kbd "<down>"))
(:cmd "left" :state normal :user-key (kbd "h") :mapped-key (kbd "<left>"))
(:cmd "right" :state normal :user-key (kbd "l") :mapped-key (kbd "<right>"))
(:cmd "room-list" :state normal :user-key (kbd "C-k") :mapped-key (kbd "C-k") :insert-after t)
(:cmd "prev-room" :state normal :user-key (kbd "J") :mapped-key (kbd "M-<down>"))
(:cmd "next-room" :state normal :user-key (kbd "K") :mapped-key (kbd "M-<up>"))
(:cmd "prev-unread-room" :state normal :user-key (kbd "M-K") :mapped-key (kbd "M-S-<up>"))
(:cmd "next-unread-room" :state normal :user-key (kbd "M-J") :mapped-key (kbd "M-S-<down>"))))
#+end_src
** Signal's client
Luckily chat clients are basically identical.
#+begin_src emacs-lisp
(nemesis-make-mode-form "signal" '("Signal")
'((:cmd "paste" :state normal :user-key (kbd "p") :mapped-key (kbd "C-v"))
(:cmd "cut" :state normal :user-key (kbd "x") :mapped-key (kbd "C-x"))
(:cmd "focus-talk" :state normal :user-key (kbd "t") :mapped-key (kbd "C-T") :insert-after t)
(:cmd "room-list" :state normal :user-key (kbd "/") :mapped-key (kbd "C-f") :insert-after t)
(:cmd "prev-room" :state normal :user-key (kbd "J") :mapped-key (kbd "M-<down>"))
(:cmd "next-room" :state normal :user-key (kbd "K") :mapped-key (kbd "M-<up>"))
(:cmd "prev-unread-room" :state normal :user-key (kbd "M-K") :mapped-key (kbd "M-S-<up>"))
(:cmd "next-unread-room" :state normal :user-key (kbd "M-J") :mapped-key (kbd "M-S-<down>"))))
#+end_src