diff --git a/.emacs.d/desktop.el b/.emacs.d/desktop.el index 25b637a..815d481 100644 --- a/.emacs.d/desktop.el +++ b/.emacs.d/desktop.el @@ -8,6 +8,21 @@ (when (string-empty-p (shell-command-to-string "pgrep -u pavel shepherd")) (call-process "shepherd"))) +(use-package pinentry + :straight t + :after (exwm) + :config + (setenv "GPG_AGENT_INFO" nil) ;; use emacs pinentry + (setq auth-source-debug t) + + (setq epg-gpg-program "gpg2") ;; not necessary + (require 'epa-file) + (epa-file-enable) + (setq epa-pinentry-mode 'loopback) + (setq epg-pinentry-mode 'loopback) + (pinentry-start) + (my/run-in-background "gpgconf --reload gpg-agent")) + (defun my/exwm-direction-exists-p (dir) (cl-some (lambda (dir) (let ((win (windmove-find-other-window dir))) @@ -35,19 +50,24 @@ (defun my/exwm-resize-window (dir kind &optional value) (unless value (setq value my/exwm-resize-value)) - (pcase kind - ('shrink - (pcase dir - ('width - (evil-window-decrease-width value)) - ('height - (evil-window-decrease-height value)))) - ('grow - (pcase dir - ('width - (evil-window-increase-width value)) - ('height - (evil-window-increase-height value)))))) + (let* ((is-exwm-floating + (and (derived-mode-p 'exwm-mode) + exwm--floating-frame)) + (func (if is-exwm-floating + (intern + (concat + "exwm-layout-" + (pcase kind ('shrink "shrink") ('grow "enlarge")) + "-window" + (pcase dir ('height "") ('width "-horizontally")))) + (intern + (concat + "evil-window" + (pcase kind ('shrink "-decrease-") ('grow "-increase-")) + (symbol-name dir)))))) + (when is-exwm-floating + (setq value (* 5 value))) + (funcall func value))) (defhydra my/exwm-resize-hydra (:color pink :hint nil :foreign-keys run) " @@ -62,6 +82,31 @@ _=_: Balance " ("=" balance-windows) ("q" nil "quit" :color blue)) +(use-package perspective-exwm + :straight (:host github :repo "SqrtMinusOne/perspective-exwm.el") + :config + (setq perspective-exwm-override-initial-name + '((0 . "misc") + (1 . "core") + (2 . "browser") + (3 . "comms") + (4 . "dev"))) + (general-define-key + :keymaps 'perspective-map + "e" #'perspective-exwm-move-to-workspace + "E" #'perspective-exwm-copy-to-workspace)) + +(defun my/exwm-workspace-switch-monitor () + (interactive) + (if (plist-get exwm-randr-workspace-monitor-plist exwm-workspace-current-index) + (setq exwm-randr-workspace-monitor-plist + (map-delete exwm-randr-workspace-monitor-plist exwm-workspace-current-index)) + (setq exwm-randr-workspace-monitor-plist + (plist-put exwm-randr-workspace-monitor-plist + exwm-workspace-current-index + my/exwm-another-monitor))) + (exwm-randr-refresh)) + (use-package transient :straight t) @@ -78,100 +123,17 @@ _=_: Balance " ("d" "Discord" (lambda () (interactive) (my/run-in-background "flatpak run com.discordapp.Discord"))) ("q" "Quit" transient-quit-one)]) -(defun my/exwm-workspace-switch-monitor () - (interactive) - (if (plist-get exwm-randr-workspace-monitor-plist exwm-workspace-current-index) - (setq exwm-randr-workspace-monitor-plist - (map-delete exwm-randr-workspace-monitor-plist exwm-workspace-current-index)) - (setq exwm-randr-workspace-monitor-plist - (plist-put exwm-randr-workspace-monitor-plist - exwm-workspace-current-index - my/exwm-another-monitor))) - (exwm-randr-refresh)) - -(defun my/cycle-persp-exwm-buffers (dir) - (let* ((current (current-buffer)) - (ignore-rx (persp--make-ignore-buffer-rx)) - (visible-buffers '()) - (exwm-data - (cl-loop for buf in (persp-current-buffers) - for is-another = (and (get-buffer-window buf) (not (eq current buf))) - if (and (buffer-live-p buf) - (eq 'exwm-mode (buffer-local-value 'major-mode buf)) - (not (string-match-p ignore-rx (buffer-name buf)))) - collect buf into all-buffers - and if (not is-another) collect buf into cycle-buffers - finally (return (list all-buffers cycle-buffers)))) - (all-buffers (nth 0 exwm-data)) - (cycle-buffers (nth 1 exwm-data)) - (current-pos (or (cl-position current cycle-buffers) -1))) - (if (seq-empty-p cycle-buffers) - (message "No EXWM buffers to cycle!") - (let* ((next-pos (% (+ current-pos (length cycle-buffers) - (if (eq dir 'forward) 1 -1)) - (length cycle-buffers))) - (next-buffer (nth next-pos cycle-buffers))) - (switch-to-buffer next-buffer) - (message - "%s" - (mapconcat - (lambda (buf) - (let ((name (string-replace "EXWM :: " "" (buffer-name buf)))) - (cond - ((eq (current-buffer) buf) - (concat - "[" - (propertize name 'face `(foreground-color . ,(doom-color 'yellow))) - "]")) - ((not (member buf cycle-buffers)) - (concat - "[" - (propertize name 'face `(foreground-color . ,(doom-color 'blue))) - "]")) - (t (format " %s " name))))) - all-buffers - " ")))))) - -(defun my/add-exwm-buffers-to-current-perspective () - (interactive) - (let ((ignore-rx (persp--make-ignore-buffer-rx))) - (cl-loop for buf in (buffer-list) - if (and (buffer-live-p buf) - (eq 'exwm-mode (buffer-local-value 'major-mode buf)) - (not (string-match-p ignore-rx (buffer-name buf)))) - do (persp-add-buffer (buffer-name buf))))) - -(defun my/exwm-revive-perspectives () - "Make perspectives in the current frame not killed." - (interactive) - (let ((to-switch nil)) - (maphash - (lambda (_ v) - (setf (persp-killed v) nil) - (unless to-switch - (setq to-switch v))) - (frame-parameter nil 'persp--hash)) - (when to-switch - (persp-switch (persp-name to-switch))))) - (defun my/exwm-lock () (interactive) (my/run-in-background "i3lock -f -i /home/pavel/Pictures/lock-wallpaper.png")) -(use-package pinentry - :straight t - :after (exwm) - :config - (setenv "GPG_AGENT_INFO" nil) ;; use emacs pinentry - (setq auth-source-debug t) - - (setq epg-gpg-program "gpg2") ;; not necessary - (require 'epa-file) - (epa-file-enable) - (setq epa-pinentry-mode 'loopback) - (setq epg-pinentry-mode 'loopback) - (pinentry-start) - (my/run-in-background "gpgconf --reload gpg-agent")) +(defun my/exwm-update-global-keys () + (interactive) + (setq exwm-input--global-keys nil) + (dolist (i exwm-input-global-keys) + (exwm-input--set-key (car i) (cdr i))) + (when exwm--connection + (exwm-input--update-global-prefix-keys))) (defun my/exwm-init () (exwm-workspace-switch 1) @@ -266,6 +228,7 @@ _=_: Balance " ;; Fullscreen (,(kbd "s-f") . exwm-layout-toggle-fullscreen) + (,(kbd "s-F") . exwm-floating-toggle-floating) ;; Quit (,(kbd "s-Q") . evil-quit) @@ -280,6 +243,8 @@ _=_: Balance " ;; Switch buffers (,(kbd "s-e") . persp-ivy-switch-buffer) + (,(kbd "s-E") . perspective-exwm-switch-perspective) + ;; Resize windows (,(kbd "s-r") . my/exwm-resize-hydra/body) @@ -309,8 +274,8 @@ _=_: Balance " (,(kbd "s-") . my/exwm-workspace-switch-monitor) ;; Cycle EXWM windows in the current perspective - (,(kbd "s-[") . (lambda () (interactive) (my/cycle-persp-exwm-buffers 'backward))) - (,(kbd "s-]") . (lambda () (interactive) (my/cycle-persp-exwm-buffers 'forward))) + (,(kbd "s-[") . perspective-exwm-cycle-exwm-buffers-backward) + (,(kbd "s-]") . perspective-exwm-cycle-exwm-buffers-forward) (,(kbd "s-o") . ,(my/app-command "rofi -show window")) ;; 's-N': Switch to certain workspace with Super (Win) plus a number key (0 - 9) @@ -337,8 +302,23 @@ _=_: Balance " (force-mode-line-update)) (add-hook 'exwm-workspace-switch-hook #'my/exwm-mode-line-info-update) + (defun exwm-input--fake-last-command () + "Fool some packages into thinking there is a change in the buffer." + (setq last-command #'exwm-input--noop) + (condition-case hook-error + (progn + (run-hooks 'pre-command-hook) + (run-hooks 'post-command-hook)) + ((error) + (exwm--log "Error occurred while running command hooks: %s\n\nBacktrace:\n\n%s" + hook-error + (with-temp-buffer + (setq-local standard-output (current-buffer)) + (backtrace) + (buffer-string)))))) (set-frame-parameter (selected-frame) 'alpha '(90 . 90)) (add-to-list 'default-frame-alist '(alpha . (90 . 90))) + (perspective-exwm-mode) (exwm-enable)) diff --git a/Desktop.org b/Desktop.org index 5663686..d2a13d2 100644 --- a/Desktop.org +++ b/Desktop.org @@ -261,8 +261,8 @@ Settings for [[https://github.com/ch11ng/exwm][Emacs X Window Manager]], a tilin References: - [[https://github.com/ch11ng/exwm/wiki][EXWM Wiki]] - [[https://github.com/daviwil/emacs-from-scratch/blob/master/Desktop.org][Emacs From Scratch config]] - -** Xsession +** Startup & UI +*** Xsession First things first, Emacs has to be launched as a window manager. On a more conventional system I'd create a .desktop file in some system folder that can be seen by a login manager, but in the case of Guix it's a bit more complicated, because all such folders are not meant to be changed manually. However, GDM, the login manager that seems to be the default on Guix, launches =~/.xsession= on the startup if it's present, which is just fine for my purposes. @@ -296,7 +296,7 @@ copyq & # exec dbus-launch --exit-with-session ~/.emacs.d/run-exwm.sh exec dbus-launch --exit-with-session emacs -mm --debug-init -l ~/.emacs.d/desktop.el #+end_src -** Startup apps +*** Startup apps Now that Emacs is launched, it is necessary to set up the EXWM-specific parts of config. I want to launch some apps from EXWM instead of the Xsession file for two purposes: @@ -315,7 +315,56 @@ As of now, these are polybar, feh and, shepherd: (when (string-empty-p (shell-command-to-string "pgrep -u pavel shepherd")) (call-process "shepherd"))) #+end_src -** Moving windows +*** Pinentry +The GUI pinentry doesn't work too well with EXWM because of issues with popup windows, so we will use the Emacs one. + +#+begin_src emacs-lisp +(use-package pinentry + :straight t + :after (exwm) + :config + (setenv "GPG_AGENT_INFO" nil) ;; use emacs pinentry + (setq auth-source-debug t) + + (setq epg-gpg-program "gpg2") ;; not necessary + (require 'epa-file) + (epa-file-enable) + (setq epa-pinentry-mode 'loopback) + (setq epg-pinentry-mode 'loopback) + (pinentry-start) + (my/run-in-background "gpgconf --reload gpg-agent")) +#+end_src + +#+begin_src conf-space :tangle ~/.gnupg/gpg-agent.conf +default-cache-ttl 3600 +max-cache-ttl 3600 +allow-emacs-pinentry +allow-loopback-pinentry +#+end_src +*** Modeline +Show current workspace in the modeline. + +#+begin_src emacs-lisp :noweb-ref exwm-mode-line-config :tangle no +(defvar my/exwm-mode-line-info "") + +(add-to-list 'mode-line-misc-info + '(:eval my/exwm-mode-line-info)) + +(defun my/exwm-mode-line-info-update () + (setq my/exwm-mode-line-info + (concat + "[" + (propertize (funcall exwm-workspace-index-map exwm-workspace-current-index) + 'face + `(foreground-color . ,(doom-color 'yellow))) + "]")) + (setq my/exwm-mode-line-info-no-props (funcall exwm-workspace-index-map exwm-workspace-current-index)) + (force-mode-line-update)) + +(add-hook 'exwm-workspace-switch-hook #'my/exwm-mode-line-info-update) +#+end_src +** Windows +*** Moving windows My functions for managing windows. I initially wrote these to mimic the i3 behavior for my Emacs + i3 integration, but I want to try to keep them for the EXWM config as well to make the transition less painful. A predicate which checks whether there is space in the given direction: @@ -347,7 +396,7 @@ And a function to move windows with the following behavior: (other-direction (evil-move-window dir))))) #+end_src -** Resizing windows +*** Resizing windows Something like this also goes for resizing windows. I'm used to the i3 "mode" for this functionality, and this seems to be a sensible approach. #+begin_src emacs-lisp @@ -356,19 +405,24 @@ Something like this also goes for resizing windows. I'm used to the i3 "mode" fo (defun my/exwm-resize-window (dir kind &optional value) (unless value (setq value my/exwm-resize-value)) - (pcase kind - ('shrink - (pcase dir - ('width - (evil-window-decrease-width value)) - ('height - (evil-window-decrease-height value)))) - ('grow - (pcase dir - ('width - (evil-window-increase-width value)) - ('height - (evil-window-increase-height value)))))) + (let* ((is-exwm-floating + (and (derived-mode-p 'exwm-mode) + exwm--floating-frame)) + (func (if is-exwm-floating + (intern + (concat + "exwm-layout-" + (pcase kind ('shrink "shrink") ('grow "enlarge")) + "-window" + (pcase dir ('height "") ('width "-horizontally")))) + (intern + (concat + "evil-window" + (pcase kind ('shrink "-decrease-") ('grow "-increase-")) + (symbol-name dir)))))) + (when is-exwm-floating + (setq value (* 5 value))) + (funcall func value))) (defhydra my/exwm-resize-hydra (:color pink :hint nil :foreign-keys run) " @@ -383,28 +437,24 @@ _=_: Balance " ("=" balance-windows) ("q" nil "quit" :color blue)) #+end_src -** App shortcuts -Also, a transient for shortcuts for the most frequent apps. - -I wanted to make the interactive lambda a macro, but this doesn't seem to work the way I expect, so the code has a bit of duplication. +** Perspectives #+begin_src emacs-lisp -(use-package transient - :straight t) - -(defun my/run-in-background (command) - (let ((command-parts (split-string command "[ ]+"))) - (apply #'call-process `(,(car command-parts) nil 0 nil ,@(cdr command-parts))))) - -(transient-define-prefix my/exwm-apps () - ["Apps" - ("t" "Termnial (Alacritty)" (lambda () (interactive) (my/run-in-background "alacritty"))) - ("b" "Browser (Firefox)" (lambda () (interactive) (my/run-in-background "firefox"))) - ("v" "VK" (lambda () (interactive) (my/run-in-background "vk"))) - ("s" "Slack" (lambda () (interactive) (my/run-in-background "slack-wrapper"))) - ("d" "Discord" (lambda () (interactive) (my/run-in-background "flatpak run com.discordapp.Discord"))) - ("q" "Quit" transient-quit-one)]) +(use-package perspective-exwm + :straight (:host github :repo "SqrtMinusOne/perspective-exwm.el") + :config + (setq perspective-exwm-override-initial-name + '((0 . "misc") + (1 . "core") + (2 . "browser") + (3 . "comms") + (4 . "dev"))) + (general-define-key + :keymaps 'perspective-map + "e" #'perspective-exwm-move-to-workspace + "E" #'perspective-exwm-copy-to-workspace)) #+end_src -** Move workspace to another monitor +** Workspaces +*** Move workspace to another monitor A function to move the current workspace to another monitor. #+begin_src emacs-lisp @@ -419,7 +469,7 @@ A function to move the current workspace to another monitor. my/exwm-another-monitor))) (exwm-randr-refresh)) #+end_src -** Switch to the opposite monitor +*** Switch to the opposite monitor Store the information about which workspace is available on which monitor. #+begin_src emacs-lisp :noweb-ref exwm-monitor-config :tangle no @@ -450,83 +500,29 @@ Switch to the opposite monitor. For now, this works only for two monitors becaus (mouse-autoselect-window nil)) (exwm-workspace-switch other))) #+end_src -** Switching buffers -A single perspective usually has only a handful of EXWM buffers, so here is a function to cycle them. +** Apps +*** App shortcuts +Also, a transient for shortcuts for the most frequent apps. -Those buffers that are visible in another window are highlighted blue and skipped. The current buffer is highlighted yellow. +I wanted to make the interactive lambda a macro, but this doesn't seem to work the way I expect, so the code has a bit of duplication. #+begin_src emacs-lisp -(defun my/cycle-persp-exwm-buffers (dir) - (let* ((current (current-buffer)) - (ignore-rx (persp--make-ignore-buffer-rx)) - (visible-buffers '()) - (exwm-data - (cl-loop for buf in (persp-current-buffers) - for is-another = (and (get-buffer-window buf) (not (eq current buf))) - if (and (buffer-live-p buf) - (eq 'exwm-mode (buffer-local-value 'major-mode buf)) - (not (string-match-p ignore-rx (buffer-name buf)))) - collect buf into all-buffers - and if (not is-another) collect buf into cycle-buffers - finally (return (list all-buffers cycle-buffers)))) - (all-buffers (nth 0 exwm-data)) - (cycle-buffers (nth 1 exwm-data)) - (current-pos (or (cl-position current cycle-buffers) -1))) - (if (seq-empty-p cycle-buffers) - (message "No EXWM buffers to cycle!") - (let* ((next-pos (% (+ current-pos (length cycle-buffers) - (if (eq dir 'forward) 1 -1)) - (length cycle-buffers))) - (next-buffer (nth next-pos cycle-buffers))) - (switch-to-buffer next-buffer) - (message - "%s" - (mapconcat - (lambda (buf) - (let ((name (string-replace "EXWM :: " "" (buffer-name buf)))) - (cond - ((eq (current-buffer) buf) - (concat - "[" - (propertize name 'face `(foreground-color . ,(doom-color 'yellow))) - "]")) - ((not (member buf cycle-buffers)) - (concat - "[" - (propertize name 'face `(foreground-color . ,(doom-color 'blue))) - "]")) - (t (format " %s " name))))) - all-buffers - " ")))))) -#+end_src -** Add all EXWM buffers to current perspective -#+begin_src emacs-lisp -(defun my/add-exwm-buffers-to-current-perspective () - (interactive) - (let ((ignore-rx (persp--make-ignore-buffer-rx))) - (cl-loop for buf in (buffer-list) - if (and (buffer-live-p buf) - (eq 'exwm-mode (buffer-local-value 'major-mode buf)) - (not (string-match-p ignore-rx (buffer-name buf)))) - do (persp-add-buffer (buffer-name buf))))) -#+end_src -** Revive perspectives -Occasionally the current perspective gets screwed up after a popup. This function attempts to fix it. +(use-package transient + :straight t) -#+begin_src emacs-lisp -(defun my/exwm-revive-perspectives () - "Make perspectives in the current frame not killed." - (interactive) - (let ((to-switch nil)) - (maphash - (lambda (_ v) - (setf (persp-killed v) nil) - (unless to-switch - (setq to-switch v))) - (frame-parameter nil 'persp--hash)) - (when to-switch - (persp-switch (persp-name to-switch))))) +(defun my/run-in-background (command) + (let ((command-parts (split-string command "[ ]+"))) + (apply #'call-process `(,(car command-parts) nil 0 nil ,@(cdr command-parts))))) + +(transient-define-prefix my/exwm-apps () + ["Apps" + ("t" "Termnial (Alacritty)" (lambda () (interactive) (my/run-in-background "alacritty"))) + ("b" "Browser (Firefox)" (lambda () (interactive) (my/run-in-background "firefox"))) + ("v" "VK" (lambda () (interactive) (my/run-in-background "vk"))) + ("s" "Slack" (lambda () (interactive) (my/run-in-background "slack-wrapper"))) + ("d" "Discord" (lambda () (interactive) (my/run-in-background "flatpak run com.discordapp.Discord"))) + ("q" "Quit" transient-quit-one)]) #+end_src -** Locking up +*** Locking up Run i3lock. #+begin_src emacs-lisp @@ -591,6 +587,7 @@ And keybindings that are available in both =char-mode= and =line-mode=: ;; Fullscreen (,(kbd "s-f") . exwm-layout-toggle-fullscreen) + (,(kbd "s-F") . exwm-floating-toggle-floating) ;; Quit (,(kbd "s-Q") . evil-quit) @@ -605,6 +602,8 @@ And keybindings that are available in both =char-mode= and =line-mode=: ;; Switch buffers (,(kbd "s-e") . persp-ivy-switch-buffer) + (,(kbd "s-E") . perspective-exwm-switch-perspective) + ;; Resize windows (,(kbd "s-r") . my/exwm-resize-hydra/body) @@ -634,8 +633,8 @@ And keybindings that are available in both =char-mode= and =line-mode=: (,(kbd "s-") . my/exwm-workspace-switch-monitor) ;; Cycle EXWM windows in the current perspective - (,(kbd "s-[") . (lambda () (interactive) (my/cycle-persp-exwm-buffers 'backward))) - (,(kbd "s-]") . (lambda () (interactive) (my/cycle-persp-exwm-buffers 'forward))) + (,(kbd "s-[") . perspective-exwm-cycle-exwm-buffers-backward) + (,(kbd "s-]") . perspective-exwm-cycle-exwm-buffers-forward) (,(kbd "s-o") . ,(my/app-command "rofi -show window")) ;; 's-N': Switch to certain workspace with Super (Win) plus a number key (0 - 9) @@ -647,53 +646,35 @@ And keybindings that are available in both =char-mode= and =line-mode=: (number-sequence 0 9)))) #+end_src -** Pinentry -The GUI pinentry doesn't work too well with EXWM because of issues with popup windows, so we will use the Emacs one. - +A function to apply changes to =exwm-input-global-keys=. #+begin_src emacs-lisp -(use-package pinentry - :straight t - :after (exwm) - :config - (setenv "GPG_AGENT_INFO" nil) ;; use emacs pinentry - (setq auth-source-debug t) - - (setq epg-gpg-program "gpg2") ;; not necessary - (require 'epa-file) - (epa-file-enable) - (setq epa-pinentry-mode 'loopback) - (setq epg-pinentry-mode 'loopback) - (pinentry-start) - (my/run-in-background "gpgconf --reload gpg-agent")) +(defun my/exwm-update-global-keys () + (interactive) + (setq exwm-input--global-keys nil) + (dolist (i exwm-input-global-keys) + (exwm-input--set-key (car i) (cdr i))) + (when exwm--connection + (exwm-input--update-global-prefix-keys))) #+end_src +** Fixes +*** Catch and report all errors raised when invoking command hooks +Thanks David! https://github.com/daviwil/exwm/commit/7b1be884124711af0a02eac740bdb69446bc54cc -#+begin_src conf-space :tangle ~/.gnupg/gpg-agent.conf -default-cache-ttl 3600 -max-cache-ttl 3600 -allow-emacs-pinentry -allow-loopback-pinentry -#+end_src -** Modeline -Show current workspace in the modeline. - -#+begin_src emacs-lisp :noweb-ref exwm-mode-line-config :tangle no -(defvar my/exwm-mode-line-info "") - -(add-to-list 'mode-line-misc-info - '(:eval my/exwm-mode-line-info)) - -(defun my/exwm-mode-line-info-update () - (setq my/exwm-mode-line-info - (concat - "[" - (propertize (funcall exwm-workspace-index-map exwm-workspace-current-index) - 'face - `(foreground-color . ,(doom-color 'yellow))) - "]")) - (setq my/exwm-mode-line-info-no-props (funcall exwm-workspace-index-map exwm-workspace-current-index)) - (force-mode-line-update)) - -(add-hook 'exwm-workspace-switch-hook #'my/exwm-mode-line-info-update) +#+begin_src emacs-lisp :noweb-ref exwm-fixes :tangle no +(defun exwm-input--fake-last-command () + "Fool some packages into thinking there is a change in the buffer." + (setq last-command #'exwm-input--noop) + (condition-case hook-error + (progn + (run-hooks 'pre-command-hook) + (run-hooks 'post-command-hook)) + ((error) + (exwm--log "Error occurred while running command hooks: %s\n\nBacktrace:\n\n%s" + hook-error + (with-temp-buffer + (setq-local standard-output (current-buffer)) + (backtrace) + (buffer-string)))))) #+end_src ** EXWM config And the EXWM config itself. @@ -733,10 +714,12 @@ And the EXWM config itself. <> <> <> + <> (set-frame-parameter (selected-frame) 'alpha '(90 . 90)) (add-to-list 'default-frame-alist '(alpha . (90 . 90))) + (perspective-exwm-mode) (exwm-enable)) #+end_src * i3wm