mirror of
https://github.com/SqrtMinusOne/sqrtminusone.github.io.git
synced 2025-12-11 16:13:03 +03:00
511 lines
74 KiB
HTML
511 lines
74 KiB
HTML
<!DOCTYPE html>
|
|
<html lang=""><head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
|
|
<title>Using EXWM and perspective.el on multi-monitor setup</title>
|
|
<meta name="description" content="Freedom is a state of mind">
|
|
<meta name="author" content='SqrtMinusOne'>
|
|
|
|
<link href="https://fonts.googleapis.com/css2?family=Inconsolata:wght@400;700&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css" integrity="sha512-iBBXm8fW90+nuLcSKlbmrPcLa0OT92xO1BIsZ+ywDWZCvqsWgccV3gFoRBv0z+8dLJgyAHIhR35VZc2oM/gI1w==" crossorigin="anonymous">
|
|
|
|
|
|
<link rel="stylesheet" href="/sass/researcher.min.css">
|
|
|
|
|
|
<link rel="icon" type="image/ico" href="https://sqrtminusone.xyz/favicon.ico">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<script defer data-domain="sqrtminusone.xyz" src="https://plausible.sqrtminusone.xyz/js/plausible.js"></script>
|
|
|
|
</head>
|
|
|
|
<body><div class="container mt-5">
|
|
<nav class="navbar navbar-expand-sm flex-column flex-sm-row text-nowrap p-0">
|
|
<a class="navbar-brand mx-0 mr-sm-auto" href="https://sqrtminusone.xyz/" title="SqrtMinusOne">
|
|
|
|
SqrtMinusOne
|
|
</a>
|
|
<div class="navbar-nav flex-row flex-wrap justify-content-center">
|
|
|
|
|
|
|
|
<a class="nav-item nav-link" href="/" title="Index">
|
|
Index
|
|
</a>
|
|
|
|
<span class="nav-item navbar-text mx-1">/</span>
|
|
|
|
|
|
<a class="nav-item nav-link" href="/posts/" title="Posts">
|
|
Posts
|
|
</a>
|
|
|
|
<span class="nav-item navbar-text mx-1">/</span>
|
|
|
|
|
|
<a class="nav-item nav-link" href="/configs/readme" title="Configs">
|
|
Configs
|
|
</a>
|
|
|
|
|
|
|
|
</div>
|
|
</nav>
|
|
</div>
|
|
<hr>
|
|
<div id="content">
|
|
<script defer language="javascript" type="text/javascript" src="/js/dynamic-toc.js"></script>
|
|
|
|
<div class="root">
|
|
<h1 id="title-small-screen">Using EXWM and perspective.el on multi-monitor setup</h1>
|
|
<div class="container" id="actual-content">
|
|
<h1 id="title-large-screen">Using EXWM and perspective.el on multi-monitor setup</h1>
|
|
<p>I wrote about <a href="https://sqrtminusone.xyz/posts/2021-10-04-emacs-i3/">Emacs and i3</a> integration around two months ago. Shortly after however, I decided to give EXWM another try, mainly because my largest reservation - lack of performance - seems to have been resolved by updates to the native compilation since my first attempt. Or I may have lost some sensitivity to that issue. Regardless, the second dive into EXWM thus far feels successful, and I think it’s the right time to share some of my thoughts on the subject.</p>
|
|
<p>Before we start though, I’ll point out that I won’t go into detail about the initial setup. I think David Wilson’s “<a href="https://systemcrafters.net/emacs-desktop-environment/">Emacs Desktop Environment</a>” series describes this part pretty well, so I don’t feel the need to repeat much of that.</p>
|
|
<p>This post is a sort of a snapshot of the path from the baseline of <a href="https://github.com/daviwil/emacs-from-scratch/blob/master/Desktop.org">Emacs From Scratch</a> to my image of a perfect window manager, and it may or may not be coincidental that the latter resembles i3 in many aspects.</p>
|
|
<p>After all, I was using i3 for more than two years, so it’s not something I can easily let go of. But I think (or would like to think) that’s because the ideas are good, not because I’m overly conservative in my workflow choices.</p>
|
|
<h2 id="perspective-dot-el">perspective.el</h2>
|
|
<p><a href="https://github.com/nex3/perspective-el">perspective.el</a> is one package I like that provides workspaces for Emacs, called “perspectives”. Each perspective has a separate buffer list, window layout, and a few other things that make it easier to separate things within Emacs.</p>
|
|
<p>One feature I’d like to highlight is integration between perspective.el and <a href="https://github.com/Alexander-Miller/treemacs">treemacs</a>, where one perspective can have a separate treemacs tree. Although now tab-bar.el seems to be getting into shape to compete with perspective.el, as of the time of this writing, there’s no such integration, at least not out of the box.</p>
|
|
<p>perspective.el works with EXWM more or less as one would expect - each EXWM workspace has its own set of perspectives. That way it feels somewhat like having multiple Emacs frames in a tiling window manager, although, of course, much more integrated with Emacs.</p>
|
|
<p>However, there are still some issues. For instance, I was having strange behaviors with floating windows, EXWM buffers in perspectives, etc. So I’ve made a package called <a href="https://github.com/SqrtMinusOne/perspective-exwm.el">perspective-exwm.el</a> that does two things:</p>
|
|
<ul>
|
|
<li>Fixes issues I found with some advises and hooks. Take a look at the package homepage for more detail on that.</li>
|
|
<li>Provides some additional functionality that makes use of both perspective.el and EXWM.</li>
|
|
</ul>
|
|
<p>So, you can install the package however you normally do so. E.g. I do that with straight.el & use-package:</p>
|
|
<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">use-package</span> <span style="color:#19177c">perspective-exwm</span>
|
|
</span></span><span style="display:flex;"><span> <span style="color:#008000">:straight</span> <span style="color:#800">t</span>
|
|
</span></span><span style="display:flex;"><span> <span style="color:#008000">:config</span>
|
|
</span></span><span style="display:flex;"><span> <span style="color:#666">...</span>)
|
|
</span></span></code></pre></div><p>Then load the provided minor mode before <code>exwm-init</code>:</p>
|
|
<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">use-package</span> <span style="color:#19177c">exwm</span>
|
|
</span></span><span style="display:flex;"><span> <span style="color:#008000">:config</span>
|
|
</span></span><span style="display:flex;"><span> <span style="color:#666">...</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">perspective-exwm-mode</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">exwm-init</span>))
|
|
</span></span></code></pre></div><h3 id="initial-perspective-names">Initial perspective names</h3>
|
|
<p>One nice thing this package can do is set up the initial perspective names for different workspaces. By default, enabling <code>perspective-exwm-mode</code> sets names like <code>main-1</code> for workspace with index 1 and so on, because otherwise different perspectives will share the same <code>*scratch*</code> buffer.</p>
|
|
<p>But names can be overridden like that:</p>
|
|
<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">perspective-exwm-override-initial-name</span>
|
|
</span></span><span style="display:flex;"><span> <span style="color:#666">'</span>((<span style="color:#666">0</span> <span style="color:#666">.</span> <span style="color:#ba2121">"misc"</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#666">1</span> <span style="color:#666">.</span> <span style="color:#ba2121">"core"</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#666">2</span> <span style="color:#666">.</span> <span style="color:#ba2121">"browser"</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#666">3</span> <span style="color:#666">.</span> <span style="color:#ba2121">"comms"</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#666">4</span> <span style="color:#666">.</span> <span style="color:#ba2121">"dev"</span>)))
|
|
</span></span></code></pre></div><h3 id="assigning-apps-to-workspaces-and-perspectives">Assigning apps to workspaces and perspectives</h3>
|
|
<p>By default, a new Emacs buffer opens in the current perspective in the current workspace, but sure enough, it’s possible to change that.</p>
|
|
<p>For EXWM windows, the <code>perspective-exwm</code> package provides a function called <code>perspective-exwm-assign-window</code>, which is intended to be used in <code>exwm-manage-finish-hook</code>, for instance:</p>
|
|
<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">defun</span> <span style="color:#19177c">my/exwm-configure-window</span> ()
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">interactive</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">pcase</span> <span style="color:#19177c">exwm-class-name</span>
|
|
</span></span><span style="display:flex;"><span> ((<span style="color:#008000">or</span> <span style="color:#ba2121">"Firefox"</span> <span style="color:#ba2121">"Nightly"</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">perspective-exwm-assign-window</span>
|
|
</span></span><span style="display:flex;"><span> <span style="color:#008000">:workspace-index</span> <span style="color:#666">2</span>
|
|
</span></span><span style="display:flex;"><span> <span style="color:#008000">:persp-name</span> <span style="color:#ba2121">"browser"</span>))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">"Alacritty"</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">perspective-exwm-assign-window</span>
|
|
</span></span><span style="display:flex;"><span> <span style="color:#008000">:persp-name</span> <span style="color:#ba2121">"term"</span>))))
|
|
</span></span><span style="display:flex;"><span>
|
|
</span></span><span style="display:flex;"><span>(<span style="color:#19177c">add-hook</span> <span style="color:#19177c">'exwm-manage-finish-hook</span> <span style="color:#00f">#'</span><span style="color:#19177c">my/exwm-configure-window</span>)
|
|
</span></span></code></pre></div><p>This hook is run after a new EXWM buffer is created and configured in the context of this buffer, so it seems customary to do such settings there. With this snippet, Firefox will always open in workspace 2 in the perspective named “browser”, and Alacritty will always open in the current workspace in the perspective named “term”.</p>
|
|
<p>To pull this off for various Emacs apps, it is necessary to open the right EXWM workspace and perspective before opening the app. As I use <a href="https://github.com/noctuid/general.el">general.el</a>, I made a macro to automate that:</p>
|
|
<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">defmacro</span> <span style="color:#19177c">my/command-in-persp</span> (<span style="color:#19177c">command-name</span> <span style="color:#19177c">persp-name</span> <span style="color:#19177c">workspace-index</span> <span style="color:#008000">&rest</span> <span style="color:#19177c">args</span>)
|
|
</span></span><span style="display:flex;"><span> <span style="color:#666">`'</span>((<span style="color:#008000">lambda</span> ()
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">interactive</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">when</span> (<span style="color:#008000">and</span> <span style="color:#666">,</span><span style="color:#19177c">workspace-index</span> (<span style="color:#00f">fboundp</span> <span style="color:#00f">#'</span><span style="color:#19177c">exwm-workspace-switch-create</span>))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">exwm-workspace-switch-create</span> <span style="color:#666">,</span><span style="color:#19177c">workspace-index</span>))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">persp-switch</span> <span style="color:#666">,</span><span style="color:#19177c">persp-name</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">delete-other-windows</span>)
|
|
</span></span><span style="display:flex;"><span> <span style="color:#666">,@</span><span style="color:#19177c">args</span>)
|
|
</span></span><span style="display:flex;"><span> <span style="color:#008000">:wk</span> <span style="color:#666">,</span><span style="color:#19177c">command-name</span>))
|
|
</span></span></code></pre></div><p><code>fboundp</code> is meant to provide compatibility with running Emacs without EXWM. Usage of the macro is as follows:</p>
|
|
<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#19177c">my-leader-def</span>
|
|
</span></span><span style="display:flex;"><span> <span style="color:#008000">:infix</span> <span style="color:#ba2121">"as"</span>
|
|
</span></span><span style="display:flex;"><span> <span style="color:#ba2121">""</span> <span style="color:#666">'</span>(<span style="color:#008000">:which-key</span> <span style="color:#ba2121">"emms"</span>)
|
|
</span></span><span style="display:flex;"><span> <span style="color:#ba2121">"s"</span> (<span style="color:#19177c">my/command-in-persp</span> <span style="color:#ba2121">"emms"</span> <span style="color:#ba2121">"EMMS"</span> <span style="color:#666">0</span> (<span style="color:#19177c">emms-smart-browse</span>))
|
|
</span></span><span style="display:flex;"><span> <span style="color:#666">...</span>)
|
|
</span></span></code></pre></div><p><code>my-leader-def</code> is a <a href="https://github.com/noctuid/general.el#creating-new-key-definers">custom definer</a>. That way the defined keybinding opens <a href="https://www.gnu.org/software/emms/">EMMS</a> in the workspace 0 in the perspective “EMMS”. I have this for several other apps, like elfeed, notmuch, dired <code>$HOME</code> and so on.</p>
|
|
<h3 id="some-workflow-notes">Some workflow notes</h3>
|
|
<p>As I said above, using perspectives in EXWM makes a lot of sense. Because all the EXWM workspace share the same buffer list (sans X windows), and because Emacs becomes the central program (for instance, it can’t be easily closed), it is only natural to split the buffer list.</p>
|
|
<p>Another aspect of using EXWM is that it becomes very easy to work with code on multiple monitors. While it may signify issues with the code in question if such need arises, having that possibility is still handy and it’s not something easily replicable on other tiling WMs. <code>perspective-exwm</code> also presents some features here, for instance, <code>M-x perspective-exwm-copy-to-workspace</code> can be used to copy the current perspective to the adjacent monitor.</p>
|
|
<p>Also, in my opinion, Emacs apps like <a href="https://www.gnu.org/software/emms/">EMMS</a> and <a href="https://github.com/skeeto/elfeed">elfeed</a> deserve to be on the same “level” as “proper” apps like a browser. On other tiling WMs, something like that can be done with Emacs daemon and multiple Emacs frames, but with EXWM and perspectives this seems natural without much extra work.</p>
|
|
<p>As for switching between X windows and perspectives, I ended up preferring to have one perspective for all X windows in the workspace, at least if these windows are full-fledged apps. For instance, all my messengers go to the workspace 3 to the perspective “comms”, and I switch between them with <code>M-x perspective-exwm-cycle-exwm-buffers-<forward|backward></code>, bound to <code>s-[</code> and <code>s-]</code>. For switching perspectives, I’ve bound <code>s-,</code> and <code>s-.</code>:</p>
|
|
<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">exwm-input-global-keys</span>
|
|
</span></span><span style="display:flex;"><span> <span style="color:#666">`</span>(
|
|
</span></span><span style="display:flex;"><span> <span style="color:#666">...</span>
|
|
</span></span><span style="display:flex;"><span> <span style="color:#408080;font-style:italic">;; Switch perspectives</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#666">,</span>(<span style="color:#19177c">kbd</span> <span style="color:#ba2121">"s-,"</span>) <span style="color:#666">.</span> <span style="color:#19177c">persp-prev</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#666">,</span>(<span style="color:#19177c">kbd</span> <span style="color:#ba2121">"s-."</span>) <span style="color:#666">.</span> <span style="color:#19177c">persp-next</span>)
|
|
</span></span><span style="display:flex;"><span>
|
|
</span></span><span style="display:flex;"><span> <span style="color:#408080;font-style:italic">;; EXWM buffers</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#666">,</span>(<span style="color:#19177c">kbd</span> <span style="color:#ba2121">"s-["</span>) <span style="color:#666">.</span> <span style="color:#19177c">perspective-exwm-cycle-exwm-buffers-backward</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#666">,</span>(<span style="color:#19177c">kbd</span> <span style="color:#ba2121">"s-]"</span>) <span style="color:#666">.</span> <span style="color:#19177c">perspective-exwm-cycle-exwm-buffers-forward</span>)
|
|
</span></span><span style="display:flex;"><span> <span style="color:#666">...</span>)
|
|
</span></span></code></pre></div><h2 id="workspaces-on-multiple-monitors">Workspaces on multiple monitors</h2>
|
|
<p>Here, <code>exwm-randr</code> provides basic functionality for running EXWM on multiple monitors. For instance, with configuration like that:</p>
|
|
<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">require</span> <span style="color:#19177c">'exwm-randr</span>)
|
|
</span></span><span style="display:flex;"><span>(<span style="color:#19177c">exwm-randr-enable</span>)
|
|
</span></span><span style="display:flex;"><span><span style="color:#408080;font-style:italic">;; The script is generated by ARandR</span>
|
|
</span></span><span style="display:flex;"><span>(<span style="color:#19177c">start-process-shell-command</span> <span style="color:#ba2121">"xrandr"</span> <span style="color:#800">nil</span> <span style="color:#ba2121">"~/bin/scripts/screen-layout"</span>)
|
|
</span></span><span style="display:flex;"><span>(<span style="color:#008000">when</span> (<span style="color:#19177c">string=</span> (<span style="color:#00f">system-name</span>) <span style="color:#ba2121">"indigo"</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">setq</span> <span style="color:#19177c">exwm-randr-workspace-monitor-plist</span> <span style="color:#666">'</span>(<span style="color:#666">2</span> <span style="color:#ba2121">"DVI-D-0"</span> <span style="color:#666">3</span> <span style="color:#ba2121">"DVI-D-0"</span>)))
|
|
</span></span><span style="display:flex;"><span>
|
|
</span></span><span style="display:flex;"><span><span style="color:#666">...</span>
|
|
</span></span><span style="display:flex;"><span>(<span style="color:#19177c">exwm-init</span>)
|
|
</span></span></code></pre></div><p>workspaces 2 and 3 on the machine with hostname “indigo” will be displayed on the monitor <code>DVI-D-0</code>.</p>
|
|
<p>However, some features, common in other tiling WMs, are missing in EXWM out of the box, namely:</p>
|
|
<ul>
|
|
<li>a command to <a href="https://i3wm.org/docs/userguide.html#_focusing_moving_containers">switch to another monitor</a>;</li>
|
|
<li>a command to <a href="https://i3wm.org/docs/userguide.html#move_to_outputs">move the current workspace to another monitor</a>;</li>
|
|
<li>using the same commands to switch between windows and monitors.</li>
|
|
</ul>
|
|
<p>Here’s my take on implementing them.</p>
|
|
<h3 id="tracking-recently-used-workspaces">Tracking recently used workspaces</h3>
|
|
<p>First up though, we need to track the workspaces in the usage order. I’m not sure if there’s some built-in functionality in EXWM for that, but it seems simple enough to implement.</p>
|
|
<p>Here is a snippet of code that does it:</p>
|
|
<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">my/exwm-last-workspaces</span> <span style="color:#666">'</span>(<span style="color:#666">1</span>))
|
|
</span></span><span style="display:flex;"><span>
|
|
</span></span><span style="display:flex;"><span>(<span style="color:#008000">defun</span> <span style="color:#19177c">my/exwm-store-last-workspace</span> ()
|
|
</span></span><span style="display:flex;"><span> <span style="color:#ba2121">"Save the last workspace to </span><span style="color:#19177c">`my/exwm-last-workspaces'</span><span style="color:#ba2121">."</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">setq</span> <span style="color:#19177c">my/exwm-last-workspaces</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">seq-uniq</span> (<span style="color:#00f">cons</span> <span style="color:#19177c">exwm-workspace-current-index</span>
|
|
</span></span><span style="display:flex;"><span> <span style="color:#19177c">my/exwm-last-workspaces</span>))))
|
|
</span></span><span style="display:flex;"><span>
|
|
</span></span><span style="display:flex;"><span>(<span style="color:#19177c">add-hook</span> <span style="color:#19177c">'exwm-workspace-switch-hook</span>
|
|
</span></span><span style="display:flex;"><span> <span style="color:#00f">#'</span><span style="color:#19177c">my/exwm-store-last-workspace</span>)
|
|
</span></span></code></pre></div><p>The variable <code>my/exwm-last-workspaces</code> stores the workspace indices; the first item is the index of the current workspace, the second item is the index of the previous workspace, and so on.</p>
|
|
<p>One note here is that workspaces may also disappear (e.g. after <code>M-x exwm-workspace-delete</code>), so we also need a function to clean the list:</p>
|
|
<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">defun</span> <span style="color:#19177c">my/exwm-last-workspaces-clear</span> ()
|
|
</span></span><span style="display:flex;"><span> <span style="color:#ba2121">"Clean </span><span style="color:#19177c">`my/exwm-last-workspaces'</span><span style="color:#ba2121"> from deleted workspaces."</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">setq</span> <span style="color:#19177c">my/exwm-last-workspaces</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">seq-filter</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">lambda</span> (<span style="color:#19177c">i</span>) (<span style="color:#00f">nth</span> <span style="color:#19177c">i</span> <span style="color:#19177c">exwm-workspace--list</span>))
|
|
</span></span><span style="display:flex;"><span> <span style="color:#19177c">my/exwm-last-workspaces</span>)))
|
|
</span></span></code></pre></div><h3 id="the-monitor-list">The monitor list</h3>
|
|
<p>The second piece of the puzzle is getting the monitor list in the right order.</p>
|
|
<p>While it is possible to retrieve the monitor list from <code>exwm-randr-workspace-output-plist</code>, this won’t scale well beyond two monitors, mainly because changing this variable may screw up the order.</p>
|
|
<p>So the easiest way is to just define the variable like that:</p>
|
|
<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">my/exwm-monitor-list</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">pcase</span> (<span style="color:#00f">system-name</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">"indigo"</span> <span style="color:#666">'</span>(<span style="color:#800">nil</span> <span style="color:#ba2121">"DVI-D-0"</span>))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">_</span> <span style="color:#666">'</span>(<span style="color:#800">nil</span>))))
|
|
</span></span></code></pre></div><p>If you are changing the RandR configuration on the fly, this variable will also need to be changed, but for now, I don’t have such a necessity.</p>
|
|
<p>A function to get the current monitor:</p>
|
|
<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">defun</span> <span style="color:#19177c">my/exwm-get-current-monitor</span> ()
|
|
</span></span><span style="display:flex;"><span> <span style="color:#ba2121">"Return the current monitor name or nil."</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#00f">plist-get</span> <span style="color:#19177c">exwm-randr-workspace-output-plist</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">cl-position</span> (<span style="color:#00f">selected-frame</span>)
|
|
</span></span><span style="display:flex;"><span> <span style="color:#19177c">exwm-workspace--list</span>)))
|
|
</span></span></code></pre></div><p>And a function to cycle the monitor list in either direction:</p>
|
|
<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">defun</span> <span style="color:#19177c">my/exwm-get-other-monitor</span> (<span style="color:#19177c">dir</span>)
|
|
</span></span><span style="display:flex;"><span> <span style="color:#ba2121">"Cycle the monitor list in the direction DIR.
|
|
</span></span></span><span style="display:flex;"><span><span style="color:#ba2121">
|
|
</span></span></span><span style="display:flex;"><span><span style="color:#ba2121">DIR is either 'left or 'right."</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#00f">nth</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#00f">%</span> (<span style="color:#00f">+</span> (<span style="color:#19177c">cl-position</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">my/exwm-get-current-monitor</span>)
|
|
</span></span><span style="display:flex;"><span> <span style="color:#19177c">my/exwm-monitor-list</span>
|
|
</span></span><span style="display:flex;"><span> <span style="color:#008000">:test</span> <span style="color:#00f">#'string-equal</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#00f">length</span> <span style="color:#19177c">my/exwm-monitor-list</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">pcase</span> <span style="color:#19177c">dir</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">'right</span> <span style="color:#666">1</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">'left</span> <span style="color:#666">-1</span>)))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#00f">length</span> <span style="color:#19177c">my/exwm-monitor-list</span>))
|
|
</span></span><span style="display:flex;"><span> <span style="color:#19177c">my/exwm-monitor-list</span>))
|
|
</span></span></code></pre></div><h3 id="switch-to-another-monitor">Switch to another monitor</h3>
|
|
<p>With the functions from the previous two sections, we can implement switching to another monitor by switching to the most recently used workspace on that monitor.</p>
|
|
<video controls width="100%">
|
|
<source src="/videos/exwm-workspace-switch.mp4" type="video/mp4">
|
|
</video>
|
|
<p>One caveat here is that on the startup the <code>my/exwm-last-workspaces</code> variable won’t have any values from other monitor(s), so this list is concatenated with the list of available workspace indices.</p>
|
|
<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">defun</span> <span style="color:#19177c">my/exwm-switch-to-other-monitor</span> (<span style="color:#008000">&optional</span> <span style="color:#19177c">dir</span>)
|
|
</span></span><span style="display:flex;"><span> <span style="color:#ba2121">"Switch to another monitor."</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">interactive</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">my/exwm-last-workspaces-clear</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">exwm-workspace-switch</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">cl-loop</span> <span style="color:#19177c">with</span> <span style="color:#19177c">other-monitor</span> <span style="color:#00f">=</span> (<span style="color:#19177c">my/exwm-get-other-monitor</span> (<span style="color:#008000">or</span> <span style="color:#19177c">dir</span> <span style="color:#19177c">'right</span>))
|
|
</span></span><span style="display:flex;"><span> <span style="color:#19177c">for</span> <span style="color:#19177c">i</span> <span style="color:#19177c">in</span> (<span style="color:#00f">append</span> <span style="color:#19177c">my/exwm-last-workspaces</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">cl-loop</span> <span style="color:#19177c">for</span> <span style="color:#19177c">i</span> <span style="color:#19177c">from</span> <span style="color:#666">0</span>
|
|
</span></span><span style="display:flex;"><span> <span style="color:#19177c">for</span> <span style="color:#19177c">_</span> <span style="color:#19177c">in</span> <span style="color:#19177c">exwm-workspace--list</span>
|
|
</span></span><span style="display:flex;"><span> <span style="color:#19177c">collect</span> <span style="color:#19177c">i</span>))
|
|
</span></span><span style="display:flex;"><span> <span style="color:#008000">if</span> (<span style="color:#008000">if</span> <span style="color:#19177c">other-monitor</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#00f">string-equal</span> (<span style="color:#00f">plist-get</span> <span style="color:#19177c">exwm-randr-workspace-output-plist</span> <span style="color:#19177c">i</span>)
|
|
</span></span><span style="display:flex;"><span> <span style="color:#19177c">other-monitor</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">not</span> (<span style="color:#00f">plist-get</span> <span style="color:#19177c">exwm-randr-workspace-output-plist</span> <span style="color:#19177c">i</span>)))
|
|
</span></span><span style="display:flex;"><span> <span style="color:#008000">return</span> <span style="color:#19177c">i</span>)))
|
|
</span></span></code></pre></div><p>I bind this function to <code>s-q</code>, as I’m used from i3.</p>
|
|
<h3 id="move-the-workspace-to-another-monitor">Move the workspace to another monitor</h3>
|
|
<p>Now, moving the workspace to another monitor.</p>
|
|
<video controls width="100%">
|
|
<source src="/videos/exwm-workspace-move.mp4" type="video/mp4">
|
|
</video>
|
|
<p>This is actually quite easy to pull off - one just has to update <code>exwm-randr-workspace-monitor-plist</code> accordingly and run <code>exwm-randr-refresh</code>. I just add another check there because I don’t want some monitor to remain without workspaces at all.</p>
|
|
<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">defun</span> <span style="color:#19177c">my/exwm-workspace-switch-monitor</span> ()
|
|
</span></span><span style="display:flex;"><span> <span style="color:#ba2121">"Move the current workspace to another monitor."</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">interactive</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">let</span> ((<span style="color:#19177c">new-monitor</span> (<span style="color:#19177c">my/exwm-get-other-monitor</span> <span style="color:#19177c">'right</span>))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">current-monitor</span> (<span style="color:#19177c">my/exwm-get-current-monitor</span>)))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">when</span> (<span style="color:#008000">and</span> <span style="color:#19177c">current-monitor</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#00f">>=</span> <span style="color:#666">1</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">cl-loop</span> <span style="color:#19177c">for</span> (<span style="color:#19177c">key</span> <span style="color:#19177c">value</span>) <span style="color:#19177c">on</span> <span style="color:#19177c">exwm-randr-workspace-monitor-plist</span>
|
|
</span></span><span style="display:flex;"><span> <span style="color:#19177c">by</span> <span style="color:#19177c">'cddr</span>
|
|
</span></span><span style="display:flex;"><span> <span style="color:#008000">if</span> (<span style="color:#00f">string-equal</span> <span style="color:#19177c">value</span> <span style="color:#19177c">current-monitor</span>) <span style="color:#19177c">sum</span> <span style="color:#666">1</span>)))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#d2413a;font-weight:bold">error</span> <span style="color:#ba2121">"Can't remove the last workspace on the monitor!"</span>))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">setq</span> <span style="color:#19177c">exwm-randr-workspace-monitor-plist</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">map-delete</span> <span style="color:#19177c">exwm-randr-workspace-monitor-plist</span> <span style="color:#19177c">exwm-workspace-current-index</span>))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">when</span> <span style="color:#19177c">new-monitor</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">setq</span> <span style="color:#19177c">exwm-randr-workspace-monitor-plist</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#00f">plist-put</span> <span style="color:#19177c">exwm-randr-workspace-monitor-plist</span>
|
|
</span></span><span style="display:flex;"><span> <span style="color:#19177c">exwm-workspace-current-index</span>
|
|
</span></span><span style="display:flex;"><span> <span style="color:#19177c">new-monitor</span>))))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">exwm-randr-refresh</span>))
|
|
</span></span></code></pre></div><p>In my configuration this is bound to <code>s-<tab></code>.</p>
|
|
<h3 id="windmove-between-monitors">Windmove between monitors</h3>
|
|
<p>And the final (for now) piece of the puzzle is using the same command to switch between windows and monitors. E.g. when the focus is on the right-most window on one monitor, I want the command to switch to the left-most window on the monitor to the right instead of saying “No window right from the selected window”, as <code>windmove-right</code> does.</p>
|
|
<p>So here is my implementation of that. It always does <code>windmove-do-select-window</code> for <code>'down</code> and <code>'up</code>. For <code>'right</code> and <code>'left</code> though, the function calls the previously defined function to switch to other monitor if <code>windmove-find-other-window</code> doesn’t return anything.</p>
|
|
<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">defun</span> <span style="color:#19177c">my/exwm-windmove</span> (<span style="color:#19177c">dir</span>)
|
|
</span></span><span style="display:flex;"><span> <span style="color:#ba2121">"Move to window or monitor in the direction DIR."</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">if</span> (<span style="color:#008000">or</span> (<span style="color:#00f">eq</span> <span style="color:#19177c">dir</span> <span style="color:#19177c">'down</span>) (<span style="color:#00f">eq</span> <span style="color:#19177c">dir</span> <span style="color:#19177c">'up</span>))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">windmove-do-window-select</span> <span style="color:#19177c">dir</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">let</span> ((<span style="color:#19177c">other-window</span> (<span style="color:#19177c">windmove-find-other-window</span> <span style="color:#19177c">dir</span>))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">other-monitor</span> (<span style="color:#19177c">my/exwm-get-other-monitor</span> <span style="color:#19177c">dir</span>))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">opposite-dir</span> (<span style="color:#008000">pcase</span> <span style="color:#19177c">dir</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">'left</span> <span style="color:#19177c">'right</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">'right</span> <span style="color:#19177c">'left</span>))))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">if</span> <span style="color:#19177c">other-window</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">windmove-do-window-select</span> <span style="color:#19177c">dir</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">my/exwm-switch-to-other-monitor</span> <span style="color:#19177c">dir</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">cl-loop</span> <span style="color:#008000">while</span> (<span style="color:#19177c">windmove-find-other-window</span> <span style="color:#19177c">opposite-dir</span>)
|
|
</span></span><span style="display:flex;"><span> <span style="color:#008000">do</span> (<span style="color:#19177c">windmove-do-window-select</span> <span style="color:#19177c">opposite-dir</span>))))))
|
|
</span></span></code></pre></div><p>I bind it to the corresponding keys like that:</p>
|
|
<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">exwm-input-global-keys</span>
|
|
</span></span><span style="display:flex;"><span> <span style="color:#666">`</span>(
|
|
</span></span><span style="display:flex;"><span> <span style="color:#666">...</span>
|
|
</span></span><span style="display:flex;"><span> <span style="color:#408080;font-style:italic">;; Switch windows</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#666">,</span>(<span style="color:#19177c">kbd</span> <span style="color:#ba2121">"s-<left>"</span>) <span style="color:#666">.</span> (<span style="color:#008000">lambda</span> () (<span style="color:#008000">interactive</span>) (<span style="color:#19177c">my/exwm-windmove</span> <span style="color:#19177c">'left</span>)))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#666">,</span>(<span style="color:#19177c">kbd</span> <span style="color:#ba2121">"s-<right>"</span>) <span style="color:#666">.</span> (<span style="color:#008000">lambda</span> () (<span style="color:#008000">interactive</span>) (<span style="color:#19177c">my/exwm-windmove</span> <span style="color:#19177c">'right</span>)))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#666">,</span>(<span style="color:#19177c">kbd</span> <span style="color:#ba2121">"s-<up>"</span>) <span style="color:#666">.</span> (<span style="color:#008000">lambda</span> () (<span style="color:#008000">interactive</span>) (<span style="color:#19177c">my/exwm-windmove</span> <span style="color:#19177c">'up</span>)))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#666">,</span>(<span style="color:#19177c">kbd</span> <span style="color:#ba2121">"s-<down>"</span>) <span style="color:#666">.</span> (<span style="color:#008000">lambda</span> () (<span style="color:#008000">interactive</span>) (<span style="color:#19177c">my/exwm-windmove</span> <span style="color:#19177c">'down</span>)))
|
|
</span></span><span style="display:flex;"><span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#666">,</span>(<span style="color:#19177c">kbd</span> <span style="color:#ba2121">"s-h"</span>) <span style="color:#666">.</span> (<span style="color:#008000">lambda</span> () (<span style="color:#008000">interactive</span>) (<span style="color:#19177c">my/exwm-windmove</span> <span style="color:#19177c">'left</span>)))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#666">,</span>(<span style="color:#19177c">kbd</span> <span style="color:#ba2121">"s-l"</span>) <span style="color:#666">.</span> (<span style="color:#008000">lambda</span> () (<span style="color:#008000">interactive</span>) (<span style="color:#19177c">my/exwm-windmove</span> <span style="color:#19177c">'right</span>)))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#666">,</span>(<span style="color:#19177c">kbd</span> <span style="color:#ba2121">"s-k"</span>) <span style="color:#666">.</span> (<span style="color:#008000">lambda</span> () (<span style="color:#008000">interactive</span>) (<span style="color:#19177c">my/exwm-windmove</span> <span style="color:#19177c">'up</span>)))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#666">,</span>(<span style="color:#19177c">kbd</span> <span style="color:#ba2121">"s-j"</span>) <span style="color:#666">.</span> (<span style="color:#008000">lambda</span> () (<span style="color:#008000">interactive</span>) (<span style="color:#19177c">my/exwm-windmove</span> <span style="color:#19177c">'down</span>)))
|
|
</span></span><span style="display:flex;"><span> <span style="color:#666">...</span>)
|
|
</span></span></code></pre></div><h2 id="managing-windows">Managing windows</h2>
|
|
<p>Another thing I want to tackle here is managing windows.</p>
|
|
<p>This section of the post depends on <a href="https://github.com/emacs-evil/evil">evil-mode</a>, which provides a reasonable set of vim-like commands to manage windows. But a few points to improve upon remain.</p>
|
|
<h3 id="moving-windows">Moving windows</h3>
|
|
<p>As I wrote in my <a href="https://sqrtminusone.xyz/posts/2021-10-04-emacs-i3/">Emacs and i3</a> post, I want to have a rather specific behavior when moving windows (which does resemble i3 in some way):</p>
|
|
<ul>
|
|
<li>if there is space in the required direction, move the Emacs window there;</li>
|
|
<li>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;</li>
|
|
</ul>
|
|
<p>I can’t say it’s better or worse than the built-in functionality or one provided by evil, but I’m used to it and I think it fits better for managing a lot of windows.</p>
|
|
<p>So, first, we need a predicate that checks whether there is space in the given direction:</p>
|
|
<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">defun</span> <span style="color:#19177c">my/exwm-direction-exists-p</span> (<span style="color:#19177c">dir</span>)
|
|
</span></span><span style="display:flex;"><span> <span style="color:#ba2121">"Check if there is space in the direction DIR.
|
|
</span></span></span><span style="display:flex;"><span><span style="color:#ba2121">
|
|
</span></span></span><span style="display:flex;"><span><span style="color:#ba2121">Does not take the minibuffer into account."</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">cl-some</span> (<span style="color:#008000">lambda</span> (<span style="color:#19177c">dir</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">let</span> ((<span style="color:#19177c">win</span> (<span style="color:#19177c">windmove-find-other-window</span> <span style="color:#19177c">dir</span>)))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">and</span> <span style="color:#19177c">win</span> (<span style="color:#19177c">not</span> (<span style="color:#00f">window-minibuffer-p</span> <span style="color:#19177c">win</span>)))))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">pcase</span> <span style="color:#19177c">dir</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">'width</span> <span style="color:#666">'</span>(<span style="color:#19177c">left</span> <span style="color:#19177c">right</span>))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">'height</span> <span style="color:#666">'</span>(<span style="color:#19177c">up</span> <span style="color:#19177c">down</span>)))))
|
|
</span></span></code></pre></div><p>And a function to implement that:</p>
|
|
<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">defun</span> <span style="color:#19177c">my/exwm-move-window</span> (<span style="color:#19177c">dir</span>)
|
|
</span></span><span style="display:flex;"><span> <span style="color:#ba2121">"Move the current window in the direction DIR."</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">let</span> ((<span style="color:#19177c">other-window</span> (<span style="color:#19177c">windmove-find-other-window</span> <span style="color:#19177c">dir</span>))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">other-direction</span> (<span style="color:#19177c">my/exwm-direction-exists-p</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">pcase</span> <span style="color:#19177c">dir</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">'up</span> <span style="color:#19177c">'width</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">'down</span> <span style="color:#19177c">'width</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">'left</span> <span style="color:#19177c">'height</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">'right</span> <span style="color:#19177c">'height</span>)))))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">cond</span>
|
|
</span></span><span style="display:flex;"><span> ((<span style="color:#008000">and</span> <span style="color:#19177c">other-window</span> (<span style="color:#19177c">not</span> (<span style="color:#00f">window-minibuffer-p</span> <span style="color:#19177c">other-window</span>)))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">window-swap-states</span> (<span style="color:#00f">selected-window</span>) <span style="color:#19177c">other-window</span>))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">other-direction</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">evil-move-window</span> <span style="color:#19177c">dir</span>)))))
|
|
</span></span></code></pre></div><p>My preferred keybindings for this part are, of course, <code>s-<H|J|K|L></code>:</p>
|
|
<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">exwm-input-global-keys</span>
|
|
</span></span><span style="display:flex;"><span> <span style="color:#666">`</span>(
|
|
</span></span><span style="display:flex;"><span> <span style="color:#408080;font-style:italic">;; Moving windows</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#666">,</span>(<span style="color:#19177c">kbd</span> <span style="color:#ba2121">"s-H"</span>) <span style="color:#666">.</span> (<span style="color:#008000">lambda</span> () (<span style="color:#008000">interactive</span>) (<span style="color:#19177c">my/exwm-move-window</span> <span style="color:#19177c">'left</span>)))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#666">,</span>(<span style="color:#19177c">kbd</span> <span style="color:#ba2121">"s-L"</span>) <span style="color:#666">.</span> (<span style="color:#008000">lambda</span> () (<span style="color:#008000">interactive</span>) (<span style="color:#19177c">my/exwm-move-window</span> <span style="color:#19177c">'right</span>)))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#666">,</span>(<span style="color:#19177c">kbd</span> <span style="color:#ba2121">"s-K"</span>) <span style="color:#666">.</span> (<span style="color:#008000">lambda</span> () (<span style="color:#008000">interactive</span>) (<span style="color:#19177c">my/exwm-move-window</span> <span style="color:#19177c">'up</span>)))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#666">,</span>(<span style="color:#19177c">kbd</span> <span style="color:#ba2121">"s-J"</span>) <span style="color:#666">.</span> (<span style="color:#008000">lambda</span> () (<span style="color:#008000">interactive</span>) (<span style="color:#19177c">my/exwm-move-window</span> <span style="color:#19177c">'down</span>)))
|
|
</span></span><span style="display:flex;"><span> <span style="color:#666">...</span>))
|
|
</span></span></code></pre></div><h3 id="resizing-windows">Resizing windows</h3>
|
|
<p>I find this odd that there are different commands to resize tiling and floating windows.</p>
|
|
<video controls width="100%">
|
|
<source src="/videos/exwm-resize-hydra.mp4" type="video/mp4">
|
|
</video>
|
|
<p>So let’s define one command to perform both resizes depending on the context:</p>
|
|
<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">my/exwm-resize-value</span> <span style="color:#666">5</span>)
|
|
</span></span><span style="display:flex;"><span>
|
|
</span></span><span style="display:flex;"><span>(<span style="color:#008000">defun</span> <span style="color:#19177c">my/exwm-resize-window</span> (<span style="color:#19177c">dir</span> <span style="color:#19177c">kind</span> <span style="color:#008000">&optional</span> <span style="color:#19177c">value</span>)
|
|
</span></span><span style="display:flex;"><span> <span style="color:#ba2121">"Resize the current window in the direction DIR.
|
|
</span></span></span><span style="display:flex;"><span><span style="color:#ba2121">
|
|
</span></span></span><span style="display:flex;"><span><span style="color:#ba2121">DIR is either 'height or 'width, KIND is either 'shrink or
|
|
</span></span></span><span style="display:flex;"><span><span style="color:#ba2121"> 'grow. VALUE is </span><span style="color:#19177c">`my/exwm-resize-value'</span><span style="color:#ba2121"> by default.
|
|
</span></span></span><span style="display:flex;"><span><span style="color:#ba2121">
|
|
</span></span></span><span style="display:flex;"><span><span style="color:#ba2121">If the window is an EXWM floating window, execute the
|
|
</span></span></span><span style="display:flex;"><span><span style="color:#ba2121">corresponding command from the exwm-layout group, execute the
|
|
</span></span></span><span style="display:flex;"><span><span style="color:#ba2121">command from the evil-window group."</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">unless</span> <span style="color:#19177c">value</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">setq</span> <span style="color:#19177c">value</span> <span style="color:#19177c">my/exwm-resize-value</span>))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">let*</span> ((<span style="color:#19177c">is-exwm-floating</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">and</span> (<span style="color:#19177c">derived-mode-p</span> <span style="color:#19177c">'exwm-mode</span>)
|
|
</span></span><span style="display:flex;"><span> <span style="color:#19177c">exwm--floating-frame</span>))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">func</span> (<span style="color:#008000">if</span> <span style="color:#19177c">is-exwm-floating</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#00f">intern</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#00f">concat</span>
|
|
</span></span><span style="display:flex;"><span> <span style="color:#ba2121">"exwm-layout-"</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">pcase</span> <span style="color:#19177c">kind</span> (<span style="color:#19177c">'shrink</span> <span style="color:#ba2121">"shrink"</span>) (<span style="color:#19177c">'grow</span> <span style="color:#ba2121">"enlarge"</span>))
|
|
</span></span><span style="display:flex;"><span> <span style="color:#ba2121">"-window"</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">pcase</span> <span style="color:#19177c">dir</span> (<span style="color:#19177c">'height</span> <span style="color:#ba2121">""</span>) (<span style="color:#19177c">'width</span> <span style="color:#ba2121">"-horizontally"</span>))))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#00f">intern</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#00f">concat</span>
|
|
</span></span><span style="display:flex;"><span> <span style="color:#ba2121">"evil-window"</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">pcase</span> <span style="color:#19177c">kind</span> (<span style="color:#19177c">'shrink</span> <span style="color:#ba2121">"-decrease-"</span>) (<span style="color:#19177c">'grow</span> <span style="color:#ba2121">"-increase-"</span>))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#00f">symbol-name</span> <span style="color:#19177c">dir</span>))))))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">when</span> <span style="color:#19177c">is-exwm-floating</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">setq</span> <span style="color:#19177c">value</span> (<span style="color:#00f">*</span> <span style="color:#666">5</span> <span style="color:#19177c">value</span>)))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#00f">funcall</span> <span style="color:#19177c">func</span> <span style="color:#19177c">value</span>)))
|
|
</span></span></code></pre></div><p>This function will call <code>exwm-layout-<shrink|grow>[-horizontally]</code> for EXWM floating window and <code>evil-window-<decrease|increase>-<width|height></code> otherwise.</p>
|
|
<p>This function can be bound to the required keybindings directly, but I prefer a hydra to emulate the i3 submode:</p>
|
|
<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#19177c">defhydra</span> <span style="color:#19177c">my/exwm-resize-hydra</span> (<span style="color:#008000">:color</span> <span style="color:#19177c">pink</span> <span style="color:#008000">:hint</span> <span style="color:#800">nil</span> <span style="color:#008000">:foreign-keys</span> <span style="color:#19177c">run</span>)
|
|
</span></span><span style="display:flex;"><span> <span style="color:#ba2121">"
|
|
</span></span></span><span style="display:flex;"><span><span style="color:#ba2121">^Resize^
|
|
</span></span></span><span style="display:flex;"><span><span style="color:#ba2121">_l_: Increase width _h_: Decrease width _j_: Increase height _k_: Decrease height
|
|
</span></span></span><span style="display:flex;"><span><span style="color:#ba2121">
|
|
</span></span></span><span style="display:flex;"><span><span style="color:#ba2121">_=_: Balance "</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">"h"</span> (<span style="color:#008000">lambda</span> () (<span style="color:#008000">interactive</span>) (<span style="color:#19177c">my/exwm-resize-window</span> <span style="color:#19177c">'width</span> <span style="color:#19177c">'shrink</span>)))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">"j"</span> (<span style="color:#008000">lambda</span> () (<span style="color:#008000">interactive</span>) (<span style="color:#19177c">my/exwm-resize-window</span> <span style="color:#19177c">'height</span> <span style="color:#19177c">'grow</span>)))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">"k"</span> (<span style="color:#008000">lambda</span> () (<span style="color:#008000">interactive</span>) (<span style="color:#19177c">my/exwm-resize-window</span> <span style="color:#19177c">'height</span> <span style="color:#19177c">'shrink</span>)))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">"l"</span> (<span style="color:#008000">lambda</span> () (<span style="color:#008000">interactive</span>) (<span style="color:#19177c">my/exwm-resize-window</span> <span style="color:#19177c">'width</span> <span style="color:#19177c">'grow</span>)))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">"="</span> <span style="color:#19177c">balance-windows</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">"q"</span> <span style="color:#800">nil</span> <span style="color:#ba2121">"quit"</span> <span style="color:#008000">:color</span> <span style="color:#19177c">blue</span>))
|
|
</span></span></code></pre></div><h3 id="splitting-windows">Splitting windows</h3>
|
|
<p><code>M-x evil-window-[v]split</code> (bound to <code>C-w v</code> and <code>C-w s</code> by default) are the default evil command to do splits.</p>
|
|
<p>One EXWM-related issue though is that by default doing such a split “copies” the current buffer to the new window. But as EXWM buffer cannot be “copied” like that, some other buffer is displayed in the split, and generally, that’s not a buffer I want.</p>
|
|
<p>For instance, I prefer to have Chrome DevTools as a separate window. When I click “Inspect” on something, the DevTools window replaces my Ungoogled Chromium window. I press <code>C-w v</code>, and most often I have something like <code>*scratch*</code> buffer in the opened split instead of the previous Chromium window.</p>
|
|
<p>To implement better behavior, I define the following advice:</p>
|
|
<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">defun</span> <span style="color:#19177c">my/exwm-fill-other-window</span> (<span style="color:#008000">&rest</span> <span style="color:#19177c">_</span>)
|
|
</span></span><span style="display:flex;"><span> <span style="color:#ba2121">"Open the most recently used buffer in the next window."</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">interactive</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">when</span> (<span style="color:#008000">and</span> (<span style="color:#00f">eq</span> <span style="color:#19177c">major-mode</span> <span style="color:#19177c">'exwm-mode</span>) (<span style="color:#19177c">not</span> (<span style="color:#00f">eq</span> (<span style="color:#00f">next-window</span>) (<span style="color:#00f">get-buffer-window</span>))))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">let</span> ((<span style="color:#19177c">other-exwm-buffer</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">cl-loop</span> <span style="color:#19177c">with</span> <span style="color:#00f">other-buffer</span> <span style="color:#00f">=</span> (<span style="color:#19177c">persp-other-buffer</span>)
|
|
</span></span><span style="display:flex;"><span> <span style="color:#19177c">for</span> <span style="color:#19177c">buf</span> <span style="color:#19177c">in</span> (<span style="color:#00f">sort</span> (<span style="color:#19177c">persp-current-buffers</span>) (<span style="color:#008000">lambda</span> (<span style="color:#19177c">a</span> <span style="color:#19177c">_</span>) (<span style="color:#00f">eq</span> <span style="color:#19177c">a</span> <span style="color:#00f">other-buffer</span>)))
|
|
</span></span><span style="display:flex;"><span> <span style="color:#19177c">with</span> <span style="color:#00f">current-buffer</span> <span style="color:#00f">=</span> (<span style="color:#00f">current-buffer</span>)
|
|
</span></span><span style="display:flex;"><span> <span style="color:#008000">when</span> (<span style="color:#008000">and</span> (<span style="color:#19177c">not</span> (<span style="color:#00f">eq</span> <span style="color:#00f">current-buffer</span> <span style="color:#19177c">buf</span>))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#00f">buffer-live-p</span> <span style="color:#19177c">buf</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">not</span> (<span style="color:#19177c">string-match-p</span> (<span style="color:#19177c">persp--make-ignore-buffer-rx</span>) (<span style="color:#00f">buffer-name</span> <span style="color:#19177c">buf</span>)))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">not</span> (<span style="color:#00f">get-buffer-window</span> <span style="color:#19177c">buf</span>)))
|
|
</span></span><span style="display:flex;"><span> <span style="color:#008000">return</span> <span style="color:#19177c">buf</span>)))
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">when</span> <span style="color:#19177c">other-exwm-buffer</span>
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">with-selected-window</span> (<span style="color:#00f">next-window</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#19177c">switch-to-buffer</span> <span style="color:#19177c">other-exwm-buffer</span>))))))
|
|
</span></span></code></pre></div><p>This is meant to be called after doing an either vertical or horizontal split, so it’s advised like that:</p>
|
|
<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#19177c">advice-add</span> <span style="color:#19177c">'evil-window-split</span> <span style="color:#008000">:after</span> <span style="color:#00f">#'</span><span style="color:#19177c">my/exwm-fill-other-window</span>)
|
|
</span></span><span style="display:flex;"><span>(<span style="color:#19177c">advice-add</span> <span style="color:#19177c">'evil-window-vsplit</span> <span style="color:#008000">:after</span> <span style="color:#00f">#'</span><span style="color:#19177c">my/exwm-fill-other-window</span>)
|
|
</span></span></code></pre></div><p>This works as follows. If the current buffer is an EXWM buffer and there are other windows open (that is, <code>(next-window)</code> is not the current window), the function tries to find another suitable buffer to be opened in the split. And that also takes the perspectives into account, so buffers are searched only within the current perspective, and the buffer returned by <code>persp-other-buffer</code> will be the top candidate.</p>
|
|
<h2 id="notes-on-floating-windows">Notes on floating windows</h2>
|
|
<p>Floating windows are not the most stable feature of EXWM.</p>
|
|
<p>One story is that closing a floating window often screws up the current perspective, but that’s advised away by my <code>perspective-exwm-mode</code>.</p>
|
|
<p>Another is that these three settings (which are reasonably <a href="https://github.com/daviwil/emacs-from-scratch/blob/5ebd390119a48cac6258843c7d5e570f4591fdd4/show-notes/Emacs-Desktop-04.org#mouse-warping">recommended</a> in the Emacs Desktop series) seem to increase chances of breaking the current EXWM session:</p>
|
|
<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">exwm-workspace-warp-cursor</span> <span style="color:#800">t</span>)
|
|
</span></span><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">mouse-autoselect-window</span> <span style="color:#800">t</span>)
|
|
</span></span><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">focus-follows-mouse</span> <span style="color:#800">t</span>)
|
|
</span></span></code></pre></div><p>Occasionally they create a loop of mouse warps and focus changes. I found that disabling them just for the floating windows greatly stabilized that part:</p>
|
|
<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">defun</span> <span style="color:#19177c">my/fix-exwm-floating-windows</span> ()
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">setq-local</span> <span style="color:#19177c">exwm-workspace-warp-cursor</span> <span style="color:#800">nil</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">setq-local</span> <span style="color:#19177c">mouse-autoselect-window</span> <span style="color:#800">nil</span>)
|
|
</span></span><span style="display:flex;"><span> (<span style="color:#008000">setq-local</span> <span style="color:#19177c">focus-follows-mouse</span> <span style="color:#800">nil</span>))
|
|
</span></span><span style="display:flex;"><span>
|
|
</span></span><span style="display:flex;"><span>(<span style="color:#19177c">add-hook</span> <span style="color:#19177c">'exwm-floating-setup-hook</span> <span style="color:#00f">#'</span><span style="color:#19177c">my/fix-exwm-floating-windows</span>)
|
|
</span></span></code></pre></div><p>However, one particularly unfriendly app is the <a href="https://zoom.us/">Zoom app</a>, which proudly creates a million various popups and still manages to break the EXWM sesssion. Fortunately, it can be used from a browser, which is what I advise to do.</p>
|
|
<h2 id="what-else-not-to-do">What else not to do</h2>
|
|
<p>A couple of final notes to make using EXWM a somewhat better experience.</p>
|
|
<p>First, <a href="https://github.com/daviwil/exwm/commit/7b1be884124711af0a02eac740bdb69446bc54cc">this fix</a> by David helped with <a href="https://github.com/ch11ng/exwm/issues/842">one case</a> of EXWM freezing, which I managed to get into a few times.</p>
|
|
<p>Second, do not run transients while there’s an active EXWM window in the workspace, especially if it’s it <code>char-mode</code>. That seems to break the session quite securely.</p>
|
|
<p>Third, running <code>shutdown</code> or something like that in the console is not the greatest idea, because things like <code>kill-emacs-hook</code> are not triggered in this case. For instance, EMMS history & elfeed databases are not saved.</p>
|
|
<h2 id="p-dot-s-dot">P.S.</h2>
|
|
<p>The way how characters aligned in my keybinding for EMMS is coincidental and does not carry any semantic value. The <code>a</code> is for “app”, <code>s</code> is because <code>e</code> and <code>m</code> were already taken by elfeed and notmuch, and the second <code>s</code> is because it’s faster to press the same character twice.</p>
|
|
|
|
</div>
|
|
<div class="table-of-contents">
|
|
<div class="table-of-contents-text">
|
|
<b><a href="#">Table of Contents</a></b>
|
|
<nav id="TableOfContents">
|
|
<ul>
|
|
<li><a href="#perspective-dot-el">perspective.el</a>
|
|
<ul>
|
|
<li><a href="#initial-perspective-names">Initial perspective names</a></li>
|
|
<li><a href="#assigning-apps-to-workspaces-and-perspectives">Assigning apps to workspaces and perspectives</a></li>
|
|
<li><a href="#some-workflow-notes">Some workflow notes</a></li>
|
|
</ul>
|
|
</li>
|
|
<li><a href="#workspaces-on-multiple-monitors">Workspaces on multiple monitors</a>
|
|
<ul>
|
|
<li><a href="#tracking-recently-used-workspaces">Tracking recently used workspaces</a></li>
|
|
<li><a href="#the-monitor-list">The monitor list</a></li>
|
|
<li><a href="#switch-to-another-monitor">Switch to another monitor</a></li>
|
|
<li><a href="#move-the-workspace-to-another-monitor">Move the workspace to another monitor</a></li>
|
|
<li><a href="#windmove-between-monitors">Windmove between monitors</a></li>
|
|
</ul>
|
|
</li>
|
|
<li><a href="#managing-windows">Managing windows</a>
|
|
<ul>
|
|
<li><a href="#moving-windows">Moving windows</a></li>
|
|
<li><a href="#resizing-windows">Resizing windows</a></li>
|
|
<li><a href="#splitting-windows">Splitting windows</a></li>
|
|
</ul>
|
|
</li>
|
|
<li><a href="#notes-on-floating-windows">Notes on floating windows</a></li>
|
|
<li><a href="#what-else-not-to-do">What else not to do</a></li>
|
|
<li><a href="#p-dot-s-dot">P.S.</a></li>
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
<a id="unhide-all-button" class="hidden"><Expand></a>
|
|
<a id="hide-all-button" class="hidden"><Collapse></a>
|
|
</div>
|
|
</div>
|
|
|
|
</div><div id="footer" class="mb-5">
|
|
<hr>
|
|
<div class="container text-center">
|
|
|
|
</div>
|
|
|
|
<div class="container text-center">
|
|
|
|
|
|
<a href="https://creativecommons.org/licenses/by/4.0/legalcode" title="Licensed under CC-BY 4.0"><small>Licensed under CC-BY 4.0</small></a>
|
|
|
|
|
|
|
|
|
|
|
<a href="https://plausible.io/" title="Uses Plausible Analytics"><small>Uses Plausible Analytics</small></a>
|
|
|
|
|
|
<br>
|
|
|
|
<a href="https://sqrtminusone.xyz/" title="Pavel Korytov, 2023"><small>Pavel Korytov, 2023</small></a>
|
|
</div>
|
|
|
|
</div>
|
|
</body>
|
|
</html>
|