<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/">
  <channel>
    <title>Blog entries tagged linux :: mwop.net</title>
    <description>Blog entries tagged linux :: mwop.net</description>
    <pubDate>Mon, 04 May 2026 08:35:08 -0500</pubDate>
    <generator>Laminas_Feed_Writer 2 (https://getlaminas.org)</generator>
    <link>https://mwop.net/blog/tag/linux</link>
    <atom:link rel="self" type="application/rss+xml" href="https://mwop.net/blog/tag/linux/rss.xml"/>
    <item>
      <title>Determining if a reboot is required on Linux</title>
      <pubDate>Mon, 04 May 2026 08:35:08 -0500</pubDate>
      <link>https://mwop.net/blog/2026-05-04-reboot-required.html</link>
      <guid>https://mwop.net/blog/2026-05-04-reboot-required.html</guid>
      <author>contact@mwop.net (Matthew Weier O'Phinney)</author>
      <dc:creator>Matthew Weier O'Phinney</dc:creator>
      <content:encoded><![CDATA[<p>I've been using the construct <code>sudo run-parts /etc/update-motd.d/</code> to determine if recent system updates required a reboot.</p>
<p>Today I learned I can simply check for the existence of the file <code>/var/run/reboot-required</code>.</p>




<div class="h-entry">
    <img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&u=79dd2ea1d4d8855944715d09ee4c86215027fa80&s=140" alt="matthew">
    <a class="u-url u-uid p-name" href="https://mwop.net/blog/2026-05-04-reboot-required.html">Determining if a reboot is required on Linux</a> was originally
    published <time class="dt-published" datetime="2026-05-04T08:35:08-05:00">4 May 2026</time>
    on <a href="https://mwop.net">https://mwop.net</a> by
    <a rel="author" class="p-author" href="https://mwop.net">Matthew Weier O&#039;Phinney</a>.
</div>
]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>Headless boot for Beelink EQ14</title>
      <pubDate>Mon, 23 Mar 2026 14:06:08 -0500</pubDate>
      <link>https://mwop.net/blog/2026-02-24-headless-boot-beelink-eq14.html</link>
      <guid>https://mwop.net/blog/2026-02-24-headless-boot-beelink-eq14.html</guid>
      <author>contact@mwop.net (Matthew Weier O'Phinney)</author>
      <dc:creator>Matthew Weier O'Phinney</dc:creator>
      <content:encoded><![CDATA[<p>A few months back, I purchased a Beelink EQ14 mini server for my homelab. One thing that has frustrated me has been that rebooting or even powering on... has required I have either my keyboard or my monitor plugged in (I haven't been able to determine exactly which one yet). This has made me reluctant to reboot, as I then have to switch my keyboard from my workstation over to the server, and plug the HDMI from one of my monitors into it. I hate it.</p>
<p>In browsing a number of forums, I saw one recommendation to switch the BIOS to use FastBoot.</p>
<p><del>Can confirm, works!</del></p>
<h4>Update</h4>
<p>While FastBoot helps, it doesn't entirely fix the issues. What I discovered was that the server was requiring my keyboard in order to boot; I tested this by rebooting the machine without cables, and plugging in only the keyboard.</p>
<p>Unfortunately, there are no BIOS settings to override this behavior. I dug up an old USB dongle for a presentation pointer, plugged that in, and voilá — it boots now. I suspect any USB HID dongle will likely work.</p>




<div class="h-entry">
    <img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&u=79dd2ea1d4d8855944715d09ee4c86215027fa80&s=140" alt="matthew">
    <a class="u-url u-uid p-name" href="https://mwop.net/blog/2026-02-24-headless-boot-beelink-eq14.html">Headless boot for Beelink EQ14</a> was originally
    published <time class="dt-published" datetime="2026-02-24T10:09:08-06:00">24 February 2026</time>
    on <a href="https://mwop.net">https://mwop.net</a> by
    <a rel="author" class="p-author" href="https://mwop.net">Matthew Weier O&#039;Phinney</a>.
</div>
]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>When docker buildx cannot resolve your container registry</title>
      <pubDate>Wed, 11 Feb 2026 09:49:31 -0600</pubDate>
      <link>https://mwop.net/blog/2026-02-11-docker-daemon-resolve-conf.html</link>
      <guid>https://mwop.net/blog/2026-02-11-docker-daemon-resolve-conf.html</guid>
      <author>contact@mwop.net (Matthew Weier O'Phinney)</author>
      <dc:creator>Matthew Weier O'Phinney</dc:creator>
      <content:encoded><![CDATA[<p>I had an odd situation today when building and pushing a container image to a registry.</p>
<p>I have a private registry in my homelab. I have an internal DNS server that can resolve it when in my home network, and my machines in my home network all use that internal DNS server. Great, fantastic, just works.</p>
<p>Except for some reason, when running a <code>docker buildx build --push</code> operation, it was unable to resolve the internal name... because it was using the wrong DNS server. It was trying to resolve via the 8.8.8.8 DNS server. The Google DNS servers. Which I've not even configured as an upstream in my recursive DNS server on the local network.</p>


<p>My guess is that the Docker daemon falls back to Google DNS servers if the system DNS cannot be reached at any point. Clearly, it then never tests to see if the system DNS has become reachable again, so it gets stuck there.</p>
<p>The solution? Restart the docker service:</p>
<pre><code class="language-bash hljs bash" data-lang="bash">sudo systemctl restart docker
</code></pre>
<p>Once I did that, it resolved using the system DNS next time.</p>


<div class="h-entry">
    <img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&u=79dd2ea1d4d8855944715d09ee4c86215027fa80&s=140" alt="matthew">
    <a class="u-url u-uid p-name" href="https://mwop.net/blog/2026-02-11-docker-daemon-resolve-conf.html">When docker buildx cannot resolve your container registry</a> was originally
    published <time class="dt-published" datetime="2026-02-11T09:49:31-06:00">11 February 2026</time>
    on <a href="https://mwop.net">https://mwop.net</a> by
    <a rel="author" class="p-author" href="https://mwop.net">Matthew Weier O&#039;Phinney</a>.
</div>
]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>Removing a filename containing a null byte or binary character on Linux</title>
      <pubDate>Fri, 06 Feb 2026 11:01:27 -0600</pubDate>
      <link>https://mwop.net/blog/2026-02-06-linux-null-byte-filename-removal.html</link>
      <guid>https://mwop.net/blog/2026-02-06-linux-null-byte-filename-removal.html</guid>
      <author>contact@mwop.net (Matthew Weier O'Phinney)</author>
      <dc:creator>Matthew Weier O'Phinney</dc:creator>
      <content:encoded><![CDATA[<p>Somehow, I got a file in my tree that started with a null byte. I only discovered it because <code>git status</code> was noting it wasn't added to my branch, and wanted to know if I wanted to add it.</p>
<p>No, no, I did not. I wanted to delete it.</p>
<p>But, of course, you cannot reference such a file by name, so I had to learn a few tricks.</p>


<p>First off, you can list names with binary bytes using:</p>
<pre><code class="language-bash hljs bash" data-lang="bash">/bin/ls -lb
</code></pre>
<p>But even better, you can get the inode if you use:</p>
<pre><code class="language-bash hljs bash" data-lang="bash">/bin/ls -li
</code></pre>
<p>When you do, the inode is in the first column of the list for each file.</p>
<p>Once you know that, you can delete the file, using find:</p>
<pre><code class="language-bash hljs bash" data-lang="bash">find . -maxdepth 1 -inum &lt;inode_id&gt; -<span class="hljs-built_in">exec</span> rm -i -- {} +
</code></pre>


<div class="h-entry">
    <img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&u=79dd2ea1d4d8855944715d09ee4c86215027fa80&s=140" alt="matthew">
    <a class="u-url u-uid p-name" href="https://mwop.net/blog/2026-02-06-linux-null-byte-filename-removal.html">Removing a filename containing a null byte or binary character on Linux</a> was originally
    published <time class="dt-published" datetime="2026-02-06T11:01:27-06:00">6 February 2026</time>
    on <a href="https://mwop.net">https://mwop.net</a> by
    <a rel="author" class="p-author" href="https://mwop.net">Matthew Weier O&#039;Phinney</a>.
</div>
]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>From zsh to fish</title>
      <pubDate>Mon, 17 Nov 2025 17:00:10 -0600</pubDate>
      <link>https://mwop.net/blog/2025-11-17-from-zsh-to-fish.html</link>
      <guid>https://mwop.net/blog/2025-11-17-from-zsh-to-fish.html</guid>
      <author>contact@mwop.net (Matthew Weier O'Phinney)</author>
      <dc:creator>Matthew Weier O'Phinney</dc:creator>
      <content:encoded><![CDATA[<p>I'm a longtime zsh user. A colleague introduced me to it in 2009, and I was an instant convert, if nothing else than for directory aliases and simpler <code>$PATH</code> management. Within a couple of years, I discovered <a href="https://ohmyz.sh">oh-my-zsh</a>, which put my shell on steroids, giving me a ton of completion capabilities, better prompts, and more.</p>
<p>But a few years ago, I started noticing that my shell load times were getting worse and worse. At that time, I discovered I could easily switch to vanilla zsh with <a href="https://zplug.github.io">zplug</a> managing a small number of plugins I used (nvm, fzf, and a few others). I also discovered <a href="https://startship.rs">starship</a>, which gave me more prompt options, with faster startup times.</p>
<p>And yet...</p>


<p>I kept reading <a href="https://jvns.ca">Julia Evans</a> recommending <a href="https://fishshell.com">fish</a>. She often would note that fish just does things that other shells need plugins or customization for: decent tab completion, better history capabilities, etc.</p>
<p>So I took the plunge finally in the past couple weeks to give fish a try.</p>
<h2>Configuration</h2>
<p>First off, the switch more than halved the amount of configuration I needed to have an equivalent setup. I was able to remove a ton of configuration I had in zsh around history management, autocompletion, and overrides.</p>
<p>As I noted, with zsh, I was using zplug, and I had a half-dozen plugins. With fish, initially I had no plugins, but in order to get <a href="https://github.com/nvm-sh/nvm">nvm</a> running, I needed to install <a href="https://github.com/jorgebucaran/fisher">fisher</a>, the de facto fish plugin manager. But that's literally the only plugin I'm now using.</p>
<p>I continued to use starship and fzf, which meant two lines of configuration in my fish configuration, and no changes otherwise.</p>
<p>And there's no lag whatsoever when starting up a shell. With zsh, even with my minimal config, I would sometimes wait a second or two for a shell to spawn. With fish, no wait.</p>
<h2>Discoveries</h2>
<p>One thing I've kept from oh-my-zsh is a utility called <code>take</code>, which does the following:</p>
<ul>
<li>If given a directory name, it creates it, and then enters it</li>
<li>If given an archive file, it unarchives it into a directory, and then enters that directory</li>
<li>If given a git repository name, it clones it, and then enters that directory</li>
</ul>
<p>When porting this to fish, I discovered some really cool features of that shell.</p>
<p>First, fish will automatically autoload functions from the <code>functions/</code> subdirectory of your fish configuration. So if you name the function the same as the file (e.g., <code>functions/take.fish</code>), it will load it <em>on demand</em>. This is a nice performance improvement over loading <em>everything</em>.</p>
<p>Second, fish uses a standard syntax for any block statement. Instead of sometimes needing braces, sometimes needing a keyword (which generally varies BASED on the block type - e.g. <code>fi</code> to end a conditional, <code>done</code> to end a loop), all blocks use an <code>end</code> keyword. This makes it far simpler to remember and less prone to errors.</p>
<p>Third, when defining a function, you can specify variable names to which to capture arguments. This is far easier to visually parse and use than standard posix shells, where you use positional parameters. As an example:</p>
<pre><code class="language-shell hljs shell" data-lang="shell">function takedir -a newpath
    mkdir -p $newpath &amp;&amp; cd $newpath
end
</code></pre>
<p>Fourth, while you <em>can</em> use the notation <code>varname=value</code> to define variables, there's a better built-in, the <code>set</code> directive, which can:</p>
<ul>
<li>Define block-local (<code>set -l</code>) and function-local (<code>set -f</code>) variables</li>
<li>Define globally-available variables (<code>set -g</code>)</li>
<li>Define environment variables (<code>set -x</code>, for e<strong>x</strong>port) that persist to child shells</li>
</ul>
<p>Using this, it's far easier both to ensure that a variable is scoped correctly, as well as to reason about the scope of a given variable. And those captured arguments I mentioned? Automatically scoped to the function, so they won't bleed outside of it.</p>
<p>(There's also a &quot;univeral&quot; flag, <code>-u</code>, which will not only set it in the current shell, at the globally available level, but make it available across any other instances, and persist it for future invocations. This seems dangerous, though!)</p>
<p>The combination of these meant that the <code>take</code> declaration took fewer lines of code, was easier to understand, and less likely to bleed state. I'll take it!</p>
<h2>But will I stick with it?</h2>
<p>I think so. I even put it on some servers I maintain, and it's instantly given me more and better functionality than the default shell available on each, which makes being on those servers more comfortable. Having less configuration is something I've been keeping an eye on, as more configuration means it's harder to reason about how things work, and more likely to break or fail in interesting ways when updating or upgrading.</p>
<p>I'll still need to keep my bash chops; provisioning scripts for containers and VMs generally have to depend on this lowest common denominator. However, having a useful out-of-the-box shell for my workstation and servers that's easy to script? I'll take it.</p>


<div class="h-entry">
    <img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&u=79dd2ea1d4d8855944715d09ee4c86215027fa80&s=140" alt="matthew">
    <a class="u-url u-uid p-name" href="https://mwop.net/blog/2025-11-17-from-zsh-to-fish.html">From zsh to fish</a> was originally
    published <time class="dt-published" datetime="2025-11-17T17:00:10-06:00">17 November 2025</time>
    on <a href="https://mwop.net">https://mwop.net</a> by
    <a rel="author" class="p-author" href="https://mwop.net">Matthew Weier O&#039;Phinney</a>.
</div>
]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>Linux desktop files and xdg-open</title>
      <pubDate>Mon, 03 Nov 2025 13:39:35 -0600</pubDate>
      <link>https://mwop.net/blog/2025-11-03-desktop-xdg-open.html</link>
      <guid>https://mwop.net/blog/2025-11-03-desktop-xdg-open.html</guid>
      <author>contact@mwop.net (Matthew Weier O'Phinney)</author>
      <dc:creator>Matthew Weier O'Phinney</dc:creator>
      <content:encoded><![CDATA[<p>I've been using Linux on the desktop for more than 25 years now. While I don't put icons on my desktop any longer (and haven't for probably around 15 years), I <em>do</em> use the gnome-shell launcher to quickly open programs, and this utilizes desktop files.</p>
<p>Recently, I wanted to create launchers for different Obsidian vaults. Obsidian provides a URL schema for this: <code>obsidian://open?vault=VaultName</code>. The application registers the schema handler with the system, so this should open, but evidently you can no longer use &quot;Type=Link&quot; in your desktop files.</p>
<p>What I found:</p>
<ul>
<li>You MUST have a &quot;Version=1.0&quot; line; gnome-shell just ignored any of my desktop files that omitted it.</li>
<li>You can use <code>xdg-open</code> in your <code>Exec</code> line to open the URL.</li>
</ul>
<pre><code class="language-ini hljs ini" data-lang="ini"><span class="hljs-section">[Desktop Entry]</span>
<span class="hljs-attr">Version</span>=<span class="hljs-number">1.0</span>
<span class="hljs-attr">Name</span>=Notes
<span class="hljs-attr">Icon</span>=/usr/share/icons/hicolor/<span class="hljs-number">256</span>x256/apps/obsidian.png
<span class="hljs-attr">Comment</span>=My Obsidian vault for notes
<span class="hljs-attr">Categories</span>=<span class="hljs-literal">Off</span>ice<span class="hljs-comment">;ProjectManagement;</span>
<span class="hljs-attr">Type</span>=Application
<span class="hljs-attr">Exec</span>=xdg-open <span class="hljs-string">"obsidian://open?vault=notes"</span>
</code></pre>




<div class="h-entry">
    <img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&u=79dd2ea1d4d8855944715d09ee4c86215027fa80&s=140" alt="matthew">
    <a class="u-url u-uid p-name" href="https://mwop.net/blog/2025-11-03-desktop-xdg-open.html">Linux desktop files and xdg-open</a> was originally
    published <time class="dt-published" datetime="2025-11-03T13:39:35-06:00">3 November 2025</time>
    on <a href="https://mwop.net">https://mwop.net</a> by
    <a rel="author" class="p-author" href="https://mwop.net">Matthew Weier O&#039;Phinney</a>.
</div>
]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>Disabling the Zoom mini window on Linux</title>
      <pubDate>Mon, 01 Dec 2025 13:38:27 -0600</pubDate>
      <link>https://mwop.net/blog/2025-09-23-zoom-disabling-mini-window.html</link>
      <guid>https://mwop.net/blog/2025-09-23-zoom-disabling-mini-window.html</guid>
      <author>contact@mwop.net (Matthew Weier O'Phinney)</author>
      <dc:creator>Matthew Weier O'Phinney</dc:creator>
      <content:encoded><![CDATA[<p>Zoom used to have a strange behavior, one I'm sure they thought would be useful, but in reality was infuriating: if you moved between virtual workspaces, Zoom would minimize to a thumbnail window that followed you around to workspaces.</p>
<p>At some point, it went away, thankfully... but after a recent release, it turned back on, and it's been a huge pain for me. There's a bug in that the mini window follows me to the initial virtual workspace, but then doesn't follow around from there, requiring me to use the workspace tools to move it to the workspace I want, and then re-maximize it, only to have to do the whole thing again if I switch screens.</p>


<p>The solution turns out to be relatively simple, if incredibly unintuitive.</p>
<p>Zoom does NOT have a GUI setting for this, for some reason. But evidently it creates the file <code>$HOME/.config/zoomus.conf</code>, which is where all configuration is stored.</p>
<p>This file is in <a href="https://en.wikipedia.org/wiki/INI_file">INI format</a>. Find the entry <code>enableMiniWindow</code>, and set it to false:</p>
<pre><code class="language-ini hljs ini" data-lang="ini"><span class="hljs-attr">enableMiniWindow</span>=<span class="hljs-literal">false</span>
</code></pre>
<p>Restart Zoom, and you're set.</p>


<div class="h-entry">
    <img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&u=79dd2ea1d4d8855944715d09ee4c86215027fa80&s=140" alt="matthew">
    <a class="u-url u-uid p-name" href="https://mwop.net/blog/2025-09-23-zoom-disabling-mini-window.html">Disabling the Zoom mini window on Linux</a> was originally
    published <time class="dt-published" datetime="2025-09-23T09:20:27-05:00">23 September 2025</time>
    on <a href="https://mwop.net">https://mwop.net</a> by
    <a rel="author" class="p-author" href="https://mwop.net">Matthew Weier O&#039;Phinney</a>.
</div>
]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>Wezterm GUI Notifications</title>
      <pubDate>Mon, 21 Oct 2024 17:01:18 -0500</pubDate>
      <link>https://mwop.net/blog/2024-10-21-wezterm-notify-send.html</link>
      <guid>https://mwop.net/blog/2024-10-21-wezterm-notify-send.html</guid>
      <author>contact@mwop.net (Matthew Weier O'Phinney)</author>
      <dc:creator>Matthew Weier O'Phinney</dc:creator>
      <content:encoded><![CDATA[<p><a href="https://wezfurlong.org/wezterm/index.html">Wezterm</a> has a utility for raising GUI system notifications, <a href="https://wezfurlong.org/wezterm/config/lua/window/toast_notification.html">window:toast_notification()</a>, which is a handy way to bring notifications to you that you might otherwise miss if the window is hidden or if a given tab is inactive.</p>
<p>However, on Linux, it's a far from ideal tool, at least under gnome-shell.
(I don't know how it does on KDE or other desktop environments.)
It raises the notification, but the notification never times out, even if you provide a timeout value (fourth argument to the function).
This means that you have to manually dismiss the notification, which can be annoying, particularly if the notifications happen regularly.</p>
<p>So, I worked up my own utility.</p>


<h2>notify.send</h2>
<p>Since Wezterm uses Lua for configuration, configuration actually also acts as an extension mechanism.
The primary wezterm Lua module itself provides a <code>run_child_process</code> function for spawning a system process, which allows me to call on the system <code>notify-send</code> utility.</p>
<p>As such, I wrote up the following Lua module:</p>
<pre><code class="language-lua hljs lua" data-lang="lua"># File: notify.lua
<span class="hljs-keyword">local</span> wezterm = <span class="hljs-built_in">require</span> <span class="hljs-string">'wezterm'</span>
<span class="hljs-keyword">local</span> module  = {}

<span class="hljs-keyword">local</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">has_value</span> <span class="hljs-params">(tab, val)</span></span>
    <span class="hljs-keyword">for</span> index, value <span class="hljs-keyword">in</span> <span class="hljs-built_in">ipairs</span>(tab) <span class="hljs-keyword">do</span> <span class="hljs-comment">-- luacheck: ignore 213</span>
        <span class="hljs-keyword">if</span> value == val <span class="hljs-keyword">then</span>
            <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>
        <span class="hljs-keyword">end</span>
    <span class="hljs-keyword">end</span>

    <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
<span class="hljs-keyword">end</span>

<span class="hljs-keyword">local</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">notify</span> <span class="hljs-params">(subject, msg, urgency)</span></span>
    <span class="hljs-keyword">local</span> allowed_urgency = { <span class="hljs-string">'low'</span>, <span class="hljs-string">'normal'</span>, <span class="hljs-string">'critical'</span> }
    urgency = urgency <span class="hljs-keyword">or</span> <span class="hljs-string">'normal'</span>
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> has_value(allowed_urgency, urgency) <span class="hljs-keyword">then</span>
        urgency = <span class="hljs-string">'normal'</span>
    <span class="hljs-keyword">end</span>

    wezterm.run_child_process {
        <span class="hljs-string">'notify-send'</span>,
        <span class="hljs-string">'-i'</span>,
        <span class="hljs-string">'org.wezfurlong.wezterm'</span>,
        <span class="hljs-string">'-a'</span>,
        <span class="hljs-string">'wezterm'</span>,
        <span class="hljs-string">'-u'</span>,
        urgency,
        subject,
        msg
    }
<span class="hljs-keyword">end</span>

module.send = notify

<span class="hljs-keyword">return</span> module
</code></pre>
<p>Within other configuration, when I want to send GUI notifications, I can do the following:</p>
<pre><code class="language-lua hljs lua" data-lang="lua"><span class="hljs-keyword">local</span> notify = <span class="hljs-built_in">require</span> <span class="hljs-string">'./notify'</span>

notify.send(<span class="hljs-string">'Subject Line'</span>, <span class="hljs-string">'This is the full message'</span>, <span class="hljs-string">'low'</span>)
</code></pre>
<p>Some notes on usage:</p>
<ul>
<li>The <code>urgency</code> argument is one of 'low', 'normal', or 'critical', and defaults to 'normal'.
These correspond to the same <code>--urgency</code> option of <code>notify-send</code>, with the following behavior:
<ul>
<li>'low' urgency messages are collected in the notification panel, but not displayed.</li>
<li>'normal' urgency messages display until the system timeout for notifications is met.
(For me, that's 3 seconds.)
After that, it disappears into the notification panel.</li>
<li>'critical' urgency messages require manual dismissal.</li>
</ul>
</li>
<li>The <code>subject</code> argument is used for the notification subject; it's what you see without expanding the notification.</li>
<li>The <code>msg</code> argument is the full detail you want in the notification, and is shown when you expand the notification.</li>
</ul>
<p><code>notify-send</code> has a variety of other flags which can control things like timeout, whether or not the message is transient (i.e., will never be displayed in the notification panel), application category, etc.
I've found for myself that just these three items are sufficient for probably 99% of any uses cases I'll need.</p>


<div class="h-entry">
    <img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&u=79dd2ea1d4d8855944715d09ee4c86215027fa80&s=140" alt="matthew">
    <a class="u-url u-uid p-name" href="https://mwop.net/blog/2024-10-21-wezterm-notify-send.html">Wezterm GUI Notifications</a> was originally
    published <time class="dt-published" datetime="2024-10-21T17:01:18-05:00">21 October 2024</time>
    on <a href="https://mwop.net">https://mwop.net</a> by
    <a rel="author" class="p-author" href="https://mwop.net">Matthew Weier O&#039;Phinney</a>.
</div>
]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>Wezterm Dropdown in Gnome</title>
      <pubDate>Tue, 17 Sep 2024 15:37:01 -0500</pubDate>
      <link>https://mwop.net/blog/2024-09-17-wezterm-dropdown.html</link>
      <guid>https://mwop.net/blog/2024-09-17-wezterm-dropdown.html</guid>
      <author>contact@mwop.net (Matthew Weier O'Phinney)</author>
      <dc:creator>Matthew Weier O'Phinney</dc:creator>
      <content:encoded><![CDATA[<p>In <a href="/blog/2024-07-04-how-i-use-wezterm.html">a previous article</a>, I detailed how I use Wezterm.
One goal I had when switching to Wezterm to was to ensure I was able to continue using a dropdown terminal, and in that article, I detailed using the <code>tdrop</code> utility to implement this... but with the caveat that it didn't work well under the Wayland environment.</p>
<p>Well, I've now found a better solution.</p>


<p>Recently, when looking for a completely unrelated gnome-shell extension, I stumbled across the <a href="https://github.com/diegodario88/quake-terminal">Quake terminal extension</a>.
I decided to give it a try to see if it was using a built-in terminal library, or calling out to existing terminals, and was surprised to discover it was the latter... and that it found my Wezterm install!</p>
<p>Not only that, but it worked exactly as I would expect - it dropped in the terminal from the top, at a partial height, and hitting the hot key scrolled it back out.</p>
<h3>Customizing the terminal</h3>
<p>That said, I wanted to do a couple things different, and needed to experiment:</p>
<ul>
<li>I wanted the terminal to be partially transparent, so that I can see what's under it on the screen.</li>
<li>I don't want window decorations for my dropdown terminal.</li>
<li>I want to use Wezterm's multiplexer, and open in a dedicated workspace.</li>
</ul>
<p>I'd achieved this in <code>tdrop</code> by writing a separate command that <code>tdrop</code> would invoke, and I wondered if I could do that here.</p>
<p>At first, I had no luck.
The terminal would open, but as a regular window, and not as a dropdown.</p>
<p>And then I did a bit more reading, and realized what I needed to do.</p>
<p>I discovered that Quake Terminal looks for <code>.desktop</code> files where the <code>Categories</code> field includes <code>TerminalEmulator</code>.
From there, the next important bit is that the <code>WM_CLASS</code> of the running application has to match the application ID.</p>
<p>In practice, this means the following:</p>
<ul>
<li>The <code>.desktop</code> file needs the name to be a valid application ID.
The recommendation in the XDG spec is that this follows the <code>TLD.HOST.APP</code> format, and needs to be unique.
I chose <code>net.mwop.wezterm-dropdown</code> for mine, and thus the file is named <code>net.mwop.wezterm-dropdown.desktop</code>.</li>
<li>When invoking Wezterm, I need to specify the <code>WM_CLASS</code>.
I can do this with the <code>--class</code> option to the <code>start</code> subcommand: <code>wezterm start --class=net.mwop.wezterm-dropdown</code>.</li>
</ul>
<p>In the end, the full command I would run became:</p>
<pre><code class="language-bash hljs bash" data-lang="bash">wezterm \
  --config <span class="hljs-string">"window_background_opacity=0.85"</span> \
  --config <span class="hljs-string">"window_decorations='NONE'"</span> \
  start \
    --cwd . \
    --class=net.mwop.dropdown-terminal \
    --domain unix \
    --attach \
    --workspace dropdown
</code></pre>
<p>I specified this originally in the <code>Exec=</code> line of my <code>net.mwop.wezterm-dropdown.desktop</code> file, but I found that it would open a bare window on first execution, and only on the second and subsequent executions would it become a dropdown.</p>
<p>So I moved that invocation to the file <code>$HOME/.local/bin/wezterm-dropdown</code>, and added an <code>exec</code> at the front of it, and modified the desktop file to read <code>Exec=/home/matthew/.local/bin/wezterm-dropdown</code>.
This works slightly better - the first invocation opens it as a bare window, but when you hit the hot key again, it hides it, and after that, it opens as a dropdown.
I can live with this.</p>
<h3>The final files</h3>
<p>The <code>wezterm-dropdown</code> script:</p>
<pre><code class="language-bash hljs bash" data-lang="bash"><span class="hljs-meta">#!/usr/bin/zsh</span>
<span class="hljs-comment"># File: $HOME/.local/bin/wezterm-dropdown</span>
<span class="hljs-built_in">exec</span> wezterm \
  --config <span class="hljs-string">"window_background_opacity=0.85"</span> \
  --config <span class="hljs-string">"window_decorations='NONE'"</span> \
  start \
    --cwd . \
    --class=net.mwop.dropdown-terminal \
    --domain unix \
    --attach \
    --workspace dropdown
</code></pre>
<p>The <code>net.mwop.wezterm-dropdown.desktop</code> file (this goes in <code>$HOME/.local/share/applications/</code>):</p>
<pre><code class="language-ini hljs ini" data-lang="ini"><span class="hljs-section">[Desktop Entry]</span>
<span class="hljs-attr">Name</span>=Dropdown Terminal
<span class="hljs-attr">Comment</span>=Wezterm as a dropdown terminal
<span class="hljs-attr">Keywords</span>=shell<span class="hljs-comment">;prompt;command;commandline;cmd;</span>
<span class="hljs-attr">Icon</span>=org.wezfurlong.wezterm
<span class="hljs-attr">StartupWMClass</span>=org.wezfurlong.wezterm
<span class="hljs-attr">Exec</span>=/home/matthew/.local/bin/wezterm-dropdown
<span class="hljs-attr">Type</span>=Application
<span class="hljs-attr">Categories</span>=System<span class="hljs-comment">;TerminalEmulator;Utility;</span>
<span class="hljs-attr">Terminal</span>=<span class="hljs-literal">false</span>
</code></pre>


<div class="h-entry">
    <img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&u=79dd2ea1d4d8855944715d09ee4c86215027fa80&s=140" alt="matthew">
    <a class="u-url u-uid p-name" href="https://mwop.net/blog/2024-09-17-wezterm-dropdown.html">Wezterm Dropdown in Gnome</a> was originally
    published <time class="dt-published" datetime="2024-09-17T15:37:01-05:00">17 September 2024</time>
    on <a href="https://mwop.net">https://mwop.net</a> by
    <a rel="author" class="p-author" href="https://mwop.net">Matthew Weier O&#039;Phinney</a>.
</div>
]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
    <item>
      <title>How I use Wezterm</title>
      <pubDate>Tue, 17 Sep 2024 15:41:46 -0500</pubDate>
      <link>https://mwop.net/blog/2024-07-04-how-i-use-wezterm.html</link>
      <guid>https://mwop.net/blog/2024-07-04-how-i-use-wezterm.html</guid>
      <author>contact@mwop.net (Matthew Weier O'Phinney)</author>
      <dc:creator>Matthew Weier O'Phinney</dc:creator>
      <content:encoded><![CDATA[<p>I use the terminal a lot.
Until the past few years, I basically used <em>only</em> a browser and a terminal.
(The primary changes in the past couple years are that I'm using Logseq for tracking notes and todos, and now use native apps for Zoom and Slack.)</p>
<p>Today I'm going to detail my exploration of <a href="https://wezfurlong.org/wezterm/index.html">Wezterm</a>, my current daily driver.</p>


<h2>My terminal history</h2>
<p>I used gnome-terminal for a long time.
At a certain point, I learned about &quot;Quake&quot;-style, dropdown terminals, and realized that these would be ideal for running one-off tasks or doing quick lookups.
I used <a href="https://guake.github.io">Guake</a> for a number of years, as it was very similar to gnome-terminal.</p>
<p>At a certain point, however, I found gnome-terminal to be a bit slow, and started looking for alternatives.
I eventually landed on one I'd used in my early days using LInux: <a href="https://en.wikipedia.org/wiki/Xfce#Xfce_Terminal">xfce4-terminal</a>.
This one was useful as it has a native dropdown mode, which meant I could use the same terminal for dropdowns as well as my main driver.
It used very few resources, and &quot;just worked.&quot;</p>
<p>Until it didn't.
I adopted Wayland last year, as it was clear that Ubuntu 2024.04 would likely default to it, if not outright require it.
And when I did, xfce4-terminal became an issue.
I cannot recall if it became slow (as it had to run via the Xorg bindings), or if it outright didn't work, but I had to switch, regardless.</p>
<p>I tried a number of terminals, settling on <a href="https://sw.kovidgoyal.net/kitty/">Kitty</a>. While it didn't have a dropdown mode, I was able to accomplish it via <a href="https://github.com/noctuid/tdrop">tdrop</a>.</p>
<p>Now, the interesting thing is that I discovered tdrop when I was investigating new terminal alternatives, and tried out <a href="https://wezfurlong.org/wezterm/index.html">wezterm</a>.
At the time, I didn't go with wezterm, as I found the configuration confusing (I hadn't dove into Lua yet).
I'll get back to wezterm later.</p>
<h2>Tmux saves my bacon</h2>
<p>Now, one tool I use <em>all the time</em> in a terminal is <a href="https://github.com/tmux/tmux/wiki">tmux</a>.
Tmux basically gives you windows and panes inside your terminal.
On top of that, you can have different sessions, and attach to and detach from sessions independently; this allows you to attach to the same session from multiple windows, or detach from a session you're not actively working in so you can come back to it later.
When you do return, you have all the windows and panes that were open previously.</p>
<p>Tmux is also pluggable, and I use a couple of plugins regularly:</p>
<ul>
<li>
<p><a href="https://github.com/christoomey/vim-tmux-navigator">vim-tmux-navigator</a> is technically a vim/nvim plugin, but it works in tandem with configuration you set it tmux itself.
What it does is make it so that you can use the same key combinations to navigate between vim/nvim and tmux panes.
So, let's say you have vim in a tmux pane on the left, and it has two vertical panes.
If you're in the right-most vim pane, and hit the keystroke to move right, with this plugin, that moves you into the tmux pane to the right.</p>
<p>This is tremendously useful, as you don't need to think about what context you're in as you move around between vim/nvim and tmux panes.</p>
</li>
<li>
<p><a href="https://github.com/tmux-plugins/tmux-resurrect">tmux-resurrect</a> allows you to persist sessions between tmux server invocations.
In other words, if you restart your computer, the first time you start tmux, all your sessions re-appear, and each has any previous history already present.
This plugin is has helped me feel confident that I won't lose track of what I was doing if I need to reboot.</p>
</li>
</ul>
<p>What I have done for many years is setup my dropdown terminal to create or attach to a named tmux session.
This means that I always have the ability to create new windows and panes as needed from my dropdown terminal, which is hugely useful.</p>
<p>Additionally, most times I open a terminal, I'm starting a tmux session named to reflect what I'm doing.
This ensures if I need to reboot, I don't lose my place.
In fact, I find myself getting upset quite often when I start working on something and only later realize I didn't start tmux, as I have to quit what I'm doing and start back up after starting tmux.
And when I SSH to another server, I hate not having tmux available; I'll often use tmux locally, which, while it means I need multiple SSH sessions, it at least gives me what I need for functionality.</p>
<p>I depend on terminal multiplexing.
It's a basic feature I absolutely need.</p>
<h2>Learning wezterm</h2>
<p>When I first came across wezterm, one thing I noted is that it had a built-in multiplexing.
However, it was sufficiently different from tmux that I only noted it in passing.</p>
<p>Sometime this past winter, I became reacquainted with Wez Furlong, author of wezterm, via Mastodon, where he's fairly active.
Wez used to contribute heavily to PHP, and we would cross paths at PHP conferences in the late oughts, and it was fun to see what he's been up to since — which includes creating wezterm.
As such, I figured I'd try it out again.</p>
<p>It soon became my daily driver.
While it took a bit to get it working well with tdrop, once I did, I found it was fast, unobtrusive, and &quot;just worked&quot;.</p>
<p>But at the back of my head, I kept thinking, &quot;I wonder if this could replace tmux for me?&quot;</p>
<p>When I looked at what I actually <em>use</em> in tmux, I found that it was just these things:</p>
<p>_ Powerline.
And, honestly, I discovered that I never actually LOOK at my powerline, other than to see what session and window I'm in.
I decided that I'd be happy with minimally seeing active windows, better yet the session, and only vaguely interested in seeing anything else (CPU usage, memory usage, time, etc.).</p>
<ul>
<li>Scrollback per pane</li>
<li>Select and copy</li>
<li>&quot;Zoom&quot;ing a pane (taking a single pane as &quot;fullscreen&quot; within a window, and then restoring to the previous layout)</li>
<li>vim-tmux-navigator (see the description in the previous section)</li>
<li>tmux-resurrect (natch)</li>
</ul>
<p>This is not a huge featureset, so I set out to see if I could do these things.</p>
<blockquote>
<p>One note: I never did figure out how to show the current session name in the tab bar.
Otherwise, I accomplished all other goals!</p>
</blockquote>
<h3>Lua</h3>
<p>Wezterm, while written in Rust, manages configuration using <a href="https://www.lua.org">Lua</a>.
As it turns out...
Lua can be picked up very quickly if you've used basically any programming language at all.
It looks a lot like JSON, but with a limited number of statements and expressions also available.</p>
<p>Wezterm ships with a small number of modules that allow you to configure the terminal, as well as perform a certain number of actions, from interacting with sessions, windows, and panes, to performing prompts.</p>
<p>Once I picked up Lua, configuring Wezterm became relatively straightforward.
I'm now considering updating my nvim configuration to be entirely in Lua as well!</p>
<h3>Take me to your Leader</h3>
<p>Tmux and vim/nvim each have a concept of a <em>leader</em> character or key sequence.
These allow you a bit more flexibility when creating keyboard shortcuts, as you don't need to worry about conflicts with other programs.
The program &quot;screen&quot; (the OG terminal multiplexer) uses &quot;Ctrl-A&quot; as the leader, while tmux uses &quot;Ctrl-B&quot; by default (but most tmux users remap it to Ctrl-A).
In vim, I map <code>,</code> as my leader character.</p>
<p>Wezterm allows defining a leader as well.
I decided I'd use &quot;Ctrl-A&quot;, as the plan is to replace tmux:</p>
<pre><code class="language-lua hljs lua" data-lang="lua">wezterm.<span class="hljs-built_in">config</span>.leader = {
  key = <span class="hljs-string">'a'</span>,
  mods = <span class="hljs-string">'CTRL'</span>,
  timeout_milliseconds = <span class="hljs-number">2000</span>,
}
</code></pre>
<h3>Scrollback, select, and copy</h3>
<p>Wezterm allows scrollback in its copy mode.
I setup a keybinding mimicing the backscroll/copy mode in tmux:</p>
<pre><code class="language-lua hljs lua" data-lang="lua">wezterm.<span class="hljs-built_in">config</span>.keys = {
  {
    key = <span class="hljs-string">'['</span>,
    mods = <span class="hljs-string">'LEADER'</span>,
    action = wezterm.action.ActivateCopyMode,
  },
}
</code></pre>
<p>Interestingly, I found the copy mode in wezterm to be more predictable than in tmux!</p>
<h3>Zooming</h3>
<p>Sometimes I'm in a pane within a split window, and realize I need a bit more visual room.
I had mapped <code>Alt-F</code> to this in tmux, so I did the same with wezterm.</p>
<pre><code class="language-lua hljs lua" data-lang="lua">wezterm.<span class="hljs-built_in">config</span>.keys = {
  {
    key = <span class="hljs-string">'f'</span>,
    mods = <span class="hljs-string">'ALT'</span>,
    action = wezterm.action.TogglePaneZoomState,
  },
}
</code></pre>
<h3>Windows versus Tabs</h3>
<p>Tmux calls them windows, but wezterm calls them tabs.
The idea is the same: a discrete, indexed and/or named terminal that can handle one or more panes.</p>
<p>Tmux puts the list of windows at the bottom by default (I <em>think</em> this can be configured, but I've literally only ever had them at the bottom of the screen).
Wezterm puts them at the top.
I wanted to switch the behavior, but could not find the setting... so I popped into the <a href="https://app.element.io/#/room/#wezterm:matrix.org">wezterm matrix room</a> to ask.
Wez responded within minutes, and pointed me to the <a href="https://wezfurlong.org/wezterm/config/lua/config/tab_bar_at_bottom.html"><code>tab_bar_at_bottom</code> setting</a>.
(The matrix room is incredibly friendly and positive!)</p>
<pre><code class="language-lua hljs lua" data-lang="lua">wezterm.<span class="hljs-built_in">config</span>.tab_bar_at_bottom = <span class="hljs-literal">true</span>
</code></pre>
<p>How do I create a tab?
Wezterm, like most terminals, uses <code>Ctrl-Shift-Tab</code>, but with tmux, I would use <code>&lt;Leader&gt;c</code>, so I added that binding:</p>
<pre><code class="language-lua hljs lua" data-lang="lua">wezterm.<span class="hljs-built_in">config</span>.keys = {
  {
    key = <span class="hljs-string">'c'</span>,
    mods = <span class="hljs-string">'LEADER'</span>,
    action = act.SpawnTab <span class="hljs-string">'CurrentPaneDomain'</span>,
  },
}
</code></pre>
<p>Next, I wanted a way to move between tabs without a mouse.
I mapped these the same as I did in tmux: <code>&lt;Leader&gt;n</code> for the next tab, <code>&lt;Leader&gt;p</code> for the previous:</p>
<pre><code class="language-lua hljs lua" data-lang="lua">wezterm.<span class="hljs-built_in">config</span>.keys = {
  {
    key = <span class="hljs-string">'n'</span>,
    mods = <span class="hljs-string">'LEADER'</span>,
    action = wezterm.action.ActivateTabRelative(<span class="hljs-number">1</span>),
  },
  {
    key = <span class="hljs-string">'p'</span>,
    mods = <span class="hljs-string">'LEADER'</span>,
    action = wezterm.action.ActivateTabRelative(<span class="hljs-number">-1</span>),
  },
}
</code></pre>
<p>I like to name my windows/tabs.
This helps me understand what work I'm doing in each: tests, running docker images, etc.
In tmux, I use <code>&lt;Leader&gt;,</code> to do this, and this was where my first real Lua came in, as I had to define a callback with a conditional to make it happen:</p>
<pre><code class="language-lua hljs lua" data-lang="lua">wezterm.<span class="hljs-built_in">config</span>.keys = {
  {
    key = <span class="hljs-string">','</span>,
    mods = <span class="hljs-string">'LEADER'</span>,
    action = act.PromptInputLine {
      description = <span class="hljs-string">'Enter new name for tab'</span>,
      action = wezterm.action_callback(
        <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(window, pane, line)</span></span>
          <span class="hljs-keyword">if</span> line <span class="hljs-keyword">then</span>
            window:active_tab():set_title(line)
          <span class="hljs-keyword">end</span>
        <span class="hljs-keyword">end</span>
      ),
    },
  },
}
</code></pre>
<p>How about switching to a specific tab?
With tmux, you would do <code>&lt;Leader&gt;&lt;index&gt;</code>, where <code>&lt;index&gt;</code> is the index of the window you want (e.g. &quot;1&quot; or &quot;3&quot;).
For wezterm, I decided to use the tab navigator, which gives a dialog displaying each tab with their index; you then specify the one you want, either by navigating to it, or entering the index.
Even better, you can use <code>/</code> to filter through them, which is handy if you have more than ten windows, as otherwise indexes only get you from 0-9!</p>
<pre><code class="language-lua hljs lua" data-lang="lua">wezterm.<span class="hljs-built_in">config</span>.keys = {
  {
    key = <span class="hljs-string">'w'</span>,
    mods = <span class="hljs-string">'LEADER'</span>,
    action = act.ShowTabNavigator,
  },
}
</code></pre>
<p>I also want to be able to kill/close a tab without needing to type &quot;exit&quot;; this is particularly important if a process is stuck.
Again, I went with what I knew from tmux, and used <code>&lt;Leader&gt;&amp;</code>... and learned a valuable lesson in wezterm configuration in the process!</p>
<pre><code class="language-lua hljs lua" data-lang="lua">wezterm.<span class="hljs-built_in">config</span>.keys = {
  <span class="hljs-comment">-- Close tab</span>
  {
    key = <span class="hljs-string">'&amp;'</span>,
    mods = <span class="hljs-string">'LEADER|SHIFT'</span>,
    action = act.CloseCurrentTab{ confirm = <span class="hljs-literal">true</span> },
  },
}
</code></pre>
<blockquote>
<p>As it turns out, if a character requires hitting the shift key normally, such as any of the symbols above the numeric keys, or even keys like <code>?</code>, <code>&lt;</code>, <code>&gt;</code>, the <code>|</code> and <code>{</code>/<code>}</code> symbols, the <code>SHIFT</code> mod MUST be included!</p>
</blockquote>
<p>Finally, there were a smattering of other configuration settings I changed to make the behavior match my expectations, as well as to make it visually easier to parse:</p>
<pre><code class="language-lua hljs lua" data-lang="lua"><span class="hljs-comment">-- Make it look like tabs, with better GUI controls</span>
wezterm.<span class="hljs-built_in">config</span>.use_fancy_tab_bar = <span class="hljs-literal">true</span>
<span class="hljs-comment">-- Don't let any individual tab name take too much room</span>
wezterm.<span class="hljs-built_in">config</span>.tab_max_width = <span class="hljs-number">32</span>
wezterm.<span class="hljs-built_in">config</span>.colors = {
  tab_bar = {
    active_tab = {
      <span class="hljs-comment">-- I use a solarized dark theme; this gives a teal background to the active tab</span>
      fg_color = <span class="hljs-string">'#073642'</span>
      bg_color = <span class="hljs-string">'#2aa198'</span>
    }
  }
}
<span class="hljs-comment">-- Switch to the last active tab when I close a tab</span>
wezterm.<span class="hljs-built_in">config</span>.switch_to_last_active_tab_when_closing_tab = <span class="hljs-literal">true</span>
</code></pre>
<p>I really appreciate the attention to detail when naming configuration options.
Once you figure out what option you need, you don't need to look up what it does!</p>
<h3>Panes</h3>
<p>The huge benefit that screen, and later, tmux, introduced is the ability to have multiple <em>panes</em> within a window.
This allows you to essentially treat your terminal like you would a tiled window manager.
During my years as an engineer, this allowed me to treat my CLI as an IDE: I'd have one pane open with vim/nvim, split between a test case and the source code I was modifying; another would be used to run unit tests, which also gave me the ability to scroll back and view specific error messages and traces.</p>
<p>So if I was going to use wezterm, I'd need to be able to split my window/tab into panes.
Additionally, I would need the ability to keep a fair amount of scrollback history, be able to use my mouse when desired, etc.
On top of all that, I'd want to be able to navigate seamlessly between vim/nvim panes and wezterm panes.</p>
<p>Let's tackle the (n)vim &lt;-&gt; wezterm pane issues first, as this also dictates keybindings eventually.
I found <a href="https://github.com/aca/wezterm.nvim">wezterm.nvim</a>, which, despite its name, interacts primarily with wezterm.</p>
<p>The first step of setup is to modify your shell configuration to add the following line to your shell configuation (assuming you use bash or zsh):</p>
<pre><code class="language-bash hljs bash" data-lang="bash">[ -n <span class="hljs-string">"<span class="hljs-variable">$WEZTERM_PANE</span>"</span> ] &amp;&amp; <span class="hljs-built_in">export</span> NVIM_LISTEN_ADDRESS=<span class="hljs-string">"/tmp/nvim<span class="hljs-variable">$WEZTERM_PANE</span>"</span>
</code></pre>
<p>Once that's in place, you clone the project, and use the <code>go</code> language compiler to build and install it; this will install the utility<code>wezterm.nvim.navigator</code>.
Run <code>which wezterm.nvim.navigator</code> when done to find the path on your sytem where it's installed, as it will be needed to setup your wezterm configuration.</p>
<p>From there, you define a function in the configuration that uses that utility to determine if you're in a vim/nvim pane or in a wezterm pane, and, from there, determines what needs to be done to move in the direction requested.
You then also define a number of event handlers that invoke this function, and finally some keybindings to trigger those events.</p>
<p>The beauty of this is that you don't need <em>anything</em> in your vim/nvim configuration; it just uses the default keybindings to move between panes, as wezterm now invokes them!</p>
<p>Now, on top of this, you can <em>also</em> specify some keybindings for <em>resizing</em> panes, which is really useful.
This requires setting up some very similar functionality (a function to perform the resizing, event handlers to invoke it, and keybindings).</p>
<p>I bound <code>&lt;Ctrl&gt;-(hjkl)</code> for moving between windows (these are the standard vim movement mappings), and <code>&lt;Alt&gt;-(hjkl)</code> for resizing.</p>
<p>The full configuration for these motions is here:</p>
<pre><code class="language-lua hljs lua" data-lang="lua"><span class="hljs-comment">-- Pull in the wezterm API</span>
<span class="hljs-keyword">local</span> <span class="hljs-built_in">os</span>              = <span class="hljs-built_in">require</span> <span class="hljs-string">'os'</span>
<span class="hljs-keyword">local</span> wezterm         = <span class="hljs-built_in">require</span> <span class="hljs-string">'wezterm'</span>
<span class="hljs-keyword">local</span> act             = wezterm.action

<span class="hljs-comment">-- This table will hold the configuration.</span>
<span class="hljs-keyword">local</span> <span class="hljs-built_in">config</span> = {}

<span class="hljs-comment">-- In newer versions of wezterm, use the config_builder which will</span>
<span class="hljs-comment">-- help provide clearer error messages</span>
<span class="hljs-keyword">if</span> wezterm.config_builder <span class="hljs-keyword">then</span>
  <span class="hljs-built_in">config</span> = wezterm.config_builder()
<span class="hljs-keyword">end</span>


<span class="hljs-keyword">local</span> move_around = <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(window, pane, direction_wez, direction_nvim)</span></span>
  <span class="hljs-keyword">local</span> result = <span class="hljs-built_in">os</span>.<span class="hljs-built_in">execute</span>(<span class="hljs-string">"env NVIM_LISTEN_ADDRESS=/tmp/nvim"</span> .. pane:pane_id() .. <span class="hljs-string">" "</span> .. wezterm.home_dir .. <span class="hljs-string">"/.local/bin/wezterm.nvim.navigator"</span> .. <span class="hljs-string">" "</span> .. direction_nvim)
  <span class="hljs-keyword">if</span> result <span class="hljs-keyword">then</span>
  window:perform_action(
      act({ SendString = <span class="hljs-string">"\x17"</span> .. direction_nvim }),
      pane
    )
  <span class="hljs-keyword">else</span>
    window:perform_action(
      act({ ActivatePaneDirection = direction_wez }),
      pane
    )
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>

wezterm.on(<span class="hljs-string">"move-left"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(window, pane)</span></span>
	move_around(window, pane, <span class="hljs-string">"Left"</span>, <span class="hljs-string">"h"</span>)
<span class="hljs-keyword">end</span>)

wezterm.on(<span class="hljs-string">"move-right"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(window, pane)</span></span>
	move_around(window, pane, <span class="hljs-string">"Right"</span>, <span class="hljs-string">"l"</span>)
<span class="hljs-keyword">end</span>)

wezterm.on(<span class="hljs-string">"move-up"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(window, pane)</span></span>
	move_around(window, pane, <span class="hljs-string">"Up"</span>, <span class="hljs-string">"k"</span>)
<span class="hljs-keyword">end</span>)

wezterm.on(<span class="hljs-string">"move-down"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(window, pane)</span></span>
	move_around(window, pane, <span class="hljs-string">"Down"</span>, <span class="hljs-string">"j"</span>)
<span class="hljs-keyword">end</span>)

<span class="hljs-keyword">local</span> vim_resize = <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(window, pane, direction_wez, direction_nvim)</span></span>
	<span class="hljs-keyword">local</span> result = <span class="hljs-built_in">os</span>.<span class="hljs-built_in">execute</span>(
		<span class="hljs-string">"env NVIM_LISTEN_ADDRESS=/tmp/nvim"</span>
			.. pane:pane_id()
			.. <span class="hljs-string">" "</span>
            .. wezterm.home_dir
			.. <span class="hljs-string">"/.local/bin/wezterm.nvim.navigator"</span>
			.. <span class="hljs-string">" "</span>
			.. direction_nvim
	)
	<span class="hljs-keyword">if</span> result <span class="hljs-keyword">then</span>
		window:perform_action(act({ SendString = <span class="hljs-string">"\x1b"</span> .. direction_nvim }), pane)
	<span class="hljs-keyword">else</span>
		window:perform_action(act({ ActivatePaneDirection = direction_wez }), pane)
	<span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>

wezterm.on(<span class="hljs-string">"resize-left"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(window, pane)</span></span>
	vim_resize(window, pane, <span class="hljs-string">"Left"</span>, <span class="hljs-string">"h"</span>)
<span class="hljs-keyword">end</span>)

wezterm.on(<span class="hljs-string">"resize-right"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(window, pane)</span></span>
	vim_resize(window, pane, <span class="hljs-string">"Right"</span>, <span class="hljs-string">"l"</span>)
<span class="hljs-keyword">end</span>)

wezterm.on(<span class="hljs-string">"resize-up"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(window, pane)</span></span>
	vim_resize(window, pane, <span class="hljs-string">"Up"</span>, <span class="hljs-string">"k"</span>)
<span class="hljs-keyword">end</span>)

wezterm.on(<span class="hljs-string">"resize-down"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(window, pane)</span></span>
	vim_resize(window, pane, <span class="hljs-string">"Down"</span>, <span class="hljs-string">"j"</span>)
<span class="hljs-keyword">end</span>)

<span class="hljs-built_in">config</span>.keys = {
    <span class="hljs-comment">-- CTRL + (h,j,k,l) to move between panes</span>
    {
        key = <span class="hljs-string">'h'</span>,
        mods = <span class="hljs-string">'CTRL'</span>,
        action = act({ EmitEvent = <span class="hljs-string">"move-left"</span> }),
    },
    {
        key = <span class="hljs-string">'j'</span>,
        mods = <span class="hljs-string">'CTRL'</span>,
        action = act({ EmitEvent = <span class="hljs-string">"move-down"</span> }),
    },
    {
        key = <span class="hljs-string">'k'</span>,
        mods = <span class="hljs-string">'CTRL'</span>,
        action = act({ EmitEvent = <span class="hljs-string">"move-up"</span> }),
    },
    {
        key = <span class="hljs-string">'l'</span>,
        mods = <span class="hljs-string">'CTRL'</span>,
        action = act({ EmitEvent = <span class="hljs-string">"move-right"</span> }),
    },
    <span class="hljs-comment">-- ALT + (h,j,k,l) to resize panes</span>
    {
        key = <span class="hljs-string">'h'</span>,
        mods = <span class="hljs-string">'ALT'</span>,
        action = act({ EmitEvent = <span class="hljs-string">"resize-left"</span> }),
    },
    {
        key = <span class="hljs-string">'j'</span>,
        mods = <span class="hljs-string">'ALT'</span>,
        action = act({ EmitEvent = <span class="hljs-string">"resize-down"</span> }),
    },
    {
        key = <span class="hljs-string">'k'</span>,
        mods = <span class="hljs-string">'ALT'</span>,
        action = act({ EmitEvent = <span class="hljs-string">"resize-up"</span> }),
    },
    {
        key = <span class="hljs-string">'l'</span>,
        mods = <span class="hljs-string">'ALT'</span>,
        action = act({ EmitEvent = <span class="hljs-string">"resize-right"</span> }),
    },
}
</code></pre>
<p>So this gives me movement, but how do I actually <em>split</em> the window into panes?
For this, I've always favored <code>&lt;Leader&gt;|</code> to split into vertical panes, and <code>&lt;Leader&gt;-</code> to split into horizontal panes:</p>
<pre><code class="language-lua hljs lua" data-lang="lua"><span class="hljs-built_in">config</span>.keys = {
  <span class="hljs-comment">-- Vertical split</span>
  {
    <span class="hljs-comment">-- |</span>
    key = <span class="hljs-string">'|'</span>,
    mods = <span class="hljs-string">'LEADER|SHIFT'</span>,
    action = act.SplitPane {
      direction = <span class="hljs-string">'Right'</span>,
      size = { Percent = <span class="hljs-number">50</span> },
    },
  },
  <span class="hljs-comment">-- Horizontal split</span>
  {
    <span class="hljs-comment">-- -</span>
    key = <span class="hljs-string">'-'</span>,
    mods = <span class="hljs-string">'LEADER'</span>,
    action = act.SplitPane {
      direction = <span class="hljs-string">'Down'</span>,
      size = { Percent = <span class="hljs-number">50</span> },
    },
  },
}
</code></pre>
<p>Sometimes I want to swap a pane with another one, because they're not positioned the way I'd like.
I bound <code>&lt;Leader&gt;{</code> to the wezterm <code>PaneSelect</code> action, which allows me to choose the pane I wish to swap with using an alphanumerical index:</p>
<pre><code class="language-lua hljs lua" data-lang="lua"><span class="hljs-built_in">config</span>.keys = {
  {
    <span class="hljs-comment">-- |</span>
    key = <span class="hljs-string">'{'</span>,
    mods = <span class="hljs-string">'LEADER|SHIFT'</span>,
    action = act.PaneSelect { mode = <span class="hljs-string">'SwapWithActiveKeepFocus'</span> }
  },
}
</code></pre>
<p>And sometimes I like to move back and forth between panes; I use <code>&lt;Leader&gt;;</code> and <code>&lt;Leader&gt;;</code> for that:</p>
<pre><code class="language-lua hljs lua" data-lang="lua"><span class="hljs-built_in">config</span>.keys = {
  {
    key = <span class="hljs-string">';'</span>,
    mods = <span class="hljs-string">'LEADER'</span>,
    action = act.ActivatePaneDirection(<span class="hljs-string">'Prev'</span>),
  },
  {
    key = <span class="hljs-string">'o'</span>,
    mods = <span class="hljs-string">'LEADER'</span>,
    action = act.ActivatePaneDirection(<span class="hljs-string">'Next'</span>),
  },
}
</code></pre>
<p>With all of these in place, I'm very nearly there. Just a couple of minor settings:</p>
<pre><code class="language-lua hljs lua" data-lang="lua"><span class="hljs-built_in">config</span>.pane_focus_follows_mouse = <span class="hljs-literal">true</span>
<span class="hljs-built_in">config</span>.scrollback_lines = <span class="hljs-number">5000</span>
<span class="hljs-comment">-- I don't really have need for padding between panes</span>
<span class="hljs-built_in">config</span>.window_padding = {
  left = <span class="hljs-number">0</span>,
  right = <span class="hljs-number">0</span>,
  top = <span class="hljs-number">0</span>,
  bottom = <span class="hljs-number">0</span>,
}
</code></pre>
<h3>Sessions</h3>
<p>Next up on my list of functionality: sessions.</p>
<p>As noted before, tmux sessions, paired with tmux-resurrect, have given me confidence that I won't lose work if I need to restart my computer, or if the battery dies.
How can I reproduce this with wezterm?</p>
<p>By default, each wezterm instance defines its own <em>workspaces</em>, which are analogous to tmux sessions... kind of.
You can create additional workspaces and switch between workspaces using a variety of tools.</p>
<p>However, if you want to share workspaces <em>between</em> wezterm instances, you need to have a <em>mux domain</em> running, and <em>connect</em> to it.
A <em>mux domain</em> is a set of workspaces, windows, and tabs.
By default, a &quot;local&quot; domain is created everytime you  start a wezterm terminal, but you can define additional domains as well, including remote mux servers running on an SSH-accessible server or TLS-guarded remote port, or a simple unix domain.</p>
<p>In my case, I'm configuring a unix domain to run out of the box:</p>
<pre><code class="language-lua hljs lua" data-lang="lua"><span class="hljs-built_in">config</span>.unix_domains = {
  {
    name = <span class="hljs-string">'unix'</span>,
  },
}
</code></pre>
<p>This tells wezterm to setup a mux server on a unix socket.
It does not connect to it out of the box, but it does ensure it's running.</p>
<p>From there, I've created two keybindings: one for connecting to the mux server, and another for detaching from it.
This approach somewhat mirrors how you use tmux: you have to start tmux to create sessions, and detaching from it will drop you back into your original shell.</p>
<pre><code class="language-lua hljs lua" data-lang="lua"><span class="hljs-built_in">config</span>.keys = {
  <span class="hljs-comment">-- Attach to muxer</span>
  {
    key = <span class="hljs-string">'a'</span>,
    mods = <span class="hljs-string">'LEADER'</span>,
    action = act.AttachDomain <span class="hljs-string">'unix'</span>,
  },

  <span class="hljs-comment">-- Detach from muxer</span>
  {
    key = <span class="hljs-string">'d'</span>,
    mods = <span class="hljs-string">'LEADER'</span>,
    action = act.DetachDomain { DomainName = <span class="hljs-string">'unix'</span> },
  },
}
</code></pre>
<p>With the muxer, I can create <em>workspaces</em>.
These are analogous to tmux sessions, and are a group of windows and panes.
When you start a wezterm terminal, you're in a &quot;default&quot; workspace.
So the first thing to do is to allow <em>renaming</em> the workspace to reflect what I'm working on:</p>
<pre><code class="language-lua hljs lua" data-lang="lua"><span class="hljs-built_in">config</span>.keys = {
  <span class="hljs-comment">-- Rename current session; analagous to command in tmux</span>
  {
    key = <span class="hljs-string">'$'</span>,
    mods = <span class="hljs-string">'LEADER|SHIFT'</span>,
    action = act.PromptInputLine {
      description = <span class="hljs-string">'Enter new name for session'</span>,
      action = wezterm.action_callback(
        <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(window, pane, line)</span></span>
          <span class="hljs-keyword">if</span> line <span class="hljs-keyword">then</span>
            mux.rename_workspace(
              window:mux_window():get_workspace(),
              line
            )
          <span class="hljs-keyword">end</span>
        <span class="hljs-keyword">end</span>
      ),
    },
  },
}
</code></pre>
<p>The above maps <code>&lt;Leader&gt;$</code> to prompt you to rename the session.</p>
<p>From here, how do I switch to another workspace, or create a new one?
For that, I mapped <code>&lt;Leader&gt;s</code> to show the workspace launcher; this <em>lists</em> available workspaces, allows you to <em>switch</em> between them, as well as <em>create</em> new workspaces:</p>
<pre><code class="language-lua hljs lua" data-lang="lua"><span class="hljs-built_in">config</span>.keys = {
  <span class="hljs-comment">-- Show list of workspaces</span>
  {
    key = <span class="hljs-string">'s'</span>,
    mods = <span class="hljs-string">'LEADER'</span>,
    action = act.ShowLauncherArgs { flags = <span class="hljs-string">'WORKSPACES'</span> },
  },
}
</code></pre>
<p>The workflow then looks like this:</p>
<ul>
<li>
<p>Start wezterm.</p>
</li>
<li>
<p>If I want to be able to switch between mux workspaces on other windows, I then hit <code>&lt;Leader&gt;a</code>.
This will connect to the mux server, and throw me into the <code>default</code> workspace.</p>
</li>
<li>
<p>If I want to leave the mux session, I can hit <code>&lt;Leader&gt;d</code>, which will detach me from it, taking me back to the &quot;Local&quot; domain for the GUI window I'm in.</p>
<ul>
<li>At this point, typing <code>exit</code> will close the window, as I'll have left the last pane of the last window in the workspace.</li>
</ul>
</li>
</ul>
<p>One thing that is odd about the situation: if you are in a mux session with multiple workspaces, and you <code>exit</code> out of the last pane of the last window of a workspace... it doesn't close the window or drop you out of the mux domain.
Instead, it takes you to the last active workspace.
This can be confusing at first.
However, all you need to do at that point is hit <code>&lt;Leader&gt;d</code> to detach, and that will take you back to the local domain, from which you can exit normally.</p>
<p>Interestingly, you can use the system controls to close the window at any point (e.g., hitting the close button, or using a keybinding like <code>&lt;Ctrl&gt;w</code> or <code>&lt;Alt&gt;F4</code>).
The mux server will keep the state for you.
Of course, if you do this and you don't want the workspace to persist, you'll need to return to that workspace at some point and exit out of it.</p>
<h3>Keeping things safe</h3>
<p>Now that I have a concept of sessions, what about recovery?
One of my most used features in tmux was the tmux-resurrect plugin, which allowed me to save and restore sessions (and did some background periodic saving for me).
Can I do that with wezterm?</p>
<p>Yes, to an extent, via the <a href="https://github.com/danielcopper/wezterm-session-manager">wezterm-session-manager project</a>.
This is a Lua module that you add to your wezterm configuration; you clone it in the <code>$HOME/.config/wezterm/</code> directory, and then import it into your configuration:</p>
<pre><code class="language-lua hljs lua" data-lang="lua"><span class="hljs-keyword">local</span> session_manager = <span class="hljs-built_in">require</span> <span class="hljs-string">'wezterm-session-manager/session-manager'</span>
</code></pre>
<p>From there, you need to register some event listeners:</p>
<pre><code class="language-lua hljs lua" data-lang="lua">wezterm.on(<span class="hljs-string">"save_session"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(window)</span></span> session_manager.save_state(window) <span class="hljs-keyword">end</span>)
wezterm.on(<span class="hljs-string">"load_session"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(window)</span></span> session_manager.load_state(window) <span class="hljs-keyword">end</span>)
wezterm.on(<span class="hljs-string">"restore_session"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(window)</span></span> session_manager.restore_state(window) <span class="hljs-keyword">end</span>)
</code></pre>
<p>Finally, add some keybindings; I registered <code>&lt;Leader&gt;S</code> to save the session, <code>&lt;Leader&gt;l</code> to load a session (this doesn't currently work), and <code>&lt;Leader&gt;R</code> to restore from the most recent previous save point:</p>
<pre><code class="language-lua hljs lua" data-lang="lua"><span class="hljs-built_in">config</span>.keys = {
  <span class="hljs-comment">-- Session manager bindings</span>
  {
    key = <span class="hljs-string">'s'</span>,
    mods = <span class="hljs-string">'LEADER|SHIFT'</span>,
    action = act({ EmitEvent = <span class="hljs-string">"save_session"</span> }),
  },
  {
    key = <span class="hljs-string">'L'</span>,
    mods = <span class="hljs-string">'LEADER|SHIFT'</span>,
    action = act({ EmitEvent = <span class="hljs-string">"load_session"</span> }),
  },
  {
    key = <span class="hljs-string">'R'</span>,
    mods = <span class="hljs-string">'LEADER|SHIFT'</span>,
    action = act({ EmitEvent = <span class="hljs-string">"restore_session"</span> }),
  },
}
</code></pre>
<h3>Ready to Launch</h3>
<p>Wezterm ships with an XDG desktop file, which works, but it will always open the same window by default.
Also, I prefer to have a simpler shortcut than <code>&lt;Super&gt;</code> + <em>type something to search for the app</em>.
As such, I usually bind <code>&lt;Super&gt;t</code> to open my terminal.</p>
<p>For the actual command, I use <code>wezterm-gui start --always-new-process</code>.
This ensures that I get a new window each time, and that it's not bound to another domain or workspace at start; I can use my other keybindings to attach to a domain, rename or create new workspaces, etc.</p>
<h3>Watch for the drop!</h3>
<blockquote>
<h4>Update 2024-09-17</h4>
<p>I have found a new solution for implementing a dropdown terminal via the gnome-shell Quake Terminal extension; you can <a href="/blog/2024-09-17-wezterm-dropdown.html">read my blog post about setting up Quake Terminal with Wezterm</a> for instructions.</p>
</blockquote>
<details>
    <summary>Click to expand my old setup using tdrop</summary>
<p>I mentioned earlier that I find a dropdown terminal handy, and that I started using <a href="https://github.com/noctuid/tdrop">tdrop</a> to provide one when I adopted <code>kitty</code> as my terminal.
I was able to do the same with wezterm.</p>
<p>A few things to note:</p>
<ul>
<li>
<p>tdrop does NOT play well with Wayland, but it provides a sneaky way to work within Wayland.
You can prefix your command with <code>WAYLAND_DISPLAY=no</code> to force usage of XWayland when invoking your terminal.
I had to do this with Wezterm, as otherwise it simply didn't work.
It would spawn the terminal, but without resizing it.</p>
</li>
<li>
<p>For my dropdown terminal, I like to have it slightly transparent, and without window decorations.</p>
</li>
<li>
<p>I ALWAYS want this one using the mux domain.
Since it's always running, I need to be able to look back at history and keep the panes in the same state between reboots or logging out.</p>
</li>
</ul>
<p>To make things simpler for myself, I created a simple shell script:</p>
<pre><code class="language-bash hljs bash" data-lang="bash"><span class="hljs-meta">#!/bin/bash</span>
<span class="hljs-comment"># File: $HOME/.local/bin/dropdown-terminal </span>
WAYLAND_DISPLAY=no \
  <span class="hljs-variable">$HOME</span>/.<span class="hljs-built_in">local</span>/bin/tdrop --class=dropdown -mta \
  wezterm-gui \
    --config <span class="hljs-string">"window_background_opacity=0.85"</span> \
    --config <span class="hljs-string">"window_decorations='NONE'"</span> \
    start \
      --domain unix \
      --attach \
      --workspace dropdown
</code></pre>
<p>What does this do?</p>
<ul>
<li>
<p>I have <code>tdrop</code> in my <code>$HOME/.local/bin/</code> directory.
I have found gnome-shell will not look in that path, despite it being in my shell profile, so I reference it fully.</p>
</li>
<li>
<p>I set the window class for the generated window.</p>
</li>
<li>
<p>And I tell <code>tdrop</code> to be aware of which monitor I'm on, and autosize the terminal based on the monitor dimensions.</p>
</li>
<li>
<p>I tell wezterm to override two configuration values:</p>
</li>
<li>
<p>I set the background opacity to 0.85, making it slightly transparent</p>
</li>
<li>
<p>I disable window decorations</p>
</li>
<li>
<p>and I tell it to connect to the &quot;unix&quot; domain on startup, attach to the existing window, and use the &quot;dropdown&quot; workspace (creating it if it does not exist).</p>
</li>
</ul>
<p>In my gnome-shell keybindings, I bind <code>&lt;F12&gt;</code> to run <code>$HOME/.local/bin/dropdown-terminal</code>.</p>
<p><code>tdrop</code> will then toggle the dropdown state everytime I hit that key. By default, it takes up 50% of the screen.</p>
</details>
<h2>The full configuration</h2>
<p>Here it is. I'm sure there's stuff I could do better, but it works.</p>
<details>
    <summary>wezterm.lua (click to expand/hide)</summary>
<pre><code class="language-lua hljs lua" data-lang="lua"><span class="hljs-comment">-- Pull in the wezterm API</span>
<span class="hljs-keyword">local</span> <span class="hljs-built_in">os</span>              = <span class="hljs-built_in">require</span> <span class="hljs-string">'os'</span>
<span class="hljs-keyword">local</span> wezterm         = <span class="hljs-built_in">require</span> <span class="hljs-string">'wezterm'</span>
<span class="hljs-keyword">local</span> session_manager = <span class="hljs-built_in">require</span> <span class="hljs-string">'wezterm-session-manager/session-manager'</span>
<span class="hljs-keyword">local</span> act             = wezterm.action
<span class="hljs-keyword">local</span> mux             = wezterm.mux

<span class="hljs-comment">-- --------------------------------------------------------------------</span>
<span class="hljs-comment">-- FUNCTIONS AND EVENT BINDINGS</span>
<span class="hljs-comment">-- --------------------------------------------------------------------</span>

<span class="hljs-comment">-- Session Manager event bindings</span>
<span class="hljs-comment">-- See https://github.com/danielcopper/wezterm-session-manager</span>
wezterm.on(<span class="hljs-string">"save_session"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(window)</span></span> session_manager.save_state(window) <span class="hljs-keyword">end</span>)
wezterm.on(<span class="hljs-string">"load_session"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(window)</span></span> session_manager.load_state(window) <span class="hljs-keyword">end</span>)
wezterm.on(<span class="hljs-string">"restore_session"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(window)</span></span> session_manager.restore_state(window) <span class="hljs-keyword">end</span>)

<span class="hljs-comment">-- Wezterm &lt;-&gt; nvim pane navigation</span>
<span class="hljs-comment">-- You will need to install https://github.com/aca/wezterm.nvim</span>
<span class="hljs-comment">-- and ensure you export NVIM_LISTEN_ADDRESS per the README in that repo</span>

<span class="hljs-keyword">local</span> move_around = <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(window, pane, direction_wez, direction_nvim)</span></span>
    <span class="hljs-keyword">local</span> result = <span class="hljs-built_in">os</span>.<span class="hljs-built_in">execute</span>(<span class="hljs-string">"env NVIM_LISTEN_ADDRESS=/tmp/nvim"</span> .. pane:pane_id() .. <span class="hljs-string">" "</span> .. wezterm.home_dir .. <span class="hljs-string">"/.local/bin/wezterm.nvim.navigator"</span> .. <span class="hljs-string">" "</span> .. direction_nvim)
    <span class="hljs-keyword">if</span> result <span class="hljs-keyword">then</span>
		window:perform_action(
            act({ SendString = <span class="hljs-string">"\x17"</span> .. direction_nvim }),
            pane
        )
    <span class="hljs-keyword">else</span>
        window:perform_action(
            act({ ActivatePaneDirection = direction_wez }),
            pane
        )
    <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>

wezterm.on(<span class="hljs-string">"move-left"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(window, pane)</span></span>
	move_around(window, pane, <span class="hljs-string">"Left"</span>, <span class="hljs-string">"h"</span>)
<span class="hljs-keyword">end</span>)

wezterm.on(<span class="hljs-string">"move-right"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(window, pane)</span></span>
	move_around(window, pane, <span class="hljs-string">"Right"</span>, <span class="hljs-string">"l"</span>)
<span class="hljs-keyword">end</span>)

wezterm.on(<span class="hljs-string">"move-up"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(window, pane)</span></span>
	move_around(window, pane, <span class="hljs-string">"Up"</span>, <span class="hljs-string">"k"</span>)
<span class="hljs-keyword">end</span>)

wezterm.on(<span class="hljs-string">"move-down"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(window, pane)</span></span>
	move_around(window, pane, <span class="hljs-string">"Down"</span>, <span class="hljs-string">"j"</span>)
<span class="hljs-keyword">end</span>)

<span class="hljs-keyword">local</span> vim_resize = <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(window, pane, direction_wez, direction_nvim)</span></span>
	<span class="hljs-keyword">local</span> result = <span class="hljs-built_in">os</span>.<span class="hljs-built_in">execute</span>(
		<span class="hljs-string">"env NVIM_LISTEN_ADDRESS=/tmp/nvim"</span>
			.. pane:pane_id()
			.. <span class="hljs-string">" "</span>
            .. wezterm.home_dir
			.. <span class="hljs-string">"/.local/bin/wezterm.nvim.navigator"</span>
			.. <span class="hljs-string">" "</span>
			.. direction_nvim
	)
	<span class="hljs-keyword">if</span> result <span class="hljs-keyword">then</span>
		window:perform_action(act({ SendString = <span class="hljs-string">"\x1b"</span> .. direction_nvim }), pane)
	<span class="hljs-keyword">else</span>
		window:perform_action(act({ ActivatePaneDirection = direction_wez }), pane)
	<span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>

wezterm.on(<span class="hljs-string">"resize-left"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(window, pane)</span></span>
	vim_resize(window, pane, <span class="hljs-string">"Left"</span>, <span class="hljs-string">"h"</span>)
<span class="hljs-keyword">end</span>)

wezterm.on(<span class="hljs-string">"resize-right"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(window, pane)</span></span>
	vim_resize(window, pane, <span class="hljs-string">"Right"</span>, <span class="hljs-string">"l"</span>)
<span class="hljs-keyword">end</span>)

wezterm.on(<span class="hljs-string">"resize-up"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(window, pane)</span></span>
	vim_resize(window, pane, <span class="hljs-string">"Up"</span>, <span class="hljs-string">"k"</span>)
<span class="hljs-keyword">end</span>)

wezterm.on(<span class="hljs-string">"resize-down"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(window, pane)</span></span>
	vim_resize(window, pane, <span class="hljs-string">"Down"</span>, <span class="hljs-string">"j"</span>)
<span class="hljs-keyword">end</span>)

<span class="hljs-comment">-- --------------------------------------------------------------------</span>
<span class="hljs-comment">-- CONFIGURATION</span>
<span class="hljs-comment">-- --------------------------------------------------------------------</span>

<span class="hljs-comment">-- This table will hold the configuration.</span>
<span class="hljs-keyword">local</span> <span class="hljs-built_in">config</span> = {}

<span class="hljs-comment">-- In newer versions of wezterm, use the config_builder which will</span>
<span class="hljs-comment">-- help provide clearer error messages</span>
<span class="hljs-keyword">if</span> wezterm.config_builder <span class="hljs-keyword">then</span>
  <span class="hljs-built_in">config</span> = wezterm.config_builder()
<span class="hljs-keyword">end</span>

<span class="hljs-built_in">config</span>.adjust_window_size_when_changing_font_size = <span class="hljs-literal">false</span>
<span class="hljs-built_in">config</span>.automatically_reload_config = <span class="hljs-literal">true</span>
<span class="hljs-built_in">config</span>.color_scheme = <span class="hljs-string">'Solarized (dark) (terminal.sexy)'</span>
<span class="hljs-built_in">config</span>.enable_scroll_bar = <span class="hljs-literal">true</span>
<span class="hljs-built_in">config</span>.enable_wayland = <span class="hljs-literal">true</span>
<span class="hljs-comment">-- config.font = wezterm.font('Hack')</span>
<span class="hljs-built_in">config</span>.font = wezterm.font(<span class="hljs-string">'Monaspace Neon'</span>)
<span class="hljs-built_in">config</span>.font_size = <span class="hljs-number">12.0</span>
<span class="hljs-built_in">config</span>.hide_tab_bar_if_only_one_tab = <span class="hljs-literal">true</span>
<span class="hljs-comment">-- The leader is similar to how tmux defines a set of keys to hit in order to</span>
<span class="hljs-comment">-- invoke tmux bindings. Binding to ctrl-a here to mimic tmux</span>
<span class="hljs-built_in">config</span>.leader = { key = <span class="hljs-string">'a'</span>, mods = <span class="hljs-string">'CTRL'</span>, timeout_milliseconds = <span class="hljs-number">2000</span> }
<span class="hljs-built_in">config</span>.mouse_bindings = {
    <span class="hljs-comment">-- Open URLs with Ctrl+Click</span>
    {
        event = { Up = { streak = <span class="hljs-number">1</span>, button = <span class="hljs-string">'Left'</span> } },
        mods = <span class="hljs-string">'CTRL'</span>,
        action = act.OpenLinkAtMouseCursor,
    }
}
<span class="hljs-built_in">config</span>.pane_focus_follows_mouse = <span class="hljs-literal">true</span>
<span class="hljs-built_in">config</span>.scrollback_lines = <span class="hljs-number">5000</span>
<span class="hljs-built_in">config</span>.use_dead_keys = <span class="hljs-literal">false</span>
<span class="hljs-built_in">config</span>.warn_about_missing_glyphs = <span class="hljs-literal">false</span>
<span class="hljs-built_in">config</span>.window_decorations = <span class="hljs-string">'TITLE | RESIZE'</span>
<span class="hljs-built_in">config</span>.window_padding = {
    left = <span class="hljs-number">0</span>,
    right = <span class="hljs-number">0</span>,
    top = <span class="hljs-number">0</span>,
    bottom = <span class="hljs-number">0</span>,
}

<span class="hljs-comment">-- Tab bar</span>
<span class="hljs-built_in">config</span>.use_fancy_tab_bar = <span class="hljs-literal">true</span>
<span class="hljs-built_in">config</span>.tab_bar_at_bottom = <span class="hljs-literal">true</span>
<span class="hljs-built_in">config</span>.switch_to_last_active_tab_when_closing_tab = <span class="hljs-literal">true</span>
<span class="hljs-built_in">config</span>.tab_max_width = <span class="hljs-number">32</span>
<span class="hljs-built_in">config</span>.colors = {
    tab_bar = {
        active_tab = {
            fg_color = <span class="hljs-string">'#073642'</span>,
            bg_color = <span class="hljs-string">'#2aa198'</span>,
        }
    }
}

<span class="hljs-comment">-- Setup muxing by default</span>
<span class="hljs-built_in">config</span>.unix_domains = {
  {
    name = <span class="hljs-string">'unix'</span>,
  },
}

<span class="hljs-comment">-- Custom key bindings</span>
<span class="hljs-built_in">config</span>.keys = {
    <span class="hljs-comment">-- -- Disable Alt-Enter combination (already used in tmux to split pane)</span>
    <span class="hljs-comment">-- {</span>
    <span class="hljs-comment">--     key = 'Enter',</span>
    <span class="hljs-comment">--     mods = 'ALT',</span>
    <span class="hljs-comment">--     action = act.DisableDefaultAssignment,</span>
    <span class="hljs-comment">-- },</span>

    <span class="hljs-comment">-- Copy mode</span>
    {
        key = <span class="hljs-string">'['</span>,
        mods = <span class="hljs-string">'LEADER'</span>,
        action = act.ActivateCopyMode,
    },

    <span class="hljs-comment">-- ----------------------------------------------------------------</span>
    <span class="hljs-comment">-- TABS</span>
    <span class="hljs-comment">--</span>
    <span class="hljs-comment">-- Where possible, I'm using the same combinations as I would in tmux</span>
    <span class="hljs-comment">-- ----------------------------------------------------------------</span>

    <span class="hljs-comment">-- Show tab navigator; similar to listing panes in tmux</span>
    {
        key = <span class="hljs-string">'w'</span>,
        mods = <span class="hljs-string">'LEADER'</span>,
        action = act.ShowTabNavigator,
    },
    <span class="hljs-comment">-- Create a tab (alternative to Ctrl-Shift-Tab)</span>
    {
        key = <span class="hljs-string">'c'</span>,
        mods = <span class="hljs-string">'LEADER'</span>,
        action = act.SpawnTab <span class="hljs-string">'CurrentPaneDomain'</span>,
    },
    <span class="hljs-comment">-- Rename current tab; analagous to command in tmux</span>
    {
        key = <span class="hljs-string">','</span>,
        mods = <span class="hljs-string">'LEADER'</span>,
        action = act.PromptInputLine {
            description = <span class="hljs-string">'Enter new name for tab'</span>,
            action = wezterm.action_callback(
                <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(window, pane, line)</span></span>
                    <span class="hljs-keyword">if</span> line <span class="hljs-keyword">then</span>
                        window:active_tab():set_title(line)
                    <span class="hljs-keyword">end</span>
                <span class="hljs-keyword">end</span>
            ),
        },
    },
    <span class="hljs-comment">-- Move to next/previous TAB</span>
    {
        key = <span class="hljs-string">'n'</span>,
        mods = <span class="hljs-string">'LEADER'</span>,
        action = act.ActivateTabRelative(<span class="hljs-number">1</span>),
    },
    {
        key = <span class="hljs-string">'p'</span>,
        mods = <span class="hljs-string">'LEADER'</span>,
        action = act.ActivateTabRelative(<span class="hljs-number">-1</span>),
    },
    <span class="hljs-comment">-- Close tab</span>
    {
        key = <span class="hljs-string">'&amp;'</span>,
        mods = <span class="hljs-string">'LEADER|SHIFT'</span>,
        action = act.CloseCurrentTab{ confirm = <span class="hljs-literal">true</span> },
    },

    <span class="hljs-comment">-- ----------------------------------------------------------------</span>
    <span class="hljs-comment">-- PANES</span>
    <span class="hljs-comment">--</span>
    <span class="hljs-comment">-- These are great and get me most of the way to replacing tmux</span>
    <span class="hljs-comment">-- entirely, particularly as you can use "wezterm ssh" to ssh to another</span>
    <span class="hljs-comment">-- server, and still retain Wezterm as your terminal there.</span>
    <span class="hljs-comment">-- ----------------------------------------------------------------</span>

    <span class="hljs-comment">-- -- Vertical split</span>
    {
        <span class="hljs-comment">-- |</span>
        key = <span class="hljs-string">'|'</span>,
        mods = <span class="hljs-string">'LEADER|SHIFT'</span>,
        action = act.SplitPane {
            direction = <span class="hljs-string">'Right'</span>,
            size = { Percent = <span class="hljs-number">50</span> },
        },
    },
    <span class="hljs-comment">-- Horizontal split</span>
    {
        <span class="hljs-comment">-- -</span>
        key = <span class="hljs-string">'-'</span>,
        mods = <span class="hljs-string">'LEADER'</span>,
        action = act.SplitPane {
            direction = <span class="hljs-string">'Down'</span>,
            size = { Percent = <span class="hljs-number">50</span> },
        },
    },
    <span class="hljs-comment">-- CTRL + (h,j,k,l) to move between panes</span>
    {
        key = <span class="hljs-string">'h'</span>,
        mods = <span class="hljs-string">'CTRL'</span>,
        action = act({ EmitEvent = <span class="hljs-string">"move-left"</span> }),
    },
    {
        key = <span class="hljs-string">'j'</span>,
        mods = <span class="hljs-string">'CTRL'</span>,
        action = act({ EmitEvent = <span class="hljs-string">"move-down"</span> }),
    },
    {
        key = <span class="hljs-string">'k'</span>,
        mods = <span class="hljs-string">'CTRL'</span>,
        action = act({ EmitEvent = <span class="hljs-string">"move-up"</span> }),
    },
    {
        key = <span class="hljs-string">'l'</span>,
        mods = <span class="hljs-string">'CTRL'</span>,
        action = act({ EmitEvent = <span class="hljs-string">"move-right"</span> }),
    },
    <span class="hljs-comment">-- ALT + (h,j,k,l) to resize panes</span>
    {
        key = <span class="hljs-string">'h'</span>,
        mods = <span class="hljs-string">'ALT'</span>,
        action = act({ EmitEvent = <span class="hljs-string">"resize-left"</span> }),
    },
    {
        key = <span class="hljs-string">'j'</span>,
        mods = <span class="hljs-string">'ALT'</span>,
        action = act({ EmitEvent = <span class="hljs-string">"resize-down"</span> }),
    },
    {
        key = <span class="hljs-string">'k'</span>,
        mods = <span class="hljs-string">'ALT'</span>,
        action = act({ EmitEvent = <span class="hljs-string">"resize-up"</span> }),
    },
    {
        key = <span class="hljs-string">'l'</span>,
        mods = <span class="hljs-string">'ALT'</span>,
        action = act({ EmitEvent = <span class="hljs-string">"resize-right"</span> }),
    },
    <span class="hljs-comment">-- Close/kill active pane</span>
    {
        key = <span class="hljs-string">'x'</span>,
        mods = <span class="hljs-string">'LEADER'</span>,
        action = act.CloseCurrentPane { confirm = <span class="hljs-literal">true</span> },
    },
    <span class="hljs-comment">-- Swap active pane with another one</span>
    {
        key = <span class="hljs-string">'{'</span>,
        mods = <span class="hljs-string">'LEADER|SHIFT'</span>,
        action = act.PaneSelect { mode = <span class="hljs-string">"SwapWithActiveKeepFocus"</span> },
    },
    <span class="hljs-comment">-- Zoom current pane (toggle)</span>
    {
        key = <span class="hljs-string">'z'</span>,
        mods = <span class="hljs-string">'LEADER'</span>,
        action = act.TogglePaneZoomState,
    },
    {
        key = <span class="hljs-string">'f'</span>,
        mods = <span class="hljs-string">'ALT'</span>,
        action = act.TogglePaneZoomState,
    },
    <span class="hljs-comment">-- Move to next/previous pane</span>
    {
        key = <span class="hljs-string">';'</span>,
        mods = <span class="hljs-string">'LEADER'</span>,
        action = act.ActivatePaneDirection(<span class="hljs-string">'Prev'</span>),
    },
    {
        key = <span class="hljs-string">'o'</span>,
        mods = <span class="hljs-string">'LEADER'</span>,
        action = act.ActivatePaneDirection(<span class="hljs-string">'Next'</span>),
    },

    <span class="hljs-comment">-- ----------------------------------------------------------------</span>
    <span class="hljs-comment">-- Workspaces</span>
    <span class="hljs-comment">--</span>
    <span class="hljs-comment">-- These are roughly equivalent to tmux sessions.</span>
    <span class="hljs-comment">-- ----------------------------------------------------------------</span>

    <span class="hljs-comment">-- Attach to muxer</span>
    {
        key = <span class="hljs-string">'a'</span>,
        mods = <span class="hljs-string">'LEADER'</span>,
        action = act.AttachDomain <span class="hljs-string">'unix'</span>,
    },

    <span class="hljs-comment">-- Detach from muxer</span>
    {
        key = <span class="hljs-string">'d'</span>,
        mods = <span class="hljs-string">'LEADER'</span>,
        action = act.DetachDomain { DomainName = <span class="hljs-string">'unix'</span> },
    },

    <span class="hljs-comment">-- Show list of workspaces</span>
    {
        key = <span class="hljs-string">'s'</span>,
        mods = <span class="hljs-string">'LEADER'</span>,
        action = act.ShowLauncherArgs { flags = <span class="hljs-string">'WORKSPACES'</span> },
    },
    <span class="hljs-comment">-- Rename current session; analagous to command in tmux</span>
    {
        key = <span class="hljs-string">'$'</span>,
        mods = <span class="hljs-string">'LEADER|SHIFT'</span>,
        action = act.PromptInputLine {
            description = <span class="hljs-string">'Enter new name for session'</span>,
            action = wezterm.action_callback(
                <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(window, pane, line)</span></span>
                    <span class="hljs-keyword">if</span> line <span class="hljs-keyword">then</span>
                        mux.rename_workspace(
                            window:mux_window():get_workspace(),
                            line
                        )
                    <span class="hljs-keyword">end</span>
                <span class="hljs-keyword">end</span>
            ),
        },
    },

    <span class="hljs-comment">-- Session manager bindings</span>
    {
        key = <span class="hljs-string">'s'</span>,
        mods = <span class="hljs-string">'LEADER|SHIFT'</span>,
        action = act({ EmitEvent = <span class="hljs-string">"save_session"</span> }),
    },
    {
        key = <span class="hljs-string">'L'</span>,
        mods = <span class="hljs-string">'LEADER|SHIFT'</span>,
        action = act({ EmitEvent = <span class="hljs-string">"load_session"</span> }),
    },
    {
        key = <span class="hljs-string">'R'</span>,
        mods = <span class="hljs-string">'LEADER|SHIFT'</span>,
        action = act({ EmitEvent = <span class="hljs-string">"restore_session"</span> }),
    },
}

<span class="hljs-comment">-- and finally, return the configuration to wezterm</span>
<span class="hljs-keyword">return</span> <span class="hljs-built_in">config</span>
</code></pre>
</details>
<h2>Closing</h2>
<p>I've been really impressed by Wezterm!
One thing that's absolutely magical is that I don't ever have to think about whether or not I've started <code>tmux</code>; I can just start splitting the window into panes on the fly as needed.
On top of that, having the configuration be a limited programming language, and one that is NOT specific to Wezterm, means that I can (a) use a skill I already have, and (b) do some limited programming of terminal behavior, which allows me to customize it for my own use cases.</p>
<p>Would I recommend Wezterm to others?
Absolutely!
One reason I was excited to try it is so I could use a terminal I could potentially port <em>elsewhere</em>; Wezterm works on Linux, obviously, but also Windows and Mac, making it a great cross-platform replacement for whatever native terminals you were using previously.
This can be hugely useful if you switch between systems regularly, or even if you are contemplating a switch in the future and want to make your landing as soft as possible.</p>
<p>Thanks, Wez, for this great software, and a huge thank you to your community and contributors as well!</p>


<div class="h-entry">
    <img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&u=79dd2ea1d4d8855944715d09ee4c86215027fa80&s=140" alt="matthew">
    <a class="u-url u-uid p-name" href="https://mwop.net/blog/2024-07-04-how-i-use-wezterm.html">How I use Wezterm</a> was originally
    published <time class="dt-published" datetime="2024-07-04T13:50:46-05:00">4 July 2024</time>
    on <a href="https://mwop.net">https://mwop.net</a> by
    <a rel="author" class="p-author" href="https://mwop.net">Matthew Weier O&#039;Phinney</a>.
</div>
]]></content:encoded>
      <slash:comments>0</slash:comments>
    </item>
  </channel>
</rss>
