<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title type="text">Blog entries :: mwop.net</title>
  <updated>2025-01-25T12:46:59-06:00</updated>
  <generator uri="https://getlaminas.org" version="2">Laminas_Feed_Writer</generator>
  <link rel="alternate" type="text/html" href="https://mwop.net/blog/"/>
  <link rel="self" type="application/atom+xml" href="https://mwop.net/blog/atom.xml"/>
  <id>https://mwop.net/blog/</id>
  <entry xmlns:xhtml="http://www.w3.org/1999/xhtml">
    <title type="html"><![CDATA[Comments are Back]]></title>
    <published>2025-01-25T12:46:59-06:00</published>
    <updated>2025-01-25T12:46:59-06:00</updated>
    <link rel="alternate" type="text/html" href="https://mwop.net/blog/2025-01-25-comments-are-back.html"/>
    <id>https://mwop.net/blog/2025-01-25-comments-are-back.html</id>
    <author>
      <name>Matthew Weier O'Phinney</name>
      <email>contact@mwop.net</email>
      <uri>https://mwop.net</uri>
    </author>
    <content xmlns:xhtml="http://www.w3.org/1999/xhtml" type="xhtml">
      <xhtml:div xmlns:xhtml="http://www.w3.org/1999/xhtml"><xhtml:p>For a number of years, I was using <xhtml:a href="https://disqus.com">Disqus</xhtml:a> to provide comments on my blog.
However, I was increasingly unhappy with how bloated the solution
was, how many additional entries I was having to put into my
Content Security Policy, and unsure how comfortable I was with
having a third party own comments to my own site.</xhtml:p>
<xhtml:p>So last year, I removed comments from my site entirely.</xhtml:p>
<xhtml:p>This worked fine, and I didn't really think about it much, until
somebody reached out to me via email recently, with what was
essentially a comment on a blog post, and I realized that nobody
else but me was going to benefit from it.</xhtml:p>
<xhtml:p>So I started thinking about how to go about adding comments
again.</xhtml:p>
<xhtml:h3>Build it?</xhtml:h3>
<xhtml:p>I started, of course, by building.</xhtml:p>
<xhtml:p>I modeled what was essential to a comment; decided I'd use
Markdown, with limits, to allow commenters some ability to style
and format their comments; even got a schema setup in my
database.</xhtml:p>
<xhtml:p>And then I realized a few things:</xhtml:p>
<xhtml:ul>
<xhtml:li>I'd need an admin for moderating comments.</xhtml:li>
<xhtml:li>I'd likely need some way to notify at least myself when a new
comment was added.</xhtml:li>
<xhtml:li>What about letting folks know their comment was submitted
successfully? Or that it was published? (I planned to moderate all
comments by default.)</xhtml:li>
<xhtml:li>What about letting folks know when a new comment was published
on a post they'd previously commented on?</xhtml:li>
<xhtml:li>What about allowing folks to unsubscribe from those
notifications? And would I allow both granular (per blog post) and
general (full site) unsubscription?</xhtml:li>
<xhtml:li>What about allowing folks to request all comments be deleted
(per GDPR)?</xhtml:li>
<xhtml:li>What if I want to lock comments for a post? Or later unlock
them?</xhtml:li>
</xhtml:ul>
<xhtml:p>The more I thought about it, the more work I was seeing, and I
wasn't sure how much I wanted to develop it.</xhtml:p>
<xhtml:h3>Research</xhtml:h3>
<xhtml:p>So I started researching commenting systems, and quickly found a
variety of generic solutions exist, thankfully. The research then
boiled down to identifying which ones are actively maintained
(because anything like a commenting system will likely need
security updates and occasional updates to ensure compatibility
with browsers and evolving security policies), which ones had the
features I wanted, and how easily I'd be able to implement the
solution.</xhtml:p>
<xhtml:p>I eventually settled on <xhtml:a href="https://remark42.com">Remark42</xhtml:a>.</xhtml:p>
<xhtml:p>Remark42 is privacy-focused, and allows me to keep all the data.
While there are a number of SSO integrations, they primarily use
OAuth2, meaning that the integration is only for purposes of
authentication. I was also able to enable an email authentication
option; this sends an email to the user with a token that they then
paste back into the form to authenticate.</xhtml:p>
<xhtml:p>As for the email, I was able to set it up to use an existing
SMTP user to send out emails. These are used for users who
authenticate via email, sending notifications to me of new
comments, and managing individual user notifications of
comments.</xhtml:p>
<xhtml:p>I run it under its own domain (comments.mwop.net), and have
functionality to embed comments via JavaScript in my site. I was
able to configure it such that it will only accept comments from
specific domains, which helps prevent abuse of the system. While
I'd love to have the comments integrated on my site without
JavaScript, the fact that I did not need to develop any of this on
my own was a huge benefit.</xhtml:p>
<xhtml:p>Better: it uses limited Markdown for comment styling, and has
built-in links to documentation on what you can use.</xhtml:p>
<xhtml:h3>Configuring Remark42</xhtml:h3>
<xhtml:p>I run the service using Docker Compose, using a single service
in my Compose file:</xhtml:p>
<xhtml:pre><xhtml:code class="language-yaml hljs yaml" data-lang="yaml"><xhtml:span class="hljs-attr">services:</xhtml:span>
  <xhtml:span class="hljs-attr">remark42:</xhtml:span>
    <xhtml:span class="hljs-attr">image:</xhtml:span> <xhtml:span class="hljs-string">umputun/remark42:latest</xhtml:span>
    <xhtml:span class="hljs-attr">container_name:</xhtml:span> <xhtml:span class="hljs-string">"remark42"</xhtml:span>
    <xhtml:span class="hljs-attr">hostname:</xhtml:span> <xhtml:span class="hljs-string">"comments.mwop.net"</xhtml:span>
    <xhtml:span class="hljs-attr">restart:</xhtml:span> <xhtml:span class="hljs-string">always</xhtml:span>

    <xhtml:span class="hljs-attr">logging:</xhtml:span>
      <xhtml:span class="hljs-attr">driver:</xhtml:span> <xhtml:span class="hljs-string">json-file</xhtml:span>
      <xhtml:span class="hljs-attr">options:</xhtml:span>
        <xhtml:span class="hljs-attr">max-size:</xhtml:span> <xhtml:span class="hljs-string">"10m"</xhtml:span>
        <xhtml:span class="hljs-attr">max-file:</xhtml:span> <xhtml:span class="hljs-string">"5"</xhtml:span>

    <xhtml:span class="hljs-attr">ports:</xhtml:span>
      <xhtml:span class="hljs-bullet">-</xhtml:span> <xhtml:span class="hljs-string">"127.0.0.1:9010:8080"</xhtml:span>

    <xhtml:span class="hljs-attr">env_file:</xhtml:span>
      <xhtml:span class="hljs-bullet">-</xhtml:span> <xhtml:span class="hljs-string">.env</xhtml:span>
    <xhtml:span class="hljs-attr">volumes:</xhtml:span>
      <xhtml:span class="hljs-bullet">-</xhtml:span> <xhtml:span class="hljs-string">./var:/srv/var</xhtml:span>
      <xhtml:span class="hljs-bullet">-</xhtml:span> <xhtml:span class="hljs-string">./templates/email_confirmation_login.html.tmpl:/srv/email_confirmation_login.html.tmpl:ro</xhtml:span>
      <xhtml:span class="hljs-bullet">-</xhtml:span> <xhtml:span class="hljs-string">./templates/email_confirmation_subscription.html.tmpl:/srv/email_confirmation_subscription.html.tmpl:ro</xhtml:span>
      <xhtml:span class="hljs-bullet">-</xhtml:span> <xhtml:span class="hljs-string">./templates/email_reply.html.tmpl:/srv/email_reply.html.tmpl:ro</xhtml:span>
      <xhtml:span class="hljs-bullet">-</xhtml:span> <xhtml:span class="hljs-string">./templates/email_unsubscribe.html.tmpl:/srv/email_unsubscribe.html.tmpl:ro</xhtml:span>
      <xhtml:span class="hljs-bullet">-</xhtml:span> <xhtml:span class="hljs-string">./templates/error_response.html.tmpl:/srv/error_response.html.tmpl:ro</xhtml:span>
</xhtml:code></xhtml:pre>
<xhtml:p>A few remarks here:</xhtml:p>
<xhtml:ul>
<xhtml:li>I found that the "hostname" field needs to match the host that
the service will answer to publicly. This was not documented, but
until I made that change, it was inaccessible.</xhtml:li>
<xhtml:li>The service runs on port 8080 internally. I mapped that to a
port on my localhost, which I then reverse proxy to via Caddy (more
on that below).</xhtml:li>
<xhtml:li>There are a number of templates for the emails. I grabbed these
from the <xhtml:a href="https://github.com/umputun/remark42/tree/master/backend/app/templates/static">
Remark42 git repository</xhtml:a> and (minimally) customized them.</xhtml:li>
</xhtml:ul>
<xhtml:p>Configuration of Remark42 when run via Docker is done via
environment variables. I configured mine to allow email
authentication, as well as OAuth2 via GitHub (as a large number of
my readers have GitHub accounts). (You can also configure Google,
Facebook, Apple, Microsoft, Patreon, Discord, Telegram, and
Yandex.)</xhtml:p>
<xhtml:p>These were the values I configured; the <xhtml:a href="https://remark42.com/docs/configuration/parameters/">full list of
configuration values is in the documentation</xhtml:a>:</xhtml:p>
<xhtml:pre><xhtml:code class="language-bash hljs bash" data-lang="bash"><xhtml:span class="hljs-comment"># This is the actual URL to the Remark42 instance</xhtml:span>
REMARK_URL=

<xhtml:span class="hljs-comment"># This is an identifier used to allow the instance to handle multiple sites.</xhtml:span>
<xhtml:span class="hljs-comment"># It can be a single value, or multiple, comma-separated values</xhtml:span>
SITE=

<xhtml:span class="hljs-comment"># A shared secret key for signing JWTs</xhtml:span>
SECRET=

<xhtml:span class="hljs-comment"># Whether or not to log debug messages</xhtml:span>
DEBUG=<xhtml:span class="hljs-literal">false</xhtml:span>

<xhtml:span class="hljs-comment"># A comma-separated list of hosts that are allowed to interact with the service</xhtml:span>
ALLOWED_HOSTS=

<xhtml:span class="hljs-comment"># The "Same-Site" cookie policy to use.</xhtml:span>
<xhtml:span class="hljs-comment"># I discovered through trial and error it needed to be "none"</xhtml:span>
AUTH_SAME_SITE=none

