aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMagdalena Szweda <hi@roughlynamed.net>2026-10-08 11:16:43 +0200
committerMagdalena Szweda <hi@roughlynamed.net>2026-10-09 12:46:22 +0200
commit3c34eccc548477e100c8947721aa29478cb7b275 (patch)
tree9e4f66aa2461e420d40e45249911995b3d271e80
parent2490296efbd4ec9407fe92d1049af9d663c72f4c (diff)
EXWM: add EXWM
-rw-r--r--config.org141
1 files changed, 141 insertions, 0 deletions
diff --git a/config.org b/config.org
index 6811436..0ac739a 100644
--- a/config.org
+++ b/config.org
@@ -1453,6 +1453,147 @@ This section is purely used to help me with editing root owned files, without ca
(tramp-remote-shell-args ("-c"))
(tramp-connection-timeout 10))))
#+end_src
+* EXWM
+#+begin_quote
+Who needs a WM if you have Emacs?
+#+end_quote
+EXWM is the Emacs X Window Manager.
+#+begin_src emacs-lisp :tangle ~/.config/doom/packages.el
+(package! exwm)
+(package! edwina) ; dynamic tiling
+(package! exwm-edit) ; edit X text fields in an Emacs buffer
+(package! exwm-mff) ; mouse follows focus
+(package! app-launcher :recipe (:host github :repo "SebastienWae/app-launcher"))
+#+end_src
+
+** Helpers and bindings
+#+begin_src emacs-lisp
+(defconst my/exwm-session-p (and (getenv "EXWM") t))
+
+(defun my/spawn (cmd)
+ "Return a command that runs shell CMD."
+ (lambda () (interactive) (start-process-shell-command cmd nil cmd)))
+
+(defun my/run-once (cmd)
+ "Run CMD only if no process named like its executable is already running."
+ (let ((name (file-name-nondirectory (car (split-string cmd)))))
+ (start-process-shell-command
+ cmd nil (format "pgrep -x %s >/dev/null || %s" (shell-quote-argument name) cmd))))
+
+(defun my/close-window ()
+ (interactive)
+ (kill-buffer)
+ (when (cdr (window-list))
+ (edwina-delete-window)))
+
+(defconst my/spawn-binds
+ '(("s-<return>" . "alacritty") ("s-w" . "vivaldi") ("s-e" . "xfe")
+ ("s-s" . "discord") ("s-x" . "loginctl lock-session")
+ ("<print>" . "screenshot") ("S-<print>" . "screenrecord")
+ ("s-S" . "screenshot select") ("s-R" . "screenrecord select")
+ ("<XF86AudioRaiseVolume>" . "wpctl set-volume -l 1.0 @DEFAULT_AUDIO_SINK@ 5%+")
+ ("<XF86AudioLowerVolume>" . "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-")
+ ("<XF86AudioMute>" . "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle")
+ ("<XF86AudioPlay>" . "playerctl play-pause")
+ ("<XF86AudioNext>" . "playerctl next")
+ ("<XF86AudioPrev>" . "playerctl previous")))
+
+(defconst my/wm-binds
+ '(("s-r" . exwm-reset)
+ ("s-p" . app-launcher-run-app)
+ ("s-j" . edwina-select-next-window)
+ ("s-k" . edwina-select-previous-window)
+ ("s-J" . edwina-swap-next-window)
+ ("s-K" . edwina-swap-previous-window)
+ ("s-h" . edwina-dec-mfact)
+ ("s-l" . edwina-inc-mfact)
+ ("s-H" . edwina-inc-nmaster)
+ ("s-L" . edwina-dec-nmaster)
+ ("s-m" . edwina-zoom) ; promote to master, dwm-style
+ ("s-q" . my/close-window)
+ ("s-f" . exwm-floating-toggle-floating)
+ ("C-s-q" . save-buffers-kill-emacs)))
+
+(defconst my/ws-binds
+ (cl-loop for i from 0 below 9
+ for sym in '("!" "@" "#" "$" "%" "^" "&" "*" "(")
+ collect (cons (format "s-%d" (1+ i))
+ (let ((ws i)) (lambda () (interactive) (exwm-workspace-switch-create ws))))
+ collect (cons (format "s-%s" sym)
+ (let ((ws i)) (lambda () (interactive) (exwm-workspace-move-window ws))))))
+
+(defun my/exwm-key (spec)
+ (cons (kbd (car spec))
+ (if (stringp (cdr spec)) (my/spawn (cdr spec)) (cdr spec))))
+#+end_src
+
+** EXWM itself
+#+begin_src emacs-lisp
+(use-package! exwm
+ :when my/exwm-session-p
+ :config
+ (setenv "EXWM" nil)
+ (setq exwm-workspace-number 9
+ exwm-manage-configurations
+ '(((member exwm-class-name '("Alacritty")) char-mode t)
+ ((equal exwm-class-name "discord") workspace 8)
+ ((member exwm-class-name '("Pavucontrol" "Blueman-manager" "ssh-askpass" "Gcr-prompter"))
+ floating t)
+ ((equal exwm-title "pinentry") floating t))
+ exwm-input-global-keys
+ (mapcar #'my/exwm-key (append my/spawn-binds my/wm-binds my/ws-binds))
+ exwm-input-simulation-keys
+ '(([?\C-b] . [left]) ([?\C-f] . [right])
+ ([?\C-p] . [up]) ([?\C-n] . [down])
+ ([?\C-a] . [home]) ([?\C-e] . [end])
+ ([?\C-d] . [delete]) ([?\C-k] . [S-end delete])
+ ([?\C-w] . [?\C-x]) ([?\M-w] . [?\C-c]) ([?\C-y] . [?\C-v])))
+
+ (cl-pushnew ?\C-c exwm-input-prefix-keys)
+
+ (add-hook 'exwm-update-class-hook
+ (lambda () (exwm-workspace-rename-buffer exwm-class-name)))
+ (add-hook 'exwm-update-title-hook
+ (lambda ()
+ (exwm-workspace-rename-buffer
+ (format "%s | %s" exwm-class-name
+ (truncate-string-to-width (or exwm-title "") 40 nil nil "…")))))
+
+ (add-hook 'exwm-mode-hook #'doom-mark-buffer-as-real-h)
+ (advice-add #'suspend-frame :override #'ignore)
+
+ (add-hook 'exwm-init-hook
+ (lambda ()
+ (start-process-shell-command
+ "dbus-env" nil "dbus-update-activation-environment DISPLAY XAUTHORITY")
+ (dolist (c '("feh --bg-scale ~/Pictures/floating_gnu.png" "xset m 0"))
+ (start-process-shell-command c nil c))
+ (my/run-once "~/.cargo/bin/mprisence")))
+
+ (if (fboundp 'exwm-systemtray-mode) (exwm-systemtray-mode 1) (exwm-systemtray-enable))
+ (if (fboundp 'exwm-wm-mode) (exwm-wm-mode 1) (exwm-enable))) ; 0.34+ vs 0.33
+
+(use-package! exwm-edit
+ :when my/exwm-session-p
+ :after exwm
+ :config (setq exwm-edit-split "below"))
+
+(use-package! exwm-mff
+ :when my/exwm-session-p
+ :hook (exwm-init . exwm-mff-mode))
+#+end_src
+** Edwina
+Edwina lets EXWM become a dynamically tiling window manager.
+#+begin_src emacs-lisp
+(use-package! edwina
+ :when my/exwm-session-p
+ :after exwm
+ :config
+ (setq display-buffer-base-action '(display-buffer-below-selected)
+ edwina-mfact 0.50
+ edwina-narrow-threshold 0)
+ (edwina-mode 1))
+#+end_src
* Evil
~Mapping at its finest~
EVIl mode emulates VIM bindings in Emacs.