From 20af706fa28d1c33e594b99e531aa385bb9ce26d Mon Sep 17 00:00:00 2001 From: SqrtMinusOne Date: Sun, 3 Oct 2021 20:46:40 +0300 Subject: [PATCH] feat(*): Emacs & i3 integration --- .config/guix/manifests/desktop.scm | 1 + .config/i3/config | 81 +++++++++---------- .emacs.d/init.el | 76 +++++++++++++++++- Desktop.org | 95 +++++++++++++---------- Emacs.org | 112 +++++++++++++++++++++++++++ bin/scripts/emacs-i3-integration | 9 +++ bin/scripts/i3-emacs-balance-windows | 7 ++ 7 files changed, 300 insertions(+), 81 deletions(-) create mode 100755 bin/scripts/emacs-i3-integration create mode 100755 bin/scripts/i3-emacs-balance-windows diff --git a/.config/guix/manifests/desktop.scm b/.config/guix/manifests/desktop.scm index 5b4c7c3..d4880a8 100644 --- a/.config/guix/manifests/desktop.scm +++ b/.config/guix/manifests/desktop.scm @@ -42,6 +42,7 @@ "curl" "bind:utils" "polybar" + "python-i3-balance-workspace" "i3-gaps" "xinput" "xgamma" diff --git a/.config/i3/config b/.config/i3/config index e054749..9b1f00e 100644 --- a/.config/i3/config +++ b/.config/i3/config @@ -27,47 +27,48 @@ bindsym $mod+Shift+e exec "i3-nagbar -t warning -m 'You pressed the exit shortcu # General settings:1 ends here # [[file:../../Desktop.org::*Managing windows][Managing windows:1]] -bindsym $mod+Shift+q kill +bindsym $mod+Shift+q exec emacs-i3-integration kill # Managing windows:1 ends here # [[file:../../Desktop.org::*Managing windows][Managing windows:2]] -bindsym $mod+h focus left -bindsym $mod+j focus down -bindsym $mod+k focus up -bindsym $mod+l focus right +bindsym $mod+h exec emacs-i3-integration focus left +bindsym $mod+j exec emacs-i3-integration focus down +bindsym $mod+k exec emacs-i3-integration focus up +bindsym $mod+l exec emacs-i3-integration focus right -bindsym $mod+Left focus left -bindsym $mod+Down focus down -bindsym $mod+Up focus up -bindsym $mod+Right focus right +bindsym $mod+Left exec emacs-i3-integration focus left +bindsym $mod+Down exec emacs-i3-integration focus down +bindsym $mod+Up exec emacs-i3-integration focus up +bindsym $mod+Right exec emacs-i3-integration focus right # Managing windows:2 ends here # [[file:../../Desktop.org::*Managing windows][Managing windows:3]] -bindsym $mod+Shift+h move left -bindsym $mod+Shift+j move down -bindsym $mod+Shift+k move up -bindsym $mod+Shift+l move right +bindsym $mod+Shift+h exec emacs-i3-integration move left +bindsym $mod+Shift+j exec emacs-i3-integration move down +bindsym $mod+Shift+k exec emacs-i3-integration move up +bindsym $mod+Shift+l exec emacs-i3-integration move right -bindsym $mod+Shift+Left move left -bindsym $mod+Shift+Down move down -bindsym $mod+Shift+Up move up -bindsym $mod+Shift+Right move right +bindsym $mod+Shift+Left exec emacs-i3-integration move left +bindsym $mod+Shift+Down exec emacs-i3-integration move down +bindsym $mod+Shift+Up exec emacs-i3-integration move up +bindsym $mod+Shift+Right exec emacs-i3-integration move right # Managing windows:3 ends here # [[file:../../Desktop.org::*Managing windows][Managing windows:4]] -bindsym $mod+s split h -bindsym $mod+v split v +bindsym $mod+s exec emacs-i3-integration split h +bindsym $mod+v exec emacs-i3-integration split v # Managing windows:4 ends here # [[file:../../Desktop.org::*Managing windows][Managing windows:5]] # enter fullscreen mode for the focused container bindsym $mod+f fullscreen toggle +bindsym $mod+c fullscreen toggle global # Managing windows:5 ends here # [[file:../../Desktop.org::*Managing windows][Managing windows:6]] bindsym $mod+w layout stacking bindsym $mod+t layout tabbed -bindsym $mod+e layout toggle split +bindsym $mod+e exec emacs-i3-integration layout toggle split # Managing windows:6 ends here # [[file:../../Desktop.org::*Managing windows][Managing windows:7]] @@ -226,31 +227,31 @@ bindsym $mod+g mode "inner gaps" bindsym $mod+Shift+g mode "outer gaps" # Keybindings:1 ends here -# [[file:../../Desktop.org::*Move & resize windows][Move & resize windows:1]] -# resize window (you can also use the mouse for that) +# [[file:../../Desktop.org::*Move & resize windows][Move & resize windows:2]] mode "resize" { - # These bindings trigger as soon as you enter the resize mode - bindsym h resize shrink width 10 px or 10 ppt - bindsym j resize grow height 10 px or 10 ppt - bindsym k resize shrink height 10 px or 10 ppt - bindsym l resize grow width 10 px or 10 ppt + bindsym h exec emacs-i3-integration resize shrink width 10 px or 10 ppt + bindsym j exec emacs-i3-integration resize grow height 10 px or 10 ppt + bindsym k exec emacs-i3-integration resize shrink height 10 px or 10 ppt + bindsym l exec emacs-i3-integration resize grow width 10 px or 10 ppt - bindsym Shift+h resize shrink width 100 px or 100 ppt - bindsym Shift+j resize grow height 100 px or 100 ppt - bindsym Shift+k resize shrink height 100 px or 100 ppt - bindsym Shift+l resize grow width 100 px or 100 ppt + bindsym Shift+h exec emacs-i3-integration resize shrink width 100 px or 100 ppt + bindsym Shift+j exec emacs-i3-integration resize grow height 100 px or 100 ppt + bindsym Shift+k exec emacs-i3-integration resize shrink height 100 px or 100 ppt + bindsym Shift+l exec emacs-i3-integration resize grow width 100 px or 100 ppt # same bindings, but for the arrow keys - bindsym Left resize shrink width 10 px or 10 ppt - bindsym Down resize grow height 10 px or 10 ppt - bindsym Up resize shrink height 10 px or 10 ppt - bindsym Right resize grow width 10 px or 10 ppt + bindsym Left exec emacs-i3-integration resize shrink width 10 px or 10 ppt + bindsym Down exec emacs-i3-integration resize grow height 10 px or 10 ppt + bindsym Up exec emacs-i3-integration resize shrink height 10 px or 10 ppt + bindsym Right exec emacs-i3-integration resize grow width 10 px or 10 ppt - bindsym Shift+Left resize shrink width 100 px or 100 ppt - bindsym Shift+Down resize grow height 100 px or 100 ppt - bindsym Shift+Up resize shrink height 100 px or 100 ppt - bindsym Shift+Right resize grow width 100 px or 100 ppt + bindsym Shift+Left exec emacs-i3-integration resize shrink width 100 px or 100 ppt + bindsym Shift+Down exec emacs-i3-integration resize grow height 100 px or 100 ppt + bindsym Shift+Up exec emacs-i3-integration resize shrink height 100 px or 100 ppt + bindsym Shift+Right exec emacs-i3-integration resize grow width 100 px or 100 ppt + + bindsym equal exec i3-emacs-balance-windows # back to normal: Enter or Escape bindsym Return mode "default" @@ -278,7 +279,7 @@ mode "move" { } bindsym $mod+m mode "move" focus floating -# Move & resize windows:1 ends here +# Move & resize windows:2 ends here # [[file:../../Desktop.org::*Integration with rofi][Integration with rofi:1]] bindsym $mod+d exec "rofi -modi 'drun,run' -show drun" diff --git a/.emacs.d/init.el b/.emacs.d/init.el index e33f7e2..6e98c7e 100644 --- a/.emacs.d/init.el +++ b/.emacs.d/init.el @@ -211,7 +211,8 @@ comint git-timemachine magit - prodigy))) + prodigy + slime))) (defun minibuffer-keyboard-quit () "Abort recursive edit. @@ -303,6 +304,7 @@ then it takes a second \\[keyboard-quit] to abort the minibuffer." "h" 'previous-buffer "k" 'kill-buffer "b" 'persp-ivy-switch-buffer + "r" 'revert-buffer "u" 'ibuffer) (general-nmap @@ -335,6 +337,72 @@ then it takes a second \\[keyboard-quit] to abort the minibuffer." (global-set-key (kbd "C-+") 'my/zoom-in) (global-set-key (kbd "C-=") 'my/zoom-out) +(defmacro i3-msg (&rest args) + `(start-process "emacs-i3-windmove" nil "i3-msg" ,@args)) + +(add-hook 'after-init-hook #'server-start) + +(defun my/emacs-i3-windmove (dir) + (let ((other-window (windmove-find-other-window dir))) + (if (or (null other-window) (window-minibuffer-p other-window)) + (i3-msg "focus" (symbol-name dir)) + (windmove-do-window-select dir)))) + +(defun my/emacs-i3-move-window (dir) + (let ((other-window (windmove-find-other-window dir))) + (if (null other-window) + (i3-msg "move" (symbol-name dir)) + (window-swap-states (selected-window) other-window)))) + +(defun my/emacs-i3-resize-direction-exists-p (dir) + (some #'windmove-find-other-window + (pcase dir + ('width '(left right)) + ('height '(up down))))) + +(defun my/emacs-i3-resize-window (dir kind value) + (if (or (one-window-p) + (not (my/emacs-i3-resize-direction-exists-p dir))) + (i3-msg "resize" (symbol-name kind) (symbol-name dir) + (format "%s px or %s ppt" value value)) + (setq value (/ value 2)) + (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))))))) + +(use-package transpose-frame + :straight t + :commands (transpose-frame)) + +(defun my/emacs-i3-integration (command) + (pcase command + ((rx bos "focus") + (my/emacs-i3-windmove + (intern (elt (split-string command) 1)))) + ((rx bos "move") + (my/emacs-i3-move-window + (intern (elt (split-string command) 1)))) + ((rx bos "resize") + (my/emacs-i3-resize-window + (intern (elt (split-string command) 2)) + (intern (elt (split-string command) 1)) + (string-to-number (elt (split-string command) 3)))) + ("layout toggle split" (transpose-frame)) + ("split h" (evil-window-split)) + ("split v" (evil-window-vsplit)) + ("kill" (evil-quit)) + (- (i3-msg command)))) + (use-package visual-fill-column :straight t :config @@ -2851,6 +2919,12 @@ then it takes a second \\[keyboard-quit] to abort the minibuffer." ;; (add-hook 'emacs-lisp-mode-hook #'smartparens-strict-mode) (add-hook 'emacs-lisp-mode-hook #'lispy-mode) +(use-package slime + :straight t + :config + (setq inferior-lisp-program "sbcl") + (add-hook 'slime-repl-mode 'smartparens-mode)) + (add-hook 'lisp-mode-hook #'aggressive-indent-mode) ;; (add-hook 'emacs-lisp-mode-hook #'smartparens-strict-mode) (add-hook 'lisp-mode-hook #'lispy-mode) diff --git a/Desktop.org b/Desktop.org index b42a33b..1949c42 100644 --- a/Desktop.org +++ b/Desktop.org @@ -283,54 +283,57 @@ bindsym $mod+Shift+e exec "i3-nagbar -t warning -m 'You pressed the exit shortcu ** Managing windows Some keybindings for managing windows. +=emacs-i3-integration= is a script to pass some command to Emacs to get a consistent set of keybindings in both i3 and Emacs. Check out [[file:Emacs.org::i3 integration][the section in Emacs.org]] for details. + Kill focused windows #+begin_src conf-space -bindsym $mod+Shift+q kill +bindsym $mod+Shift+q exec emacs-i3-integration kill #+end_src Change focus #+begin_src conf-space -bindsym $mod+h focus left -bindsym $mod+j focus down -bindsym $mod+k focus up -bindsym $mod+l focus right +bindsym $mod+h exec emacs-i3-integration focus left +bindsym $mod+j exec emacs-i3-integration focus down +bindsym $mod+k exec emacs-i3-integration focus up +bindsym $mod+l exec emacs-i3-integration focus right -bindsym $mod+Left focus left -bindsym $mod+Down focus down -bindsym $mod+Up focus up -bindsym $mod+Right focus right +bindsym $mod+Left exec emacs-i3-integration focus left +bindsym $mod+Down exec emacs-i3-integration focus down +bindsym $mod+Up exec emacs-i3-integration focus up +bindsym $mod+Right exec emacs-i3-integration focus right #+end_src Move windows around #+begin_src conf-space -bindsym $mod+Shift+h move left -bindsym $mod+Shift+j move down -bindsym $mod+Shift+k move up -bindsym $mod+Shift+l move right +bindsym $mod+Shift+h exec emacs-i3-integration move left +bindsym $mod+Shift+j exec emacs-i3-integration move down +bindsym $mod+Shift+k exec emacs-i3-integration move up +bindsym $mod+Shift+l exec emacs-i3-integration move right -bindsym $mod+Shift+Left move left -bindsym $mod+Shift+Down move down -bindsym $mod+Shift+Up move up -bindsym $mod+Shift+Right move right +bindsym $mod+Shift+Left exec emacs-i3-integration move left +bindsym $mod+Shift+Down exec emacs-i3-integration move down +bindsym $mod+Shift+Up exec emacs-i3-integration move up +bindsym $mod+Shift+Right exec emacs-i3-integration move right #+end_src Split windows #+begin_src conf-space -bindsym $mod+s split h -bindsym $mod+v split v +bindsym $mod+s exec emacs-i3-integration split h +bindsym $mod+v exec emacs-i3-integration split v #+end_src Enter fullscreen mode #+begin_src conf-space # enter fullscreen mode for the focused container bindsym $mod+f fullscreen toggle +bindsym $mod+c fullscreen toggle global #+end_src Changing layout #+begin_src conf-space bindsym $mod+w layout stacking bindsym $mod+t layout tabbed -bindsym $mod+e layout toggle split +bindsym $mod+e exec emacs-i3-integration layout toggle split #+end_src Toggle tiling/floating, switch between tiled and floating windows @@ -518,34 +521,46 @@ bindsym $mod+g mode "inner gaps" bindsym $mod+Shift+g mode "outer gaps" #+end_src ** Move & resize windows -A more or less standard set of keybindings to move & resize floating windows. +| Guix dependency | +|-----------------------------| +| python-i3-balance-workspace | + +A more or less standard set of keybindings to move & resize floating windows. Just be careful to always make a way to return from these new modes, otherwise you'd end up in a rather precarious situation. + +[[https://github.com/atreyasha/i3-balance-workspace][i3-balance-workspace]] is a small Python package to balance the i3 windows, but for the Emacs integration I also want this button to balance the Emacs windows, so here is a small script to do just that. + +#+begin_src bash :tangle ~/bin/scripts/i3-emacs-balance-windows +if [[ $(xdotool getactivewindow getwindowname) =~ ^emacs(:.*)?@.* ]]; then + emacsclient -e "(balance-windows)" & +fi +i3_balance_workspace +#+end_src -Just be careful to always make a way to return from these new modes, otherwise you'd end up in a rather precarious situation. #+begin_src conf-space -# resize window (you can also use the mouse for that) mode "resize" { - # These bindings trigger as soon as you enter the resize mode - bindsym h resize shrink width 10 px or 10 ppt - bindsym j resize grow height 10 px or 10 ppt - bindsym k resize shrink height 10 px or 10 ppt - bindsym l resize grow width 10 px or 10 ppt + bindsym h exec emacs-i3-integration resize shrink width 10 px or 10 ppt + bindsym j exec emacs-i3-integration resize grow height 10 px or 10 ppt + bindsym k exec emacs-i3-integration resize shrink height 10 px or 10 ppt + bindsym l exec emacs-i3-integration resize grow width 10 px or 10 ppt - bindsym Shift+h resize shrink width 100 px or 100 ppt - bindsym Shift+j resize grow height 100 px or 100 ppt - bindsym Shift+k resize shrink height 100 px or 100 ppt - bindsym Shift+l resize grow width 100 px or 100 ppt + bindsym Shift+h exec emacs-i3-integration resize shrink width 100 px or 100 ppt + bindsym Shift+j exec emacs-i3-integration resize grow height 100 px or 100 ppt + bindsym Shift+k exec emacs-i3-integration resize shrink height 100 px or 100 ppt + bindsym Shift+l exec emacs-i3-integration resize grow width 100 px or 100 ppt # same bindings, but for the arrow keys - bindsym Left resize shrink width 10 px or 10 ppt - bindsym Down resize grow height 10 px or 10 ppt - bindsym Up resize shrink height 10 px or 10 ppt - bindsym Right resize grow width 10 px or 10 ppt + bindsym Left exec emacs-i3-integration resize shrink width 10 px or 10 ppt + bindsym Down exec emacs-i3-integration resize grow height 10 px or 10 ppt + bindsym Up exec emacs-i3-integration resize shrink height 10 px or 10 ppt + bindsym Right exec emacs-i3-integration resize grow width 10 px or 10 ppt - bindsym Shift+Left resize shrink width 100 px or 100 ppt - bindsym Shift+Down resize grow height 100 px or 100 ppt - bindsym Shift+Up resize shrink height 100 px or 100 ppt - bindsym Shift+Right resize grow width 100 px or 100 ppt + bindsym Shift+Left exec emacs-i3-integration resize shrink width 100 px or 100 ppt + bindsym Shift+Down exec emacs-i3-integration resize grow height 100 px or 100 ppt + bindsym Shift+Up exec emacs-i3-integration resize shrink height 100 px or 100 ppt + bindsym Shift+Right exec emacs-i3-integration resize grow width 100 px or 100 ppt + + bindsym equal exec i3-emacs-balance-windows # back to normal: Enter or Escape bindsym Return mode "default" diff --git a/Emacs.org b/Emacs.org index 9815359..b2a0391 100644 --- a/Emacs.org +++ b/Emacs.org @@ -817,6 +817,118 @@ Evil does a pretty good job of uniting these two in the set of vim-like keybindi (global-set-key (kbd "C-+") 'my/zoom-in) (global-set-key (kbd "C-=") 'my/zoom-out) #+end_src +** i3 integration +I'm a bit jealous of EXWM having a consistent set of keybindings to manage both Emacs windows and X windows, so I figured I could try to implement something like this with i3. + +The basic idea is to launch a normal i3 command with =i3-msg= in case the current window is not Emacs, otherwise pass that command to Emacs with =emacsclient=. In Emacs, execute the command if possible, otherwise pass the command back to i3. + +I actually tried to use EXWM but returned back to i3 because EXWM wasn't snappy enough. While this integration adds some overhead, I can't detect it even in the worst case (i3 -> Emacs -> i3), so the integration feels seamless. + +At any rate, we need a script to do the i3 -> Emacs part: +#+begin_src bash :tangle ~/bin/scripts/emacs-i3-integration +if [[ $(xdotool getactivewindow getwindowname) =~ ^emacs(:.*)?@.* ]]; then + command="(my/emacs-i3-integration \"$@\")" + emacsclient -e "$command" +else + i3-msg $@ +fi +#+end_src + +This script is being run from the [[file:Desktop.org::*i3wm][i3 configuration]]. + +For the second part, we need to call i3 from emacs. Here is a simple macro to do just that: +#+begin_src emacs-lisp +(defmacro i3-msg (&rest args) + `(start-process "emacs-i3-windmove" nil "i3-msg" ,@args)) +#+end_src + +Also, we have to make sure that Emacs starts a server, so here is a hook to do just that: +#+begin_src emacs-lisp +(add-hook 'after-init-hook #'server-start) +#+end_src + +Then we have to handle the required i3 commands. First, for the =focus= command I want to move to the Emacs window in the given direction if there is one, otherwise move to the X window in the same direction. Fortunately, i3 and windmove have the same names for directions, so the function is rather straightforward. + +Also, one caveat here is that the minibuffer is always below, so it is necessary to check for that as well. +#+begin_src emacs-lisp +(defun my/emacs-i3-windmove (dir) + (let ((other-window (windmove-find-other-window dir))) + (if (or (null other-window) (window-minibuffer-p other-window)) + (i3-msg "focus" (symbol-name dir)) + (windmove-do-window-select dir)))) +#+end_src + +For the =move= command I want to move the Emacs window in the required direction if there is space in there, otherwise move an X window, which will be the Emacs frame. + +I tried to use =evil-move-window= here, but it doesn't behave quite like i3, for instance, =(evil-move-window 'right)= in a tree-column split would move the window from the far left side to the far right side (bypassing center). =window-swap-states= with a window in the given direction does better. +#+begin_src emacs-lisp +(defun my/emacs-i3-move-window (dir) + (let ((other-window (windmove-find-other-window dir))) + (if (null other-window) + (i3-msg "move" (symbol-name dir)) + (window-swap-states (selected-window) other-window)))) +#+end_src + +Next, =resize grow= and =resize shrink=. To avoid running resize when there is no place to resize to, here is a simple predicate which checks whether there is space in the given direction. +#+begin_src emacs-lisp +(defun my/emacs-i3-resize-direction-exists-p (dir) + (some #'windmove-find-other-window + (pcase dir + ('width '(left right)) + ('height '(up down))))) +#+end_src + +Now we can use that to perform the actual resizing. =evil-window-= functions do that nicely. +#+begin_src emacs-lisp +(defun my/emacs-i3-resize-window (dir kind value) + (if (or (one-window-p) + (not (my/emacs-i3-resize-direction-exists-p dir))) + (i3-msg "resize" (symbol-name kind) (symbol-name dir) + (format "%s px or %s ppt" value value)) + (setq value (/ value 2)) + (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))))))) +#+end_src + +[[https://github.com/emacsorphanage/transpose-frame][transpose-frame]] is a package to "transpose" the current frame layout, which behaves someone similar to the =layout toggle split= command in i3. +#+begin_src emacs-lisp +(use-package transpose-frame + :straight t + :commands (transpose-frame)) +#+end_src + +Finally, an entrypoint for the Emacs integration. In addition to the commands defined above, it processes =split= and =kill= commands and passes every other command back to i3. +#+begin_src emacs-lisp +(defun my/emacs-i3-integration (command) + (pcase command + ((rx bos "focus") + (my/emacs-i3-windmove + (intern (elt (split-string command) 1)))) + ((rx bos "move") + (my/emacs-i3-move-window + (intern (elt (split-string command) 1)))) + ((rx bos "resize") + (my/emacs-i3-resize-window + (intern (elt (split-string command) 2)) + (intern (elt (split-string command) 1)) + (string-to-number (elt (split-string command) 3)))) + ("layout toggle split" (transpose-frame)) + ("split h" (evil-window-split)) + ("split v" (evil-window-vsplit)) + ("kill" (evil-quit)) + (- (i3-msg command)))) +#+end_src ** Editing helpers *** Visual fill column mode #+begin_src emacs-lisp diff --git a/bin/scripts/emacs-i3-integration b/bin/scripts/emacs-i3-integration new file mode 100755 index 0000000..8344e5d --- /dev/null +++ b/bin/scripts/emacs-i3-integration @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# [[file:../../Emacs.org::*i3 integration][i3 integration:1]] +if [[ $(xdotool getactivewindow getwindowname) =~ ^emacs(:.*)?@.* ]]; then + command="(my/emacs-i3-integration \"$@\")" + emacsclient -e "$command" +else + i3-msg $@ +fi +# i3 integration:1 ends here diff --git a/bin/scripts/i3-emacs-balance-windows b/bin/scripts/i3-emacs-balance-windows new file mode 100755 index 0000000..be177a9 --- /dev/null +++ b/bin/scripts/i3-emacs-balance-windows @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# [[file:../../Desktop.org::*Move & resize windows][Move & resize windows:1]] +if [[ $(xdotool getactivewindow getwindowname) =~ ^emacs(:.*)?@.* ]]; then + emacsclient -e "(balance-windows)" & +fi +i3_balance_workspace +# Move & resize windows:1 ends here