<xhtml:span class="hljs-comment"># GITHUB config</xhtml:span>
<xhtml:span class="hljs-comment"># If you use GitHub for OAuth2, these are the client ID and secret, respectively</xhtml:span>
AUTH_GITHUB_CID=
AUTH_GITHUB_CSEC=

<xhtml:span class="hljs-comment"># EMAIL SERVER CONNECTION</xhtml:span>
SMTP_HOST=
SMTP_PORT=465
SMTP_TLS=<xhtml:span class="hljs-literal">true</xhtml:span>
SMTP_INSECURE_SKIP_VERIFY=<xhtml:span class="hljs-literal">false</xhtml:span>
SMTP_USERNAME=
SMTP_PASSWORD=

<xhtml:span class="hljs-comment"># USER NOTIFICATION</xhtml:span>
NOTIFY_USERS=email
<xhtml:span class="hljs-comment"># The "From" address when sending email notifications</xhtml:span>
NOTIFY_EMAIL_FROM=
<xhtml:span class="hljs-comment"># The email subject line for email verifications</xhtml:span>
NOTIFY_EMAIL_VERIFICATION_SUBJ=<xhtml:span class="hljs-string">"mwop.net comments email verification"</xhtml:span>

<xhtml:span class="hljs-comment"># ADMIN NOTIFICATIONS</xhtml:span>
NOTIFY_ADMINS=email
<xhtml:span class="hljs-comment"># The "From" address for admin notifications</xhtml:span>
NOTIFY_EMAIL_FROM=
<xhtml:span class="hljs-comment"># A single email or comma-separated list of emails to which to send admin</xhtml:span>
<xhtml:span class="hljs-comment"># notifications</xhtml:span>
ADMIN_SHARED_EMAIL=
<xhtml:span class="hljs-comment"># User identifiers for admin users, to enable admin features for those users</xhtml:span>
ADMIN_SHARED_ID=

<xhtml:span class="hljs-comment"># Enable email authentication</xhtml:span>
AUTH_EMAIL_ENABLE=<xhtml:span class="hljs-literal">true</xhtml:span>
<xhtml:span class="hljs-comment"># The "From" address when sending email verifications</xhtml:span>
AUTH_EMAIL_FROM=
<xhtml:span class="hljs-comment"># The email subject line when sending email confirmation</xhtml:span>
AUTH_EMAIL_SUBJ=<xhtml:span class="hljs-string">"mwop.net confirmation"</xhtml:span>

<xhtml:span class="hljs-comment"># MISC</xhtml:span>
EMOJI=<xhtml:span class="hljs-literal">true</xhtml:span>
</xhtml:code></xhtml:pre>
<xhtml:blockquote>
<xhtml:p>The <xhtml:code>ADMIN_SHARED_ID</xhtml:code> value must be setup AFTER
you've authenticated a user in the system. You can click on a user
from any comment thread, which opens a sidebar. At the top of the
sidebar is the user display name, as well as their identifier, and
you will copy this identifier to put in the
<xhtml:code>ADMIN_SHARED_ID</xhtml:code> field. When you do so, you'll need to
restart the container.</xhtml:p>
</xhtml:blockquote>
<xhtml:h3>Serving it with Caddy</xhtml:h3>
<xhtml:p>The Remark42 docs detail a number of different reverse proxy
setups for serving it, including <xhtml:a href="https://caddyserver.com">Caddy</xhtml:a>... but, interestingly, only
show Caddy configuration for serving it from a sub-path of an
existing site.</xhtml:p>
<xhtml:p>My Caddy configuration for this was very minimal:</xhtml:p>
<xhtml:pre><xhtml:code class="language-lua hljs lua" data-lang="lua">comments.mwop.net {
    reverse_proxy localhost:<xhtml:span class="hljs-number">9010</xhtml:span> {
        header_up X-Real-IP {remote}
        header_down Strict-Transport-Security <xhtml:span class="hljs-built_in">max</xhtml:span>-age=<xhtml:span class="hljs-number">3153600</xhtml:span>
    }
    header {
        Strict-Transport-Security <xhtml:span class="hljs-built_in">max</xhtml:span>-age=<xhtml:span class="hljs-number">3153600</xhtml:span>;
    }
}
</xhtml:code></xhtml:pre>
<xhtml:h3>Integrating with the application</xhtml:h3>
<xhtml:p>Once I had setup the server, I needed to integrate it in my
application, and this is where things got tricky. Why? Because of
some choices I've made along the way while developing my site:</xhtml:p>
<xhtml:ul>
<xhtml:li>I have a pretty robust Content Security Policy, and needed to
configure it to allow (a) pulling the JS for Remark42 from my
comments host, and (b) allow it to display frames from it.</xhtml:li>
<xhtml:li>I use HTMX, and have enabled <xhtml:a href="https://htmx.org/docs/#boosting">hx-boost</xhtml:a>, which means I need
to (a) load the Remark42 JS on every page, and (b) have some
functionality for re-initializing it when a user navigates to a new
page. Further, I use Webpack to concatenate and minimize my JS,
which means I'd need to ensure that this integration is done in a
way that will work correctly.</xhtml:li>
</xhtml:ul>
<xhtml:h4>Content Security Policy</xhtml:h4>
<xhtml:p>The values used in <xhtml:code>ALLOW_HOSTS</xhtml:code> are used to populate
a <xhtml:code>frame-ancestors</xhtml:code> Content Security Policy header by
the Remark42 server.</xhtml:p>
<xhtml:p>This is important.</xhtml:p>
<xhtml:p>If you are defining a Content Security Policy on your
application, you'll need to ensure that you have a
<xhtml:code>frame-src</xhtml:code> setting that allows your Remark42 server.
This is in addition to allowing it as a
<xhtml:code>script-src</xhtml:code>:</xhtml:p>
<xhtml:pre><xhtml:code class="language-http hljs http" data-lang="http"><xhtml:span class="hljs-attribute">Content-Security-Policy</xhtml:span>: script-src https://comments.mwop.net; frame-src https://comments.mwop.net
</xhtml:code></xhtml:pre>
<xhtml:p>I use <xhtml:a href="https://github.com/paragonie/csp-builder">paragonie/csp-builder</xhtml:a>,
and you can set these via the following:</xhtml:p>
<xhtml:pre><xhtml:code class="language-php hljs php" data-lang="php"><xhtml:span class="hljs-meta">&lt;?php</xhtml:span>
<xhtml:span class="hljs-keyword">use</xhtml:span> <xhtml:span class="hljs-title">ParagonIE</xhtml:span>\<xhtml:span class="hljs-title">CSPBuilder</xhtml:span>\<xhtml:span class="hljs-title">CSPBuilder</xhtml:span>;

$csp = <xhtml:span class="hljs-keyword">new</xhtml:span> CSPBuilder([
    <xhtml:span class="hljs-string">'frame-src'</xhtml:span> =&gt; [
        <xhtml:span class="hljs-string">'allow'</xhtml:span> =&gt; [
            <xhtml:span class="hljs-string">'https://comments.mwop.net'</xhtml:span>,
        ],
    ],
    <xhtml:span class="hljs-string">'script-src'</xhtml:span> =&gt; [
        <xhtml:span class="hljs-string">'allow'</xhtml:span> =&gt; [
            <xhtml:span class="hljs-string">'https://comments.mwop.net'</xhtml:span>,
        ],
    ],
])
</xhtml:code></xhtml:pre>
<xhtml:p>(I clearly have more in it than these values; this is to
illustrate the pieces necessary to integrate Remark42.)</xhtml:p>
<xhtml:h4>HTMX Integration</xhtml:h4>
<xhtml:p>To start, I added the following tag to the
<xhtml:code>&lt;head&gt;</xhtml:code> element of my layout:</xhtml:p>
<xhtml:pre><xhtml:code class="language-html hljs xml" data-lang="html"><xhtml:span class="hljs-tag">&lt;<xhtml:span class="hljs-name">script</xhtml:span> <xhtml:span class="hljs-attr">defer</xhtml:span> <xhtml:span class="hljs-attr">src</xhtml:span>=<xhtml:span class="hljs-string">"https://comments.mwop.net/web/embed.js"</xhtml:span>&gt;</xhtml:span><xhtml:span class="hljs-tag">&lt;/<xhtml:span class="hljs-name">script</xhtml:span>&gt;</xhtml:span>
</xhtml:code></xhtml:pre>
<xhtml:p>This ensures that the first page a user comes to on my site
loads the JS, but that it's done in the background.</xhtml:p>
<xhtml:p>Next, I needed to make a change to my site JavaScript.</xhtml:p>
<xhtml:p>I use Webpack, and have the following in my site JS, among other
things:</xhtml:p>
<xhtml:pre><xhtml:code class="language-javascript hljs javascript" data-lang="javascript"><xhtml:span class="hljs-built_in">window</xhtml:span>.htmx = <xhtml:span class="hljs-built_in">require</xhtml:span>(<xhtml:span class="hljs-string">"htmx.org"</xhtml:span>);
</xhtml:code></xhtml:pre>
<xhtml:p>I discovered through trial and error that you MUST define a
<xhtml:code>remark_config</xhtml:code> variable at the global (window) level of
your JS; otherwise, regardless of how you try and create a Remark42
instance, it will fail.</xhtml:p>
<xhtml:p>Additionally, I was going to need to (a) register a listener on
the <xhtml:code>REMARK42::ready</xhtml:code> event so it would initialize on an
initial page load, and (b) register a listener on the
<xhtml:code>htmx:load</xhtml:code> event so that it would re-initialize after a
page swap occurs.</xhtml:p>
<xhtml:p>The end result looks like this:</xhtml:p>
<xhtml:pre><xhtml:code class="language-javascript hljs javascript" data-lang="javascript"><xhtml:span class="hljs-keyword">const</xhtml:span> remark_config = {
    <xhtml:span class="hljs-attr">host</xhtml:span>: <xhtml:span class="hljs-string">"USE SAME VALUE AS 'REMARK_URL' CONFIG HERE"</xhtml:span>,
    <xhtml:span class="hljs-attr">site_id</xhtml:span>: <xhtml:span class="hljs-string">"USE A VALUE FROM 'SITE' CONFIG HERE"</xhtml:span>,
    <xhtml:span class="hljs-attr">theme</xhtml:span>: <xhtml:span class="hljs-string">"dark"</xhtml:span>, <xhtml:span class="hljs-comment">// can be light, dark, or system</xhtml:span>
    <xhtml:span class="hljs-attr">no_footer</xhtml:span>: <xhtml:span class="hljs-literal">true</xhtml:span>, <xhtml:span class="hljs-comment">// I didn't want to include the "powered by" footer</xhtml:span>
};

<xhtml:span class="hljs-keyword">let</xhtml:span> remark42Instance = <xhtml:span class="hljs-literal">null</xhtml:span>;

