feat(emacs-i3): done

This commit is contained in:
Pavel Korytov 2021-10-06 12:11:53 +03:00
parent d1b197945d
commit d2a41a0a76
2 changed files with 65 additions and 35 deletions

View file

@ -1,20 +1,18 @@
+++
title = "Getting a consistent set of keybindings between i3 and Emacs"
author = ["Pavel Korytov"]
date = 2021-10-04
date = 2021-10-06
tags = ["emacs", "i3wm"]
draft = true
draft = false
+++
## Intro {#intro}
I got 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.
One advantage of EXWM for an Emacs user is that EXWM gives one set of keybindings to manage both Emacs windows and X windows. In every other WM, like my preferred [i3wm](https://i3wm.org), two orthogonal keymaps seem to be necessary. But, as both programs are quite customizable, I want to see whether I can replicate at least some part of the EXWM goodness in i3.
Why not just use EXWM? One key reason is that to my taste (and perhaps on my hardware) EXWM didn't feel snappy enough. Also, I really like i3's tree-based layout structure; I feel like it fits my workflow much better than master/stack paradigm of [XMonad](https://xmonad.org/), for instance. Although to be clear, I haven't tried WMs other than these.
But why not just use EXWM? One key reason is that to my taste (and perhaps on my hardware) EXWM didn't feel snappy enough. Also, I really like i3's tree-based layout structure; I feel like it fits my workflow much better than anything else I tried, including the master/stack paradigm of [XMonad](https://xmonad.org/), for instance.
One common point of criticism of i3 is that it is not extensible enough, especially compared to WMs which are configured in an actual programing language, like the mentioned XMonad, [Qtile](http://www.qtile.org/), [Awesome](https://awesomewm.org/), etc. But I think i3's extensibility is underappreciated.
Indeed, i3 may not give as much freedom as the former three do, but it is still possible can execute arbitrary logic with `i3-msg` and interact with i3 in arbitrary way using its IPC.
One common point of criticism of i3 is that it is not extensible enough, especially compared to WMs that are configured in an actual programing language, like the mentioned XMonad, [Qtile](http://www.qtile.org/), [Awesome](https://awesomewm.org/), etc. But I think i3's extensibility is underappreciated, although the contents of this article may lie closer to the limits of how far one can go there.
## Emacs integration {#emacs-integration}
@ -23,7 +21,9 @@ What I'm trying to do is actually quite simple, so I'm somewhat surprised I didn
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.
This may seem like a lot of overhead, but I didn't feel it even in the worst case (i3 -> Emacs -> i3), so at least in that regard the interaction feels seamless. The only concern is that this command flow is vunerable to Emacs getting stuck, but it is still much less of a problem than with EXWM.
This may seem like a lot of overhead, but I didn't feel it even in the worst case (i3 -> Emacs -> i3), so at least in that regard, the interaction feels seamless. The only concern is that this command flow is vulnerable to Emacs getting stuck, but it is still much less of a problem than with EXWM.
One interesting observation here is that Emacs windows and X windows are sort of one-level entities, so I can talk just about "windows".
At any rate, we need a script to do the i3 -> Emacs part:
@ -36,7 +36,7 @@ else
fi
```
My Emacs window title is set to `emacs[:<projectile-project-name>]@<hostname>`, hence the regex. The script is saved to an executable called `emacs-i3-integration`.
My [Emacs frame title is set](https://sqrtminusone.xyz/configs/emacs/#custom-frame-title) to `emacs[:<projectile-project-name>]@<hostname>`, hence the regex. The script is saved to an executable called `emacs-i3-integration`.
For this to work, we need to make sure that Emacs starts a server, so here is an expression to do just that:
@ -56,10 +56,14 @@ And here is a simple macro to do the Emacs -> i3 part:
## Handling i3 commands {#handling-i3-commands}
Now we have to handle the required set of i3 commands. It is worth noting here that I'm not trying to implement a general mechanism to apply i3 commands to Emacs, rather I'm implementing a small subset that I use in my i3 configuration and that maps reasonably to the Emacs concepts.
Also, I use [evil-mode](https://github.com/emacs-evil/evil) and generally configure the software to have vim-style bindings where possible. So if you don't use evil-mode you'd have to detangle the given functions from evil, but then I guess you do not use super+hjkl to manage windows either.
### `focus` {#focus}
Now we have to handle the required i3 commands. First, for the `focus` command I want to move to an Emacs window in the given direction if there is one, otherwise move to an X window in the same direction. Fortunately, i3 and `windmove` have the same names for directions, so the function is rather straightforward.
First, for the `focus` command I want to move to an Emacs window in the given direction if there is one, otherwise move to an X window in the same direction. Fortunately, i3 and `windmove` have the same names for directions, so the function is rather straightforward.
One caveat here is that the minibuffer is always the bottom-most Emacs window, so it is necessary to check for that as well.
@ -85,18 +89,24 @@ bindsym $mod+Up exec emacs-i3-integration focus up
bindsym $mod+Right exec emacs-i3-integration focus right
```
The Emacs function has to be called like that:
```emacs-lisp
(my/emacs-i3-windmove 'right)
```
### `move` {#move}
For the `move` I want the following behavior:
For the `move` command I want the following behavior:
- if there is space in the required directon, move the Emacs window there;
- if there is no space in the required direction, but space in two orthogonal directions, move the Emacs window so that there is no more space in the orthogonal directions;
- otherwise, move an X window (Emacs frame).
- if there is space in the required direction, move the Emacs window there;
- if there is no space in the required direction, but space in the orthogonal directions, move the Emacs window so that there is no more space in the orthogonal directions;
- otherwise, move an X window (which has to be an Emacs frame).
For the first part, `window-swap-states` with `windmove-find-other-window` do well enough.
`evil-move-window` works well for the second part. By itself 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). Hence the combination as described above.
`evil-move-window` works well for the second part. By itself it doesn't behave quite like i3, for instance, `(evil-move-window 'right)` in a three-column split would move the window from the far left side to the far right side (bypassing center). Hence the combination as described here.
So here is a simple predicate which checks whether there is space in the given direction.
@ -206,7 +216,7 @@ mode "resize" {
}
```
One note here is that Emacs has a built-in function called `balance-windows`, but i3 doesn't. Fortunately, there is a Python package called [i3-balance-workspace](https://github.com/atreyasha/i3-balance-workspace), which performs a similar operation with i3's IPC. If you use Guix like I do, I've written a [package definition](https://github.com/SqrtMinusOne/channel-q/blob/master/i3-balance-workspace.scm).
Next, Emacs has a built-in function called `balance-windows`, but i3 doesn't. Fortunately, there is a Python package called [i3-balance-workspace](https://github.com/atreyasha/i3-balance-workspace), which performs a similar operation with i3's IPC. If you use Guix as I do, I've written a [package definition](https://github.com/SqrtMinusOne/channel-q/blob/master/i3-balance-workspace.scm).
So here is a small wrapper which calls `i3_balance_workspace` and `M-x balance-windows` if the current window is Emacs.
@ -220,7 +230,7 @@ i3_balance_workspace
### `layout toggle split` {#layout-toggle-split}
[transpose-frame](https://github.com/emacsorphanage/transpose-frame) is a package to "transpose" the current frame layout, which behaves somewhat similar to the `layout toggle split` command in i3, so I'll use it as well.
[transpose-frame](https://github.com/emacsorphanage/transpose-frame) is a package to "transpose" the current Emacs windows layout, which behaves somewhat similar to the `layout toggle split` command in i3, so I'll use it as well.
```emacs-lisp
(use-package transpose-frame
@ -283,4 +293,11 @@ As a workaround, I found a small Rust program called [i3-switch-tabs](https://gi
```conf-space
bindsym $mod+period exec i3-switch-tabs right
bindsym $mod+comma exec i3-switch-tabs left
```
```
## Conclusion {#conclusion}
So, how does all of that feel? Actually, I got used to that setup pretty quickly. Using `<s-Q>` to quit windows and the `<s-r>` submode to resize them is particularly nice. I've seen people making hydras in Emacs to do the latter.
All of that would probably be easier to do in a WM which is configured in a programming language rather than a self-cooked DSL, so I may try to replicate that somewhere else in an unknown time in the future. Meanwhile, it's pretty good.

View file

@ -1,26 +1,25 @@
#+HUGO_SECTION: posts
#+HUGO_BASE_DIR: ../
#+TITLE: Getting a consistent set of keybindings between i3 and Emacs
#+DATE: 2021-10-04
#+HUGO_DRAFT: true
#+DATE: 2021-10-06
#+HUGO_TAGS: emacs
#+HUGO_TAGS: i3wm
* Intro
I got 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.
One advantage of EXWM for an Emacs user is that EXWM gives one set of keybindings to manage both Emacs windows and X windows. In every other WM, like my preferred [[https://i3wm.org][i3wm]], two orthogonal keymaps seem to be necessary. But, as both programs are quite customizable, I want to see whether I can replicate at least some part of the EXWM goodness in i3.
Why not just use EXWM? One key reason is that to my taste (and perhaps on my hardware) EXWM didn't feel snappy enough. Also, I really like i3's tree-based layout structure; I feel like it fits my workflow much better than master/stack paradigm of [[https://xmonad.org/][XMonad]], for instance. Although to be clear, I haven't tried WMs other than these.
But why not just use EXWM? One key reason is that to my taste (and perhaps on my hardware) EXWM didn't feel snappy enough. Also, I really like i3's tree-based layout structure; I feel like it fits my workflow much better than anything else I tried, including the master/stack paradigm of [[https://xmonad.org/][XMonad]], for instance.
One common point of criticism of i3 is that it is not extensible enough, especially compared to WMs which are configured in an actual programing language, like the mentioned XMonad, [[http://www.qtile.org/][Qtile]], [[https://awesomewm.org/][Awesome]], etc. But I think i3's extensibility is underappreciated.
Indeed, i3 may not give as much freedom as the former three do, but it is still possible can execute arbitrary logic with =i3-msg= and interact with i3 in arbitrary way using its IPC.
One common point of criticism of i3 is that it is not extensible enough, especially compared to WMs that are configured in an actual programing language, like the mentioned XMonad, [[http://www.qtile.org/][Qtile]], [[https://awesomewm.org/][Awesome]], etc. But I think i3's extensibility is underappreciated, although the contents of this article may lie closer to the limits of how far one can go there.
* Emacs integration
What I'm trying to do is actually quite simple, so I'm somewhat surprised I didn't find anything similar on the Internet. But I didn't look too hard.
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.
This may seem like a lot of overhead, but I didn't feel it even in the worst case (i3 -> Emacs -> i3), so at least in that regard the interaction feels seamless. The only concern is that this command flow is vunerable to Emacs getting stuck, but it is still much less of a problem than with EXWM.
This may seem like a lot of overhead, but I didn't feel it even in the worst case (i3 -> Emacs -> i3), so at least in that regard, the interaction feels seamless. The only concern is that this command flow is vulnerable to Emacs getting stuck, but it is still much less of a problem than with EXWM.
One interesting observation here is that Emacs windows and X windows are sort of one-level entities, so I can talk just about "windows".
At any rate, we need a script to do the i3 -> Emacs part:
#+BEGIN_SRC bash
@ -32,7 +31,7 @@ else
fi
#+END_SRC
My Emacs window title is set to =emacs[:<projectile-project-name>]@<hostname>=, hence the regex. The script is saved to an executable called =emacs-i3-integration=.
My [[https://sqrtminusone.xyz/configs/emacs/#custom-frame-title][Emacs frame title is set]] to =emacs[:<projectile-project-name>]@<hostname>=, hence the regex. The script is saved to an executable called =emacs-i3-integration=.
For this to work, we need to make sure that Emacs starts a server, so here is an expression to do just that:
#+BEGIN_SRC emacs-lisp
@ -48,8 +47,12 @@ And here is a simple macro to do the Emacs -> i3 part:
#+END_SRC
* Handling i3 commands
Now we have to handle the required set of i3 commands. It is worth noting here that I'm not trying to implement a general mechanism to apply i3 commands to Emacs, rather I'm implementing a small subset that I use in my i3 configuration and that maps reasonably to the Emacs concepts.
Also, I use [[https://github.com/emacs-evil/evil][evil-mode]] and generally configure the software to have vim-style bindings where possible. So if you don't use evil-mode you'd have to detangle the given functions from evil, but then I guess you do not use super+hjkl to manage windows either.
** =focus=
Now we have to handle the required i3 commands. First, for the =focus= command I want to move to an Emacs window in the given direction if there is one, otherwise move to an X window in the same direction. Fortunately, i3 and =windmove= have the same names for directions, so the function is rather straightforward.
First, for the =focus= command I want to move to an Emacs window in the given direction if there is one, otherwise move to an X window in the same direction. Fortunately, i3 and =windmove= have the same names for directions, so the function is rather straightforward.
One caveat here is that the minibuffer is always the bottom-most Emacs window, so it is necessary to check for that as well.
#+BEGIN_SRC emacs-lisp
@ -72,15 +75,20 @@ 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
The Emacs function has to be called like that:
#+begin_src emacs-lisp
(my/emacs-i3-windmove 'right)
#+end_src
** =move=
For the =move= I want the following behavior:
- if there is space in the required directon, move the Emacs window there;
- if there is no space in the required direction, but space in two orthogonal directions, move the Emacs window so that there is no more space in the orthogonal directions;
- otherwise, move an X window (Emacs frame).
For the =move= command I want the following behavior:
- if there is space in the required direction, move the Emacs window there;
- if there is no space in the required direction, but space in the orthogonal directions, move the Emacs window so that there is no more space in the orthogonal directions;
- otherwise, move an X window (which has to be an Emacs frame).
For the first part, =window-swap-states= with =windmove-find-other-window= do well enough.
=evil-move-window= works well for the second part. By itself 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). Hence the combination as described above.
=evil-move-window= works well for the second part. By itself it doesn't behave quite like i3, for instance, =(evil-move-window 'right)= in a three-column split would move the window from the far left side to the far right side (bypassing center). Hence the combination as described here.
So here is a simple predicate which checks whether there is space in the given direction.
#+BEGIN_SRC emacs-lisp
@ -183,7 +191,7 @@ mode "resize" {
}
#+END_SRC
One note here is that Emacs has a built-in function called =balance-windows=, but i3 doesn't. Fortunately, there is a Python package called [[https://github.com/atreyasha/i3-balance-workspace][i3-balance-workspace]], which performs a similar operation with i3's IPC. If you use Guix like I do, I've written a [[https://github.com/SqrtMinusOne/channel-q/blob/master/i3-balance-workspace.scm][package definition]].
Next, Emacs has a built-in function called =balance-windows=, but i3 doesn't. Fortunately, there is a Python package called [[https://github.com/atreyasha/i3-balance-workspace][i3-balance-workspace]], which performs a similar operation with i3's IPC. If you use Guix as I do, I've written a [[https://github.com/SqrtMinusOne/channel-q/blob/master/i3-balance-workspace.scm][package definition]].
So here is a small wrapper which calls =i3_balance_workspace= and =M-x balance-windows= if the current window is Emacs.
#+BEGIN_SRC bash
@ -194,7 +202,7 @@ i3_balance_workspace
#+END_SRC
** =layout toggle split=
[[https://github.com/emacsorphanage/transpose-frame][transpose-frame]] is a package to "transpose" the current frame layout, which behaves somewhat similar to the =layout toggle split= command in i3, so I'll use it as well.
[[https://github.com/emacsorphanage/transpose-frame][transpose-frame]] is a package to "transpose" the current Emacs windows layout, which behaves somewhat similar to the =layout toggle split= command in i3, so I'll use it as well.
#+BEGIN_SRC emacs-lisp
(use-package transpose-frame
:straight t
@ -249,3 +257,8 @@ As a workaround, I found a small Rust program called [[https://github.com/nikola
bindsym $mod+period exec i3-switch-tabs right
bindsym $mod+comma exec i3-switch-tabs left
#+END_SRC
* Conclusion
So, how does all of that feel? Actually, I got used to that setup pretty quickly. Using =<s-Q>= to quit windows and the =<s-r>= submode to resize them is particularly nice. I've seen people making hydras in Emacs to do the latter.
All of that would probably be easier to do in a WM which is configured in a programming language rather than a self-cooked DSL, so I may try to replicate that somewhere else in an unknown time in the future. Meanwhile, it's pretty good.