<?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 :: mwop.net</title>
    <description>Blog entries :: mwop.net</description>
    <pubDate>Sat, 25 Jan 2025 12:46:59 -0600</pubDate>
    <generator>Laminas_Feed_Writer 2 (https://getlaminas.org)</generator>
    <link>https://mwop.net/blog/</link>
    <atom:link rel="self" type="application/rss+xml" href="https://mwop.net/blog/rss.xml"/>
    <item>
      <title>Comments are Back</title>
      <pubDate>Sat, 25 Jan 2025 12:46:59 -0600</pubDate>
      <link>https://mwop.net/blog/2025-01-25-comments-are-back.html</link>
      <guid>https://mwop.net/blog/2025-01-25-comments-are-back.html</guid>
      <author>contact@mwop.net (Matthew Weier O'Phinney)</author>
      <dc:creator>Matthew Weier O'Phinney</dc:creator>
      <content:encoded><![CDATA[<p>For a number of years, I was using <a href="https://disqus.com">Disqus</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.</p>
<p>So last year, I removed comments from my site entirely.</p>
<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.</p>
<p>So I started thinking about how to go about adding comments again.</p>


<h3>Build it?</h3>
<p>I started, of course, by building.</p>
<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.</p>
<p>And then I realized a few things:</p>
<ul>
<li>I'd need an admin for moderating comments.</li>
<li>I'd likely need some way to notify at least myself when a new comment was added.</li>
<li>What about letting folks know their comment was submitted successfully?
Or that it was published?
(I planned to moderate all comments by default.)</li>
<li>What about letting folks know when a new comment was published on a post they'd previously commented on?</li>
<li>What about allowing folks to unsubscribe from those notifications?
And would I allow both granular (per blog post) and general (full site) unsubscription?</li>
<li>What about allowing folks to request all comments be deleted (per GDPR)?</li>
<li>What if I want to lock comments for a post? Or later unlock them?</li>
</ul>
<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.</p>
<h3>Research</h3>
<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.</p>
<p>I eventually settled on <a href="https://remark42.com">Remark42</a>.</p>
<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.</p>
<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.</p>
<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.</p>
<p>Better: it uses limited Markdown for comment styling, and has built-in links to documentation on what you can use.</p>
<h3>Configuring Remark42</h3>
<p>I run the service using Docker Compose, using a single service in my Compose file:</p>
<pre><code class="language-yaml hljs yaml" data-lang="yaml"><span class="hljs-attr">services:</span>
  <span class="hljs-attr">remark42:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">umputun/remark42:latest</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">"remark42"</span>
    <span class="hljs-attr">hostname:</span> <span class="hljs-string">"comments.mwop.net"</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">always</span>

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

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

    <span class="hljs-attr">env_file:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">.env</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./var:/srv/var</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./templates/email_confirmation_login.html.tmpl:/srv/email_confirmation_login.html.tmpl:ro</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./templates/email_confirmation_subscription.html.tmpl:/srv/email_confirmation_subscription.html.tmpl:ro</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./templates/email_reply.html.tmpl:/srv/email_reply.html.tmpl:ro</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./templates/email_unsubscribe.html.tmpl:/srv/email_unsubscribe.html.tmpl:ro</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./templates/error_response.html.tmpl:/srv/error_response.html.tmpl:ro</span>
</code></pre>
<p>A few remarks here:</p>
<ul>
<li>I found that the &quot;hostname&quot; 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.</li>
<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).</li>
<li>There are a number of templates for the emails.
I grabbed these from the <a href="https://github.com/umputun/remark42/tree/master/backend/app/templates/static">Remark42 git repository</a> and (minimally) customized them.</li>
</ul>
<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.)</p>
<p>These were the values I configured; the <a href="https://remark42.com/docs/configuration/parameters/">full list of configuration values is in the documentation</a>:</p>
<pre><code class="language-bash hljs bash" data-lang="bash"><span class="hljs-comment"># This is the actual URL to the Remark42 instance</span>
REMARK_URL=

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

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

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

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

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

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

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

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

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

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

<span class="hljs-comment"># MISC</span>
EMOJI=<span class="hljs-literal">true</span>
</code></pre>
<blockquote>
<p>The <code>ADMIN_SHARED_ID</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 <code>ADMIN_SHARED_ID</code> field.
When you do so, you'll need to restart the container.</p>
</blockquote>
<h3>Serving it with Caddy</h3>
<p>The Remark42 docs detail a number of different reverse proxy setups for serving it, including <a href="https://caddyserver.com">Caddy</a>... but, interestingly, only show Caddy configuration for serving it from a sub-path of an existing site.</p>
<p>My Caddy configuration for this was very minimal:</p>
<pre><code class="language-lua hljs lua" data-lang="lua">comments.mwop.net {
    reverse_proxy localhost:<span class="hljs-number">9010</span> {
        header_up X-Real-IP {remote}
        header_down Strict-Transport-Security <span class="hljs-built_in">max</span>-age=<span class="hljs-number">3153600</span>
    }
    header {
        Strict-Transport-Security <span class="hljs-built_in">max</span>-age=<span class="hljs-number">3153600</span>;
    }
}
</code></pre>
<h3>Integrating with the application</h3>
<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:</p>
<ul>
<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.</li>
<li>I use HTMX, and have enabled <a href="https://htmx.org/docs/#boosting">hx-boost</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.</li>
</ul>
<h4>Content Security Policy</h4>
<p>The values used in <code>ALLOW_HOSTS</code> are used to populate a <code>frame-ancestors</code> Content Security Policy header by the Remark42 server.</p>
<p>This is important.</p>
<p>If you are defining a Content Security Policy on your application, you'll need to ensure that you have a <code>frame-src</code> setting that allows your Remark42 server.
This is in addition to allowing it as a <code>script-src</code>:</p>
<pre><code class="language-http hljs http" data-lang="http"><span class="hljs-attribute">Content-Security-Policy</span>: script-src https://comments.mwop.net; frame-src https://comments.mwop.net
</code></pre>
<p>I use <a href="https://github.com/paragonie/csp-builder">paragonie/csp-builder</a>, and you can set these via the following:</p>
<pre><code class="language-php hljs php" data-lang="php"><span class="hljs-meta">&lt;?php</span>
<span class="hljs-keyword">use</span> <span class="hljs-title">ParagonIE</span>\<span class="hljs-title">CSPBuilder</span>\<span class="hljs-title">CSPBuilder</span>;

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

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

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

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

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

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

<span class="hljs-built_in">window</span>.remark_config         = remark_config;
<span class="hljs-built_in">window</span>.htmx                  = <span class="hljs-built_in">require</span>(<span class="hljs-string">"htmx.org"</span>);
<span class="hljs-built_in">window</span>.addEventListener(<span class="hljs-string">'REMARK42::ready'</span>, () =&gt; {
    initComments();
});
<span class="hljs-built_in">window</span>.htmx.on(<span class="hljs-string">"htmx:load"</span>, () =&gt; {
    initComments();
});
</code></pre>
<p>The <code>initComments()</code> function checks to see if a global <code>REMARK42</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 <code>remark42Instance</code> variable is non-null, and, if so, calls its <code>destroy()</code> method.
Next, it checks to see if a DOM element with the ID <code>remark42</code> is present; if so, it creates a new Remark42 instance, passing that node and the previously defined Remark42 configuration.</p>
<p>When the Remark42 JS emits the <code>REMARK42::ready</code> event, or HTMX emits the <code>htmx:load</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.</p>
<h4>Adding comments to a page</h4>
<p>Now, to add comments to a page, I only need to add the following:</p>
<pre><code class="language-html hljs xml" data-lang="html"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"remark42"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<p>And I can use any tag I want here, just so long as the ID is set.</p>
<h3>Final thoughts</h3>
<p>I'm fairly happy with this solution.</p>
<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.</p>
<p>I'd love it if I didn't have to use JS for this, and could theme the comments myself.
That said, <a href="https://remark42.com/docs/contributing/api/">Remark42 has an API</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 &quot;dark&quot; theme integrates reasonably well with my existing site styles, so there's no immediate need for me to do this currently.</p>
<p>Let me know what you think... comments are on, after all!</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-01-25-comments-are-back.html">Comments are Back</a> was originally
    published <time class="dt-published" datetime="2025-01-25T12:46:59-06:00">25 January 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>Fixing issues with Yubico's PAM U2F bindings in version 1.3.1</title>
      <pubDate>Wed, 15 Jan 2025 14:31:20 -0600</pubDate>
      <link>https://mwop.net/blog/2025-01-15-pam-yubikey-1.3.1-fix.html</link>
      <guid>https://mwop.net/blog/2025-01-15-pam-yubikey-1.3.1-fix.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 a <a href="https://www.yubico.com/products/yubikey-5-overview/">Yubikey</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.</p>
<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.</p>


<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.</p>
<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 &quot;yubikey pam u2f 1.3.1 breaks&quot;, which took me to <a href="https://github.com/Yubico/pam-u2f/issues/330">this report</a>.</p>
<p>The gist?</p>
<p><strong>Due to a CVE, the PAM U2F bindings now require that the <code>u2f_keys</code> file is writeable only by the owner.</strong></p>
<p>This can be accomplished pretty easily:</p>
<pre><code class="language-bash hljs bash" data-lang="bash"><span class="hljs-comment"># If you have systemwide keys:</span>
sudo chmod g-w,o-w /etc/yubico/u2f_keys
<span class="hljs-comment"># If you have per-user keys:</span>
chmod g-w,o-w <span class="hljs-variable">$HOME</span>/.config/Yubico/u2f_keys
</code></pre>
<p>Once I did that, I re-enabled my PAM U2F bindings, rebooted, and all worked fine again.</p>
<h2>Final thoughts</h2>
<p>I rarely think about permissions in my <code>$HOME/.config</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.</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-01-15-pam-yubikey-1.3.1-fix.html">Fixing issues with Yubico&#039;s PAM U2F bindings in version 1.3.1</a> was originally
    published <time class="dt-published" datetime="2025-01-15T14:31:20-06:00">15 January 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>A Weekly Priority List in Logseq Journal View</title>
      <pubDate>Tue, 17 Dec 2024 09:01:32 -0600</pubDate>
      <link>https://mwop.net/blog/2024-12-17-logseq-journal-priority-list.html</link>
      <guid>https://mwop.net/blog/2024-12-17-logseq-journal-priority-list.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 Logseq for a few years now, and even <a href="/blog/2023-12-01-advent-logseq.html">blogged about it last year</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.</p>


<p>For these, I generally keep a page in Logseq for the given week, named something like &quot;2024w50&quot;, and I've been adding that into my &quot;Favorites&quot; 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.</p>
<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.</p>
<h2>My solution</h2>
<p>The solution ended up being quite simple.</p>
<p>I'm now creating a structure like the following on that weekly page:</p>
<pre><code class="language-markdown hljs markdown" data-lang="markdown"><span class="hljs-section">## Priorities</span>
priorities:: 20241220
<span class="hljs-bullet">
- </span>First item in the list
<span class="hljs-bullet">- </span>Second item in the list
<span class="hljs-bullet">- </span>and so on
</code></pre>
<p>In my Logseq <code>config.edn</code>, I've added the following into my <code>:default-queries { :journals: }</code> section:</p>
<pre><code class="language-clojure hljs clojure" data-lang="clojure">#+BEGIN_QUERY
{
  <span class="hljs-symbol">:title</span> <span class="hljs-string">"PRIORITIES"</span>
  <span class="hljs-symbol">:inputs</span> [<span class="hljs-symbol">:today</span> <span class="hljs-symbol">:+1w</span>]
  <span class="hljs-symbol">:query</span> [
    <span class="hljs-symbol">:find</span> (<span class="hljs-name">pull</span> ?b [*])
    <span class="hljs-symbol">:in</span> $ ?start ?end
    <span class="hljs-symbol">:where</span>
      [?b <span class="hljs-symbol">:block/properties</span> ?properties]
      [(<span class="hljs-name"><span class="hljs-builtin-name">get</span></span> ?properties <span class="hljs-symbol">:priorities</span>) ?priorities]
      [(<span class="hljs-name"><span class="hljs-builtin-name">&gt;=</span></span> ?priorities ?start)]
      [(<span class="hljs-name"><span class="hljs-builtin-name">&lt;</span></span> ?priorities ?end)]
  ]
}
#+END_QUERY
</code></pre>
<p>This adds a block named &quot;PRIORITIES&quot; to the current journal, pulling all blocks marked with the tag &quot;priorities&quot; 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.</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-12-17-logseq-journal-priority-list.html">A Weekly Priority List in Logseq Journal View</a> was originally
    published <time class="dt-published" datetime="2024-12-17T09:01:32-06:00">17 December 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>Roundup of PHP 8.4 Posts</title>
      <pubDate>Fri, 13 Dec 2024 11:00:23 -0600</pubDate>
      <link>https://mwop.net/blog/2024-12-04-php-8.4-roundup.html</link>
      <guid>https://mwop.net/blog/2024-12-04-php-8.4-roundup.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 recently written several blog posts for <a href="https://www.zend.com">Zend</a> (one of the brands I help manage at <a href="https://www.perforce.com">Perforce</a>) covering changes in the recently released <a href="https://www.php.net/releases/8.4/en.php">PHP 8.4</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:</p>
<ul>
<li>
<a href="https://www.zend.com/blog/php-8-4">What's New in PHP 8.4: Features, Changes, and Deprecations</a>
</li>
<li>
<a href="https://www.zend.com/blog/php-8-4-property-hooks">A Guide to PHP 8.4 Property Hooks</a>
</li>
<li>
<a href="https://www.zend.com/blog/php-asymmetric-visibility">Asymmetric Visibility in PHP 8.4: What It Means for PHP Teams</a>
</li>
<li>
<a href="https://www.zend.com/blog/http-verbs-php-8-4">HTTP Verbs Changes in PHP 8.4</a>
</li>
</ul>




<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-12-04-php-8.4-roundup.html">Roundup of PHP 8.4 Posts</a> was originally
    published <time class="dt-published" datetime="2024-12-04T14:03:23-06:00">4 December 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>Collapsing the Vivaldi Tab Sidebar</title>
      <pubDate>Fri, 25 Oct 2024 08:16:06 -0500</pubDate>
      <link>https://mwop.net/blog/2024-10-25-vivaldi-tab-sidebar-collapse.html</link>
      <guid>https://mwop.net/blog/2024-10-25-vivaldi-tab-sidebar-collapse.html</guid>
      <author>contact@mwop.net (Matthew Weier O'Phinney)</author>
      <dc:creator>Matthew Weier O'Phinney</dc:creator>
      <content:encoded><![CDATA[<p><strong>tl;dr:</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.</p>
<p>If you want to know how I got to that point, read on.</p>


<p>I've been using Vivaldi the past nine months or so.</p>
<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.)</p>
<p>I also <em>love</em> having tabs in a sidebar instead of at the top.</p>
<p>Vivaldi actually allows you to choose <em>any</em> side of the screen for displaying tabs, and displaying them on the left or right stacks them vertically in a sidebar.</p>
<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 &quot;tab hygiene&quot;, 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.</p>
<p>There's one problem, though: they take up screen real estate.</p>
<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.</p>
<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:</p>
<ul>
<li>Auto-hides the tab bar so that only the favicons show</li>
<li>Expands it when you mouse over the tab bar</li>
</ul>
<p>This was pretty magical.</p>
<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.</p>
<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.</p>
<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.</p>
<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.</p>
<p>Specifically, a middle-click, on the separator between the sidebar and the web page.</p>
<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.</p>
<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.</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-25-vivaldi-tab-sidebar-collapse.html">Collapsing the Vivaldi Tab Sidebar</a> was originally
    published <time class="dt-published" datetime="2024-10-25T08:16:06-05:00">25 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>Using resurrect.wezterm to manage Wezterm session state</title>
      <pubDate>Mon, 21 Oct 2024 17:28:21 -0500</pubDate>
      <link>https://mwop.net/blog/2024-10-21-wezterm-resurrect.html</link>
      <guid>https://mwop.net/blog/2024-10-21-wezterm-resurrect.html</guid>
      <author>contact@mwop.net (Matthew Weier O'Phinney)</author>
      <dc:creator>Matthew Weier O'Phinney</dc:creator>
      <content:encoded><![CDATA[<p>One of my goals when adopting <a href="https://wezfurlong.org/wezterm/index.html">Wezterm</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 <a href="https://github.com/tmux-plugins/tmux-resurrect">tmux-resurrect</a> plugin.</p>
<p>I tried a number of options, but was eventually pointed to <a href="https://github.com/MLFlexer/resurrect.wezterm">resurrect.wezterm</a>.</p>
<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.</p>


<blockquote>
<h2>A note on sharing sessions</h2>
<p>What this solution <strong>does not do</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.</p>
</blockquote>
<h2>Configuration</h2>
<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 <code>resurrect/config.lua</code> file that would do the following:</p>
<ul>
<li>Setup encryption for the saved state files</li>
<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.</li>
<li>Setup the maximum number of lines per pane to save.</li>
<li>Return the default keybinding I want to use with the module.</li>
</ul>
<p>That file ends up looking like the following:</p>
<pre><code class="language-lua hljs lua" data-lang="lua"><span class="hljs-comment">-- File: resurrect/config.lua</span>
<span class="hljs-comment">-- resurrect.wezterm configuration and settings</span>
<span class="hljs-comment">--</span>
<span class="hljs-comment">-- This module:</span>
<span class="hljs-comment">-- * Configures the resurrect.wezterm plugin</span>
<span class="hljs-comment">-- * Configures event listener configuration (via an additional required file)</span>
<span class="hljs-comment">-- * Returns wezterm keybinding configuration for resurrect-related actions.</span>
<span class="hljs-comment">--</span>
<span class="hljs-comment">-- The main wezterm configuration is then responsible for merging the</span>
<span class="hljs-comment">-- keybindings with other keybindings, or setting up its own.</span>

<span class="hljs-keyword">local</span> <span class="hljs-built_in">config</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> resurrect = wezterm.plugin.<span class="hljs-built_in">require</span>(<span class="hljs-string">"https://github.com/MLFlexer/resurrect.wezterm"</span>)

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

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

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

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

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

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

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

<span class="hljs-keyword">return</span> <span class="hljs-built_in">config</span>
</code></pre>
<p>I've setup keybindings that mimic what I had in tmux:</p>
<ul>
<li>Leader-S will save the current workspace session</li>
<li>Leader-L will give me a way to select a workspace session to load</li>
<li>Leader-D will give me a way to select a workspace session to delete</li>
</ul>
<p>(I map <code>Leader</code> to <code>Ctrl-a</code>, which is a common convention in both <code>screen</code> and <code>tmux</code>, so I don't lose any muscle memory here.)</p>
<p>In my main <code>wezterm.lua</code> file, I then make use of my <a href="/blog/2024-10-21-wezterm-keybindings">merge.all function</a> to merge the returned keybindings configuration:</p>
<pre><code class="language-lua hljs lua" data-lang="lua"><span class="hljs-keyword">local</span> resurrect = <span class="hljs-built_in">require</span> <span class="hljs-string">'resurrect/config'</span>
<span class="hljs-built_in">config</span>.keys = merge.all(<span class="hljs-built_in">config</span>.keys, resurrect.keys)
</code></pre>
<p>But what is that <code>require 'resurrect/events'</code> line for?</p>
<h2>Listening to resurrect events</h2>
<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 <a href="/blog/2024-10-21-wezterm-notify-send">notify.send</a> module to emit the notifications:</p>
<pre><code class="language-lua hljs lua" data-lang="lua"><span class="hljs-comment">-- File: resurrect/events.lua</span>
<span class="hljs-comment">-- resurrect.wezterm event listener configuration</span>
<span class="hljs-comment">--</span>
<span class="hljs-comment">-- This module configures event listeners for the resurrect.wezterm plugin.</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> notify                = <span class="hljs-built_in">require</span> <span class="hljs-string">'../notify'</span>
<span class="hljs-keyword">local</span> suppress_notification = <span class="hljs-literal">false</span>

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

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

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

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

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

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

wezterm.on(<span class="hljs-string">'resurrect.load_state.finished'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(name, type)</span></span>
    <span class="hljs-keyword">local</span> msg  = <span class="hljs-string">'Completed loading '</span> .. <span class="hljs-built_in">type</span> .. <span class="hljs-string">' state: '</span> .. name
    notify.send(<span class="hljs-string">"Wezterm - Restore session"</span>, msg)
<span class="hljs-keyword">end</span>)
</code></pre>
<p>If you follow the write-up in the <a href="https://github.com/MLFlexer/resurrect.wezterm/blob/main/README.md#events">resurrect.wezterm README</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 <code>resurrect.save_state.finished</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 <code>periodic_save</code> would trigger.
Since I'm really only concerned about <em>workspace</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 &quot;state/workspace&quot; in the session filename passed to the callback.
(It's not perfect, but it's good enough for my needs.)</p>
<p>From there, I can do the normal logic around identifying if I'm within a <code>periodic_save</code> event.</p>
<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).</p>
<p>One final thing to note is my handling of the <code>resurrect.error</code> event. I use the &quot;critical&quot; urgency flag to my <code>notify.send</code> functionality here so that the notification requires manual dismissal.
Doing this ensures I do not miss these errors!</p>
<h2>Day to day usage</h2>
<p>The <code>periodic_save</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?</p>
<p>The interesting thing about resurrect.wezterm is that when you load a session, <em>it does not change the current workspace name</em>.
This means that if you start in the &quot;default&quot; workspace, and then load your &quot;project&quot; workspace, <em>you're still in the &quot;default&quot; workspace, and that's where <code>periodic_save</code> will save the workspace contents</em>.</p>
<p>This may sound bad, but I've found in practice that it's actually a huge benefit.
<strong>It allows me to &quot;fork&quot; my workspace state.</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.</p>
<p>So, my workflow has become:</p>
<ul>
<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!</li>
<li>If I want to start from a previous saved session, load the saved workspace.</li>
<li>Manually save only when I am planning to close a terminal window or reboot, but otherwise have <code>periodic_save</code> handle saving state.</li>
</ul>
<p>I find this to be an improvement over tmux-resurrect!</p>
<h2>Final thoughts</h2>
<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.</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-resurrect.html">Using resurrect.wezterm to manage Wezterm session state</a> was originally
    published <time class="dt-published" datetime="2024-10-21T17:28:21-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>Managing Wezterm Keybindings, or Merging with Lua</title>
      <pubDate>Mon, 21 Oct 2024 17:15:17 -0500</pubDate>
      <link>https://mwop.net/blog/2024-10-21-wezterm-keybindings.html</link>
      <guid>https://mwop.net/blog/2024-10-21-wezterm-keybindings.html</guid>
      <author>contact@mwop.net (Matthew Weier O'Phinney)</author>
      <dc:creator>Matthew Weier O'Phinney</dc:creator>
      <content:encoded><![CDATA[<p>As I expand my <a href="https://wezfurlong.org/wezterm/index.html">Wezterm</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.</p>
<p>Keybindings are stored as a list of tables (what we call <em>associative arrays</em> in PHP).
Simple, right?</p>
<p>Unlike in other languages I use, Lua doesn't have a built-in way to merge lists.</p>
<p>So, I wrote up a re-usable function.</p>


<p>First, the file:</p>
<pre><code class="language-lua hljs lua" data-lang="lua"><span class="hljs-comment">-- File: merge.lua</span>
<span class="hljs-comment">-- Provide generalized functionality for merging tables</span>

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

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

<span class="hljs-keyword">return</span> merge
</code></pre>
<p>Then in my main <code>wezterm.lua</code>, I import it:</p>
<pre><code class="language-lua hljs lua" data-lang="lua"><span class="hljs-keyword">local</span> merge = <span class="hljs-built_in">require</span> <span class="hljs-string">'merge'</span>
</code></pre>
<p>My keybindings are in <code>config.keys</code>, which is initialized as a list:</p>
<pre><code class="language-lua hljs lua" data-lang="lua"><span class="hljs-built_in">config</span>.keys = {}
</code></pre>
<p>Another module might return configuration, and I can merge the keybindings it provides with what I have already defined:</p>
<pre><code class="language-lua hljs lua" data-lang="lua"><span class="hljs-built_in">config</span>.keys = merge.all(<span class="hljs-built_in">config</span>.keys, smart_splits.keys)
</code></pre>
<p>It's a simple piece of functionality, but it helps me keep things organized and modular.</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-keybindings.html">Managing Wezterm Keybindings, or Merging with Lua</a> was originally
    published <time class="dt-published" datetime="2024-10-21T17:15:17-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 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>Escaping Regex Characters in Lua</title>
      <pubDate>Fri, 18 Oct 2024 10:56:33 -0500</pubDate>
      <link>https://mwop.net/blog/2024-10-18-lua-regex-escape.html</link>
      <guid>https://mwop.net/blog/2024-10-18-lua-regex-escape.html</guid>
      <author>contact@mwop.net (Matthew Weier O'Phinney)</author>
      <dc:creator>Matthew Weier O'Phinney</dc:creator>
      <content:encoded><![CDATA[<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. <code>.</code>, <code>?</code>, <code>+</code>, etc.). In PCRE, you escape these with a leading backslash (e.g., <code>\.</code>, <code>\?</code>, <code>\+</code>). However, with Lua, you use the <code>%</code> character: <code>%.</code>, <code>%?</code>, <code>%+</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/2024-10-18-lua-regex-escape.html">Escaping Regex Characters in Lua</a> was originally
    published <time class="dt-published" datetime="2024-10-18T10:56:33-05:00">18 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>Diagnosing Vivaldi resource usage</title>
      <pubDate>Thu, 10 Oct 2024 12:39:09 -0500</pubDate>
      <link>https://mwop.net/blog/2024-10-10-vivaldi-task-manager.html</link>
      <guid>https://mwop.net/blog/2024-10-10-vivaldi-task-manager.html</guid>
      <author>contact@mwop.net (Matthew Weier O'Phinney)</author>
      <dc:creator>Matthew Weier O'Phinney</dc:creator>
      <content:encoded><![CDATA[<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.</p>
<p>It turns out that <code>Shift-Esc</code> will open a task manager, and you can sort on any of:</p>
<ul>
<li>Task (a string representing high level things like the browser as a whole, GPU process, worker tabs, and more)</li>
<li>Memory footprint</li>
<li>CPU (this was what I was interested in!)</li>
<li>Network usage</li>
<li>Process ID</li>
</ul>
<p>You can select any task to end its process.</p>
<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.</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-10-vivaldi-task-manager.html">Diagnosing Vivaldi resource usage</a> was originally
    published <time class="dt-published" datetime="2024-10-10T12:39:09-05:00">10 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>
  </channel>
</rss>