<xhtml:span class="hljs-keyword">const</xhtml:span> initComments = <xhtml:span class="hljs-function"><xhtml:span class="hljs-keyword">function</xhtml:span>() </xhtml:span>{
    <xhtml:span class="hljs-keyword">if</xhtml:span> (<xhtml:span class="hljs-built_in">window</xhtml:span>.REMARK42) {
        <xhtml:span class="hljs-keyword">if</xhtml:span> (remark42Instance) {
            remark42Instance.destroy();
        }

        node = <xhtml:span class="hljs-built_in">document</xhtml:span>.getElementById(<xhtml:span class="hljs-string">'remark42'</xhtml:span>);

        <xhtml:span class="hljs-keyword">if</xhtml:span> (node === <xhtml:span class="hljs-literal">null</xhtml:span>) {
            <xhtml:span class="hljs-keyword">return</xhtml:span>;
        }

        remark42Instance = <xhtml:span class="hljs-built_in">window</xhtml:span>.REMARK42.createInstance({
            <xhtml:span class="hljs-attr">node</xhtml:span>: node,
            ...remark_config,
        });
    }
};

<xhtml:span class="hljs-built_in">window</xhtml:span>.remark_config         = remark_config;
<xhtml:span class="hljs-built_in">window</xhtml:span>.htmx                  = <xhtml:span class="hljs-built_in">require</xhtml:span>(<xhtml:span class="hljs-string">"htmx.org"</xhtml:span>);
<xhtml:span class="hljs-built_in">window</xhtml:span>.addEventListener(<xhtml:span class="hljs-string">'REMARK42::ready'</xhtml:span>, () =&gt; {
    initComments();
});
<xhtml:span class="hljs-built_in">window</xhtml:span>.htmx.on(<xhtml:span class="hljs-string">"htmx:load"</xhtml:span>, () =&gt; {
    initComments();
});
</xhtml:code></xhtml:pre>
<xhtml:p>The <xhtml:code>initComments()</xhtml:code> function checks to see if a
global <xhtml:code>REMARK42</xhtml:code> object exists; this is registered by
the Remark42 JS when it loads, so if it doesn't exist, it means the
JS hasn't been loaded on the page yet. From there, it checks to see
if the <xhtml:code>remark42Instance</xhtml:code> variable is non-null, and, if
so, calls its <xhtml:code>destroy()</xhtml:code> method. Next, it checks to see
if a DOM element with the ID <xhtml:code>remark42</xhtml:code> is present; if
so, it creates a new Remark42 instance, passing that node and the
previously defined Remark42 configuration.</xhtml:p>
<xhtml:p>When the Remark42 JS emits the <xhtml:code>REMARK42::ready</xhtml:code>
event, or HTMX emits the <xhtml:code>htmx:load</xhtml:code> event, I trigger
the function. This ensures it's triggered on an initial page load,
and on any subsequent DOM load event triggered by HTMX.</xhtml:p>
<xhtml:h4>Adding comments to a page</xhtml:h4>
<xhtml:p>Now, to add comments to a page, I only need to add the
following:</xhtml:p>
<xhtml:pre><xhtml:code class="language-html hljs xml" data-lang="html"><xhtml:span class="hljs-tag">&lt;<xhtml:span class="hljs-name">div</xhtml:span> <xhtml:span class="hljs-attr">id</xhtml:span>=<xhtml:span class="hljs-string">"remark42"</xhtml:span>&gt;</xhtml:span><xhtml:span class="hljs-tag">&lt;/<xhtml:span class="hljs-name">div</xhtml:span>&gt;</xhtml:span>
</xhtml:code></xhtml:pre>
<xhtml:p>And I can use any tag I want here, just so long as the ID is
set.</xhtml:p>
<xhtml:h3>Final thoughts</xhtml:h3>
<xhtml:p>I'm fairly happy with this solution.</xhtml:p>
<xhtml:p>I didn't have to build it all myself, I keep ownership of the
data instead of handing it to a third-party, my users get
reasonable privacy (including the right to request deletion of all
their comments), and I get tools to moderate and manage
comments.</xhtml:p>
<xhtml:p>I'd love it if I didn't have to use JS for this, and could theme
the comments myself. That said, <xhtml:a href="https://remark42.com/docs/contributing/api/">Remark42 has an
API</xhtml:a>, so technically I could build some site integration that
delegates to it behind the scenes if I really want to. As it is,
the "dark" theme integrates reasonably well with my existing site
styles, so there's no immediate need for me to do this
currently.</xhtml:p>
<xhtml:p>Let me know what you think... comments are on, after all!</xhtml:p>
<xhtml:div class="h-entry"><xhtml:img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&amp;u=79dd2ea1d4d8855944715d09ee4c86215027fa80&amp;s=140" alt="matthew"/> <xhtml:a class="u-url u-uid p-name" href="https://mwop.net/blog/2025-01-25-comments-are-back.html">Comments
are Back</xhtml:a> was originally published <xhtml:time class="dt-published" datetime="2025-01-25T12:46:59-06:00">25 January 2025</xhtml:time> on
<xhtml:a href="https://mwop.net">https://mwop.net</xhtml:a> by <xhtml:a rel="author" class="p-author" href="https://mwop.net">Matthew Weier
O'Phinney</xhtml:a>.</xhtml:div>
</xhtml:div>
    </content>
  </entry>
  <entry xmlns:xhtml="http://www.w3.org/1999/xhtml">
    <title type="html"><![CDATA[Fixing issues with Yubico's PAM U2F bindings in version 1.3.1]]></title>
    <published>2025-01-15T14:31:20-06:00</published>
    <updated>2025-01-15T14:31:20-06:00</updated>
    <link rel="alternate" type="text/html" href="https://mwop.net/blog/2025-01-15-pam-yubikey-1.3.1-fix.html"/>
    <id>https://mwop.net/blog/2025-01-15-pam-yubikey-1.3.1-fix.html</id>
    <author>
      <name>Matthew Weier O'Phinney</name>
      <email>contact@mwop.net</email>
      <uri>https://mwop.net</uri>
    </author>
    <content xmlns:xhtml="http://www.w3.org/1999/xhtml" type="xhtml">
      <xhtml:div xmlns:xhtml="http://www.w3.org/1999/xhtml"><xhtml:p>I've been using a <xhtml:a href="https://www.yubico.com/products/yubikey-5-overview/">Yubikey</xhtml:a>
for years, now, and on each computer I use, I install their U2F
(Universal 2 Factor) bindings for the linux Pluggable
Authentication Modules (PAM) support, requiring usage of my Yubikey
for login and sudo access.</xhtml:p>
<xhtml:p>Today, I updated my work machine, and didn't even notice that
there were new pamu2fcfg and libpam-u2f packages, updating to
version 1.3.1; I never really care, as everything just works. But
when I came back to my machine after lunch, I was unable to login:
I'd provide my password, but my Yubikey wouldn't activate.</xhtml:p>
<xhtml:p>I tested it on my personal machine, and everything was working
fine. I tried pressing the key on my work machine, when in the
password field, and it pasted in the OTP code, so clearly there's
no USB issue.</xhtml:p>
<xhtml:p>So, after booting my rescue USB drive and disabling the U2F
support, I (a) discovered that I'd had updates for the PAM U2F
support earlier, and (b) searched for the phrase "yubikey pam u2f
1.3.1 breaks", which took me to <xhtml:a href="https://github.com/Yubico/pam-u2f/issues/330">this report</xhtml:a>.</xhtml:p>
<xhtml:p>The gist?</xhtml:p>
<xhtml:p><xhtml:strong>Due to a CVE, the PAM U2F bindings now require that the
<xhtml:code>u2f_keys</xhtml:code> file is writeable only by the
owner.</xhtml:strong></xhtml:p>
<xhtml:p>This can be accomplished pretty easily:</xhtml:p>
<xhtml:pre><xhtml:code class="language-bash hljs bash" data-lang="bash"><xhtml:span class="hljs-comment"># If you have systemwide keys:</xhtml:span>
sudo chmod g-w,o-w /etc/yubico/u2f_keys
<xhtml:span class="hljs-comment"># If you have per-user keys:</xhtml:span>
chmod g-w,o-w <xhtml:span class="hljs-variable">$HOME</xhtml:span>/.config/Yubico/u2f_keys
</xhtml:code></xhtml:pre>
<xhtml:p>Once I did that, I re-enabled my PAM U2F bindings, rebooted, and
all worked fine again.</xhtml:p>
<xhtml:h2>Final thoughts</xhtml:h2>
<xhtml:p>I rarely think about permissions in my
<xhtml:code>$HOME/.config</xhtml:code> directory, but I'm well aware that
configuration for things like SSH and GPG require similar
permissions masks. I think it's great that Yubico is doing this,
but (a) it should have likely been like this all along, and (b)
they really should have provided some sort of tooling or messaging
with the update to help folks fix permissions issues before they
become a problem. The fact that I only found out when I was unable
to login to my machine was horrible, and I feel incredibly
fortunate and privileged that I (a) had a rescue USB drive handy,
and (b) the knowledge of what I needed to do to disable U2F so I
could access my machine. Not all their users will be in that
position.</xhtml:p>
<xhtml:div class="h-entry"><xhtml:img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&amp;u=79dd2ea1d4d8855944715d09ee4c86215027fa80&amp;s=140" alt="matthew"/> <xhtml:a class="u-url u-uid p-name" href="https://mwop.net/blog/2025-01-15-pam-yubikey-1.3.1-fix.html">Fixing
issues with Yubico's PAM U2F bindings in version 1.3.1</xhtml:a> was
originally published <xhtml:time class="dt-published" datetime="2025-01-15T14:31:20-06:00">15 January 2025</xhtml:time> on <xhtml:a href="https://mwop.net">https://mwop.net</xhtml:a> by <xhtml:a rel="author" class="p-author" href="https://mwop.net">Matthew Weier
O'Phinney</xhtml:a>.</xhtml:div>
</xhtml:div>
    </content>
  </entry>
  <entry xmlns:xhtml="http://www.w3.org/1999/xhtml">
    <title type="html"><![CDATA[A Weekly Priority List in Logseq Journal View]]></title>
    <published>2024-12-17T09:01:32-06:00</published>
    <updated>2024-12-17T09:01:32-06:00</updated>
    <link rel="alternate" type="text/html" href="https://mwop.net/blog/2024-12-17-logseq-journal-priority-list.html"/>
    <id>https://mwop.net/blog/2024-12-17-logseq-journal-priority-list.html</id>
    <author>
      <name>Matthew Weier O'Phinney</name>
      <email>contact@mwop.net</email>
      <uri>https://mwop.net</uri>
    </author>
    <content xmlns:xhtml="http://www.w3.org/1999/xhtml" type="xhtml">
      <xhtml:div xmlns:xhtml="http://www.w3.org/1999/xhtml"><xhtml:p>I've been using Logseq for a few years now, and even <xhtml:a href="/blog/2023-12-01-advent-logseq.html">blogged about it last
year</xhtml:a>. I appreciate how it surfaces todo items on the upcoming
day(s), as well as anything I've given a due date in the coming
week. That said, one exercise I engage each week is a weekly
prioritization for the upcoming week, where I note down the items
most important for me to complete. While many of these might have
due dates, some might be aspirational or more along the lines of
things I can do when I have a few minutes of down time between
meetings.</xhtml:p>
<xhtml:p>For these, I generally keep a page in Logseq for the given week,
named something like "2024w50", and I've been adding that into my
"Favorites" list (and taking out the previous week's page!) so that
it's easy to navigate to. I also keep my weekly notes here; I have
to do weekly reports for each brand, so having a dedicated location
I can either tag in other notes or write them in directly is
useful.</xhtml:p>
<xhtml:p>While it's easy to open this page into a sidebar, I generally do
not want a sidebar open, as I find it distracting. So I've been
mulling over alternatives that would allow me to continue seeing my
priorities easily, while giving me the same ability to group them
by week.</xhtml:p>
<xhtml:h2>My solution</xhtml:h2>
<xhtml:p>The solution ended up being quite simple.</xhtml:p>
<xhtml:p>I'm now creating a structure like the following on that weekly
page:</xhtml:p>
<xhtml:pre><xhtml:code class="language-markdown hljs markdown" data-lang="markdown"><xhtml:span class="hljs-section">## Priorities</xhtml:span>
priorities:: 20241220
<xhtml:span class="hljs-bullet">
- </xhtml:span>First item in the list
<xhtml:span class="hljs-bullet">- </xhtml:span>Second item in the list
<xhtml:span class="hljs-bullet">- </xhtml:span>and so on
</xhtml:code></xhtml:pre>
<xhtml:p>In my Logseq <xhtml:code>config.edn</xhtml:code>, I've added the following
into my <xhtml:code>:default-queries { :journals: }</xhtml:code> section:</xhtml:p>
<xhtml:pre><xhtml:code class="language-clojure hljs clojure" data-lang="clojure">#+BEGIN_QUERY
{
  <xhtml:span class="hljs-symbol">:title</xhtml:span> <xhtml:span class="hljs-string">"PRIORITIES"</xhtml:span>
  <xhtml:span class="hljs-symbol">:inputs</xhtml:span> [<xhtml:span class="hljs-symbol">:today</xhtml:span> <xhtml:span class="hljs-symbol">:+1w</xhtml:span>]
  <xhtml:span class="hljs-symbol">:query</xhtml:span> [
    <xhtml:span class="hljs-symbol">:find</xhtml:span> (<xhtml:span class="hljs-name">pull</xhtml:span> ?b [*])
    <xhtml:span class="hljs-symbol">:in</xhtml:span> $ ?start ?end
    <xhtml:span class="hljs-symbol">:where</xhtml:span>
      [?b <xhtml:span class="hljs-symbol">:block/properties</xhtml:span> ?properties]
      [(<xhtml:span class="hljs-name"><xhtml:span class="hljs-builtin-name">get</xhtml:span></xhtml:span> ?properties <xhtml:span class="hljs-symbol">:priorities</xhtml:span>) ?priorities]
      [(<xhtml:span class="hljs-name"><xhtml:span class="hljs-builtin-name">&gt;=</xhtml:span></xhtml:span> ?priorities ?start)]
      [(<xhtml:span class="hljs-name"><xhtml:span class="hljs-builtin-name">&lt;</xhtml:span></xhtml:span> ?priorities ?end)]
  ]
}
#+END_QUERY
</xhtml:code></xhtml:pre>
<xhtml:p>This adds a block named "PRIORITIES" to the current journal,
pulling all blocks marked with the tag "priorities" where the date
is today or no more than a week in the future. Since I do my
planning only a week in advance, this ensures I typically only see
one such block on any given day, and ensures it's in front of me
each day as I start the journal. On Friday, I see the current
week's and the next week's priorities, allowing me to see what
slipped through the cracks as I plan, as well as get a handle on
what I want to accomplish next week.</xhtml:p>
<xhtml:div class="h-entry"><xhtml:img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&amp;u=79dd2ea1d4d8855944715d09ee4c86215027fa80&amp;s=140" alt="matthew"/> <xhtml:a class="u-url u-uid p-name" href="https://mwop.net/blog/2024-12-17-logseq-journal-priority-list.html">
A Weekly Priority List in Logseq Journal View</xhtml:a> was originally
published <xhtml:time class="dt-published" datetime="2024-12-17T09:01:32-06:00">17 December 2024</xhtml:time> on <xhtml:a href="https://mwop.net">https://mwop.net</xhtml:a> by <xhtml:a rel="author" class="p-author" href="https://mwop.net">Matthew Weier
O'Phinney</xhtml:a>.</xhtml:div>
</xhtml:div>
    </content>
  </entry>
  <entry xmlns:xhtml="http://www.w3.org/1999/xhtml">
    <title type="html"><![CDATA[Roundup of PHP 8.4 Posts]]></title>
    <published>2024-12-04T14:03:23-06:00</published>
    <updated>2024-12-13T11:00:23-06:00</updated>
    <link rel="alternate" type="text/html" href="https://mwop.net/blog/2024-12-04-php-8.4-roundup.html"/>
    <id>https://mwop.net/blog/2024-12-04-php-8.4-roundup.html</id>
    <author>
      <name>Matthew Weier O'Phinney</name>
      <email>contact@mwop.net</email>
      <uri>https://mwop.net</uri>
    </author>
    <content xmlns:xhtml="http://www.w3.org/1999/xhtml" type="xhtml">
      <xhtml:div xmlns:xhtml="http://www.w3.org/1999/xhtml"><xhtml:p>I've recently written several blog posts for <xhtml:a href="https://www.zend.com">Zend</xhtml:a> (one of the brands I help manage at
<xhtml:a href="https://www.perforce.com">Perforce</xhtml:a>) covering changes
in the recently released <xhtml:a href="https://www.php.net/releases/8.4/en.php">PHP 8.4</xhtml:a>. If you're
curious what to look out for, and how to use some of the new major
features, they're worth a read:</xhtml:p>
<xhtml:ul>
<xhtml:li><xhtml:a href="https://www.zend.com/blog/php-8-4">What's New in PHP
8.4: Features, Changes, and Deprecations</xhtml:a></xhtml:li>
<xhtml:li><xhtml:a href="https://www.zend.com/blog/php-8-4-property-hooks">A
Guide to PHP 8.4 Property Hooks</xhtml:a></xhtml:li>
<xhtml:li><xhtml:a href="https://www.zend.com/blog/php-asymmetric-visibility">Asymmetric
Visibility in PHP 8.4: What It Means for PHP Teams</xhtml:a></xhtml:li>
<xhtml:li><xhtml:a href="https://www.zend.com/blog/http-verbs-php-8-4">HTTP
Verbs Changes in PHP 8.4</xhtml:a></xhtml:li>
</xhtml:ul>
<xhtml:div class="h-entry"><xhtml:img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&amp;u=79dd2ea1d4d8855944715d09ee4c86215027fa80&amp;s=140" alt="matthew"/> <xhtml:a class="u-url u-uid p-name" href="https://mwop.net/blog/2024-12-04-php-8.4-roundup.html">Roundup of
PHP 8.4 Posts</xhtml:a> was originally published <xhtml:time class="dt-published" datetime="2024-12-04T14:03:23-06:00">4 December
2024</xhtml:time> on <xhtml:a href="https://mwop.net">https://mwop.net</xhtml:a> by
<xhtml:a rel="author" class="p-author" href="https://mwop.net">Matthew
Weier O'Phinney</xhtml:a>.</xhtml:div>
</xhtml:div>
    </content>
  </entry>
  <entry xmlns:xhtml="http://www.w3.org/1999/xhtml">
    <title type="html"><![CDATA[Collapsing the Vivaldi Tab Sidebar]]></title>
    <published>2024-10-25T08:16:06-05:00</published>
    <updated>2024-10-25T08:16:06-05:00</updated>
    <link rel="alternate" type="text/html" href="https://mwop.net/blog/2024-10-25-vivaldi-tab-sidebar-collapse.html"/>
    <id>https://mwop.net/blog/2024-10-25-vivaldi-tab-sidebar-collapse.html</id>
    <author>
      <name>Matthew Weier O'Phinney</name>
      <email>contact@mwop.net</email>
      <uri>https://mwop.net</uri>
    </author>
    <content xmlns:xhtml="http://www.w3.org/1999/xhtml" type="xhtml">
      <xhtml:div xmlns:xhtml="http://www.w3.org/1999/xhtml"><xhtml:p><xhtml:strong>tl;dr:</xhtml:strong> In Vivaldi, middle clicking the
separator between the tab sidebar and the web page will either
collapse or expand the sidebar; when collapsed, it shows just the
tab favicons, and the workspace selector icon.</xhtml:p>
<xhtml:p>If you want to know how I got to that point, read on.</xhtml:p>
<xhtml:p>I've been using Vivaldi the past nine months or so.</xhtml:p>
<xhtml:p>I'd used it once before, but had to abandon it due to issues I
was having with sites and applications I use for work, but tried it
again earlier this year, and found it (a) worked everywhere I
needed it, (b) performed better than any other browser I'd used,
and (c) had features I'd never seen in other browsers that were
hugely useful. (The quick search feature and workspaces are
amazing, if you've not tried them! And tab tiling has allowed me to
compare information or fill out forms in far easier ways.)</xhtml:p>
<xhtml:p>I also <xhtml:em>love</xhtml:em> having tabs in a sidebar instead of at the
top.</xhtml:p>
<xhtml:p>Vivaldi actually allows you to choose <xhtml:em>any</xhtml:em> side of the
screen for displaying tabs, and displaying them on the left or
right stacks them vertically in a sidebar.</xhtml:p>
<xhtml:p>When you get a dozen or more tabs open, the side-scrolling, top
tab bar becomes quickly untenable, as the tabs shrink and the
useful information contained in their titles disappears. Sure, I
could perform better "tab hygiene", but sometimes when you're in
the middle of a project, that's not an option. Having them listed
vertically in a sidebar means they are a consistent size, and easy
to locate.</xhtml:p>
<xhtml:p>There's one problem, though: they take up screen real
estate.</xhtml:p>
<xhtml:p>Most of the time, I don't actually need to see them; I only need
to see them if I'm trying to switch to a specific tab.</xhtml:p>
<xhtml:p>For a while, I used a solution that utilized an experimental
feature of the Vivaldi browser: CSS modifications. This feature
allows you to define a stylesheet that interacts with the browser
chrome. Coupled with a tool that allows you to inspect the
application's DOM itself, you can do a lot, and I found a mod
that:</xhtml:p>
<xhtml:ul>
<xhtml:li>Auto-hides the tab bar so that only the favicons show</xhtml:li>
<xhtml:li>Expands it when you mouse over the tab bar</xhtml:li>
</xhtml:ul>
<xhtml:p>This was pretty magical.</xhtml:p>
<xhtml:p>However, I noticed that if the load on my machine were high, or
there was a tab using a lot of resources, it could get pretty
laggy. Additionally, it meant that any time my cursor moved over
that area, it would expand — which might not be what I wanted if,
for instance, I was just moving my cursor between windows on the
screen.</xhtml:p>
<xhtml:p>But worse: it hid the workspaces feature, which I've started to
use to, well, do better tab hygiene. Workspaces allow me to group
related tabs, and then I can switch between workspaces within a
window. This keeps resource usage lower, and helps me focus
better.</xhtml:p>
<xhtml:p>But by hiding the tooling, I had to go through a lot of hoops to
use it, and I found I was avoiding the feature.</xhtml:p>
<xhtml:p>So I recently started looking to see if Vivaldi had added any
features to enable tab bar hiding natively, without a mod, and
discovered that it's basically existed all along; it just requires
a click.</xhtml:p>
<xhtml:p>Specifically, a middle-click, on the separator between the
sidebar and the web page.</xhtml:p>
<xhtml:p>Middle clicking on this separator will either collapse or expand
the sidebar; when collapsed, it shows just the tab favicons, and
the workspace selector icon.</xhtml:p>
<xhtml:p>While I'd love to see a keyboard shortcut for this, or even a
native auto-hide/expand feature, this is still an easy workflow to
adopt, and has been more predictable in usage.</xhtml:p>
<xhtml:div class="h-entry"><xhtml:img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&amp;u=79dd2ea1d4d8855944715d09ee4c86215027fa80&amp;s=140" alt="matthew"/> <xhtml:a class="u-url u-uid p-name" href="https://mwop.net/blog/2024-10-25-vivaldi-tab-sidebar-collapse.html">
Collapsing the Vivaldi Tab Sidebar</xhtml:a> was originally published
<xhtml:time class="dt-published" datetime="2024-10-25T08:16:06-05:00">25
October 2024</xhtml:time> on <xhtml:a href="https://mwop.net">https://mwop.net</xhtml:a> by <xhtml:a rel="author" class="p-author" href="https://mwop.net">Matthew Weier
O'Phinney</xhtml:a>.</xhtml:div>
</xhtml:div>
    </content>
  </entry>
  <entry xmlns:xhtml="http://www.w3.org/1999/xhtml">
    <title type="html"><![CDATA[Using resurrect.wezterm to manage Wezterm session state]]></title>
    <published>2024-10-21T17:28:21-05:00</published>
    <updated>2024-10-21T17:28:21-05:00</updated>
    <link rel="alternate" type="text/html" href="https://mwop.net/blog/2024-10-21-wezterm-resurrect.html"/>
    <id>https://mwop.net/blog/2024-10-21-wezterm-resurrect.html</id>
    <author>
      <name>Matthew Weier O'Phinney</name>
      <email>contact@mwop.net</email>
      <uri>https://mwop.net</uri>
    </author>
    <content xmlns:xhtml="http://www.w3.org/1999/xhtml" type="xhtml">
      <xhtml:div xmlns:xhtml="http://www.w3.org/1999/xhtml"><xhtml:p>One of my goals when adopting <xhtml:a href="https://wezfurlong.org/wezterm/index.html">Wezterm</xhtml:a> was to
replace tmux. To do that, I needed not just the ability to open
additional tabs/windows and to split into panes, but also a feature
I'd come to rely on heavily in the tmux ecosystem: session saving
and restoration, which I accomplished with the <xhtml:a href="https://github.com/tmux-plugins/tmux-resurrect">tmux-resurrect</xhtml:a>
plugin.</xhtml:p>
<xhtml:p>I tried a number of options, but was eventually pointed to
<xhtml:a href="https://github.com/MLFlexer/resurrect.wezterm">resurrect.wezterm</xhtml:a>.</xhtml:p>
<xhtml:p>In this post, I'll detail how I've configured it, as well as a
workflow I've developed for interacting with it that gives me (a)
reasonable satisfaction that I won't lose work, and (b) additional
flexibility for branching off work.</xhtml:p>
<xhtml:blockquote>
<xhtml:h2>A note on sharing sessions</xhtml:h2>
<xhtml:p>What this solution <xhtml:strong>does not do</xhtml:strong> is allow me to
share a Wezterm session, or open it simultaneously in another
wezterm session. For that, you need a Unix muxer session, and
resurrect.wezterm does not play well with it. As such, you will
need to do any such sessions without wezterm.resurrect.</xhtml:p>
</xhtml:blockquote>
<xhtml:h2>Configuration</xhtml:h2>
<xhtml:p>I've been finding it's most maintainable to throw my
configuration for specific plugins or features into their own
modules whenever possible. As such, I started by creating a
<xhtml:code>resurrect/config.lua</xhtml:code> file that would do the
following:</xhtml:p>
<xhtml:ul>
<xhtml:li>Setup encryption for the saved state files</xhtml:li>
<xhtml:li>Setup periodic saving, so I don't have to remember to save or
worry about what happens if there's a crash before I save.</xhtml:li>
<xhtml:li>Setup the maximum number of lines per pane to save.</xhtml:li>
<xhtml:li>Return the default keybinding I want to use with the
module.</xhtml:li>
</xhtml:ul>
<xhtml:p>That file ends up looking like the following:</xhtml:p>
<xhtml:pre><xhtml:code class="language-lua hljs lua" data-lang="lua"><xhtml:span class="hljs-comment">-- File: resurrect/config.lua</xhtml:span>
<xhtml:span class="hljs-comment">-- resurrect.wezterm configuration and settings</xhtml:span>
<xhtml:span class="hljs-comment">--</xhtml:span>
<xhtml:span class="hljs-comment">-- This module:</xhtml:span>
<xhtml:span class="hljs-comment">-- * Configures the resurrect.wezterm plugin</xhtml:span>
<xhtml:span class="hljs-comment">-- * Configures event listener configuration (via an additional required file)</xhtml:span>
<xhtml:span class="hljs-comment">-- * Returns wezterm keybinding configuration for resurrect-related actions.</xhtml:span>
<xhtml:span class="hljs-comment">--</xhtml:span>
<xhtml:span class="hljs-comment">-- The main wezterm configuration is then responsible for merging the</xhtml:span>
<xhtml:span class="hljs-comment">-- keybindings with other keybindings, or setting up its own.</xhtml:span>

<xhtml:span class="hljs-keyword">local</xhtml:span> <xhtml:span class="hljs-built_in">config</xhtml:span>    = {}
<xhtml:span class="hljs-keyword">local</xhtml:span> wezterm   = <xhtml:span class="hljs-built_in">require</xhtml:span> <xhtml:span class="hljs-string">'wezterm'</xhtml:span>
<xhtml:span class="hljs-keyword">local</xhtml:span> resurrect = wezterm.plugin.<xhtml:span class="hljs-built_in">require</xhtml:span>(<xhtml:span class="hljs-string">"https://github.com/MLFlexer/resurrect.wezterm"</xhtml:span>)

<xhtml:span class="hljs-comment">-- resurrect.wezterm encryption</xhtml:span>
<xhtml:span class="hljs-comment">-- Uncomment the following to use encryption.</xhtml:span>
<xhtml:span class="hljs-comment">-- If you do, ensure you have the age tool installed, you have created an</xhtml:span>
<xhtml:span class="hljs-comment">-- encryption key at ~/.config/age/wezterm-resurrect.txt, and that you supply</xhtml:span>
<xhtml:span class="hljs-comment">-- the associated public_key below</xhtml:span>
resurrect.set_encryption({
    enable      = <xhtml:span class="hljs-literal">true</xhtml:span>,
    method      = <xhtml:span class="hljs-string">"age"</xhtml:span>,
    private_key = wezterm.home_dir .. <xhtml:span class="hljs-string">"/.config/age/wezterm-resurrect.txt"</xhtml:span>,
    public_key  = <xhtml:span class="hljs-string">"THE-PUBLIC-KEY-VALUE-GOES-HERE"</xhtml:span>,
})

<xhtml:span class="hljs-comment">-- resurrect.wezterm periodic save every 5 minutes</xhtml:span>
resurrect.periodic_save({
    interval_seconds = <xhtml:span class="hljs-number">300</xhtml:span>,
    save_tabs = <xhtml:span class="hljs-literal">true</xhtml:span>,
    save_windows = <xhtml:span class="hljs-literal">true</xhtml:span>,
    save_workspaces = <xhtml:span class="hljs-literal">true</xhtml:span>,
})

<xhtml:span class="hljs-comment">-- Save only 5000 lines per pane</xhtml:span>
resurrect.set_max_nlines(<xhtml:span class="hljs-number">5000</xhtml:span>)

<xhtml:span class="hljs-comment">-- Default keybindings</xhtml:span>
<xhtml:span class="hljs-comment">-- These will need to be merged with the main wezterm keys.</xhtml:span>
<xhtml:span class="hljs-built_in">config</xhtml:span>.keys = {
    {
        <xhtml:span class="hljs-comment">-- Save current and window state</xhtml:span>
        <xhtml:span class="hljs-comment">-- See https://github.com/MLFlexer/resurrect.wezterm for options around</xhtml:span>
        <xhtml:span class="hljs-comment">-- saving workspace and window state separately</xhtml:span>
        key = <xhtml:span class="hljs-string">'S'</xhtml:span>,
        mods = <xhtml:span class="hljs-string">'LEADER|SHIFT'</xhtml:span>,
        action = wezterm.action_callback(<xhtml:span class="hljs-function"><xhtml:span class="hljs-keyword">function</xhtml:span><xhtml:span class="hljs-params">(win, pane)</xhtml:span></xhtml:span> <xhtml:span class="hljs-comment">-- luacheck: ignore 212</xhtml:span>
            <xhtml:span class="hljs-keyword">local</xhtml:span> state = resurrect.workspace_state.get_workspace_state()
            resurrect.save_state(state)
            resurrect.window_state.save_window_action()
        <xhtml:span class="hljs-keyword">end</xhtml:span>),
    },
    {
        <xhtml:span class="hljs-comment">-- Load workspace or window state, using a fuzzy finder</xhtml:span>
        key = <xhtml:span class="hljs-string">'L'</xhtml:span>,
        mods = <xhtml:span class="hljs-string">'LEADER|SHIFT'</xhtml:span>,
        action = wezterm.action_callback(<xhtml:span class="hljs-function"><xhtml:span class="hljs-keyword">function</xhtml:span><xhtml:span class="hljs-params">(win, pane)</xhtml:span></xhtml:span>
            resurrect.fuzzy_load(win, pane, <xhtml:span class="hljs-function"><xhtml:span class="hljs-keyword">function</xhtml:span><xhtml:span class="hljs-params">(id, label)</xhtml:span></xhtml:span> <xhtml:span class="hljs-comment">-- luacheck: ignore 212</xhtml:span>
                <xhtml:span class="hljs-keyword">local</xhtml:span> <xhtml:span class="hljs-built_in">type</xhtml:span> = <xhtml:span class="hljs-built_in">string</xhtml:span>.<xhtml:span class="hljs-built_in">match</xhtml:span>(id, <xhtml:span class="hljs-string">"^([^/]+)"</xhtml:span>) <xhtml:span class="hljs-comment">-- match before '/'</xhtml:span>
                id         = <xhtml:span class="hljs-built_in">string</xhtml:span>.<xhtml:span class="hljs-built_in">match</xhtml:span>(id, <xhtml:span class="hljs-string">"([^/]+)$"</xhtml:span>) <xhtml:span class="hljs-comment">-- match after '/'</xhtml:span>
                id         = <xhtml:span class="hljs-built_in">string</xhtml:span>.<xhtml:span class="hljs-built_in">match</xhtml:span>(id, <xhtml:span class="hljs-string">"(.+)%..+$"</xhtml:span>) <xhtml:span class="hljs-comment">-- remove file extension</xhtml:span>

                <xhtml:span class="hljs-keyword">local</xhtml:span> opts = {
                    window          = win:mux_window(),
                    relative        = <xhtml:span class="hljs-literal">true</xhtml:span>,
                    restore_text    = <xhtml:span class="hljs-literal">true</xhtml:span>,
                    on_pane_restore = resurrect.tab_state.default_on_pane_restore,
                }

                <xhtml:span class="hljs-keyword">if</xhtml:span> <xhtml:span class="hljs-built_in">type</xhtml:span> == <xhtml:span class="hljs-string">"workspace"</xhtml:span> <xhtml:span class="hljs-keyword">then</xhtml:span>
                    <xhtml:span class="hljs-keyword">local</xhtml:span> state = resurrect.load_state(id, <xhtml:span class="hljs-string">"workspace"</xhtml:span>)
                    resurrect.workspace_state.restore_workspace(state, opts)
                <xhtml:span class="hljs-keyword">elseif</xhtml:span> <xhtml:span class="hljs-built_in">type</xhtml:span> == <xhtml:span class="hljs-string">"window"</xhtml:span> <xhtml:span class="hljs-keyword">then</xhtml:span>
                    <xhtml:span class="hljs-keyword">local</xhtml:span> state = resurrect.load_state(id, <xhtml:span class="hljs-string">"window"</xhtml:span>)
                    <xhtml:span class="hljs-comment">-- opts.tab = win:active_tab()</xhtml:span>
                    resurrect.window_state.restore_window(pane:window(), state, opts)
                <xhtml:span class="hljs-keyword">elseif</xhtml:span> <xhtml:span class="hljs-built_in">type</xhtml:span> == <xhtml:span class="hljs-string">"tab"</xhtml:span> <xhtml:span class="hljs-keyword">then</xhtml:span>
                    <xhtml:span class="hljs-keyword">local</xhtml:span> state = resurrect.load_state(id, <xhtml:span class="hljs-string">"tab"</xhtml:span>)
                    resurrect.tab_state.restore_tab(pane:tab(), state, opts)
                <xhtml:span class="hljs-keyword">end</xhtml:span>
            <xhtml:span class="hljs-keyword">end</xhtml:span>)
        <xhtml:span class="hljs-keyword">end</xhtml:span>),
    },
    {
        <xhtml:span class="hljs-comment">-- Delete a saved session using a fuzzy finder</xhtml:span>
        key = <xhtml:span class="hljs-string">'d'</xhtml:span>,
        mods = <xhtml:span class="hljs-string">'LEADER|SHIFT'</xhtml:span>,
        action = wezterm.action_callback(<xhtml:span class="hljs-function"><xhtml:span class="hljs-keyword">function</xhtml:span><xhtml:span class="hljs-params">(win, pane)</xhtml:span></xhtml:span>
            resurrect.fuzzy_load(
                win,
                pane,
                <xhtml:span class="hljs-function"><xhtml:span class="hljs-keyword">function</xhtml:span><xhtml:span class="hljs-params">(id)</xhtml:span></xhtml:span>
                    resurrect.delete_state(id)
                <xhtml:span class="hljs-keyword">end</xhtml:span>,
                {
                    title             = <xhtml:span class="hljs-string">'Delete State'</xhtml:span>,
                    description       = <xhtml:span class="hljs-string">'Select session to delete and press Enter = accept, Esc = cancel, / = filter'</xhtml:span>,
                    fuzzy_description = <xhtml:span class="hljs-string">'Search session to delete: '</xhtml:span>,
                    is_fuzzy          = <xhtml:span class="hljs-literal">true</xhtml:span>,
                }
            )
        <xhtml:span class="hljs-keyword">end</xhtml:span>),
    }
}

<xhtml:span class="hljs-built_in">require</xhtml:span> <xhtml:span class="hljs-string">'resurrect/events'</xhtml:span>

<xhtml:span class="hljs-keyword">return</xhtml:span> <xhtml:span class="hljs-built_in">config</xhtml:span>
</xhtml:code></xhtml:pre>
<xhtml:p>I've setup keybindings that mimic what I had in tmux:</xhtml:p>
<xhtml:ul>
<xhtml:li>Leader-S will save the current workspace session</xhtml:li>
<xhtml:li>Leader-L will give me a way to select a workspace session to
load</xhtml:li>
<xhtml:li>Leader-D will give me a way to select a workspace session to
delete</xhtml:li>
</xhtml:ul>
<xhtml:p>(I map <xhtml:code>Leader</xhtml:code> to <xhtml:code>Ctrl-a</xhtml:code>, which is a
common convention in both <xhtml:code>screen</xhtml:code> and
<xhtml:code>tmux</xhtml:code>, so I don't lose any muscle memory here.)</xhtml:p>
<xhtml:p>In my main <xhtml:code>wezterm.lua</xhtml:code> file, I then make use of my
<xhtml:a href="/blog/2024-10-21-wezterm-keybindings">merge.all
function</xhtml:a> to merge the returned keybindings configuration:</xhtml:p>
<xhtml:pre><xhtml:code class="language-lua hljs lua" data-lang="lua"><xhtml:span class="hljs-keyword">local</xhtml:span> resurrect = <xhtml:span class="hljs-built_in">require</xhtml:span> <xhtml:span class="hljs-string">'resurrect/config'</xhtml:span>
<xhtml:span class="hljs-built_in">config</xhtml:span>.keys = merge.all(<xhtml:span class="hljs-built_in">config</xhtml:span>.keys, resurrect.keys)
</xhtml:code></xhtml:pre>
<xhtml:p>But what is that <xhtml:code>require 'resurrect/events'</xhtml:code> line
for?</xhtml:p>
<xhtml:h2>Listening to resurrect events</xhtml:h2>
<xhtml:p>When I save a session manually or load a session, I'd like some
notification that the operation was successful. resurrect.wezterm
emits a number of events for different workflow states, and you can
tie into those. I did exactly that, and had my handlers use my
<xhtml:a href="/blog/2024-10-21-wezterm-notify-send">notify.send</xhtml:a>
module to emit the notifications:</xhtml:p>
<xhtml:pre><xhtml:code class="language-lua hljs lua" data-lang="lua"><xhtml:span class="hljs-comment">-- File: resurrect/events.lua</xhtml:span>
<xhtml:span class="hljs-comment">-- resurrect.wezterm event listener configuration</xhtml:span>
<xhtml:span class="hljs-comment">--</xhtml:span>
<xhtml:span class="hljs-comment">-- This module configures event listeners for the resurrect.wezterm plugin.</xhtml:span>

<xhtml:span class="hljs-keyword">local</xhtml:span> wezterm               = <xhtml:span class="hljs-built_in">require</xhtml:span> <xhtml:span class="hljs-string">'wezterm'</xhtml:span>
<xhtml:span class="hljs-keyword">local</xhtml:span> notify                = <xhtml:span class="hljs-built_in">require</xhtml:span> <xhtml:span class="hljs-string">'../notify'</xhtml:span>
<xhtml:span class="hljs-keyword">local</xhtml:span> suppress_notification = <xhtml:span class="hljs-literal">false</xhtml:span>

wezterm.on(<xhtml:span class="hljs-string">'resurrect.error'</xhtml:span>, <xhtml:span class="hljs-function"><xhtml:span class="hljs-keyword">function</xhtml:span> <xhtml:span class="hljs-params">(error)</xhtml:span></xhtml:span>
    notify.send(<xhtml:span class="hljs-string">"Wezterm - ERROR"</xhtml:span>, <xhtml:span class="hljs-built_in">error</xhtml:span>, <xhtml:span class="hljs-string">'critical'</xhtml:span>)
<xhtml:span class="hljs-keyword">end</xhtml:span>)

wezterm.on(<xhtml:span class="hljs-string">'resurrect.periodic_save'</xhtml:span>, <xhtml:span class="hljs-function"><xhtml:span class="hljs-keyword">function</xhtml:span> <xhtml:span class="hljs-params">()</xhtml:span></xhtml:span>
    suppress_notification = <xhtml:span class="hljs-literal">true</xhtml:span>
<xhtml:span class="hljs-keyword">end</xhtml:span>)

wezterm.on(<xhtml:span class="hljs-string">'resurrect.save_state.finished'</xhtml:span>, <xhtml:span class="hljs-function"><xhtml:span class="hljs-keyword">function</xhtml:span> <xhtml:span class="hljs-params">(session_path)</xhtml:span></xhtml:span>
    <xhtml:span class="hljs-keyword">local</xhtml:span> is_workspace_save = session_path:<xhtml:span class="hljs-built_in">find</xhtml:span>(<xhtml:span class="hljs-string">"state/workspace"</xhtml:span>)

    <xhtml:span class="hljs-keyword">if</xhtml:span> is_workspace_save == <xhtml:span class="hljs-literal">nil</xhtml:span> <xhtml:span class="hljs-keyword">then</xhtml:span>
        <xhtml:span class="hljs-keyword">return</xhtml:span>
    <xhtml:span class="hljs-keyword">end</xhtml:span>

    <xhtml:span class="hljs-keyword">if</xhtml:span> suppress_notification <xhtml:span class="hljs-keyword">then</xhtml:span>
        suppress_notification = <xhtml:span class="hljs-literal">false</xhtml:span>
        <xhtml:span class="hljs-keyword">return</xhtml:span>
    <xhtml:span class="hljs-keyword">end</xhtml:span>

    <xhtml:span class="hljs-keyword">local</xhtml:span> <xhtml:span class="hljs-built_in">path</xhtml:span> = session_path:<xhtml:span class="hljs-built_in">match</xhtml:span>(<xhtml:span class="hljs-string">".+/([^+]+)$"</xhtml:span>)
    <xhtml:span class="hljs-keyword">local</xhtml:span> name = <xhtml:span class="hljs-built_in">path</xhtml:span>:<xhtml:span class="hljs-built_in">match</xhtml:span>(<xhtml:span class="hljs-string">"^(.+)%.json$"</xhtml:span>)
    notify.send(<xhtml:span class="hljs-string">"Wezterm - Save workspace"</xhtml:span>, <xhtml:span class="hljs-string">'Saved workspace '</xhtml:span> .. name .. <xhtml:span class="hljs-string">"\n\n"</xhtml:span> .. session_path)
<xhtml:span class="hljs-keyword">end</xhtml:span>)

wezterm.on(<xhtml:span class="hljs-string">'resurrect.load_state.finished'</xhtml:span>, <xhtml:span class="hljs-function"><xhtml:span class="hljs-keyword">function</xhtml:span><xhtml:span class="hljs-params">(name, type)</xhtml:span></xhtml:span>
    <xhtml:span class="hljs-keyword">local</xhtml:span> msg  = <xhtml:span class="hljs-string">'Completed loading '</xhtml:span> .. <xhtml:span class="hljs-built_in">type</xhtml:span> .. <xhtml:span class="hljs-string">' state: '</xhtml:span> .. name
    notify.send(<xhtml:span class="hljs-string">"Wezterm - Restore session"</xhtml:span>, msg)
<xhtml:span class="hljs-keyword">end</xhtml:span>)
</xhtml:code></xhtml:pre>
<xhtml:p>If you follow the write-up in the <xhtml:a href="https://github.com/MLFlexer/resurrect.wezterm/blob/main/README.md#events">
resurrect.wezterm README</xhtml:a> file, you'll note that my approach is
a bit different, particularly when it comes to tracking a periodic
save. Why? Well, when a periodic save happens, it saves the window,
the workspace, and all tabs, but each of those triggers separately,
and each triggers a <xhtml:code>resurrect.save_state.finished</xhtml:code>
event on completion. As a result, the periodic_save state often
gets reset by one of these events, which causes another event to
flow through. The upshot is that if I followed the documented
example, I was still seeing notifications each time
<xhtml:code>periodic_save</xhtml:code> would trigger. Since I'm really only
concerned about <xhtml:em>workspace</xhtml:em> save events, I've modified the
logic to only trigger if it's a workspace save, which I can
identify by searching for the string "state/workspace" in the
session filename passed to the callback. (It's not perfect, but
it's good enough for my needs.)</xhtml:p>
<xhtml:p>From there, I can do the normal logic around identifying if I'm
within a <xhtml:code>periodic_save</xhtml:code> event.</xhtml:p>
<xhtml:p>I've also done some logic so I can see the name of the session
at a glance, as well as see the full path to the file (for
debugging purposes).</xhtml:p>
<xhtml:p>One final thing to note is my handling of the
<xhtml:code>resurrect.error</xhtml:code> event. I use the "critical" urgency
flag to my <xhtml:code>notify.send</xhtml:code> functionality here so that the
notification requires manual dismissal. Doing this ensures I do not
miss these errors!</xhtml:p>
<xhtml:h2>Day to day usage</xhtml:h2>
<xhtml:p>The <xhtml:code>periodic_save</xhtml:code> settings mean that as I'm
working, Wezterm is periodically saving my open sessions, meaning I
never have to lose my place. But how do I restore?</xhtml:p>
<xhtml:p>The interesting thing about resurrect.wezterm is that when you
load a session, <xhtml:em>it does not change the current workspace
name</xhtml:em>. This means that if you start in the "default" workspace,
and then load your "project" workspace, <xhtml:em>you're still in the
"default" workspace, and that's where <xhtml:code>periodic_save</xhtml:code>
will save the workspace contents</xhtml:em>.</xhtml:p>
<xhtml:p>This may sound bad, but I've found in practice that it's
actually a huge benefit. <xhtml:strong>It allows me to "fork" my
workspace state.</xhtml:strong> I can name the workspace something
different, and its state will diverge... which allows me to go back
to the original state if I want to later.</xhtml:p>
<xhtml:p>So, my workflow has become:</xhtml:p>
<xhtml:ul>
<xhtml:li>Change the name of the current workspace workspace to what I
want it to be saved as eventually; this may be the name of a
previously saved session!</xhtml:li>
<xhtml:li>If I want to start from a previous saved session, load the
saved workspace.</xhtml:li>
<xhtml:li>Manually save only when I am planning to close a terminal
window or reboot, but otherwise have <xhtml:code>periodic_save</xhtml:code>
handle saving state.</xhtml:li>
</xhtml:ul>
<xhtml:p>I find this to be an improvement over tmux-resurrect!</xhtml:p>
<xhtml:h2>Final thoughts</xhtml:h2>
<xhtml:p>I've tried a few different approaches to saving sessions since
adopting Wezterm, but this is the one that has finally given me all
the features — and then some! — that I had in tmux-resurrect. While
being able to load and mirror an existing session would sometimes
be useful, it's a niche feature for me; I never do XP-style
programming anymore, and I rarely find myself in a position where I
want or need to load a current session into another window.
However, I've often wished I could go back in time to a previous
state, and this set of tools gives me that.</xhtml:p>
<xhtml:div class="h-entry"><xhtml:img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&amp;u=79dd2ea1d4d8855944715d09ee4c86215027fa80&amp;s=140" alt="matthew"/> <xhtml:a class="u-url u-uid p-name" href="https://mwop.net/blog/2024-10-21-wezterm-resurrect.html">Using
resurrect.wezterm to manage Wezterm session state</xhtml:a> was
originally published <xhtml:time class="dt-published" datetime="2024-10-21T17:28:21-05:00">21 October 2024</xhtml:time> on <xhtml:a href="https://mwop.net">https://mwop.net</xhtml:a> by <xhtml:a rel="author" class="p-author" href="https://mwop.net">Matthew Weier
O'Phinney</xhtml:a>.</xhtml:div>
</xhtml:div>
    </content>
  </entry>
  <entry xmlns:xhtml="http://www.w3.org/1999/xhtml">
    <title type="html"><![CDATA[Managing Wezterm Keybindings, or Merging with Lua]]></title>
    <published>2024-10-21T17:15:17-05:00</published>
    <updated>2024-10-21T17:15:17-05:00</updated>
    <link rel="alternate" type="text/html" href="https://mwop.net/blog/2024-10-21-wezterm-keybindings.html"/>
    <id>https://mwop.net/blog/2024-10-21-wezterm-keybindings.html</id>
    <author>
      <name>Matthew Weier O'Phinney</name>
      <email>contact@mwop.net</email>
      <uri>https://mwop.net</uri>
    </author>
    <content xmlns:xhtml="http://www.w3.org/1999/xhtml" type="xhtml">
      <xhtml:div xmlns:xhtml="http://www.w3.org/1999/xhtml"><xhtml:p>As I expand my <xhtml:a href="https://wezfurlong.org/wezterm/index.html">Wezterm</xhtml:a> usage, I
find that either (a) a third-party module will have default
keybinding configuration I want to adopt, and/or (b) I want to
segregate keybindings related to specific contexts into separate
modules to simplify my configuration.</xhtml:p>
<xhtml:p>Keybindings are stored as a list of tables (what we call
<xhtml:em>associative arrays</xhtml:em> in PHP). Simple, right?</xhtml:p>
<xhtml:p>Unlike in other languages I use, Lua doesn't have a built-in way
to merge lists.</xhtml:p>
<xhtml:p>So, I wrote up a re-usable function.</xhtml:p>
<xhtml:p>First, the file:</xhtml:p>
<xhtml:pre><xhtml:code class="language-lua hljs lua" data-lang="lua"><xhtml:span class="hljs-comment">-- File: merge.lua</xhtml:span>
<xhtml:span class="hljs-comment">-- Provide generalized functionality for merging tables</xhtml:span>

<xhtml:span class="hljs-keyword">local</xhtml:span> merge = {}

<xhtml:span class="hljs-function"><xhtml:span class="hljs-keyword">function</xhtml:span> <xhtml:span class="hljs-title">merge.all</xhtml:span><xhtml:span class="hljs-params">(base, overrides)</xhtml:span></xhtml:span>
    <xhtml:span class="hljs-keyword">local</xhtml:span> ret    = base <xhtml:span class="hljs-keyword">or</xhtml:span> {}
    <xhtml:span class="hljs-keyword">local</xhtml:span> second = overrides <xhtml:span class="hljs-keyword">or</xhtml:span> {}
    <xhtml:span class="hljs-keyword">for</xhtml:span> _, v <xhtml:span class="hljs-keyword">in</xhtml:span> <xhtml:span class="hljs-built_in">pairs</xhtml:span>(second) <xhtml:span class="hljs-keyword">do</xhtml:span> <xhtml:span class="hljs-built_in">table</xhtml:span>.<xhtml:span class="hljs-built_in">insert</xhtml:span>(ret, v) <xhtml:span class="hljs-keyword">end</xhtml:span>
    <xhtml:span class="hljs-keyword">return</xhtml:span> ret
<xhtml:span class="hljs-keyword">end</xhtml:span>

<xhtml:span class="hljs-keyword">return</xhtml:span> merge
</xhtml:code></xhtml:pre>
<xhtml:p>Then in my main <xhtml:code>wezterm.lua</xhtml:code>, I import it:</xhtml:p>
<xhtml:pre><xhtml:code class="language-lua hljs lua" data-lang="lua"><xhtml:span class="hljs-keyword">local</xhtml:span> merge = <xhtml:span class="hljs-built_in">require</xhtml:span> <xhtml:span class="hljs-string">'merge'</xhtml:span>
</xhtml:code></xhtml:pre>
<xhtml:p>My keybindings are in <xhtml:code>config.keys</xhtml:code>, which is
initialized as a list:</xhtml:p>
<xhtml:pre><xhtml:code class="language-lua hljs lua" data-lang="lua"><xhtml:span class="hljs-built_in">config</xhtml:span>.keys = {}
</xhtml:code></xhtml:pre>
<xhtml:p>Another module might return configuration, and I can merge the
keybindings it provides with what I have already defined:</xhtml:p>
<xhtml:pre><xhtml:code class="language-lua hljs lua" data-lang="lua"><xhtml:span class="hljs-built_in">config</xhtml:span>.keys = merge.all(<xhtml:span class="hljs-built_in">config</xhtml:span>.keys, smart_splits.keys)
</xhtml:code></xhtml:pre>
<xhtml:p>It's a simple piece of functionality, but it helps me keep
things organized and modular.</xhtml:p>
<xhtml:div class="h-entry"><xhtml:img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&amp;u=79dd2ea1d4d8855944715d09ee4c86215027fa80&amp;s=140" alt="matthew"/> <xhtml:a class="u-url u-uid p-name" href="https://mwop.net/blog/2024-10-21-wezterm-keybindings.html">Managing
Wezterm Keybindings, or Merging with Lua</xhtml:a> was originally
published <xhtml:time class="dt-published" datetime="2024-10-21T17:15:17-05:00">21 October 2024</xhtml:time> on <xhtml:a href="https://mwop.net">https://mwop.net</xhtml:a> by <xhtml:a rel="author" class="p-author" href="https://mwop.net">Matthew Weier
O'Phinney</xhtml:a>.</xhtml:div>
</xhtml:div>
    </content>
  </entry>
  <entry xmlns:xhtml="http://www.w3.org/1999/xhtml">
    <title type="html"><![CDATA[Wezterm GUI Notifications]]></title>
    <published>2024-10-21T17:01:18-05:00</published>
    <updated>2024-10-21T17:01:18-05:00</updated>
    <link rel="alternate" type="text/html" href="https://mwop.net/blog/2024-10-21-wezterm-notify-send.html"/>
    <id>https://mwop.net/blog/2024-10-21-wezterm-notify-send.html</id>
    <author>
      <name>Matthew Weier O'Phinney</name>
      <email>contact@mwop.net</email>
      <uri>https://mwop.net</uri>
    </author>
    <content xmlns:xhtml="http://www.w3.org/1999/xhtml" type="xhtml">
      <xhtml:div xmlns:xhtml="http://www.w3.org/1999/xhtml"><xhtml:p><xhtml:a href="https://wezfurlong.org/wezterm/index.html">Wezterm</xhtml:a>
has a utility for raising GUI system notifications, <xhtml:a href="https://wezfurlong.org/wezterm/config/lua/window/toast_notification.html">
window:toast_notification()</xhtml: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.</xhtml:p>
<xhtml: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.</xhtml:p>
<xhtml:p>So, I worked up my own utility.</xhtml:p>
<xhtml:h2>notify.send</xhtml:h2>
<xhtml:p>Since Wezterm uses Lua for configuration, configuration actually
also acts as an extension mechanism. The primary wezterm Lua module
itself provides a <xhtml:code>run_child_process</xhtml:code> function for
spawning a system process, which allows me to call on the system
<xhtml:code>notify-send</xhtml:code> utility.</xhtml:p>
<xhtml:p>As such, I wrote up the following Lua module:</xhtml:p>
<xhtml:pre><xhtml:code class="language-lua hljs lua" data-lang="lua"># File: notify.lua
<xhtml:span class="hljs-keyword">local</xhtml:span> wezterm = <xhtml:span class="hljs-built_in">require</xhtml:span> <xhtml:span class="hljs-string">'wezterm'</xhtml:span>
<xhtml:span class="hljs-keyword">local</xhtml:span> module  = {}

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

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

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

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

module.send = notify

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

notify.send(<xhtml:span class="hljs-string">'Subject Line'</xhtml:span>, <xhtml:span class="hljs-string">'This is the full message'</xhtml:span>, <xhtml:span class="hljs-string">'low'</xhtml:span>)
</xhtml:code></xhtml:pre>
<xhtml:p>Some notes on usage:</xhtml:p>
<xhtml:ul>
<xhtml:li>The <xhtml:code>urgency</xhtml:code> argument is one of 'low', 'normal', or
'critical', and defaults to 'normal'. These correspond to the same
<xhtml:code>--urgency</xhtml:code> option of <xhtml:code>notify-send</xhtml:code>, with the
following behavior:
<xhtml:ul>
<xhtml:li>'low' urgency messages are collected in the notification panel,
but not displayed.</xhtml:li>
<xhtml: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.</xhtml:li>
<xhtml:li>'critical' urgency messages require manual dismissal.</xhtml:li>
</xhtml:ul>
</xhtml:li>
<xhtml:li>The <xhtml:code>subject</xhtml:code> argument is used for the notification
subject; it's what you see without expanding the notification.</xhtml:li>
<xhtml:li>The <xhtml:code>msg</xhtml:code> argument is the full detail you want in
the notification, and is shown when you expand the
notification.</xhtml:li>
</xhtml:ul>
<xhtml:p><xhtml:code>notify-send</xhtml: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.</xhtml:p>
<xhtml:div class="h-entry"><xhtml:img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&amp;u=79dd2ea1d4d8855944715d09ee4c86215027fa80&amp;s=140" alt="matthew"/> <xhtml:a class="u-url u-uid p-name" href="https://mwop.net/blog/2024-10-21-wezterm-notify-send.html">Wezterm
GUI Notifications</xhtml:a> was originally published <xhtml:time class="dt-published" datetime="2024-10-21T17:01:18-05:00">21 October
2024</xhtml:time> on <xhtml:a href="https://mwop.net">https://mwop.net</xhtml:a> by
<xhtml:a rel="author" class="p-author" href="https://mwop.net">Matthew
Weier O'Phinney</xhtml:a>.</xhtml:div>
</xhtml:div>
    </content>
  </entry>
  <entry xmlns:xhtml="http://www.w3.org/1999/xhtml">
    <title type="html"><![CDATA[Escaping Regex Characters in Lua]]></title>
    <published>2024-10-18T10:56:33-05:00</published>
    <updated>2024-10-18T10:56:33-05:00</updated>
    <link rel="alternate" type="text/html" href="https://mwop.net/blog/2024-10-18-lua-regex-escape.html"/>
    <id>https://mwop.net/blog/2024-10-18-lua-regex-escape.html</id>
    <author>
      <name>Matthew Weier O'Phinney</name>
      <email>contact@mwop.net</email>
      <uri>https://mwop.net</uri>
    </author>
    <content xmlns:xhtml="http://www.w3.org/1999/xhtml" type="xhtml">
      <xhtml:div xmlns:xhtml="http://www.w3.org/1999/xhtml"><xhtml:p>Quick little note mainly for myself: Lua regex is different than
PCRE. The big place it differs is in where you escape pattern
matching characters (e.g. <xhtml:code>.</xhtml:code>, <xhtml:code>?</xhtml:code>,
<xhtml:code>+</xhtml:code>, etc.). In PCRE, you escape these with a leading
backslash (e.g., <xhtml:code>\.</xhtml:code>, <xhtml:code>\?</xhtml:code>,
<xhtml:code>\+</xhtml:code>). However, with Lua, you use the <xhtml:code>%</xhtml:code>
character: <xhtml:code>%.</xhtml:code>, <xhtml:code>%?</xhtml:code>, <xhtml:code>%+</xhtml:code>.</xhtml:p>
<xhtml:div class="h-entry"><xhtml:img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&amp;u=79dd2ea1d4d8855944715d09ee4c86215027fa80&amp;s=140" alt="matthew"/> <xhtml:a class="u-url u-uid p-name" href="https://mwop.net/blog/2024-10-18-lua-regex-escape.html">Escaping
Regex Characters in Lua</xhtml:a> was originally published <xhtml:time class="dt-published" datetime="2024-10-18T10:56:33-05:00">18 October
2024</xhtml:time> on <xhtml:a href="https://mwop.net">https://mwop.net</xhtml:a> by
<xhtml:a rel="author" class="p-author" href="https://mwop.net">Matthew
Weier O'Phinney</xhtml:a>.</xhtml:div>
</xhtml:div>
    </content>
  </entry>
  <entry xmlns:xhtml="http://www.w3.org/1999/xhtml">
    <title type="html"><![CDATA[Diagnosing Vivaldi resource usage]]></title>
    <published>2024-10-10T12:39:09-05:00</published>
    <updated>2024-10-10T12:39:09-05:00</updated>
    <link rel="alternate" type="text/html" href="https://mwop.net/blog/2024-10-10-vivaldi-task-manager.html"/>
    <id>https://mwop.net/blog/2024-10-10-vivaldi-task-manager.html</id>
    <author>
      <name>Matthew Weier O'Phinney</name>
      <email>contact@mwop.net</email>
      <uri>https://mwop.net</uri>
    </author>
    <content xmlns:xhtml="http://www.w3.org/1999/xhtml" type="xhtml">
      <xhtml:div xmlns:xhtml="http://www.w3.org/1999/xhtml"><xhtml:p>I recently noticed my CPU usage was high, and it was due to my
open Vivaldi browser. I wasn't sure what tab was causing the issue,
so I searched to see if Vivaldi had any tools for reporting
this.</xhtml:p>
<xhtml:p>It turns out that <xhtml:code>Shift-Esc</xhtml:code> will open a task
manager, and you can sort on any of:</xhtml:p>
<xhtml:ul>
<xhtml:li>Task (a string representing high level things like the browser
as a whole, GPU process, worker tabs, and more)</xhtml:li>
<xhtml:li>Memory footprint</xhtml:li>
<xhtml:li>CPU (this was what I was interested in!)</xhtml:li>
<xhtml:li>Network usage</xhtml:li>
<xhtml:li>Process ID</xhtml:li>
</xhtml:ul>
<xhtml:p>You can select any task to end its process.</xhtml:p>
<xhtml:p>I was able to quickly track down the issue to a background
worker running for a PWA window I'd closed earlier, and ended the
process.</xhtml:p>
<xhtml:div class="h-entry"><xhtml:img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&amp;u=79dd2ea1d4d8855944715d09ee4c86215027fa80&amp;s=140" alt="matthew"/> <xhtml:a class="u-url u-uid p-name" href="https://mwop.net/blog/2024-10-10-vivaldi-task-manager.html">Diagnosing
Vivaldi resource usage</xhtml:a> was originally published <xhtml:time class="dt-published" datetime="2024-10-10T12:39:09-05:00">10 October
2024</xhtml:time> on <xhtml:a href="https://mwop.net">https://mwop.net</xhtml:a> by
<xhtml:a rel="author" class="p-author" href="https://mwop.net">Matthew
Weier O'Phinney</xhtml:a>.</xhtml:div>
</xhtml:div>
    </content>
  </entry>
</feed>
