<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[twiddling bits]]></title><description><![CDATA[Systems from 1s and 0s]]></description><link>https://blog.jamesreed.dev</link><generator>RSS for Node</generator><lastBuildDate>Fri, 10 Apr 2026 18:06:16 GMT</lastBuildDate><atom:link href="https://blog.jamesreed.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Packaging apps for Linux with AppImage]]></title><description><![CDATA[AppImage is one of many popular “image”-based packaging options for modern Linux distros, along with flatpak and snap. I chose to package my software project with AppImage because the software is written in Go with Wails. It produces a single binary,...]]></description><link>https://blog.jamesreed.dev/packaging-apps-for-linux-with-appimage</link><guid isPermaLink="true">https://blog.jamesreed.dev/packaging-apps-for-linux-with-appimage</guid><category><![CDATA[Linux]]></category><category><![CDATA[appimage]]></category><dc:creator><![CDATA[James Reed]]></dc:creator><pubDate>Thu, 12 Sep 2024 15:00:13 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1726202915344/f29303a3-f1a0-41fc-abcb-7acc5133e128.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>AppImage is one of many popular “image”-based packaging options for modern Linux distros, along with flatpak and snap. I chose to package my software project with AppImage because the software is written in Go with <a target="_blank" href="https://wails.io/">Wails</a>. It produces a single binary, which I felt would require minimal packaging effort, and AppImage seemed like the easiest option. The binary still needs specific runtime libraries, and AppImage allows us to package these in the AppImage file, making it truly standalone. The best part is, modern tooling makes this process very straightforward.</p>
<p>First we’ll see how it all works, then I’ll describe an easy implementation of a build script used in my project 👉</p>
<h1 id="heading-building-appimages">Building AppImages</h1>
<p>The modern ecosystem of <a target="_blank" href="https://github.com/AppImage/AppImageKit">AppImage tooling</a> provides an essential tool, <code>appimagetool</code>, that can be included in a build pipeline to produce an AppImage file from a basic directory structure.</p>
<h2 id="heading-getting-the-tool">Getting the tool</h2>
<p>Downloading <code>appimagetool</code> for your architecture is easy. Here’s how you can do it using <code>curl</code>:</p>
<pre><code class="lang-bash">ARCH=<span class="hljs-string">"x86_64"</span>
APPIMAGETOOL=<span class="hljs-string">"appimagetool-<span class="hljs-variable">$ARCH</span>.AppImage"</span>

curl -L -o <span class="hljs-variable">$APPIMAGETOOL</span> https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-<span class="hljs-variable">$ARCH</span>.AppImage
chmod +x <span class="hljs-variable">$APPIMAGETOOL</span>
</code></pre>
<h2 id="heading-setting-up-the-app-directory">Setting up the app directory</h2>
<p>Now the tool can be used to create an AppImage file from an app directory. Setting up this directory is easy due to its minimal structure:</p>
<pre><code class="lang-bash">appdir/
    AppRun
    myapp.desktop
    myapp.png
</code></pre>
<p>The app directory only requires three files:</p>
<ul>
<li><p>An executable file named <code>AppRun</code></p>
<ul>
<li>This can be your application binary itself or a script that performs set up before executing your binary</li>
</ul>
</li>
<li><p>A <code>.desktop</code> file in the Desktop Entry specification format, used by desktop environments on Linux to launch applications</p>
</li>
<li><p>An icon PNG file</p>
</li>
</ul>
<p>Let’s explore the first two in more detail.</p>
<h3 id="heading-apprun-file">AppRun file</h3>
<p>This file is the entry point into your package executed by the AppImage package structure. There are two approaches to providing this file:</p>
<ol>
<li>Use a Bash script to perform setup before executing your binary:</li>
</ol>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/sh</span>

<span class="hljs-built_in">cd</span> <span class="hljs-string">"<span class="hljs-subst">$(dirname <span class="hljs-string">"<span class="hljs-variable">$0</span>"</span>)</span>"</span>
<span class="hljs-comment"># Setup here</span>
<span class="hljs-built_in">exec</span> ./myapp
</code></pre>
<p>This file needs to be made executable:</p>
<pre><code class="lang-bash">chmod +x appdir/AppRun
</code></pre>
<p>Next, simply move your binary into the directory so it will be executed by the script:</p>
<pre><code class="lang-bash">mv myapp appdir/myapp
</code></pre>
<ol start="2">
<li>The other option is to rename your binary executable to <code>AppRun</code> and have it executed directly:</li>
</ol>
<pre><code class="lang-bash">mv myapp appdir/AppRun
</code></pre>
<h3 id="heading-desktop-entry-file">Desktop entry file</h3>
<p>Desktop entry files have an INI-style format that looks like this:</p>
<pre><code class="lang-ini"><span class="hljs-section">[Desktop Entry]</span>
<span class="hljs-attr">Type</span>=Application
<span class="hljs-attr">Name</span>=MyApp
<span class="hljs-attr">Comment</span>=The next big thing
<span class="hljs-attr">Icon</span>=myapp
<span class="hljs-attr">Categories</span>=Utility
</code></pre>
<p>The key-value pairings are self-explanatory. The important things to note are:</p>
<ul>
<li><p>The <code>Icon</code> value must match the filename of your icon, i.e. <code>myapp</code> refers to <code>myapp.png</code> in the directory</p>
</li>
<li><p><code>appimagetool</code> requires the <code>Categories</code> key to be present. See a list of valid categories <a target="_blank" href="https://specifications.freedesktop.org/menu-spec/latest/category-registry.html">here</a></p>
</li>
</ul>
<p>Check out <a target="_blank" href="https://www.baeldung.com/linux/desktop-entry-files">this</a> tutorial for a more in-depth look at these files.</p>
<h2 id="heading-building-the-appimage">Building the AppImage</h2>
<p>With the tool downloaded and the app directory structure in place, producing an AppImage file requires only one command:</p>
<pre><code class="lang-bash">./<span class="hljs-variable">$APPIMAGETOOL</span> appdir
</code></pre>
<p>If the process succeeds, you will find the AppImage file in the current directory. The final directory set up would look like this (assuming a <code>x86_64</code> architecture):</p>
<pre><code class="lang-bash">myapp-project/
    appdir/
        AppRun
        myapp.desktop
        myapp.png
    appimagetool-x86_64.AppImage
    MyApp-x86_64.AppImage
</code></pre>
<p>Finally, make the AppImage file executable and run it:</p>
<pre><code class="lang-bash">chmod +x MyApp-x86_64.AppImage
./MyApp-x86_64.AppImage
</code></pre>
<h1 id="heading-makefile-implementation">Makefile implementation</h1>
<p>Makefiles may seem archaic to some, but I find they’re perfectly suited for declaring simple build pipelines like the one above. Here’s a Makefile I created for the Wails project:</p>
<pre><code class="lang-bash">ARCH ?= x86_64

BIN = build/bin/brainstack
APPIMAGETOOL = build/appimagetool-$(ARCH).AppImage
APPIMAGE = Brainstack-$(ARCH).AppImage

$(BIN):
    wails build

appimage: build/$(APPIMAGE)

$(APPIMAGETOOL):
    curl -L -o $(APPIMAGETOOL) https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-$(ARCH).AppImage
    chmod +x $(APPIMAGETOOL)

build/$(APPIMAGE): $(BIN) $(APPIMAGETOOL)
    cp $(BIN) appdir/AppRun
    ./$(APPIMAGETOOL) appdir
    chmod +x $(APPIMAGE)
    mv $(APPIMAGE) build

.PHONY: appimage
</code></pre>
<p>Makefiles follow a simple paradigm. Rules with targets and prerequisites are declared like this:</p>
<pre><code class="lang-makefile"><span class="hljs-section">targets: prerequisites</span>
    command
</code></pre>
<p>Let’s walk through what happens when we run the <code>appimage</code> rule found in the above Makefile:</p>
<pre><code class="lang-bash">make appimage
</code></pre>
<p>The first thing to note is that the target of this rule is phony, as declared at the bottom of the Makefile. This means the target doesn’t represent a real file, so it will always run when called from the command-line like above. This is useful for high-level rules intended to be run by hand or from another script. When the <code>appimage</code> rule runs, it checks its prerequisites to find that <code>build/$(APPIMAGE)</code> doesn’t exist, so it in turn runs the matching rule:</p>
<pre><code class="lang-makefile"><span class="hljs-section">build/$(APPIMAGE): <span class="hljs-variable">$(BIN)</span> <span class="hljs-variable">$(APPIMAGETOOL)</span></span>
    cp <span class="hljs-variable">$(BIN)</span> appdir/AppRun
    ./<span class="hljs-variable">$(APPIMAGETOOL)</span> appdir
    chmod +x <span class="hljs-variable">$(APPIMAGE)</span>
    mv <span class="hljs-variable">$(APPIMAGE)</span> build
</code></pre>
<p>Because this rule depends on the binary built by Wails and the downloaded tool, those rules will be triggered to produce the requisite files. Lastly, the rule’s commands are executed, which copy the application binary into the app directory, run the <code>appimagetool</code> to produce an AppImage file, make the file executable, and move the file into the <code>build</code> directory to keep the project directory clean. It’s that easy!</p>
<p>You can learn more about Makefiles <a target="_blank" href="https://makefiletutorial.com/">here</a>.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>Packaging software as AppImages is one of the easiest options available to us on modern Linux desktops. The simple build pipeline can be integrated into CI systems such as GitHub Actions to build and publish standalone AppImage files automatically when you release your software!</p>
<p>Check back for more articles on this topic as I explore it further.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">👀</div>
<div data-node-type="callout-text">By the way, you may have noticed my Wails project is called Brainstack. It’s a minimalist desktop app for intuitive task management, a.k.a. everyone’s favorite to-do list app! It’s still a work-in-progress at the time of writing, but feel free to check it out <a target="_blank" href="https://github.com/jcrd/brainstack">here</a>.</div>
</div>]]></content:encoded></item><item><title><![CDATA[Synchronizing and backing up data with Syncthing and rclone]]></title><description><![CDATA[In a world dominated by cloud-based software... it's still useful to set up custom synchronization and backup solutions! In my case, this necessity presented itself while using both Logseq and Obsidian, powerful and popular offline-first knowledge ma...]]></description><link>https://blog.jamesreed.dev/synchronizing-and-backing-up-data</link><guid isPermaLink="true">https://blog.jamesreed.dev/synchronizing-and-backing-up-data</guid><category><![CDATA[syncthing]]></category><category><![CDATA[rclone]]></category><category><![CDATA[Backup]]></category><category><![CDATA[Logseq]]></category><category><![CDATA[obsidian]]></category><dc:creator><![CDATA[James Reed]]></dc:creator><pubDate>Thu, 25 May 2023 11:00:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1684864433862/52b73a98-a8d2-4284-bb39-f9773119d892.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In a world dominated by cloud-based software... it's still useful to set up custom synchronization and backup solutions! In my case, this necessity presented itself while using both <a target="_blank" href="https://logseq.com/">Logseq</a> and <a target="_blank" href="https://obsidian.md/">Obsidian</a>, powerful and popular offline-first knowledge management tools. They both offer sync features as a paid service, but we can replicate much of their functionality using open-source tools for free!</p>
<p>In this article I will explore my solution to synchronize text file data between devices and additionally back this data up to <a target="_blank" href="https://www.google.com/drive/">Google Drive</a>. While this article focuses on Logseq as an example, the information presented here is easily adaptable to general synchronization and backup requirements.</p>
<p>My open-source utilities of choice for this purpose are:</p>
<ul>
<li><p><a target="_blank" href="https://syncthing.net/">Syncthing</a>, a "continuous file synchronization program"</p>
</li>
<li><p><a target="_blank" href="https://rclone.org/">rclone</a>, a "command-line program to manage files on cloud storage"</p>
</li>
</ul>
<p>My integrated solution involves the synchronization of data from two machines to a local server where cloud backups are performed. All of my devices run Linux, but this setup should be adaptable to other operating systems.</p>
<p>Let's take a look at how to set this up! 🛠️</p>
<h2 id="heading-synchronizing-with-syncthing">Synchronizing with Syncthing</h2>
<p>I suggest you begin with Syncthing's thorough <a target="_blank" href="https://docs.syncthing.net/intro/getting-started.html">Getting Started</a> guide. Here you will find installation instructions as well as introductions to its specific terminology including "local" and "remote" devices.</p>
<p>For my setup, I chose to sync the entire <code>~/Documents</code> directory, home to Logseq's data folder. The following screenshots are from the "local" device (my local home server) hosting a Syncthing instance in a container - this is why the folder path exists inside the <code>/config</code> directory.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684858351489/35faccdc-589c-4fc2-a907-7b94fb802e32.png" alt class="image--center mx-auto" /></p>
<p>I share this directory between two devices with the hostnames <code>intent</code> and <code>sol</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684858460051/035a89a2-abcb-43a5-b771-9ada03152074.png" alt class="image--center mx-auto" /></p>
<p>This approach means the two devices don't need to be powered on simultaneously to sync data between them. The local server acts as an "always on" intermediary.</p>
<p>For more information about running Syncthing in a container on a home server, check out <a target="_blank" href="https://twiddlingbits.net/setting-up-a-linux-home-server">my blog post</a> about using Ansible for precisely this purpose!</p>
<h2 id="heading-backing-up-with-rclone">Backing up with rclone</h2>
<p>While Syncthing ensures all devices on the local network have access to the same Logseq data, it's important to back this data up outside the network. I chose to use Google Drive for this purpose as I find it an easy-to-use option with generous free storage space. The featureful rclone tool interfaces with many other cloud storage options as well, so feel free to use any of its <a target="_blank" href="https://rclone.org/#providers">supported providers</a>.</p>
<h3 id="heading-setting-up-rclone">Setting up rclone</h3>
<p>First, install rclone using <a target="_blank" href="https://rclone.org/install/">these instructions</a>.</p>
<p>Next, initialize a new remote using rclone's interactive setup processing, following the instructions provided in its excellent <a target="_blank" href="https://rclone.org/drive/">Google Drive documentation</a>.</p>
<p>Now, you can backup the synchronized <code>Documents</code> directory using this rclone command (assuming Syncthing is running in a container with a <a target="_blank" href="https://docs.docker.com/storage/volumes/">volume</a> named <code>syncthing-config</code>, using a remote named "remote"):</p>
<pre><code class="lang-bash">rclone sync ~/.<span class="hljs-built_in">local</span>/share/containers/storage/volumes/syncthing-config/_data/Documents remote:Backup/Documents
</code></pre>
<p>The <code>~/.local/share/containers/...</code> path is where container volumes are stored by <a target="_blank" href="https://podman.io/">podman</a>, an alternative to Docker.</p>
<p>If you're synchronizing the entire <code>Documents</code> directory but you're not running Syncthing in a container, you can back up the <code>Documents</code> directory directly:</p>
<pre><code class="lang-bash">rclone sync ~/Documents remote:Backup/Documents
</code></pre>
<p>Backups reside in the <code>Backup</code> directory at the top level of my Google Drive. Here you can see the "brain" graph inside the <code>Documents/logseq</code> directory backed up as intended.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684860651124/6fa35714-f351-4995-bfcb-5e59bcdb24c8.png" alt class="image--center mx-auto" /></p>
<p>However, this required manually running the rclone command-line tool. Let's automate this process using <a target="_blank" href="https://fedoramagazine.org/systemd-timers-for-scheduling-tasks/">systemd timers</a>!</p>
<h3 id="heading-scheduling-backups">Scheduling backups</h3>
<p>First, create a new <a target="_blank" href="https://www.freedesktop.org/software/systemd/man/systemd.service.html">systemd service</a> file <code>~/.config/systemd/user/rclone-sync.service</code> containing this content (adapting the rclone command for your specific setup):</p>
<pre><code class="lang-ini"><span class="hljs-section">[Unit]</span>
<span class="hljs-attr">Description</span>=rclone backup

<span class="hljs-section">[Service]</span>
<span class="hljs-attr">Type</span>=<span class="hljs-literal">on</span>eshot
<span class="hljs-attr">ExecStart</span>=/usr/bin/rclone sync ~/.local/share/containers/storage/volumes/syncthing-config/_data/Documents remote:Backup/Documents
</code></pre>
<p>Next, create a <a target="_blank" href="https://www.freedesktop.org/software/systemd/man/systemd.timer.html">systemd timer</a> file <code>~/.config/systemd/user/rclone-sync.timer</code>. Note that the filename (minus the <code>.timer</code> extension) must match the service name, i.e. <code>rclone-sync</code>. The file will look like this:</p>
<pre><code class="lang-ini"><span class="hljs-section">[Unit]</span>
<span class="hljs-attr">Description</span>=rclone hourly backup

<span class="hljs-section">[Timer]</span>
<span class="hljs-attr">OnBootSec</span>=<span class="hljs-number">1</span>h
<span class="hljs-attr">OnUnitActiveSec</span>=<span class="hljs-number">1</span>h

<span class="hljs-section">[Install]</span>
<span class="hljs-attr">WantedBy</span>=timers.target
</code></pre>
<p>Here, the <code>OnBootSec=</code> and <code>OnUnitActiveSec=</code> options instruct systemd to run this timer an hour after the machine boots, then an hour after the last activation, so this timer will run continuously every hour. Should you prefer an alternative backup schedule, see <a target="_blank" href="https://www.freedesktop.org/software/systemd/man/systemd.time.html#">this documentation</a> for information about the available time specifications.</p>
<p>Finally, enable the timer with this command:</p>
<pre><code class="lang-bash">systemctl --user <span class="hljs-built_in">enable</span> rclone-sync.timer
</code></pre>
<p>You can observe the status of timers using this command:</p>
<pre><code class="lang-bash">systemctl --user list-timers
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684862883937/f52508f5-461b-4678-9246-62a90dcf5c46.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>The availability of open-source synchronization and backup tools allows anyone to create custom solutions for their given use case. With the setup explored here, Logseq data is automatically synchronized between devices through a home server where this data is backed up to the cloud so that your knowledge base is readily available on your network and securely stored outside of it for redundancy and peace of mind.</p>
<p>Embrace the synchronicity! 🥳</p>
]]></content:encoded></item><item><title><![CDATA[Getting started with GNOME Shell extension development]]></title><description><![CDATA[GNOME Shell is the graphical shell of the GNOME desktop environment: the default interface of the popular Fedora Linux distribution, and one of great prominence in the larger ecosystem.
It allows the use of extensions to modify its behavior, which so...]]></description><link>https://blog.jamesreed.dev/gnome-shell-extension-development</link><guid isPermaLink="true">https://blog.jamesreed.dev/gnome-shell-extension-development</guid><category><![CDATA[Linux]]></category><category><![CDATA[Gnome]]></category><category><![CDATA[Window Managers]]></category><category><![CDATA[JavaScript]]></category><dc:creator><![CDATA[James Reed]]></dc:creator><pubDate>Thu, 09 Mar 2023 18:05:38 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1678307451225/2d86ebc8-daa4-4de5-93a7-acd0a76f6a31.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://wiki.gnome.org/Projects/GnomeShell">GNOME Shell</a> is the graphical shell of the <strong>GNOME</strong> desktop environment: the default interface of the popular <a target="_blank" href="https://getfedora.org/">Fedora</a> Linux distribution, and one of great prominence in the larger ecosystem.</p>
<p>It allows the use of extensions to modify its behavior, which some contest as hacks, saying they monkey-patch the live shell engine, run unchecked by discrete APIs or permissions, and constantly break after updates due to their scopeless nature.</p>
<p>In response, I offer this sage wisdom: Let us not forgo what is for what could be. Instead, let's get hacking 🔧</p>
<h1 id="heading-first-steps">First steps</h1>
<p><strong>GNOME Shell</strong> includes a handy tool for scaffolding a new extension. Get to it with haste using the following command:</p>
<pre><code class="lang-bash">gnome-extensions create --interactive
</code></pre>
<p>You'll be walked through a few prompts to initialize an extension project and the main <code>extension.js</code> file should be automatically opened in your editor. If it's not, you can find the extension in the <code>~/.local/share/gnome-shell/extensions</code> directory with the UUID given in the interactive creation process.</p>
<p>You can find more information about this process <a target="_blank" href="https://gjs.guide/extensions/development/creating.html#gnome-extensions-tool">here</a>.</p>
<h1 id="heading-the-framework">The framework</h1>
<p><strong>GNOME Shell</strong> extensions are written in JavaScript and the <code>extension.js</code> file is home to your extension's code.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">As of GNOME Shell 45, extensions are ECMAScript modules. This primarily affects module imports, so the following is only applicable to extensions targeting GNOME Shell 45 or higher.</div>
</div>

<p>The file is populated with class-based scaffolding like this:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { Extension } <span class="hljs-keyword">from</span> <span class="hljs-string">'resource:///org/gnome/shell/extensions/extension.js'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyExtension</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Extension</span> </span>{
    <span class="hljs-keyword">constructor</span>(metadata) {
        <span class="hljs-built_in">super</span>(metadata);
    }

    enable() {
        <span class="hljs-built_in">this</span>._settings = <span class="hljs-built_in">this</span>.getSettings();
    }

    disable() {
        <span class="hljs-built_in">this</span>._settings = <span class="hljs-literal">null</span>;
    }
}
</code></pre>
<p>This simple framework determines the lifecycle of your extension:</p>
<ul>
<li><p>Extending the <code>Extension</code> class allows access to functions such as <code>this.getSettings()</code></p>
</li>
<li><p>The <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/constructor"><code>constructor</code></a> method is a standard JavaScript feature used to initialize an instance of a class</p>
<ul>
<li>The parent class, <code>Extension</code>, must also be constructed using <code>super(metadata)</code></li>
</ul>
</li>
</ul>
<ul>
<li><p>The <code>enable</code> and <code>disable</code> class methods are called when your extension is enabled and disabled in the <strong>GNOME Extensions</strong> app, respectively</p>
<ul>
<li><p><code>enable</code> is responsible for integrating into <strong>GNOME Shell</strong> to establish your extension's functionality</p>
</li>
<li><p><code>disable</code> is responsible for removing this integration so your extension no longer interferes</p>
</li>
</ul>
</li>
</ul>
<p>For more information about the changes to module imports made in GNOME Shell 45, see <a target="_blank" href="https://gjs.guide/extensions/upgrading/gnome-shell-45.html#esm">this guide</a> to porting extensions.</p>
<h1 id="heading-whats-in-an-extension">What's in an extension?</h1>
<p>As mentioned before, <strong>GNOME Shell</strong> extensions are essentially scopeless—in other words, they have boundless capabilities. The <a target="_blank" href="https://gjs.guide/">Guide to JavaScript for Gnome</a> <a target="_blank" href="https://gjs.guide/extensions/overview/architecture.html#extensions">cites</a> that extensions can:</p>
<blockquote>
<ul>
<li><p>Use Mutter to control displays, workspaces and windows</p>
</li>
<li><p>Use Clutter &amp; St to create new UI elements</p>
</li>
<li><p>Use JavaScript modules to create new UI elements</p>
</li>
<li><p>Use any other library supporting GObject-Introspection</p>
</li>
<li><p>Access and modify any internal GNOME Shell code</p>
</li>
</ul>
</blockquote>
<p>Good luck with all that! My experience creating extensions is much more limited, having built an extension that only performs basic window management. Let's look into it as a narrow example of extension development.</p>
<h2 id="heading-an-extension-case-study">An extension case study</h2>
<p>My extension is called Fifty Fifty and its code is available <a target="_blank" href="https://github.com/jcrd/gnome-extension-fiftyfifty">here</a>. Its primary function is to resize two windows to fit side-by-side. This is easily accomplished with the following code:</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">placeWindow</span>(<span class="hljs-params">window, r</span>) </span>{
  <span class="hljs-built_in">window</span>.unmaximize(Meta.MaximizeFlags.HORIZONTAL)
  <span class="hljs-built_in">window</span>.unmaximize(Meta.MaximizeFlags.VERTICAL)
  <span class="hljs-built_in">window</span>.move_resize_frame(<span class="hljs-literal">false</span>, r.x, r.y, r.width, r.height)
}
</code></pre>
<p>The complexity comes from determining which windows to tile. This depends on the history of focused windows on a workspace, which (as far as I can tell) is not stored in a manner accessible to extensions. The bulk of the extension's code is related to connecting and disconnecting signals to track and record the focus history.</p>
<p>This occurs in the extension's <code>enable</code> method, like this:</p>
<pre><code class="lang-javascript">enable() {
    <span class="hljs-built_in">this</span>.globalSignals[<span class="hljs-number">0</span>] = <span class="hljs-built_in">global</span>.workspace_manager.connect(
      <span class="hljs-string">"workspace-added"</span>,
      <span class="hljs-function">(<span class="hljs-params">_, index</span>) =&gt;</span> <span class="hljs-built_in">this</span>._onWorkspaceAdded(index)
    )
    <span class="hljs-built_in">this</span>.globalSignals[<span class="hljs-number">1</span>] = <span class="hljs-built_in">global</span>.workspace_manager.connect(
      <span class="hljs-string">"workspace-removed"</span>,
      <span class="hljs-function">(<span class="hljs-params">_, index</span>) =&gt;</span> <span class="hljs-built_in">this</span>._onWorkspaceRemoved(index)
    )

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-built_in">global</span>.workspace_manager.n_workspaces; i++) {
      <span class="hljs-built_in">this</span>._onWorkspaceAdded(i)
    }

    <span class="hljs-comment">// We'll look at the remaining code later...</span>
}
</code></pre>
<p>The signal connection IDs must be saved to remove them in the <code>disable</code> method.</p>
<p>In essence, this code sets up signals to react to the addition and removal of workspaces, which triggers the connection and disconnection of more signals responsible for reacting to the addition and removal of windows to this workspace.</p>
<p>Here's the <code>_onWorkspaceAdded</code> method:</p>
<pre><code class="lang-javascript">_onWorkspaceAdded(index) {
    <span class="hljs-keyword">const</span> workspace = <span class="hljs-built_in">global</span>.workspace_manager
        .get_workspace_by_index(index)
    <span class="hljs-keyword">const</span> data = { <span class="hljs-attr">focusHistory</span>: [], <span class="hljs-attr">signals</span>: [] }

    data.signals[<span class="hljs-number">0</span>] = 
        workspace.connect(<span class="hljs-string">"window-added"</span>, <span class="hljs-function">(<span class="hljs-params">_, <span class="hljs-built_in">window</span></span>) =&gt;</span>
        <span class="hljs-built_in">this</span>._onWindowAdded(<span class="hljs-built_in">window</span>)
    )
    data.signals[<span class="hljs-number">1</span>] = 
        workspace.connect(<span class="hljs-string">"window-removed"</span>, <span class="hljs-function">(<span class="hljs-params">_, <span class="hljs-built_in">window</span></span>) =&gt;</span>
        <span class="hljs-built_in">this</span>._onWindowRemoved(<span class="hljs-built_in">window</span>)
    )
    <span class="hljs-built_in">this</span>.workspaces.set(workspace, data)

    workspace.list_windows()
        .forEach(<span class="hljs-function">(<span class="hljs-params"><span class="hljs-built_in">window</span></span>) =&gt;</span> <span class="hljs-built_in">this</span>._onWindowAdded(<span class="hljs-built_in">window</span>))
}
</code></pre>
<p>Again, the signal IDs are stored in the <code>data.signals</code> array to be later disconnected in the <code>_onWorkspaceRemove</code> method.</p>
<p>Ultimately, it's the <code>_onWindowAdded</code> method that establishes the focus history tracking:</p>
<pre><code class="lang-javascript">_onWindowAdded(<span class="hljs-built_in">window</span>) {
    <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">window</span> || <span class="hljs-built_in">window</span>.get_window_type() !== Meta.WindowType.NORMAL)
        <span class="hljs-keyword">return</span>

    <span class="hljs-built_in">this</span>.windowSignals.set(
      <span class="hljs-built_in">window</span>,
      <span class="hljs-built_in">window</span>.connect(<span class="hljs-string">"focus"</span>, <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">this</span>.updateFocusHistory(<span class="hljs-built_in">window</span>))
    )

    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">window</span>.has_focus()) <span class="hljs-built_in">this</span>.updateFocusHistory(<span class="hljs-built_in">window</span>)
}
</code></pre>
<p>Now, all of this orchestration would be useless without some means of activating the fifty-fifty tiling.</p>
<p>This is accomplished by defining a keybinding in the aforementioned <code>enable</code> method:</p>
<pre><code class="lang-javascript">Main.wm.addKeybinding(<span class="hljs-string">"toggle"</span>, settings, flag, mode, <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> workspace = <span class="hljs-built_in">global</span>.workspace_manager.get_active_workspace()
    <span class="hljs-keyword">const</span> windows = <span class="hljs-built_in">this</span>.workspaces
      .get(workspace)
      .focusHistory.slice(<span class="hljs-number">0</span>, <span class="hljs-number">2</span>)
      .reverse()

    <span class="hljs-keyword">if</span> (windows.length === <span class="hljs-number">1</span>) {
      placeWindow(windows[<span class="hljs-number">0</span>], generateRects(windows[<span class="hljs-number">0</span>], <span class="hljs-number">0</span>).workspace)
      <span class="hljs-keyword">return</span>
    }

    <span class="hljs-keyword">let</span> cachedRects = []
    <span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>

    <span class="hljs-keyword">const</span> fullscreen = windows.reduce(<span class="hljs-function">(<span class="hljs-params">state, <span class="hljs-built_in">window</span></span>) =&gt;</span> {
      <span class="hljs-keyword">const</span> r = generateRects(<span class="hljs-built_in">window</span>, i)
      cachedRects[i++] = r
      <span class="hljs-keyword">return</span> state &amp;&amp; rectsEqual(<span class="hljs-built_in">window</span>.get_frame_rect(), r.window)
    }, <span class="hljs-literal">true</span>)

    i = <span class="hljs-number">0</span>
    windows.forEach(<span class="hljs-function">(<span class="hljs-params"><span class="hljs-built_in">window</span></span>) =&gt;</span> {
      <span class="hljs-keyword">const</span> r = cachedRects[i++]
      placeWindow(<span class="hljs-built_in">window</span>, fullscreen ? r.workspace : r.window)
      <span class="hljs-built_in">window</span>.raise()
    })
})
</code></pre>
<p>The logic here gives this extension some dynamic functionality depending on the tiled and focused state of the two most recently focused windows:</p>
<ul>
<li><p>If at least one of the two most recently focused windows is not tiled, tile both with the focused window on the left.</p>
</li>
<li><p>If the focused window is tiled on the right, swap it with the window on the left.</p>
</li>
<li><p>If the focused window is tiled on the left, maximize the two most recently focused windows.</p>
</li>
<li><p>If only one window has focus history, maximize it.</p>
</li>
</ul>
<h2 id="heading-schema-files">Schema files</h2>
<p>The above keybinding is made accessible to <strong>GNOME Shell</strong> via a GSettings schema file.</p>
<p>Create such a file in the <code>schemas</code> directory of your extension project:</p>
<pre><code class="lang-bash">mkdir schemas
touch schemas/org.gnome.shell.extensions.example.gschema.xml
</code></pre>
<p>In the above filename, <code>example</code> should reflect the UUID specified at extension initialization. In the case of my extension, the UUID is <code>fiftyfifty@jcrd.github.io</code> so the schema filename is <code>org.gnome.shell.extensions.fiftyfifty.gschema.xml</code>.</p>
<p>The schema itself looks like this:</p>
<pre><code class="lang-xml"><span class="hljs-meta">&lt;?xml version="1.0" encoding="UTF-8"?&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">schemalist</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">schema</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"org.gnome.shell.extensions.fiftyfifty"</span>
            <span class="hljs-attr">path</span>=<span class="hljs-string">"/org/gnome/shell/extensions/fiftyfifty/"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">key</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"as"</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"toggle"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">default</span>&gt;</span>&lt;![CDATA[['&lt;Super&gt;f']]]&gt;<span class="hljs-tag">&lt;/<span class="hljs-name">default</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">summary</span>&gt;</span>Toggle split<span class="hljs-tag">&lt;/<span class="hljs-name">summary</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">schema</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">schemalist</span>&gt;</span>
</code></pre>
<p>As you can see, the <code>&lt;Super&gt;f</code> keybinding is assigned to the <code>toggle</code> function.</p>
<p>For <strong>GNOME Shell</strong> to use the schema, it must first be compiled with this command:</p>
<pre><code class="lang-bash">glib-compile-schemas schemas/
</code></pre>
<p>Details about these schemas and changing them via a preferences window can be found <a target="_blank" href="https://gjs.guide/extensions/development/preferences.html#gsettings">here</a>.</p>
<h2 id="heading-using-extensions">Using extensions</h2>
<p>Now, with the extension in a functional state, there are two approaches to testing it:</p>
<ul>
<li><p>Start a nested instance of <strong>GNOME Shell</strong> with this command:</p>
<pre><code class="lang-bash">  dbus-run-session -- gnome-shell --nested --wayland
</code></pre>
</li>
<li><p>Restart your active instance of <strong>GNOME Shell</strong> by logging out and back in</p>
</li>
</ul>
<p>After either of these steps, the extension should be registered by <strong>GNOME Shell</strong> and can be enabled with the following:</p>
<pre><code class="lang-bash">gnome-extensions <span class="hljs-built_in">enable</span> fiftyfifty@jcrd.github.io
</code></pre>
<p>There we go. Not even the sky's the limit when it comes to extension functionality, so don't be afraid to shoot for the moon and hack away!</p>
<h1 id="heading-whats-next">What's next?</h1>
<p>The <a target="_blank" href="https://gjs.guide/extensions/">Extensions</a> section of the <a target="_blank" href="https://gjs.guide/">Guide to JavaScript for Gnome</a> is an invaluable resource for extension development, containing much of this information and more.</p>
<p>If you build something useful, submit your extension for distribution on <a target="_blank" href="https://extensions.gnome.org/">extensions.gnome.org</a> after checking the <a target="_blank" href="https://gjs.guide/extensions/review-guidelines/review-guidelines.html#basics">review guidelines</a>.</p>
<p>Explore the <a target="_blank" href="https://github.com/topics/gnome-shell-extension">gnome-shell-extension topic on GitHub</a> for some inspiration 🍀</p>
]]></content:encoded></item><item><title><![CDATA[Setting up a Linux home server with Ansible and containerized apps]]></title><description><![CDATA[We're fortunate to live in a time of abundant, high-quality free and open source software—and the best part is, much of it can be self-hosted on your own server 🏠
As a follow up to my article on reproducible distro configuration, let's explore how t...]]></description><link>https://blog.jamesreed.dev/setting-up-a-linux-home-server</link><guid isPermaLink="true">https://blog.jamesreed.dev/setting-up-a-linux-home-server</guid><category><![CDATA[Devops]]></category><category><![CDATA[ansible]]></category><category><![CDATA[automation]]></category><category><![CDATA[Docker]]></category><category><![CDATA[Linux]]></category><dc:creator><![CDATA[James Reed]]></dc:creator><pubDate>Fri, 03 Mar 2023 22:17:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1677881818634/8aaedb54-f594-4e57-bed5-507c9aa9852f.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>We're fortunate to live in a time of abundant, high-quality free and open source software—and the best part is, <a target="_blank" href="https://github.com/awesome-selfhosted/awesome-selfhosted">much of it can be self-hosted</a> on your own server 🏠</p>
<p>As a follow up to my article on <a target="_blank" href="https://twiddlingbits.net/on-reproducible-distro-configuration">reproducible distro configuration</a>, let's explore how to set up a home server running Linux using <a target="_blank" href="https://www.ansible.com/">Ansible</a>, plus my method of deploying self-hosted applications via containers for the ultimate combination of stability, flexibility, and modernity!</p>
<p>And as a bonus, we'll take a look at my server landing page software designed for precisely this use case.</p>
<blockquote>
<p><strong>Prerequisites</strong></p>
<ul>
<li><p>Home server hardware</p>
</li>
<li><p><strong>Ansible</strong> installed on your workstation and familiarity with its use</p>
</li>
</ul>
</blockquote>
<h1 id="heading-the-os">The OS</h1>
<p>Let's start with the operating system. Many Linux distributions can adequately fill this role:</p>
<ul>
<li><p><a target="_blank" href="https://www.debian.org/">Debian</a></p>
</li>
<li><p><a target="_blank" href="https://ubuntu.com/download/server">Ubuntu Server</a></p>
</li>
<li><p><a target="_blank" href="https://get.opensuse.org/leap/">OpenSUSE Leap</a></p>
</li>
<li><p><a target="_blank" href="https://getfedora.org/en/server/">Fedora Server</a></p>
</li>
<li><p>Any of the <a target="_blank" href="https://www.redhat.com/en/technologies/linux-platforms/enterprise-linux">RHEL</a> clones such as <a target="_blank" href="https://rockylinux.org/">Rocky Linux</a> or <a target="_blank" href="https://almalinux.org/">AlmaLinux</a></p>
</li>
</ul>
<p>I use AlmaLinux for a number of reasons:</p>
<ul>
<li><p>It's built on modern software</p>
<ul>
<li><p><a target="_blank" href="https://en.wikipedia.org/wiki/DNF_(software)">dnf</a>, a powerful RPM package manager</p>
</li>
<li><p><a target="_blank" href="https://firewalld.org/">firewalld</a>, a firewall service supported by <strong>Ansible</strong> out of the box</p>
</li>
<li><p><a target="_blank" href="https://podman.io/">podman</a>, the easy-to-use container engine we'll use to run our apps</p>
</li>
</ul>
</li>
<li><p>It's robust and stable, requiring minimal updates</p>
</li>
<li><p>Access to the <a target="_blank" href="https://docs.fedoraproject.org/en-US/epel/">Extra Packages for Enterprise Linux (EPEL)</a> repository means lots of available software</p>
<ul>
<li>Additionally, <a target="_blank" href="https://copr.fedorainfracloud.org/">Copr</a> can build against this repository so packaging your own software is easy</li>
</ul>
</li>
</ul>
<p>Documentation for your distribution of choice should walk you through the download and installation process.</p>
<p>Make sure your server is accessible via <strong>ssh</strong>!</p>
<blockquote>
<p>Going forward, this article will assume the use of an RPM-based distro with the aforementioned software.</p>
</blockquote>
<h1 id="heading-the-set-up">The set up</h1>
<p>Rather than manually installing and configuring software on my home server, I prefer to automate this process using <strong>Ansible</strong>. This means <a target="_blank" href="https://github.com/jcrd/ansible-servers">my Ansible playbook</a> is the single source of truth regarding the home server's configuration and it can be easily reproduced as necessary.</p>
<p>Here's my <code>localserver.yml</code> playbook file:</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">hosts:</span> <span class="hljs-string">localserver</span>
  <span class="hljs-attr">pre_tasks:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Get</span> <span class="hljs-string">host</span> <span class="hljs-string">IP</span>
      <span class="hljs-attr">shell:</span> <span class="hljs-string">hostname</span> <span class="hljs-string">-I</span> <span class="hljs-string">|</span> <span class="hljs-string">awk</span> <span class="hljs-string">'{print $1}'</span>
      <span class="hljs-attr">register:</span> <span class="hljs-string">hostip_cmd</span>
  <span class="hljs-attr">vars:</span>
    <span class="hljs-attr">hostip:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ hostip_cmd.stdout }}</span>"</span>
    <span class="hljs-attr">homedir:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ lookup('env', 'HOME') }}</span>"</span>
    <span class="hljs-attr">configdir:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ homedir }}</span>/.config"</span>
    <span class="hljs-attr">mediadir:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ homedir }}</span>/media"</span>
    <span class="hljs-attr">easymodeconfig:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ homedir }}</span>/.easymode-config"</span>
  <span class="hljs-attr">roles:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">base</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">dotfiles</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">openvpn_client</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">jellyfin</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">qbittorrent</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">rclone</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">syncthing</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">easymode</span>
</code></pre>
<p>As you can see, many <a target="_blank" href="https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_reuse_roles.html">roles</a> are involved in the set up of my server. We will look into these shortly.</p>
<p>In order to run such a playbook, first configure <strong>Ansible</strong>'s <a target="_blank" href="https://docs.ansible.com/ansible/latest/inventory_guide/intro_inventory.html#inventory-basics-formats-hosts-and-groups"><code>/etc/ansible/hosts</code></a> file like so:</p>
<pre><code class="lang-ini"><span class="hljs-section">[localserver]</span>
192.168.1.111 <span class="hljs-comment"># Your server's local IP address</span>
</code></pre>
<p>Then the following command can be used:</p>
<pre><code class="lang-bash">ansible-playbook --ask-pass --ask-become-pass localserver.yml
</code></pre>
<p>It will prompt for the server's password, connect via <strong>ssh</strong>, and proceed to run the playbook's roles.</p>
<p>Now, let's see how these roles make use of podman for deploying containerized applications!</p>
<h1 id="heading-the-apps">The apps</h1>
<p>Before going any further, let's answer the question: why use containers?</p>
<p>The primary rationale is this:</p>
<ul>
<li><p>Any software designed to be self-hosted likely offers a containerized version or has been packaged as such by a third party for easy installation</p>
</li>
<li><p>These user-facing applications needn't follow your distro's release cadence or even be packaged by said distro</p>
</li>
</ul>
<p>Let's look at <a target="_blank" href="https://syncthing.net/">syncthing</a> as an example. The <a target="_blank" href="https://www.linuxserver.io/">LinuxServer</a> community offers a containerized image on <a target="_blank" href="https://hub.docker.com/r/linuxserver/syncthing">Docker Hub</a> 👏</p>
<blockquote>
<p>Keep in mind: Docker images comply with the <a target="_blank" href="https://opencontainers.org/">OCI</a> Container standard and are fully compatible with podman.</p>
</blockquote>
<p>Installing this software with <strong>Ansible</strong> is simple enough—it requires only a few tasks and files.</p>
<p>I keep all the necessary tasks in a <code>main.yml</code> tasks file:</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Check</span> <span class="hljs-string">existence</span> <span class="hljs-string">of</span> <span class="hljs-string">container</span>
  <span class="hljs-attr">command:</span> <span class="hljs-string">podman</span> <span class="hljs-string">container</span> <span class="hljs-string">exists</span> <span class="hljs-string">syncthing</span>
  <span class="hljs-attr">register:</span> <span class="hljs-string">container_exists</span>
  <span class="hljs-attr">failed_when:</span> <span class="hljs-string">container_exists.rc</span> <span class="hljs-string">&gt;</span> <span class="hljs-number">1</span>

<span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Create</span> <span class="hljs-string">container</span>
  <span class="hljs-attr">command:</span> <span class="hljs-string">&gt;
    podman create --name=syncthing
    --userns=""
    -e PUID=0
    -e PGID=0
    -p 8384:8384
    -p 22000:22000/tcp
    -p 22000:22000/udp
    -p 21027:21027/udp
    -v syncthing-config:/config
    docker.io/linuxserver/syncthing
</span>  <span class="hljs-attr">when:</span> <span class="hljs-string">container_exists.rc</span> <span class="hljs-string">==</span> <span class="hljs-number">1</span>

<span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Configure</span> <span class="hljs-string">firewalld</span>
  <span class="hljs-attr">firewalld:</span>
    <span class="hljs-attr">service:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ item }}</span>"</span>
    <span class="hljs-attr">state:</span> <span class="hljs-string">enabled</span>
    <span class="hljs-attr">immediate:</span> <span class="hljs-literal">true</span>
    <span class="hljs-attr">permanent:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">loop:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">syncthing</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">syncthing-gui</span>
  <span class="hljs-attr">become:</span> <span class="hljs-literal">true</span>

<span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Install</span> <span class="hljs-string">systemd</span> <span class="hljs-string">service</span>
  <span class="hljs-attr">copy:</span>
    <span class="hljs-attr">src:</span> <span class="hljs-string">syncthing.service</span>
    <span class="hljs-attr">dest:</span> <span class="hljs-string">'<span class="hljs-template-variable">{{ configdir }}</span>/systemd/user/syncthing.service'</span>
    <span class="hljs-attr">mode:</span> <span class="hljs-number">0644</span>

<span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Enable</span> <span class="hljs-string">systemd</span> <span class="hljs-string">service</span>
  <span class="hljs-attr">systemd:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">syncthing.service</span>
    <span class="hljs-attr">scope:</span> <span class="hljs-string">user</span>
    <span class="hljs-attr">state:</span> <span class="hljs-string">started</span>
    <span class="hljs-attr">enabled:</span> <span class="hljs-literal">true</span>
    <span class="hljs-attr">daemon_reload:</span> <span class="hljs-literal">true</span>

<span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Install</span> <span class="hljs-string">easymode</span> <span class="hljs-string">config</span>
  <span class="hljs-attr">copy:</span>
    <span class="hljs-attr">src:</span> <span class="hljs-string">syncthing.yml</span>
    <span class="hljs-attr">dest:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ easymodeconfig }}</span>/"</span>
</code></pre>
<p>These tasks are fairly self-documenting, but let's go through them in order nonetheless:</p>
<ol>
<li><p>First, check if the container already exists using podman</p>
</li>
<li><p>If it doesn't yet exist, create it using podman</p>
<ul>
<li>You can typically find the appropriate container creation flags on the app's Docker Hub page</li>
</ul>
</li>
<li><p>Configure firewalld to open <strong>syncthing</strong>'s ports</p>
<ul>
<li>firewalld comes with "services" that define the ports used by common software such as <strong>syncthing</strong>!</li>
</ul>
</li>
<li><p>Install and enable the systemd service (we'll look into this next)</p>
</li>
<li><p>Install the accompanying <strong>easymode</strong> configuration file</p>
<ul>
<li><a target="_blank" href="https://github.com/jcrd/easymode">easymode</a> is a server landing page of my own creation—stick around to the end for more info!</li>
</ul>
</li>
</ol>
<p>Because podman doesn't operate as a daemon in the manner of docker, we need a <code>syncthing.service</code> file to run our container at system startup:</p>
<pre><code class="lang-ini"><span class="hljs-section">[Unit]</span>
<span class="hljs-attr">Description</span>=Syncthing podman container

<span class="hljs-section">[Service]</span>
<span class="hljs-attr">ExecStart</span>=/usr/bin/podman start -a syncthing
<span class="hljs-attr">ExecStop</span>=/usr/bin/podman stop -t <span class="hljs-number">10</span> syncthing
<span class="hljs-attr">Restart</span>=<span class="hljs-literal">on</span>-failure

<span class="hljs-section">[Install]</span>
<span class="hljs-attr">WantedBy</span>=default.target
</code></pre>
<p>The location of these files must follow <strong>Ansible</strong>'s <a target="_blank" href="https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_reuse_roles.html#role-directory-structure">role directory structure</a>:</p>
<pre><code class="lang-bash">roles/
    syncthing/
        files/
            syncthing.service
            syncthing.yml <span class="hljs-comment"># easymode config file</span>
        tasks/
            main.yml
</code></pre>
<p>And that's all it takes! Repeat as needed to install the containerized apps of your choosing 🤩</p>
<h1 id="heading-the-landing-page">The landing page</h1>
<p>I find it useful to set up a server landing page so I don't need to memorize the ports used by the various web interfaces of running apps.</p>
<p>There are a few popular projects in this domain:</p>
<ul>
<li><p><a target="_blank" href="https://github.com/causefx/Organizr">Organizr</a></p>
</li>
<li><p><a target="_blank" href="https://heimdall.site/">Heimdall</a></p>
</li>
</ul>
<p>I used Heimdall for a time, and it works well, but I wanted a landing page app with a setup procedure more conducive to automation. Thus, <a target="_blank" href="https://github.com/jcrd/easymode">easymode</a> was born! It's designed to be configured by automation systems such as <strong>Ansible</strong>.</p>
<p>The configuration file for <strong>syncthing</strong> is this simple:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">name:</span> <span class="hljs-string">syncthing</span>
<span class="hljs-attr">url:</span> <span class="hljs-number">8384</span>
<span class="hljs-attr">icon:</span> <span class="hljs-string">arcticons:syncthing</span>
</code></pre>
<p>The file is installed to <strong>easymode</strong>'s configuration directory; the files here become entries in the generated landing page.</p>
<p>You may have noticed the server's IP address is stored as a variable when running my <strong>Ansible</strong> playbook. This is used by <strong>easymode</strong> to construct the correct URL given the above port!</p>
<p>Here's the task responsible for creating <strong>easymode</strong>'s container:</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Create</span> <span class="hljs-string">container</span>
  <span class="hljs-attr">command:</span> <span class="hljs-string">&gt;
    podman create --name=easymode
    -p 80:80
    -e EASYMODE_HOSTNAME={{ ansible_hostname }}
    -e EASYMODE_IP={{ hostip }}
    -v {{ easymodeconfig }}:/config:z
    docker.io/supplantr/easymode</span>
</code></pre>
<p>The rest of the role closely resembles the <strong>syncthing</strong> role we explored above.</p>
<p>Finally, navigate to your home server's IP address and enjoy!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677880617699/935ac9c7-aff9-40e1-90ec-58768d3b4f63.jpeg" alt class="image--center mx-auto" /></p>
]]></content:encoded></item><item><title><![CDATA[A brief experiment in detecting ambient light]]></title><description><![CDATA[Inspired by my previous project, lighten, I experimented with automatically updating a laptop screen's brightness using a webcam rather than a dedicated sensor. This required determining the ambient light level via a captured webcam image—with all th...]]></description><link>https://blog.jamesreed.dev/detecting-ambient-light</link><guid isPermaLink="true">https://blog.jamesreed.dev/detecting-ambient-light</guid><category><![CDATA[Python]]></category><category><![CDATA[image processing]]></category><category><![CDATA[face detection]]></category><dc:creator><![CDATA[James Reed]]></dc:creator><pubDate>Wed, 22 Feb 2023 00:10:56 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1677024043933/a16d24f7-969e-47f1-abd7-bb32527eca71.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Inspired by my previous project, <a target="_blank" href="https://twiddlingbits.net/lighten">lighten</a>, I experimented with automatically updating a laptop screen's brightness using a webcam rather than a dedicated sensor. This required determining the ambient light level via a captured webcam image—with all the variance introduced by the outside world. The takeaway: it's much harder!</p>
<p>There are a few naive approaches to making this calculation:</p>
<ol>
<li><p>Convert the image to greyscale and use either the average or the <a target="_blank" href="https://mathworld.wolfram.com/Root-Mean-Square.html">root mean square</a> pixel brightness.</p>
</li>
<li><p>Calculate perceived brightness using the pixel average.</p>
</li>
</ol>
<p>The latter uses magic numbers found in <a target="_blank" href="https://alienryderflex.com/hsp.html">this article</a> about the HSP color model:</p>
<p>$$brightness  =  sqrt( .299 R^2 + .587 G^2 + .114 B^2 )$$</p>
<p><a target="_blank" href="https://stackoverflow.com/a/3498247">This</a> Stack Overflow answer provides some alternatives and their implementations.</p>
<p>I chose the first option, so let's see what the full implementation looks like in Python:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> io
<span class="hljs-keyword">import</span> subprocess

<span class="hljs-keyword">from</span> PIL <span class="hljs-keyword">import</span> Image, ImageStat


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_brightness</span>(<span class="hljs-params">path</span>):</span>
        <span class="hljs-comment"># Get a single frame from the webcam device using ffmpeg,</span>
        <span class="hljs-comment"># sending the image bytes to standard output to be captured.</span>
        ps = subprocess.run(
            <span class="hljs-string">f"ffmpeg -i <span class="hljs-subst">{path}</span> -vframes 1 -f image2pipe -"</span>.split(),
            capture_output=<span class="hljs-literal">True</span>,
        )
        <span class="hljs-comment"># Convert the image bytes into a BytesIO object and open it</span>
        <span class="hljs-comment"># as a Pillow Image.</span>
        im = Image.open(io.BytesIO(ps.stdout))
        <span class="hljs-comment"># Convert the image to greyscale.</span>
        im = im.convert(<span class="hljs-string">"L"</span>)
        <span class="hljs-comment"># Return the root mean square (rms) brightness value.</span>
        <span class="hljs-keyword">return</span> ImageStat.Stat(im).rms[<span class="hljs-number">0</span>]

get_brightness(<span class="hljs-string">"/dev/video0"</span>)
</code></pre>
<p>The Python <a target="_blank" href="https://pillow.readthedocs.io/en/stable/">Pillow</a> library makes this easy, so what's the problem? 🤔</p>
<p>The first problem arose immediately: on my laptop, a 7th generation ThinkPad X1 Carbon, the webcam doesn't seem to fully calibrate itself to ambient light within the time it takes to capture a single frame, so this calculation would yield extremely low values.</p>
<p>I worked around this by "waking up" the webcam using this function:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">wakeup</span>(<span class="hljs-params">path</span>):</span>
        subprocess.run(
            <span class="hljs-string">f"ffmpeg -i <span class="hljs-subst">{path}</span> -vframes 10 -f image2pipe -"</span>.split(),
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
        )
</code></pre>
<p>I chose 10 frames arbitrarily. These frames are discarded by directing <code>ffmpeg</code>'s output to the null device. Afterward, the above <code>get_brightness</code> function returns a value that much better reflects the ambient brightness level.</p>
<p>Indoors, at least. Office? No problem. Coffee shop? Should be fine. Outside, lit by the sun from the front, against a backdrop of dark-leaved trees? In this case, the calculation is wildly undervalued—which makes sense: most of the image <em>is</em> dark.</p>
<p>How can this be worked around? It's possible to detect the user's face in this image and calculate the brightness of that area of the image alone, which could work in this specific case, but consider another scenario: in a dark room, a very bright laptop screen illuminates its user's face. These captured images would likely produce similar pixel brightness values, yet the ambient light levels are completely opposite! Clearly, these methods utilizing averages are too dependent on an image's background composition.</p>
<p>Ultimately, I don't think there's a reliable and consistent method of correctly detecting ambient light level based on a webcam image, but the resulting <a target="_blank" href="https://github.com/jcrd/plight">code</a> might still be useful in some situations.</p>
<p>If you have any insights, please share them!</p>
]]></content:encoded></item><item><title><![CDATA[Let there be light]]></title><description><![CDATA[There is something fascinating about observing a miniature world, be it swimming in an aquarium, simulated on a screen, or planted in a terrarium. With this in mind, I decided to build a terrarium with an immersive twist: a programmable set of LEDs w...]]></description><link>https://blog.jamesreed.dev/artificial-sun</link><guid isPermaLink="true">https://blog.jamesreed.dev/artificial-sun</guid><category><![CDATA[arduino]]></category><category><![CDATA[hardware]]></category><category><![CDATA[projects]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[James Reed]]></dc:creator><pubDate>Mon, 13 Feb 2023 23:54:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1676333112701/7ae6c93b-7d34-4def-ad01-92cb0f29ce10.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>There is something fascinating about observing a miniature world, be it swimming in an aquarium, simulated on a screen, or planted in a terrarium. With this in mind, I decided to build a terrarium with an immersive twist: a programmable set of LEDs whose brightness and color mimic that of the sun.</p>
<p>Let's explore the hardware, software, and terrarium setup that bring this world to life! 🌍</p>
<h1 id="heading-the-hardware">The hardware</h1>
<p>This project uses several hardware components:</p>
<ul>
<li><p>An Arduino board capable of running <a target="_blank" href="https://circuitpython.org/">CircuitPython</a> (<a target="_blank" href="https://www.adafruit.com/product/4900">Adafruit QT Py RP2040</a>)</p>
</li>
<li><p>A compatible real-time clock (<a target="_blank" href="https://www.adafruit.com/product/5189">Adafruit PCF8523</a>)</p>
<ul>
<li>This will also require an appropriate battery</li>
</ul>
</li>
<li><p>A means of connecting them (<a target="_blank" href="https://www.adafruit.com/product/4399">STEMMA QT cable</a>)</p>
</li>
<li><p>A <a target="_blank" href="https://learn.adafruit.com/adafruit-neopixel-uberguide/neopixel-rings">NeoPixel LED ring</a> (<a target="_blank" href="https://www.adafruit.com/product/1586">NeoPixel Ring 24 x 5050</a>)</p>
</li>
<li><p>A means of connecting that to the Arduino board</p>
<ul>
<li>I wanted a way to detach the LED ring from the rest of the assembly so I used a pair of matching Molex PicoBlade-compatible 3-pin cables (such as <a target="_blank" href="https://www.adafruit.com/product/4721">these</a>)</li>
</ul>
</li>
<li><p>An external power supply for the LEDs</p>
</li>
<li><p>A "project box" capable of holding these components</p>
</li>
</ul>
<p>I power both the LED ring and Arduino board via a 5V 2A power supply with a DC power adapter. This requires additional components:</p>
<ul>
<li>A 1000 µF capacitor used across the + and - terminals of the DC adapter, as noted <a target="_blank" href="https://learn.adafruit.com/adafruit-neopixel-uberguide/basic-connections">here</a>:</li>
</ul>
<blockquote>
<p>When using a DC power supply, or an especially large battery, we recommend adding a large capacitor (100 to 1000 µF, 6.3V or higher) across the + and – terminals.</p>
</blockquote>
<ul>
<li>A diode (such as the common 1N4007 rectifier diode) between the 5V pin on your board and the power supply, as noted <a target="_blank" href="https://learn.adafruit.com/adafruit-qt-py-2040/pinouts">here</a>:</li>
</ul>
<blockquote>
<p><strong>5V</strong> - This is 5v out from the USB port. <strong>You can also use this as a voltage <em>input</em> but you must have some sort of diode (schottky, signal, power, really anything) between your external power source and this pin with anode to battery, cathode to 5V pin.</strong></p>
</blockquote>
<p>Solder the LED ring input (labeled "DIN" or "DI") to the <strong>digital pin 5</strong> (labeled "SCL") on your board, and run the power and ground wires to their appropriate DC adapter terminals. Power the Arduino board with a wire to ground and the diode mentioned above. Make sure the capacitor is connected as required. Lastly, hook up the real-time clock using the STEMMA QT cable. My finished assembly looks like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1676230470268/7b818bef-d8ae-43f4-847d-983403d57358.jpeg" alt class="image--center mx-auto" /></p>
<p>To prepare the project box, I drilled out a hole for the female DC connector and sawed a slot on the opposite side for the LED ring wires:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1676230933808/ca12eb7e-91bc-41b1-9ad8-0f95288e4043.jpeg" alt class="image--center mx-auto" /></p>
<p>Fit the assembly into the project box:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1676330705945/0133cdce-0d8b-49bd-9356-15eb5e1d3b7b.jpeg" alt class="image--center mx-auto" /></p>
<h1 id="heading-the-software">The software</h1>
<blockquote>
<p>This process assumes use of a Linux system and basic experience working with Arduino boards and the command line.</p>
</blockquote>
<p>The software controlling the artificial sun is written in <a target="_blank" href="https://circuitpython.org/">CircuitPython</a>. Follow <a target="_blank" href="https://learn.adafruit.com/adafruit-qt-py-2040/circuitpython">this guide</a> to set it up on a QT Py RP2040 board.</p>
<p>Make sure you <a target="_blank" href="https://github.com/adafruit/Adafruit_CircuitPython_Bundle#use">install the library bundle</a>, then obtain the <a target="_blank" href="https://github.com/jcrd/artificial-sun">source code</a>:</p>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> https://github.com/jcrd/artificial-sun
</code></pre>
<p>Assuming the name of your device is <code>CIRCUITPY</code>, you can use an included update script to set the correct current time of your real-time clock and install the code file:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> artificial-sun
./update.sh
</code></pre>
<p>Your sun should now be radiating light (and a little warmth for good measure 🌞).</p>
<h2 id="heading-how-does-it-work">How does it work?</h2>
<p>The main logic of the code is in the linear interpolation of a small set of data relating time-of-day to LED light brightness and color.</p>
<pre><code class="lang-python">light_data = [
    <span class="hljs-comment"># Time, (Brightness, (Red, Green, Blue, White color))</span>
    (<span class="hljs-number">0.0</span>, (<span class="hljs-number">0.0</span>, (<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>))),
    (<span class="hljs-number">7.0</span>, (<span class="hljs-number">0.1</span>, (<span class="hljs-number">244</span>, <span class="hljs-number">254</span>, <span class="hljs-number">169</span>, <span class="hljs-number">0</span>))),
    (<span class="hljs-number">8.0</span>, (<span class="hljs-number">0.2</span>, (<span class="hljs-number">243</span>, <span class="hljs-number">254</span>, <span class="hljs-number">174</span>, <span class="hljs-number">0</span>))),
    (<span class="hljs-number">13.0</span>, (<span class="hljs-number">1.0</span>, (<span class="hljs-number">234</span>, <span class="hljs-number">254</span>, <span class="hljs-number">202</span>, <span class="hljs-number">0</span>))),
    (<span class="hljs-number">18.0</span>, (<span class="hljs-number">0.2</span>, (<span class="hljs-number">243</span>, <span class="hljs-number">254</span>, <span class="hljs-number">174</span>, <span class="hljs-number">0</span>))),
    (<span class="hljs-number">19.0</span>, (<span class="hljs-number">0.1</span>, (<span class="hljs-number">244</span>, <span class="hljs-number">254</span>, <span class="hljs-number">169</span>, <span class="hljs-number">0</span>))),
    (<span class="hljs-number">24.0</span>, (<span class="hljs-number">0.0</span>, (<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>))),
]
</code></pre>
<p>Its meant to be an approximation of sunrise, midday, and sunset. I expect the data to change as I continue experimenting with it over time.</p>
<p>The lerp function itself is straightforward:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">lerp</span>(<span class="hljs-params">begin, end, t</span>):</span>
    <span class="hljs-keyword">return</span> begin + t * (end - begin)
</code></pre>
<p>It calculates a position between the <code>begin</code> and <code>end</code> values given a percentage <code>t</code>. For example: let's start at 0 and end at 20; a <code>t</code> value of 0.4 is the value 8.</p>
<p>The function used to interpolate between the brightness and color values associated with each time-of-day set is a bit more complex:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_light_lerp</span>(<span class="hljs-params">daytime</span>):</span>
    pt, (pb, pc) = (<span class="hljs-number">0</span>, (<span class="hljs-number">0</span>, (<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>)))

    <span class="hljs-keyword">for</span> t, (b, c) <span class="hljs-keyword">in</span> light_data:
        <span class="hljs-keyword">if</span> t == daytime:
            print(<span class="hljs-string">"{}: {} @ {}"</span>.format(t, c, b))
            <span class="hljs-keyword">return</span> (b, c)
        <span class="hljs-keyword">if</span> t &lt; daytime:
            pt, pb, pc = t, b, c
            <span class="hljs-keyword">continue</span>
        lt = (daytime - pt) / (t - pt)
        lb = lerp(pb, b, lt)
        lc = tuple(int(lerp(pc[i], v, lt)) <span class="hljs-keyword">for</span> i, v <span class="hljs-keyword">in</span> enumerate(c))
        print(<span class="hljs-string">"{} -&gt; {} ({}): {} @ {}"</span>.format(pt, t, lt, lc, lb))
        <span class="hljs-keyword">return</span> (lb, lc)
</code></pre>
<p>Let's walk through the logic imagining its 11 o'clock:</p>
<ul>
<li><p>If the current time matches a set exactly, return the associated data.</p>
</li>
<li><p>Otherwise, loop through the data recording each set with a time less than 11.</p>
</li>
<li><p>When a time greater than 11 is encountered, the last recorded set will be the beginning of the lerp function, while the current set is the end.</p>
<ul>
<li>Given the above data, the beginning and end sets will be at 8 and 13 o'clock respectively.</li>
</ul>
</li>
<li><p>With this, calculate the percentage that describes 11 o'clock:</p>
</li>
</ul>
<p>$$(11 - 8) / (13 - 8) = 0.6$$</p><ul>
<li>Now the brightness and color values can be individually interpolated between the two sets using the percentage! 🤯</li>
</ul>
<h1 id="heading-the-terrarium">The terrarium</h1>
<p>The first step of building a terrarium is to select the enclosure itself. I used a generic pentagonal "geometric" terrarium, readily available online and in local hobby stores. The design of your enclosure will also determine if your terrarium is "open" or "closed":</p>
<ul>
<li><p>An open terrarium allows air to circulate throughout, meaning moisture will evaporate;</p>
</li>
<li><p>A closed terrarium will have a lid of some sort (think: mason jar), meaning moisture will be sealed inside.</p>
</li>
</ul>
<p>This critical difference determines the plants most suitable for your terrarium. I recommend you check out one of the many online guides that go into detail about which plants to select and the ideal makeup of your substrate. As my enclosure is open, I collected the appropriate substrate materials, picked out some succulents, and got to work assembling my world. 🌵</p>
<p>Next, it was time to give it light by attaching the LED ring. There are many ways to go about this. After some failed experimentation, I settled on using corkboard to construct a "lid" for the open face of my enclosure made of two layers glued together with a hole the size of the inner diameter of the LED ring cut out of each so that air is still able to circulate. This only required the use of scissors, a precision knife, and a hot glue gun. Finally, I pushed pins through the LED ring's PCB holes to fix it to the underside of the corkboard assembly so that it can be easily detached if necessary.</p>
<p>Here's the finished product:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1676231176364/ada8dba7-6411-4c11-9d1c-c71e57737bc0.jpeg" alt class="image--center mx-auto" /></p>
<p>For more pizzazz, you might consider building something similar out of wood!</p>
<p>Finally, I cut a piece of adhesive hook and loop tape (commonly known by the brand name Velcro) to fit the project box which I attached to the back of the terrarium:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1676330849042/a6c2c0b8-d776-48d9-a21f-d6837519e4c0.jpeg" alt class="image--center mx-auto" /></p>
<p>Supply power to the DC adapter and step back to revel in the miniature majesty!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1676655671957/34a0b1e2-34b8-4160-b594-fcf9520c4a0e.jpeg" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1676856628095/fa4b633f-578e-43aa-ad8a-ac5d26e6a57c.jpeg" alt class="image--center mx-auto" /></p>
<p>Check out this brief demo video for the full effect:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/amaU5oXWW3g">https://youtu.be/amaU5oXWW3g</a></div>
]]></content:encoded></item><item><title><![CDATA[Packaging software for RPM-based distros]]></title><description><![CDATA[Welcome to the world of RPMs! The RPM Package Manager is a powerful package management system utilized by modern Linux distros such as Fedora and OpenSUSE. Despite the vast repositories of RPM packages readily available, you may desire to use some so...]]></description><link>https://blog.jamesreed.dev/packaging-software-for-rpm-based-distros</link><guid isPermaLink="true">https://blog.jamesreed.dev/packaging-software-for-rpm-based-distros</guid><category><![CDATA[Linux]]></category><category><![CDATA[Fedora]]></category><category><![CDATA[software-packaging]]></category><category><![CDATA[Devops]]></category><dc:creator><![CDATA[James Reed]]></dc:creator><pubDate>Sat, 04 Feb 2023 23:12:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1675552116407/f3aa7fc7-f977-42a0-85c4-95a4e0652e0c.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Welcome to the world of <a target="_blank" href="https://rpm.org/">RPM</a>s! The RPM Package Manager is a powerful package management system utilized by modern Linux distros such as <a target="_blank" href="https://getfedora.org/">Fedora</a> and <a target="_blank" href="https://www.opensuse.org/">OpenSUSE</a>. Despite the vast repositories of RPM packages readily available, you may desire to use some software so niche or novel that it requires packaging by your very own hand, so let's learn how! I wrote previously about my project, <a target="_blank" href="https://twiddlingbits.net/container-based-development-with-toolboxcutter">toolboxcutter</a>, which facilitates the creation of RPM packages using the nifty <a target="_blank" href="https://docs.pagure.org/rpkg/">rpkg</a> tool, and here I will describe how I use <strong>toolboxcutter</strong> to test RPM SPEC files locally before turning them into distributable RPM packages using the Fedora project's <a target="_blank" href="https://copr.fedorainfracloud.org/">Copr</a> build system. ✨</p>
<h1 id="heading-writing-spec-files">Writing SPEC files</h1>
<p>RPM packages are produced using SPEC files as input, which specify the build and installation processes of some software in addition to metadata such as its name and version. Let's look at <a target="_blank" href="https://github.com/FedeDP/Clight">Clight</a> as a case study. Here's its SPEC file:</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="5a05c5dd8d73ecabc955aedf8df4691d"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/jcrd/5a05c5dd8d73ecabc955aedf8df4691d" class="embed-card">https://gist.github.com/jcrd/5a05c5dd8d73ecabc955aedf8df4691d</a></div><p> </p>
<p>Given such a SPEC file, it's possible to generate an RPM package using the <a target="_blank" href="https://www.redhat.com/sysadmin/create-rpm-package">rpmbuild</a> tool, and this is a valid way of going about it. However, I use <strong>toolboxcutter</strong> because it builds packages in a <a target="_blank" href="https://podman.io/">podman</a> container, meaning:</p>
<ul>
<li><p>Build dependencies won't litter your base system</p>
</li>
<li><p>You won't miss a dependency specification that is coincidentally already installed</p>
</li>
</ul>
<p>I wrote about using <strong>toolboxcutter</strong> for this purpose <a target="_blank" href="https://twiddlingbits.net/container-based-development-with-toolboxcutter#heading-building-rpm-packages">here</a>!</p>
<p>The general procedure begins like this:</p>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> https://github.com/FedeDP/Clight
<span class="hljs-built_in">cd</span> Clight
tb init rpkg-toolbox
</code></pre>
<p>This <code>tb</code> command seen above is <strong>toolboxcutter</strong>, and calling <code>init</code> will create and open a <code>Dockerfile.toolbox</code> file in this repo's directory, built from <code>rpkg-toolbox</code>, which is a local <strong>podman</strong> image with <strong>rpkg</strong> and other packages installed. This is its <code>Dockerfile</code>:</p>
<pre><code class="lang-plaintext">FROM registry.fedoraproject.org/fedora-toolbox:37

RUN dnf install -y neovim
RUN dnf install -y rpkg
RUN dnf install -y rpmdevtools
RUN dnf install -y zsh

RUN ln -s /usr/bin/nvim /usr/local/bin/vim

CMD /usr/bin/zsh
</code></pre>
<p><strong>toolboxcutter</strong> will eventually build <strong>Clight</strong> in the container specified by the <code>Dockerfile</code> in its directory, so let's add the required build dependencies:</p>
<pre><code class="lang-plaintext">FROM rpkg-toolbox

RUN dnf install -y cmake
RUN dnf install -y g++
RUN dnf install -y systemd-devel
RUN dnf install -y popt-devel
RUN dnf install -y libconfig-devel
RUN dnf install -y gsl-devel
RUN dnf install -y dbus-devel
RUN dnf copr enable -y jcrd/libmodule
RUN dnf install -y libmodule
</code></pre>
<p>Thankfully, <strong>Clight</strong>'s dependencies are recorded in its documentation. For other software, some trial and error may be necessary to determine what's required. The next step is to specify these dependencies in the <strong>rpkg</strong> SPEC template file. <strong>rpkg</strong>'s <a target="_blank" href="https://docs.pagure.org/rpkg-util/v3/quick_start.html">documentation</a> describes how to get started with such a template.</p>
<p>In the directory containing the to-be-packaged software's source, let's create a subdirectory and the <strong>rpkg</strong> SPEC template file:</p>
<pre><code class="lang-bash">mkdir spec
touch spec/clight.rpkg.spec
</code></pre>
<p>Now, edit the created file, filling in the SPEC template details. <strong>Clight</strong>'s looks like this:</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="45070e2b19d18d09255083683127fd95"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/jcrd/45070e2b19d18d09255083683127fd95" class="embed-card">https://gist.github.com/jcrd/45070e2b19d18d09255083683127fd95</a></div><p> </p>
<p>As you can see, it resembles the final SPEC file but contains some <strong>rpkg</strong> macros enclosed in <code>{{{ }}}</code>. These allow the production of an RPM package directly from the source repository where this template resides. The installed files listed under the <code>%files</code> section may not be obvious without first attempting to build the package.</p>
<p>From outside the <strong>toolbox</strong> container, run this command:</p>
<pre><code class="lang-bash">tb rpkg-install
</code></pre>
<p>Without the proper installed files list, you will receive an error such as:</p>
<pre><code class="lang-plaintext">error: Installed (but unpackaged) file(s) found:
   /etc/default/clight.conf
   /usr/bin/clight
   /usr/etc/xdg/autostart/clight.desktop
   /usr/include/clight/public.h
   /usr/share/applications/clightc.desktop
   /usr/share/bash-completion/completions/clight
   /usr/share/clight/inhibit_bl.skel
   /usr/share/clight/nightmode.skel
   /usr/share/clight/synctemp_bumblebee.skel
   /usr/share/dbus-1/services/org.clight.clight.service
   /usr/share/icons/hicolor/scalable/apps/clight.svg
   /usr/share/man/man1/clight.1.gz
   /usr/share/zsh/site-functions/_clight
</code></pre>
<p>You can fill in the <code>%files</code> section with these paths using the appropriate <a target="_blank" href="https://docs.fedoraproject.org/en-US/packaging-guidelines/RPMMacros/#macros_installation">RPM macros</a> as seen above. Rerunning <strong>toolboxcutter</strong>'s <code>rpkg-install</code> command should now produce and install an RPM package.</p>
<p>Finally, convert the <strong>rpkg</strong> SPEC template into a standalone spec file by replacing rpkg's <code>{{{ }}}</code> macros with their concrete values. <strong>rpkg</strong> can handle this automatically to an extent (with <code>rpkg spec</code>), with the caveat being: the package <code>Source</code> will be directly derived from the software's hosted source code repository, whereas you probably want to specify a source code archive associated with a released version of this software.</p>
<p>Compare the aforementioned SPEC file with the above <strong>rpkg</strong> SPEC template if you need help determining which values belong where. Once this is complete, you have a working SPEC file capable of producing an installable RPM package! 💪</p>
<h1 id="heading-building-distributable-rpm-packages">Building distributable RPM packages</h1>
<p>With the SPEC file in hand, building a distributable RPM package is easy using <a target="_blank" href="https://copr.fedorainfracloud.org/"><strong>Copr</strong></a>. It's similar in theory to <a target="_blank" href="https://ubuntu.com/">Ubuntu</a>'s <a target="_blank" href="https://launchpad.net/ubuntu/+ppas">ppas</a>. You can find more information about the <strong>Copr</strong> build system <a target="_blank" href="https://docs.pagure.org/copr.copr/user_documentation.html#faq">here</a>. An alternative might be <a target="_blank" href="https://openbuildservice.org/">OBS</a> (Open Build Service) but I have not yet used it myself.</p>
<p><a target="_blank" href="https://docs.pagure.org/copr.copr/screenshots_tutorial.html">This tutorial</a> illustrates the process of creating a new <strong>Copr</strong> project and initiating a build. However, instead of specifying SRPM files by URL, you can provide the URL of a SPEC file. I host my SPEC files on <em>gist.github.com</em> for revision tracking and ease of editing. Provide the URL to the raw <code>.spec</code> file, start the build, and if everything goes according to plan, you'll see that it succeeded! Check out <strong>Clight</strong>'s <a target="_blank" href="https://copr.fedorainfracloud.org/coprs/jcrd/clight/">project</a> to see what this looks like.</p>
<p>Now, on your local machine, enable your newly created <strong>Copr</strong> repository and install the RPM package:</p>
<pre><code class="lang-bash">dnf copr <span class="hljs-built_in">enable</span> jcrd/clight
dnf install Clight
</code></pre>
<p>Voilà! This package is now distributable: it can be installed on any system for which it was built by enabling its <strong>Copr</strong> repository. 👏</p>
<p>This pairs especially well with <a target="_blank" href="https://twiddlingbits.net/on-reproducible-distro-configuration">reproducible distro configurations</a>!</p>
]]></content:encoded></item><item><title><![CDATA[On reproducible distro configuration]]></title><description><![CDATA[Modern Linux distros aim to equip both casual users and developers with many of their tools out of the box, but given the vast repositories of useful software, you'll likely install additional tools for specific use cases, and this often happens over...]]></description><link>https://blog.jamesreed.dev/on-reproducible-distro-configuration</link><guid isPermaLink="true">https://blog.jamesreed.dev/on-reproducible-distro-configuration</guid><category><![CDATA[Linux]]></category><category><![CDATA[ansible]]></category><category><![CDATA[shell]]></category><category><![CDATA[automation]]></category><category><![CDATA[Devops]]></category><dc:creator><![CDATA[James Reed]]></dc:creator><pubDate>Thu, 05 Jan 2023 20:05:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1672948715816/0329bb8f-238e-484d-999b-a4a262e67087.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Modern Linux distros aim to equip both casual users and developers with many of their tools out of the box, but given the vast repositories of useful software, you'll likely install additional tools for specific use cases, and this often happens over time. Eventually, your OS installation becomes something unique to your workflow. Many distros also offer in-place upgrades between releases or entirely rolling-release models to hypothetically allow such mutable configuration to remain on your machine throughout its life.</p>
<p>But what happens when a reinstall is required, perhaps due to data corruption (?!), or simply desired? If you were thinking "I'll painstakingly recall the additional software I installed and custom configuration and manually set up the same state!" let's think again. We can encode the custom state of our post-installation OS to automate its recreation, thus satisfying our objective: reproducibility! 🤯</p>
<h1 id="heading-reproducible-oses">Reproducible OSes?</h1>
<p>Some modern distros offer a mechanism to reproduce immutable installations, which addresses the software installation concerns. See:</p>
<ul>
<li><p><a target="_blank" href="https://nixos.org/guides/how-nix-works.html">NixOS</a></p>
</li>
<li><p><a target="_blank" href="https://silverblue.fedoraproject.org/about">Fedora Silverblue</a></p>
</li>
</ul>
<p>However, it may not address the custom configuration that often lives outside the jurisdiction of system-level package managers; think <code>~/.config</code> and friends.</p>
<p>A solution explored below could be used in conjunction with the declarative, immutable architecture to set up fully reproducible systems, and this is a fine approach should you use such a novel OS.</p>
<p>But, I don't, as I use my software projects that are reinstalled as changed and I have yet to explore how this workflow could be easily adapted to an immutable system.</p>
<p>So, what might be some alternative approaches?</p>
<h1 id="heading-ansible-and-friends">Ansible and friends?</h1>
<p>How about existing "infrastructure automation" software such as <a target="_blank" href="https://www.ansible.com/">Ansible</a> or <a target="_blank" href="https://www.puppet.com/">Puppet</a>?</p>
<p>For a long time, I used <strong>Ansible</strong> and this worked well enough. But, as I didn't use it frequently outside of this use case, and my configuration remained relatively stable, I found myself often needing to reference the docs when I had to make changes; an unacceptably unwieldy process. Additionally, the forced directory structure and method of injecting variables became cumbersome. Essentially, <strong>Ansible</strong> was overly-complex for my use case, but it gets credit for its declarative configuration, which will make an appearance in my solution.</p>
<p>It is worth noting that while using <strong>Ansible</strong> I was installing an X11 window manager and associated configuration, significantly complicating the typical post-installation state. When I switched to <strong>GNOME Shell</strong>, much of the <strong>Ansible</strong> config could be eliminated, which only made the aforementioned shortcomings all the more obvious.</p>
<p>Check out my obsolete <a target="_blank" href="https://github.com/jcrd/ansible-workstation">Ansible-based setup</a> to see what this looked like in practice.</p>
<h1 id="heading-custom-scripts">Custom scripts!</h1>
<p>Why not write custom shell scripts? One could say: because then managing reproducible configuration becomes a software project all of its own. The antidote here is declarative configs, like <strong>Ansible</strong>'s YAML-based architecture. However, one could retort: this simply moves the complexity from the user-facing configuration to the business logic of the software, which we will have to write if creating a custom solution! Fear not, in my case this code is as simple as:</p>
<pre><code class="lang-bash"><span class="hljs-function"><span class="hljs-title">dnf</span></span>() {
    sudo dnf -q -y <span class="hljs-variable">$@</span>
}

<span class="hljs-function"><span class="hljs-title">enable_coprs</span></span>() {
    <span class="hljs-built_in">echo</span> <span class="hljs-string">'--- Enabling coprs ---'</span>
    <span class="hljs-keyword">while</span> <span class="hljs-built_in">read</span> -r copr; <span class="hljs-keyword">do</span>
        dnf copr <span class="hljs-built_in">enable</span> <span class="hljs-string">"<span class="hljs-variable">$copr</span>"</span>
    <span class="hljs-keyword">done</span> &lt; <span class="hljs-string">"<span class="hljs-variable">$WORKDIR</span>"</span>/coprs.txt
}

<span class="hljs-function"><span class="hljs-title">install_packages</span></span>() {
    <span class="hljs-built_in">echo</span> <span class="hljs-string">'--- Installing packages ---'</span>
    <span class="hljs-keyword">while</span> <span class="hljs-built_in">read</span> -r package; <span class="hljs-keyword">do</span>
        dnf install <span class="hljs-string">"<span class="hljs-variable">$package</span>"</span>
    <span class="hljs-keyword">done</span> &lt; <span class="hljs-string">"<span class="hljs-variable">$WORKDIR</span>"</span>/packages.txt
}
</code></pre>
<p>The <code>$WORKDIR</code> variable allows these "library" functions to be applied to any directory in the project's root (facilitating modular configuration, like <strong>Ansible</strong>'s roles), and the only structure enforced is the naming of the plain-text declaration files containing repositories to enable and software packages to install. Additional functions can be written as needed. No more documentation to reference!</p>
<p>A main <code>configure</code> script employs these library functions as such:</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/sh</span>

<span class="hljs-comment"># Set the chassis type (desktop, laptop) to include hardware-specific configuration.</span>
<span class="hljs-built_in">export</span> CHASSIS=<span class="hljs-string">"<span class="hljs-subst">$(hostnamectl | awk '$1 == <span class="hljs-string">"Chassis:"</span> {print $2}')</span>"</span>
<span class="hljs-built_in">export</span> WORKDIR=.

<span class="hljs-comment"># Source the library functions.</span>
<span class="hljs-built_in">source</span> lib/dnf.sh

<span class="hljs-comment"># This function clones and configures dotfile repositories.</span>
<span class="hljs-function"><span class="hljs-title">configure</span></span>() {
    [ ! -e <span class="hljs-string">"<span class="hljs-variable">$2</span>"</span> ] &amp;&amp; git <span class="hljs-built_in">clone</span> https://github.com/jcrd/<span class="hljs-string">"<span class="hljs-variable">$1</span>"</span> <span class="hljs-string">"<span class="hljs-variable">$2</span>"</span>
    <span class="hljs-built_in">pushd</span> <span class="hljs-string">"<span class="hljs-variable">$2</span>"</span>
    ./configure
    <span class="hljs-built_in">popd</span>
}

<span class="hljs-comment"># Configure dotfiles.</span>
configure zsh-config ~/.config/zsh

<span class="hljs-comment"># Enable copr repos with a library function.</span>
enable_coprs

<span class="hljs-comment"># Install packages with another library function!</span>
install_packages

<span class="hljs-comment"># Custom state is easy to recreate: just include the shell commands</span>
<span class="hljs-comment"># you already know and love!</span>
<span class="hljs-comment"># Set user's shell to zsh. 😎</span>
sudo usermod --shell /usr/bin/zsh <span class="hljs-string">"<span class="hljs-variable">$USER</span>"</span>

<span class="hljs-comment"># Chassis-specific configuration, like Ansible's roles.</span>
<span class="hljs-built_in">export</span> WORKDIR=chassis/<span class="hljs-string">"<span class="hljs-variable">$CHASSIS</span>"</span>
<span class="hljs-keyword">if</span> [ -e <span class="hljs-string">"<span class="hljs-variable">$WORKDIR</span>"</span> ]; <span class="hljs-keyword">then</span>
    ./<span class="hljs-string">"<span class="hljs-variable">$WORKDIR</span>"</span>/configure
<span class="hljs-keyword">fi</span>
</code></pre>
<p>See my <a target="_blank" href="https://github.com/jcrd/fedora-workstation-config">live configuration</a> for the whole thing in action, complete with more library functions and the above desktop role!</p>
<p>Now, after a fresh installation of Fedora, reproducing my configuration is as easy as:</p>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> https://github.com/jcrd/fedora-workstation-config
<span class="hljs-built_in">cd</span> fedora-workstation-config
./configure
</code></pre>
<p>Say goodbye to complexity and hello to easily reproducible Linux installations for maximum automation ⚙️ and peace of mind 🙏!</p>
]]></content:encoded></item><item><title><![CDATA[git ship alias]]></title><description><![CDATA[I use git for version control in the development of my software projects. As it's frequently the case that I'm the sole developer of these projects, I've adopted a simple trunk-based-style branching strategy in which I make new commits to a devel bra...]]></description><link>https://blog.jamesreed.dev/git-ship-alias</link><guid isPermaLink="true">https://blog.jamesreed.dev/git-ship-alias</guid><category><![CDATA[Git]]></category><category><![CDATA[development]]></category><category><![CDATA[command line]]></category><category><![CDATA[version control]]></category><category><![CDATA[git branch]]></category><dc:creator><![CDATA[James Reed]]></dc:creator><pubDate>Tue, 22 Nov 2022 14:28:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1669127159787/SvLf4sPvr.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I use <a target="_blank" href="https://git-scm.com/">git</a> for version control in the development of my software projects. As it's frequently the case that I'm the sole developer of these projects, I've adopted a simple trunk-based-style branching strategy in which I make new commits to a <code>devel</code> branch until deeming these changes stable enough to merge into <code>master</code>. This allows me to edit commits in the development branch before "finalizing" them.</p>
<p>I found myself repeating the process of checking out the "production" branch, merging the development branch, and pushing—essentially "shipping" stable code—so I created a <a target="_blank" href="https://git-scm.com/book/en/v2/Git-Basics-Git-Aliases">git alias</a>. Let's check it out!</p>
<h2 id="heading-git-aliases">git aliases</h2>
<p><strong>git</strong> aliases allow you to specify alternative names for commands. For example, you can use an abbreviation for the <code>checkout</code> command:</p>
<pre><code class="lang-sh">git config --global alias.co checkout
git co
</code></pre>
<p>Another handy feature is the ability to create an alias to a separate executable; this is done by prefacing the command with <code>!</code> on the command-line using <code>git config</code>:</p>
<pre><code class="lang-sh">git config --global alias.ship <span class="hljs-string">'!git-ship'</span>
</code></pre>
<p>Remember to include the single quotes as <code>!</code> is a special history expansion character in popular shells such as <strong>bash</strong> and <strong>zsh</strong>. Also, be sure this executable exists in your shell's <a target="_blank" href="https://opensource.com/article/17/6/set-path-linux"><code>PATH</code></a>.</p>
<h2 id="heading-git-ship">git-ship</h2>
<p>My alias points to a separate executable shell script that chains together multiple <strong>git</strong> commands:</p>
<pre><code class="lang-sh"><span class="hljs-meta">#!/bin/sh</span>

prod=<span class="hljs-variable">${GIT_SHIP_PROD:-master}</span>
dev=<span class="hljs-variable">${GIT_SHIP_DEV:-devel}</span>

git checkout <span class="hljs-variable">$prod</span> &amp;&amp; git merge <span class="hljs-variable">$dev</span> &amp;&amp; git push &amp;&amp; git checkout <span class="hljs-variable">$dev</span>
</code></pre>
<p>These commands perform the following actions:</p>
<ol>
<li><p>check out the production branch;</p>
</li>
<li><p>merge the development branch into production;</p>
</li>
<li><p>push the production branch to the default remote;</p>
</li>
<li><p>and check out the development branch to prepare for new commits.</p>
</li>
</ol>
<p>The targeted branches can be changed using environment variables.</p>
<p>In the directory of a given project, I "finalize" development commits and "ship" them to the remote repository with:</p>
<pre><code class="lang-sh">git ship
</code></pre>
<p>And it couldn't be easier!</p>
]]></content:encoded></item><item><title><![CDATA[Container-based development with toolboxcutter]]></title><description><![CDATA[Toolbx is a tool for Linux systems, specifically Fedora, that creates and manages development environment containers. Its purpose is to group development dependencies in a mutable container separate from the base operating system both for the benefit...]]></description><link>https://blog.jamesreed.dev/toolboxcutter</link><guid isPermaLink="true">https://blog.jamesreed.dev/toolboxcutter</guid><category><![CDATA[containers]]></category><category><![CDATA[toolbox]]></category><category><![CDATA[Linux]]></category><category><![CDATA[development]]></category><category><![CDATA[Devops]]></category><dc:creator><![CDATA[James Reed]]></dc:creator><pubDate>Sun, 06 Nov 2022 22:12:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1667772562084/HVwKU1kMk.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://containertoolbx.org/">Toolbx</a> is a tool for Linux systems, specifically <a target="_blank" href="https://getfedora.org/">Fedora</a>, that creates and manages development environment containers. Its purpose is to group development dependencies in a mutable container separate from the base operating system both for the benefit of separating concerns and allowing dev. environments to be reused and shared.</p>
<blockquote>
<p>The <strong><em>toolbx</em></strong> project was originally named <strong><em>toolbox</em></strong> and the command reflects this.</p>
</blockquote>
<p>Typical usage might look like this:</p>
<pre><code class="lang-sh">[user@hostname ~]$ toolbox create
Created container: fedora-toolbox-36
Enter with: toolbox enter
[user@hostname ~]$ toolbox enter
</code></pre>
<p>While this may provide the foundation of a compelling development workflow, I felt something was missing. From my perspective, a development environment is specific to the project being developed.</p>
<p>So, you could use <strong>toolbox</strong> like this:</p>
<pre><code class="lang-sh">[user@hostname ~]$ <span class="hljs-built_in">cd</span> my-project
[user@hostname my-project]$ toolbox create my-project-toolbox
[user@hostname my-project]$ toolbox enter my-project-toolbox
</code></pre>
<p>And proceed to install dev. dependencies in your container, but I want a toolbox that can be created and recreated on-demand, from a <code>Dockerfile</code>, for example. It then makes sense to commit this dev. environment manifest to your project so that your environment's specifications follow your project's development. Ultimately, what I sought was a means to more easily manage per-project toolboxes, which prompted the creation of <a target="_blank" href="https://github.com/jcrd/toolboxcutter">toolboxcutter</a>.</p>
<h2 id="heading-toolboxcutter">toolboxcutter</h2>
<p><strong>toolboxcutter</strong> (invoked as <code>tb</code>) is a script that manages the lifecycle of per-project development containers using <code>Dockerfile</code>s.</p>
<p>It began life as a <a target="_blank" href="https://zsh.sourceforge.io/Intro/intro_4.html">zsh function</a>, but as its feature set expanded, I realized it had become a project of its own, so I ported it to a standalone <a target="_blank" href="https://www.gnu.org/software/bash/">bash</a> script.</p>
<p>It's designed to be easy to use. Within a directory containing a <strong>toolbox</strong>-based <code>Dockerfile</code>, simply run <code>tb</code> and your toolbox will be entered, creating the container if necessary.</p>
<p>Install it on Fedora with these commands:</p>
<pre><code class="lang-bash">dnf copr <span class="hljs-built_in">enable</span> jcrd/toolboxcutter
dnf install toolboxcutter
</code></pre>
<p>A complete overview of its functionality is given in its command-line usage message:</p>
<pre><code class="lang-txt">usage: tb [command]

With no command, enter toolbox.

commands:
  init IMAGE      Initialize Dockerfile based on IMAGE
  create [IMAGE]  Create container (from IMAGE if provided)
  recreate        Remove and recreate container
  build           Build image
    options:
      -n NAME       Name of image
      -c            Build without cache
  rebuild         Remove container and rebuild image
    options:
      -n NAME       Name of image
      -c            Build without cache
  stop            Stop container
  rm              Remove container
  rmi             Remove image
  rpkg            Build rpm via rpkg
    options:
      -n NAME       rpkg spec template name
      -e EXT        rpkg spec template extension
  rpkg-install    Build and install rpm via rpkg
    options:
      -n NAME       rpkg spec template name
      -e EXT        rpkg spec template extension
      -r NAME       Name of produced rpm to install
  run COMMAND     Run COMMAND in toolbox
  version         Show version
</code></pre>
<p>As <code>tb</code> is largely a wrapper around <strong>toolbox</strong>, most of its commands translate directly to their <strong>toolbox</strong> equivalents. These are supplemented with image and container management commands that would otherwise require invoking <a target="_blank" href="https://podman.io/">podman</a>.</p>
<h3 id="heading-building-rpm-packages">Building RPM packages</h3>
<p>Unique to <strong>toolboxcutter</strong> is its ability to build <a target="_blank" href="https://rpm.org/">RPM</a> packages from <a target="_blank" href="https://docs.pagure.org/rpkg/">rpkg</a> <a target="_blank" href="https://rpm-packaging-guide.github.io/#what-is-a-spec-file">SPEC</a> template files. This feature has been instrumental in the development, testing, and dogfooding of my own projects while using Fedora. I believe it elevates <code>tb</code> to a truly powerful developer tool for these reasons:</p>
<ul>
<li><p>Build dependencies need only exist in your project's container, minimizing pollution of your workstation's environment;</p>
</li>
<li><p>Most importantly, the development-packaging cycle is combined into a single fluid process, providing interactivity benefits similar to that of a <a target="_blank" href="https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop">read-eval-print loop</a>.</p>
</li>
</ul>
<p>This functionality depends on the availability of <strong>rpkg</strong> in the toolbox container itself, so your project's <code>Dockerfile</code> should install it. This is the <code>Dockerfile</code> for the base image I use:</p>
<pre><code class="lang-bash">FROM registry.fedoraproject.org/fedora-toolbox:36

RUN dnf install -y git-subtree
RUN dnf install -y make
RUN dnf install -y neovim
RUN dnf install -y rpkg <span class="hljs-comment"># Install rpkg</span>
RUN dnf install -y rpmdevtools
RUN dnf install -y zsh

CMD /usr/bin/zsh
</code></pre>
<p>Any images or containers derived from this can be used to build RPM packages provided a SPEC template. By default, <code>tb</code> looks for this file at <code>spec/*.rpkg.spec</code> in your project's root directory.</p>
<p>Here is <strong>toolboxcutter</strong>'s own SPEC template file:</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="5651377a5441a8c74807901ff5e8a197"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/jcrd/5651377a5441a8c74807901ff5e8a197" class="embed-card">https://gist.github.com/jcrd/5651377a5441a8c74807901ff5e8a197</a></div><p> </p>
<p>Documentation about the format of these templates is sparse and much of my knowledge was gleaned from existing files. Nevertheless—perhaps with a bit of trial and error—it is possible to run <code>tb rpkg-install</code> in your project's directory to produce an installable RPM package with your latest changes!</p>
]]></content:encoded></item><item><title><![CDATA[Building a D-Bus service in Python]]></title><description><![CDATA[I'm a big fan of D-Bus for implementing services and their clients on Linux. According to its website:

D-Bus is an inter-process communication mechanism—a medium for local communication between processes running on the same host.

Essentially, clien...]]></description><link>https://blog.jamesreed.dev/building-a-d-bus-service-in-python</link><guid isPermaLink="true">https://blog.jamesreed.dev/building-a-d-bus-service-in-python</guid><category><![CDATA[Python]]></category><category><![CDATA[Linux]]></category><category><![CDATA[D-Bus]]></category><dc:creator><![CDATA[James Reed]]></dc:creator><pubDate>Tue, 13 Sep 2022 05:15:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1663046019412/PsqJd5gyg.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I'm a big fan of <a target="_blank" href="https://www.freedesktop.org/wiki/Software/dbus/">D-Bus</a> for implementing services and their clients on Linux. According to its website:</p>
<blockquote>
<p>D-Bus is an inter-process communication mechanism—a medium for local communication between processes running on the same host.</p>
</blockquote>
<p>Essentially, clients connect to the system- or session-level bus where they can then communicate with associated services by calling their methods or listening to their signals, which looks something like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1663027024128/A6PDPNm1C.png" alt="D-Bus_method_invocation.svg.png" /></p>
<p>This is the approach I used with <a target="_blank" href="https://twiddlingbits.net/sessiond">sessiond</a> and one I decided to implement in my monitor brightness controller, <a target="_blank" href="https://twiddlingbits.net/lighten">lighten</a>—written in Python.</p>
<p>First, let's address <em>why</em> D-Bus is an appropriate choice for such systems:</p>
<ul>
<li><p>Using a standardized, full-featured protocol means we don't have to reinvent a client-server communication paradigm.</p>
</li>
<li><p>Its ubiquity on the Linux desktop means access to a greater ecosystem of tools, language bindings, and learning resources.</p>
</li>
</ul>
<p>Now, let's go into detail about the D-Bus architecture and explore the <em>how</em> of building a service in Python!</p>
<h1 id="heading-d-bus-architecture">D-Bus architecture</h1>
<p>There are typically at least two D-Bus <em>buses</em> running in a given Linux environment. One operates at the system-level, independent of logged-in users. This is where system services, such as <strong>systemd</strong> and <strong>systemd-logind</strong> interact. Additionally, each user gets their own bus, where session-level services communicate.</p>
<p>When a service connects to one of these buses, it registers using a <em>well-known name</em>. This is the address by which other services or clients on the bus refer to it. These names conventionally look like reversed domain names. In the case of <strong>lighten</strong>, this name is: <code>com.github.jcrd.lighten</code>.</p>
<p>Services expose communication endpoints called <em>objects</em>, identified by an object path like <code>/com/github/jcrd/lighten</code>. These objects implement interfaces, where methods and signals are defined. <code>lighten</code> has two such interfaces:</p>
<ul>
<li><p><code>com.github.jcrd.lighten.Backlight</code>, with methods for interacting with the monitor's backlight;</p>
</li>
<li><p><code>com.github.jcrd.lighten.Sensor</code>, with a method to get sensor data.</p>
</li>
</ul>
<p>Each of these interfaces is implemented by a single object, but this isn't always the case. For example, <strong>sessiond</strong> has an interface for audio sinks, with an object created for each existing audio device.</p>
<p>Interfaces define methods with signatures denoting the type of in and out parameters. These parameters can be thought of as arguments and return values, respectively. Supported types include:</p>
<ul>
<li><p>integers</p>
</li>
<li><p>booleans</p>
</li>
<li><p>strings</p>
</li>
<li><p>arrays</p>
</li>
<li><p>dictionaries</p>
</li>
</ul>
<p>See this <a target="_blank" href="https://pythonhosted.org/txdbus/dbus_overview.html">document</a> for a complete list.</p>
<p>Methods are <em>1:1</em> modes of communication, returning data to the requesting client.</p>
<p>Interfaces also define signals with type signatures. Signals are <em>1:n</em> modes of communication, publishing data to all subscribed clients.</p>
<p>This overview grants an understanding of the flow of data through the D-Bus architecture sufficient to create our own service. Let's go!</p>
<h1 id="heading-python-implementation">Python implementation</h1>
<p>There are <a target="_blank" href="https://wiki.python.org/moin/DbusExamples">numerous</a> Python libraries for building D-Bus services. Perhaps historically the most popular, <a target="_blank" href="https://dbus.freedesktop.org/doc/dbus-python/">dbus-python</a> now describes itself as a legacy API and advises the use of alternatives.</p>
<p>I think the best alternative is GDBus, the D-Bus subsystem integrated into <a target="_blank" href="https://docs.gtk.org/glib/">GLib</a>. I used the C library in <strong>sessiond</strong>. It's usable in Python via <a target="_blank" href="https://pygobject.readthedocs.io">PyGObject</a> which also provides access to much of <strong>GLib</strong> itself.</p>
<p>The biggest hurdle was the lack of Python-specific documentation. Thankfully, I found <a target="_blank" href="https://discourse.gnome.org/t/minimal-example-of-gdbus-in-python/3165/26">this</a> discussion, wherein user <em>J Arun Mani</em> (thanks!) provides working examples of both client and server code.</p>
<p>Follow <a target="_blank" href="https://pygobject.readthedocs.io/en/latest/getting_started.html">this</a> tutorial to install <strong>PyGObject</strong> and let's get to work!</p>
<p>To build out the D-Bus service in Python, first import the relevant libraries:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> gi.repository <span class="hljs-keyword">import</span> Gio, GLib
</code></pre>
<p>Next, use inline XML to specify the service's interfaces:</p>
<pre><code class="lang-xml">xml = f"""
<span class="hljs-tag">&lt;<span class="hljs-name">node</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">interface</span> <span class="hljs-attr">name</span>=<span class="hljs-string">'com.github.jcrd.lighten.Backlight'</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">method</span> <span class="hljs-attr">name</span>=<span class="hljs-string">'SetBrightness'</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">arg</span> <span class="hljs-attr">name</span>=<span class="hljs-string">'value'</span> <span class="hljs-attr">type</span>=<span class="hljs-string">'u'</span> <span class="hljs-attr">direction</span>=<span class="hljs-string">'in'</span>/&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">arg</span> <span class="hljs-attr">name</span>=<span class="hljs-string">'success'</span> <span class="hljs-attr">type</span>=<span class="hljs-string">'b'</span> <span class="hljs-attr">direction</span>=<span class="hljs-string">'out'</span>/&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">method</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">method</span> <span class="hljs-attr">name</span>=<span class="hljs-string">'AddBrightness'</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">arg</span> <span class="hljs-attr">name</span>=<span class="hljs-string">'value'</span> <span class="hljs-attr">type</span>=<span class="hljs-string">'i'</span> <span class="hljs-attr">direction</span>=<span class="hljs-string">'in'</span>/&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">arg</span> <span class="hljs-attr">name</span>=<span class="hljs-string">'success'</span> <span class="hljs-attr">type</span>=<span class="hljs-string">'b'</span> <span class="hljs-attr">direction</span>=<span class="hljs-string">'out'</span>/&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">method</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">method</span> <span class="hljs-attr">name</span>=<span class="hljs-string">'RestoreBrightness'</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">arg</span> <span class="hljs-attr">name</span>=<span class="hljs-string">'success'</span> <span class="hljs-attr">type</span>=<span class="hljs-string">'b'</span> <span class="hljs-attr">direction</span>=<span class="hljs-string">'out'</span>/&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">method</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">method</span> <span class="hljs-attr">name</span>=<span class="hljs-string">'GetBrightness'</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">arg</span> <span class="hljs-attr">name</span>=<span class="hljs-string">'value'</span> <span class="hljs-attr">type</span>=<span class="hljs-string">'i'</span> <span class="hljs-attr">direction</span>=<span class="hljs-string">'out'</span>/&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">method</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">interface</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">node</span>&gt;</span>
"""
</code></pre>
<p><a target="_blank" href="https://developer-old.gnome.org/gio/stable/ch35s05.html">This</a> page provides more information about the D-Bus Introspection XML.</p>
<p>Every interface needs an accompanying <em>interface handler function</em> to dispatch Python code based on the called D-Bus method. The handler's function signature is:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">handler</span>(<span class="hljs-params">self, conn, sender, path, iname, method, params, invo</span>):</span>
</code></pre>
<p>The useful arguments are:</p>
<ul>
<li><p><code>method</code>: the name of the called method;</p>
</li>
<li><p><code>params</code>: the parameters to the called method;</p>
</li>
<li><p><code>invo</code>: the invocation object used to return values.</p>
</li>
</ul>
<p><code>params</code> must be converted to Python types and accessed as an array. Get the first D-Bus method parameter with:</p>
<pre><code class="lang-python">params.unpack()[<span class="hljs-number">0</span>]
</code></pre>
<p>Return values must be provided to <code>invo</code> as a <strong>GLib</strong> <a target="_blank" href="https://docs.gtk.org/glib/struct.Variant.html">variant</a>:</p>
<pre><code class="lang-python">invo.return_value(GLib.Variant(<span class="hljs-string">"(b)"</span>, (<span class="hljs-literal">True</span>,)))
</code></pre>
<p>The D-Bus signature, <code>(b)</code>, is always a structure in this use case, so parentheses are required. Refer to the Signature Encoding table <a target="_blank" href="https://pythonhosted.org/txdbus/dbus_overview.html">here</a> to determine the appropriate type character.</p>
<p>To wire everything together, three different <em>bus handler functions</em> can be defined. These are called when:</p>
<ol>
<li><p>the service connects to its bus;</p>
</li>
<li><p>the service is assigned its name;</p>
</li>
<li><p>the service loses its name.</p>
</li>
</ol>
<p>There are some nuances in the operation of these handlers. The first two serve nearly the same purpose, but it could be the case that bus connection succeeds yet a service with the same name already exists, so the second handler might not be called. It is important to note that a service can replace one with the same name provided the proper initialization flag. It's in this case that the replaced service's third handler is called. Disconnection from the bus itself would also trigger it.</p>
<p>Discretion is advised in choosing which to fully implement. For <strong>sessiond</strong>, which must exhibit robust behavior, I implemented handlers 2 and 3, forgoing 1 entirely. However, for <strong>lighten</strong>, I only used the first!</p>
<p>The general approach is to register objects when the service is connected and available, and, if appropriate, tear them down should the service become unavailable.</p>
<p>I recommend encapsulating all the service logic in a Python class, like so:</p>
<pre><code class="lang-python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Service</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-comment"># Parse the XML interfaces:</span>
        self.node = Gio.DBusNodeInfo.new_for_xml(xml)
        <span class="hljs-comment"># Reference the GLib main loop:</span>
        self.loop = GLib.MainLoop()

        <span class="hljs-comment"># Connect to a bus:</span>
        self.owner_id = Gio.bus_own_name(
            <span class="hljs-comment"># Specify connection to the session bus:</span>
            Gio.BusType.SESSION,
            <span class="hljs-comment"># Set the well-known name:</span>
            <span class="hljs-string">"com.github.jcrd.lighten"</span>,
            <span class="hljs-comment"># Provide any flags</span>
            <span class="hljs-comment"># (for example, to allow replacement):</span>
            Gio.BusNameOwnerFlags.NONE,
            <span class="hljs-comment"># Provide handler 1 (defined below):</span>
            self.on_bus_acquired,
            <span class="hljs-comment"># Provide handler 2:</span>
            <span class="hljs-literal">None</span>,
            <span class="hljs-comment"># Provide handler 3:</span>
            <span class="hljs-literal">None</span>,
        )

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__del__</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-comment"># Disconnect when the class is destroyed:</span>
        Gio.bus_unown_name(self.owner_id)

    <span class="hljs-comment"># Define handler 1:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">on_bus_acquired</span>(<span class="hljs-params">self, conn, name</span>):</span>
        <span class="hljs-comment"># Register an object:</span>
        conn.register_object(
            <span class="hljs-comment"># Set the object path:</span>
            <span class="hljs-string">"/com/github/jcrd/lighten"</span>,
            <span class="hljs-comment"># Specify the interface via index</span>
            <span class="hljs-comment"># (as defined above in the XML):</span>
            self.node.interfaces[<span class="hljs-number">0</span>],
            <span class="hljs-comment"># Provide the interface handler function:</span>
            self.on_handle_backlight,
            <span class="hljs-literal">None</span>,
            <span class="hljs-literal">None</span>,
        )

  <span class="hljs-comment"># Define the interface handler function</span>
  <span class="hljs-comment"># (abbreviated here with ...):</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">on_handle_backlight</span>(<span class="hljs-params">self, conn, sender, path, iname, method, params, invo</span>):</span>
    <span class="hljs-keyword">if</span> method == <span class="hljs-string">"SetBrightness"</span>:
        v = params.unpack()[<span class="hljs-number">0</span>]
        r = ...(v)
        invo.return_value(GLib.Variant(<span class="hljs-string">"(b)"</span>, (r,)))
    <span class="hljs-keyword">elif</span> method == <span class="hljs-string">"AddBrightness"</span>:
        ...
</code></pre>
<p>Now, instantiate the class and run the D-Bus service with:</p>
<pre><code class="lang-python">Service().loop.run()
</code></pre>
<p>Finally, use a tool such as <a target="_blank" href="https://wiki.gnome.org/Apps/DFeet">d-feet</a> to admire the fruits of your labor!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1663045646750/7brC1NseO.png" alt="Screenshot from 2022-09-13 01-03-00.png" /></p>
]]></content:encoded></item><item><title><![CDATA[I made a monitor brightness controller using an Arduino-powered light sensor]]></title><description><![CDATA[Whenever I would adjust my desktop monitor's brightness as the day progressed, I opened a window, or I turned on/off a light in the room, I would think: if only this happened automatically... I'm manually changing the monitor brightness to a value di...]]></description><link>https://blog.jamesreed.dev/lighten</link><guid isPermaLink="true">https://blog.jamesreed.dev/lighten</guid><category><![CDATA[arduino]]></category><category><![CDATA[Python]]></category><category><![CDATA[Linux]]></category><category><![CDATA[hardware]]></category><dc:creator><![CDATA[James Reed]]></dc:creator><pubDate>Mon, 05 Sep 2022 03:59:05 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1676241934687/c6f04d9a-5058-40d3-a02a-271269f7fcc4.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Whenever I would adjust my desktop monitor's brightness as the day progressed, I opened a window, or I turned on/off a light in the room, I would think: if only this happened automatically... I'm manually changing the monitor brightness to a value directly related to the room's level of light!</p>
<p>I was already changing my monitor's brightness via its <a target="_blank" href="https://en.wikipedia.org/wiki/Display_Data_Channel">DDC/CI</a> interface using <a target="_blank" href="https://www.ddcutil.com/">ddcutil</a> on <a target="_blank" href="https://getfedora.org/">Fedora</a>, so I figured this could be achieved. I set out to build a software monitor brightness controller in Python that uses data from an Arduino-powered light sensor to correlate ambient light and backlight brightness. Now, as the system regularly reads the sensor data, my monitor will auto<em>magically</em> change its brightness to reflect the room's level of ambient light—I call it: <strong>lighten</strong>. 💡</p>
<p>Here I will share how to go about setting up the hardware and software for this project, how it works, and what I learned along the way.</p>
<h1 id="heading-setup">Setup</h1>
<h2 id="heading-the-hardware">The hardware</h2>
<blockquote>
<p>For any of this to work, a <a target="_blank" href="https://www.ddcutil.com/">ddcutil</a>-controllable monitor must be used.</p>
</blockquote>
<p>The hardware side of this project is composed of an Arduino board, a light sensor, and a means of connecting them.</p>
<p>I chose these components:</p>
<ul>
<li><p><a target="_blank" href="https://www.adafruit.com/product/5325">Adafruit QT Py ESP32-S2</a></p>
<ul>
<li>This board has built-in support for the <strong>TinyUSB</strong> library</li>
</ul>
</li>
<li><p><a target="_blank" href="https://www.adafruit.com/product/1980">Adafruit TSL2591 Light Sensor</a></p>
<ul>
<li>This sensor is precise, allowing for exact lux calculations</li>
</ul>
</li>
<li><p><a target="_blank" href="https://www.adafruit.com/product/5384">STEMMA QT Cable</a></p>
<ul>
<li>This cable is super convenient!</li>
</ul>
</li>
</ul>
<p>Simply connect the QT Py board to the light sensor with the STEMMA QT cable and position them behind your monitor with masking tape!</p>
<p>Behold the beautiful hack:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1676242318259/381b4b0f-0958-49bc-9370-d9dc39f9c203.jpeg" alt /></p>
<p>Remember to keep the light sensor itself exposed:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1676242328243/4c2aacbf-9234-4616-9f85-f63dbc847245.jpeg" alt /></p>
<p>Power and receive data from the Arduino with a USB-C to USB cable plugged into your computer.</p>
<h2 id="heading-the-software">The software</h2>
<p>The Arduino code to read the light sensor data and the system-side controller are open-source.</p>
<blockquote>
<p>This process requires a working Arduino IDE, basic knowledge of uploading sketches, and experience with the Linux command line.</p>
</blockquote>
<h3 id="heading-arduino">Arduino</h3>
<p>The Arduino code can be found here: <a target="_blank" href="https://github.com/jcrd/arduino-lighten">arduino-lighten</a>. It requires the following libraries:</p>
<ul>
<li><p><a target="_blank" href="https://github.com/adafruit/Adafruit_TSL2591_Library">Adafruit TSL2591 library</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/adafruit/Adafruit_Sensor">Adafruit Unified Sensor</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/adafruit/Adafruit_TinyUSB_Arduino">Adafruit TinyUSB</a> (The board used should have <a target="_blank" href="https://github.com/adafruit/Adafruit_TinyUSB_Arduino#cores-with-built-in-support">built-in support</a> for this library)</p>
</li>
</ul>
<p><a target="_blank" href="https://learn.adafruit.com/adafruit-tsl2591/wiring-and-test#install-adafruit-tsl2591-library-2980796">This tutorial</a> explains how to set up the first two.</p>
<p>The <strong>TinyUSB</strong> library can be installed similarly:</p>
<blockquote>
<p>In the Arduino IDE menus, go to Sketch -&gt; Include Library -&gt; Manage Libraries, then search for and install Adafruit TinyUSB.</p>
</blockquote>
<p>Now clone the <strong>arduino-lighten</strong> repo:</p>
<pre><code class="lang-sh">git <span class="hljs-built_in">clone</span> https://github.com/jcrd/arduino-lighten.git
</code></pre>
<p>Open the <code>arduino-lighten.ino</code> file in the Arduino IDE and upload it to the board.</p>
<p>Next, appropriate <strong>udev</strong> rules must be configured to access the HID device.</p>
<p>Create a <code>.rules</code> file in <code>/etc/udev/rules.d</code> that looks like:</p>
<pre><code class="lang-txt">SUBSYSTEM=="usb", ATTR{idVendor}=="239a", ATTR{idProduct}=="8111", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl"
KERNEL=="hidraw*", ATTRS{idVendor}=="239a", ATTRS{idProduct}=="8111", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl"
</code></pre>
<p>where the <code>idVendor</code> and <code>idProduct</code> attributes reflect the values found using <code>lsusb</code>:</p>
<pre><code class="lang-plaintext"> &gt; lsusb
 Bus 005 Device 002: ID 239a:8111 Adafruit QT Py ESP32-S2
</code></pre>
<p>Here the vendor ID is <code>239a</code> and the product ID is <code>8111</code>.</p>
<p>See <a target="_blank" href="https://pypi.org/project/hidapi/#udev-rules">this section</a> of the <a target="_blank" href="https://github.com/trezor/cython-hidapi">hidapi Python library</a> documentation for more information.</p>
<h3 id="heading-lighten">lighten</h3>
<p><a target="_blank" href="https://github.com/jcrd/lighten">lighten</a> runs on your Linux computer and controls your monitor's brightness with <a target="_blank" href="https://www.ddcutil.com/">ddcutil</a>.</p>
<p>This tool requires read/write access to <code>/dev/i2c</code> video card devices. In order to use it without root permissions:</p>
<ol>
<li><p>Add user to <code>i2c</code> group:</p>
<pre><code class="lang-sh"> sudo usermod &lt;user-name&gt; -aG i2c
</code></pre>
</li>
<li><p>Copy ddcutil's udev rule into place:</p>
<pre><code class="lang-sh"> sudo cp /usr/share/ddcutil/data/45-ddcutil-i2c.rules /etc/udev/rules.d
</code></pre>
</li>
<li><p>Reload and trigger the new rule:</p>
<pre><code class="lang-sh"> sudo udevadm control --reload
 sudo udevadm trigger
</code></pre>
</li>
</ol>
<p>See <a target="_blank" href="https://www.ddcutil.com/i2c_permissions/">this document</a> for more information.</p>
<p>Now, let's set up <strong>lighten</strong>. It's currently available as an RPM package on Fedora, but it should be compatible with any Linux distro if installed from source!</p>
<ol>
<li><p>Install with <a target="_blank" href="https://copr.fedorainfracloud.org/coprs/jcrd/lighten/">copr</a>:</p>
<pre><code class="lang-sh"> dnf copr <span class="hljs-built_in">enable</span> jcrd/lighten
 dnf install lighten
</code></pre>
</li>
<li><p>lighten requires the product and vendor ID of the Arduino HID device. These are the same values we found previously using <code>lsusb</code>, i.e. <code>239a</code> and <code>8111</code>. Create a new file at <code>~/.config/lighten/lightend.conf</code> with this content:</p>
<pre><code class="lang-txt"> [sensor]
 vendor_id=239a
 product_id=8111
</code></pre>
</li>
<li><p>Enable the daemon's <code>systemd</code> service:</p>
<pre><code class="lang-sh"> systemctl --user <span class="hljs-built_in">enable</span> --now lightend
</code></pre>
<p> If the command above succeeds, everything should be operational!</p>
</li>
</ol>
<h1 id="heading-how-it-works">How it works</h1>
<p><strong>lighten</strong> makes no assumptions about what monitor brightness value corresponds to an ambient light reading. The daemon, <code>lightend</code>, runs in the background and records manual changes to monitor brightness made with the client, <code>lighten</code>, until it's able to guess which values are appropriate.</p>
<p><code>lighten</code> is used to adjust monitor brightness like this:</p>
<pre><code class="lang-sh">lighten <span class="hljs-built_in">set</span> - 10 <span class="hljs-comment"># decrease brightness by 10</span>
lighten <span class="hljs-built_in">set</span> + 20 <span class="hljs-comment"># increase brightness by 20</span>
lighten <span class="hljs-built_in">set</span> = 100 <span class="hljs-comment"># set brightness to max</span>
</code></pre>
<p>Over time, <strong>lighten</strong> builds up a database of the ideal monitor brightness in relation to the ambient light level based on your adjustments, and restores brightness:</p>
<ul>
<li><p>when ambient light changes significantly</p>
</li>
<li><p>on demand</p>
</li>
<li><p>at startup</p>
</li>
<li><p>upon wakeup from sleep</p>
</li>
<li><p>at regular intervals as time passes</p>
</li>
</ul>
<p><strong>Update</strong>: If appropriate, lighten can set the monitor brightness to the value detected by the sensor, so that a sensor value of 100 corresponds to 100 monitor brightness. This is enabled by setting the <code>normalize_mode</code> configuration option to <code>true</code>.</p>
<h1 id="heading-what-i-learned">What I learned</h1>
<p>I learned how to implement and interface with an <a target="_blank" href="https://en.wikipedia.org/wiki/Human_interface_device">HID</a> device using <strong>TinyUSB</strong> and <a target="_blank" href="https://pypi.org/project/hid/">python-hid</a> after trying and failing to maintain a serial connection to the Arduino board upon the computer waking up from suspension.</p>
<p>I also learned how to use <strong>GLib</strong> via <a target="_blank" href="https://pygobject.readthedocs.io/en/latest/">PyGObject</a> to run a main loop with custom GSources alongside a DBus server all in Python. I found only scattered documentation about this, so I wrote about building a D-Bus service in Python <a target="_blank" href="https://twiddlingbits.net/building-a-d-bus-service-in-python">here</a>!</p>
]]></content:encoded></item><item><title><![CDATA[Debug logging in Go]]></title><description><![CDATA[My project, lifelight, is written in Go and designed to run on
low-powered Raspberry Pis. I wanted minimal overhead in the production
build, so I implemented a logging system that is compiled out in non-debug
builds. This is made easy using Go's buil...]]></description><link>https://blog.jamesreed.dev/debug-logging-in-go</link><guid isPermaLink="true">https://blog.jamesreed.dev/debug-logging-in-go</guid><category><![CDATA[Go Language]]></category><category><![CDATA[debugging]]></category><category><![CDATA[development]]></category><category><![CDATA[Raspberry Pi]]></category><dc:creator><![CDATA[James Reed]]></dc:creator><pubDate>Sun, 15 May 2022 00:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1662995943916/HzU1MkZ8F.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>My project, <a target="_blank" href="https://github.com/jcrd/lifelight">lifelight</a>, is written in Go and designed to run on
low-powered <a target="_blank" href="https://www.raspberrypi.com/products/raspberry-pi-zero/">Raspberry Pi</a>s. I wanted minimal overhead in the production
build, so I implemented a logging system that is compiled out in non-debug
builds. This is made easy using Go's <a target="_blank" href="https://pkg.go.dev/go/build#hdr-Build_Constraints"><em>build constraints</em></a>.</p>
<h2 id="heading-the-code">The code</h2>
<p>This implementation utilizes three files: the main file, and two logger
implementation files with build constraints.</p>
<p>The main file includes the interface definition and a global variable:</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">type</span> Logger <span class="hljs-keyword">interface</span> {
    <span class="hljs-comment">// format string, and objects to format</span>
    Log(<span class="hljs-keyword">string</span>, ...<span class="hljs-keyword">interface</span>{})
}

<span class="hljs-keyword">var</span> logger Logger
</code></pre>
<p>One of the logger implementations, the <em>dummy</em>, satisfies this interface with
methods that do nothing:</p>
<pre><code class="lang-go"><span class="hljs-comment">//go:build !debug</span>

<span class="hljs-keyword">package</span> main

<span class="hljs-keyword">type</span> DummyLogger <span class="hljs-keyword">struct</span> {}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(dl DummyLogger)</span> <span class="hljs-title">log</span><span class="hljs-params">(format <span class="hljs-keyword">string</span>, v ...<span class="hljs-keyword">interface</span>{})</span></span> {}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">init</span><span class="hljs-params">()</span></span> {
    logger = DummyLogger{}
}
</code></pre>
<p>The <code>//go:build !debug</code> build constraint means that it will only be included in
the production build when the <code>debug</code> tag is absent.</p>
<p>The <code>init</code> function will be called at runtime, setting the global <code>logger</code>
variable for use in the rest of the program.</p>
<p>The debug logger is implemented in the same way, using the <code>//go:build debug</code>
build constraint:</p>
<pre><code class="lang-go"><span class="hljs-comment">//go:build debug</span>

<span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> (
 <span class="hljs-string">"log"</span>
)

<span class="hljs-keyword">type</span> DebugLogger <span class="hljs-keyword">struct</span> {}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(dl *DebugLogger)</span> <span class="hljs-title">log</span><span class="hljs-params">(format <span class="hljs-keyword">string</span>, v ...<span class="hljs-keyword">interface</span>{})</span></span> {
    log.Printf(format, v...)
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">init</span><span class="hljs-params">()</span></span> {
 logger = &amp;DebugLogger{}
}
</code></pre>
<h2 id="heading-the-build">The build</h2>
<p>Specifying tags at build time is simple:</p>
<pre><code class="lang-shell">go build -tags debug .
</code></pre>
<p>This can be integrated into a <code>Makefile</code>, for example:</p>
<pre><code class="lang-shell">main:
    go build .

debug:
    go build -tags debug .

.PHONY: debug
</code></pre>
<p>Now, logging does nothing unless the <code>debug</code> tag is specified!</p>
]]></content:encoded></item><item><title><![CDATA[luarocket]]></title><description><![CDATA[The Lua programming language, like many others, employs a purpose-built package manager to download and install its ecosystem of libraries. This tool, LuaRocks, has some notable features:

downloads/installs packages to user-specific or system-wide l...]]></description><link>https://blog.jamesreed.dev/luarocket</link><guid isPermaLink="true">https://blog.jamesreed.dev/luarocket</guid><category><![CDATA[Linux]]></category><category><![CDATA[Lua]]></category><category><![CDATA[development]]></category><dc:creator><![CDATA[James Reed]]></dc:creator><pubDate>Sun, 24 Apr 2022 00:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1674766079985/0c889373-90ff-485c-81ff-1eede926f4ba.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The Lua programming language, like many others, employs a purpose-built package manager to download and install its ecosystem of libraries. This tool, <a target="_blank" href="https://luarocks.org/">LuaRocks</a>, has some notable features:</p>
<ul>
<li><p>downloads/installs packages to user-specific or system-wide locations</p>
</li>
<li><p>configurable dependency resolution</p>
</li>
<li><p>builds locally define rocks (Lua modules packaged by LuaRocks)</p>
</li>
<li><p>queries information about the active LuaRocks configuration</p>
</li>
</ul>
<p>In these ways, LuaRocks is functionally similar to the package managers of other dynamic languages such as <a target="_blank" href="https://pip.pypa.io/en/stable/">pip</a> for Python and <a target="_blank" href="https://www.npmjs.com/">npm</a> for JavaScript.</p>
<p>Where these features fall short is in managing the dependencies of multiple Lua projects—each with a potentially different version of Lua. Being that Lua is an <a target="_blank" href="https://www.lua.org/pil/24.html"><em>embedded language</em></a>, this is in fact common, and further exacerbated by the popularity of both the reference <a target="_blank" href="https://www.lua.org/">Lua</a> implementation and <a target="_blank" href="https://luajit.org/">luajit</a>.</p>
<h2 id="heading-problem">Problem</h2>
<p>I encountered this issue during the development of an experimental window manager, <a target="_blank" href="https://github.com/jcrd/dovetail">dovetail</a>. It is based on the <a target="_blank" href="https://awesomewm.org/">awesome window manager</a> framework, which can be built with Lua versions 5.1-5.3 or luajit. It also depends on a few Lua packages, and therein lies the problem. It quickly became an overwhelming task to unify the range of Lua versions and implementations, 3rd party packages, varying installation paths, and integration with an embedded interpreter.</p>
<h2 id="heading-solution">Solution</h2>
<p>After consideration of my options, I decided the easiest solution was to vendor all dependency packages. I set up Makefile rules to download the version of packages specified in a version-controlled file to a specific Lua rocks tree where the package’s files are extracted and moved into a <code>lua_modules</code> directory in the root of the project. During installation, these modules are copied to a system-wide shared data directory and referenced by awesome’s Lua interpreter.</p>
<p>When I required this functionality outside of the dovetail project, I re-implemented these Makefile rules as a shell script called <a target="_blank" href="https://github.com/jcrd/luarocket">luarocket</a>. It is meant to be included directly in a project and utilized as part of its build process.</p>
]]></content:encoded></item><item><title><![CDATA[An alternative markup language for man pages]]></title><description><![CDATA[While developing many of my projects I've found myself faced with the timeless question: to document or not to document? My goal is to provide man pages for any project used from the command-line or designed to be integrated into a larger Linux ecosy...]]></description><link>https://blog.jamesreed.dev/alt-markup-for-man-pages</link><guid isPermaLink="true">https://blog.jamesreed.dev/alt-markup-for-man-pages</guid><category><![CDATA[Linux]]></category><category><![CDATA[perl]]></category><category><![CDATA[documentation]]></category><category><![CDATA[development]]></category><dc:creator><![CDATA[James Reed]]></dc:creator><pubDate>Sun, 30 Jan 2022 00:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1662584424375/7RkZVK08C.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>While developing many of my projects I've found myself faced with the timeless question: to document or not to document? My goal is to provide <a target="_blank" href="https://en.wikipedia.org/wiki/Man_page">man pages</a> for any project used from the command-line or designed to be integrated into a larger Linux ecosystem. And so, at the onset of my open-source journey, I stood at a crossroad: learn the unwieldy <a target="_blank" href="https://en.wikipedia.org/wiki/Troff">troff</a> format of man pages or set up a process to build them from an alternative markup format.</p>
<p>This is an excerpt from the <strong>time</strong> command's man page written in <em>troff</em>:</p>
<pre><code class="lang-txt">.TH TIME 1 2019-03-06 "" "Linux User's Manual"
.SH NAME
time \- time a simple command or give resource usage
.SH SYNOPSIS
.B time \c
.RI [ options ] " command " [ arguments... ]
.SH DESCRIPTION
The
.B time
command runs the specified program
.I command
with the given arguments.
</code></pre>
<p>From my perspective, an alternative is <em>necessary</em>. The markup syntax is cryptic and what would be inline markup in other formats requires a newline in <em>troff</em>.</p>
<p>There are numerous means of generating man pages from assorted markup formats. For example, using:</p>
<ul>
<li><p><em>reStructured text</em> with <a target="_blank" href="https://manpages.debian.org/testing/docutils-common/rst2man.1.en.html">rst2man</a>,</p>
</li>
<li><p><em>AsciiDoc</em> format with <a target="_blank" href="https://docs.asciidoctor.org/asciidoctor/latest/manpage-backend/">AsciiDoc</a> itself,</p>
</li>
<li><p><em>Markdown</em> with <a target="_blank" href="https://spin.atomicobject.com/2015/05/06/man-pages-in-markdown-ronn/">ronn</a> or <a target="_blank" href="https://gabmus.org/posts/man_pages_with_markdown_and_pandoc/">pandoc</a>,</p>
</li>
<li><p>or <em>Pod</em> with <a target="_blank" href="https://perldoc.perl.org/pod2man">pod2man</a></p>
</li>
</ul>
<p>to name a few.</p>
<p>Admittedly, I'd enjoy writing man pages in <em>Markdown</em>, but in approaching the question of an appropriate build pipeline, I decided early on that I'd like to avoid any external dependencies. This eliminates most options, but the <strong>pod2man</strong> command is available by default in the Linux distributions I've used. With this in mind, Perl's <a target="_blank" href="https://perldoc.perl.org/perlpod"><em>Pod</em></a>, or Plain Old Documentation, is the top contender.</p>
<p>Here's an example of a <em>Pod</em>-formatted man page from my <a target="_blank" href="https://github.com/jcrd/iniq">iniq</a> project:</p>
<pre><code class="lang-md">=head1 NAME

iniq - INI file reader

=head1 SYNOPSIS

B<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">iniq</span>&gt;</span></span> [options] [FILE]

With no FILE, read standard input.

=head1 DESCRIPTION

iniq is a simple INI file reader for the command line. It queries an INI file
based on the path <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">I</span>&lt;<span class="hljs-attr">section</span>&gt;</span></span>&gt;<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">I</span>&lt;<span class="hljs-attr">separator</span>&gt;</span></span>&gt;<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">I</span>&lt;<span class="hljs-attr">key</span>&gt;</span></span>&gt; and allows use of custom
separators in the file and formatting of the output. Sections inherit keys from
a special DEFAULT section unless the I<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">-D</span>&gt;</span></span> flag is used. See below for examples.

=head1 OPTIONS

=over

=item B<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">-h</span>&gt;</span></span>

Show help message.
</code></pre>
<p>Compared to <em>troff</em>, this is both easier to write and to read, which are important qualities in the maintenance of documentation.</p>
<h2 id="heading-converting-pod-files-to-man-pages">Converting <code>.pod</code> files to man pages</h2>
<p>Producing a man page from a <code>.pod</code> file is straightforward using the <strong>pod2man</strong> command. In most of my projects, this step is part of a <code>Makefile</code>.</p>
<p>In the case of <strong>iniq</strong>, these <code>Makefile</code> rules build and install its man page:</p>
<pre><code class="lang-shell">VERSIONCMD = git describe --dirty --tags --always 2&gt; /dev/null
VERSION := $(shell $(VERSIONCMD) || cat VERSION)

PREFIX ?= /usr/local
MANPREFIX ?= $(PREFIX)/share/man
MANPAGE = iniq.1

$(MANPAGE): man/$(MANPAGE).pod
 pod2man -n=iniq -c=iniq -s=1 -r=$(VERSION) $&lt; $(MANPAGE)

install: $(MANPAGE)
 mkdir -p $(DESTDIR)$(MANPREFIX)/man1
 cp -p $(MANPAGE) $(DESTDIR)$(MANPREFIX)/man1
</code></pre>
<p>The conversion command:</p>
<pre><code class="lang-shell">pod2man -n=$name -c=$header -s=$section -r=$footer $input $output
</code></pre>
<ul>
<li><p>takes the input <code>.pod</code> file as its first positional argument,</p>
</li>
<li><p>and the output file name as its second;</p>
</li>
<li><p>the <code>-n</code> flag sets the man page name,</p>
</li>
<li><p>the <code>-c</code> flag sets the page header,</p>
</li>
<li><p>the <code>-s</code> flag sets the section, which should match the output file's extension,</p>
</li>
<li><p>the <code>-r</code> flag sets the page footer, which in my projects is always the version.</p>
</li>
</ul>
<h2 id="heading-more-information">More information</h2>
<p>Descriptions of <strong>pod2man</strong>'s various options can be found <a target="_blank" href="https://perldoc.perl.org/pod2man">here</a> or in its man page using <code>man pod2man</code>; and with that we've come full circle!</p>
]]></content:encoded></item><item><title><![CDATA[Debouncing with a custom GSource]]></title><description><![CDATA[While developing sessiond, a session manager written in C with GLib, I was presented with an ideal use case for debouncing: postponing the execution of code that would otherwise run too frequently based on the influx of external events. sessiond is r...]]></description><link>https://blog.jamesreed.dev/debouncing-with-gsource</link><guid isPermaLink="true">https://blog.jamesreed.dev/debouncing-with-gsource</guid><category><![CDATA[C]]></category><category><![CDATA[Linux]]></category><category><![CDATA[GLib]]></category><dc:creator><![CDATA[James Reed]]></dc:creator><pubDate>Tue, 13 Apr 2021 00:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1662584799223/Th1Bqy9RA.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>While developing <a target="_blank" href="https://twiddlingbits.net/sessiond">sessiond</a>, a session manager written in C with <a target="_blank" href="https://docs.gtk.org/glib/">GLib</a>, I was presented with an ideal use case for debouncing: postponing the execution of code that would otherwise run too frequently based on the influx of external events. sessiond is responsible for monitoring X11 input events, which are processed in the GLib event loop using a custom <a target="_blank" href="https://docs.gtk.org/glib/struct.Source.html">GSource</a>. Debouncing avoids unnecessarily handling every event, especially mouse input events which are generated constantly while the mouse pointer moves.</p>
<h2 id="heading-abstract-debouncing">Abstract debouncing</h2>
<p>In the case of sessiond, only the time an input event occurred is relevant, so an abstract implementation of debouncing might look like this:</p>
<pre><code class="lang-python">debounce_interval = <span class="hljs-number">1</span>

<span class="hljs-comment"># Called when time given to `schedule_event_processing` is reached.</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">process_event</span>():</span>
    <span class="hljs-keyword">if</span> current_time() - last_event_time &gt;= debounce_interval:
        process()

<span class="hljs-keyword">while</span> true:
    <span class="hljs-keyword">if</span> event:
        last_event_time = current_time()
        schedule_event_processing(process_event,
          last_event_time + debounce_interval)
</code></pre>
<p>When an event occurs, the current time is recorded as <code>last_event_time</code>. There should be no response to the event until at least <code>debounce_interval</code> has elapsed, so event processing is scheduled for a time in the future equal to the current time plus the debounce interval. If another event occurs before this time has passed, an additional response is scheduled, and <code>last_event_time</code> is updated.</p>
<p>In the processing function, it is important to compare the time that has elapsed since the last event to <code>debounce_interval</code> to ensure additional events were not generated after this response was scheduled. This way, event processing happens only when <code>debounce_interval</code> time has passed since the last event was received—the essence of debouncing.</p>
<h2 id="heading-custom-gsource-implementation">Custom GSource implementation</h2>
<p>This method of debouncing can be implemented with a custom GSource's <code>check</code> and <code>dispatch</code> functions using <a target="_blank" href="https://docs.gtk.org/glib/method.Source.set_ready_time.html">g_source_set_ready_time</a>.</p>
<p>First, define a custom GSource by declaring a struct containing a <code>GSource</code>:</p>
<pre><code class="lang-c"><span class="hljs-keyword">typedef</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> {</span>
    GSource source;
    gpointer fd;
    gint64 last_event_time;
} InputSource;
</code></pre>
<p>Next, implement the <code>check</code> function, which determines if the source is ready to be dispatched:</p>
<pre><code class="lang-c"><span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> DEBOUNCE_US (1 * 1000000)</span>

<span class="hljs-function">gboolean
<span class="hljs-title">inputsource_check</span><span class="hljs-params">(GSource *source)</span>
</span>{
    InputSource *self = (InputSource *)source;
    GIOCondition revents = g_source_query_unix_fd(source, self-&gt;fd);

    <span class="hljs-keyword">if</span> (!(revents &amp; G_IO_IN))
        <span class="hljs-keyword">return</span> FALSE;

    <span class="hljs-comment">// Process events...</span>

    self-&gt;last_event_time = g_get_monotonic_time();
    g_source_set_ready_time(source, self-&gt;last_event_time + DEBOUNCE_US);
}
</code></pre>
<p>In this function, a file descriptor is queried to determine if any events are pending. If there are no events to be handled, the function returns <code>FALSE</code> to signify there is no need to call the <code>dispatch</code> function. Otherwise, pending events should be processed accordingly. Finally, the current monotonic time—being that of the last event(s)—is recorded and the source is instructed to dispatch in <code>DEBOUNCE_US</code> microseconds with <code>g_source_set_ready_time</code>.</p>
<p>Now, implement the <code>dispatch</code> function, which is responsible for calling the callback function provided to this GSource at creation time:</p>
<pre><code class="lang-c"><span class="hljs-function">gboolean
<span class="hljs-title">inputsource_dispatch</span><span class="hljs-params">(GSource *source, GSourceFunc func, gpointer user_data)</span>
</span>{
    XSource *self = (XSource *)source;

    <span class="hljs-keyword">if</span> (g_get_monotonic_time() - self-&gt;last_event_time &gt;= DEBOUNCE_US) {
        g_source_set_ready_time(source, <span class="hljs-number">-1</span>);
        <span class="hljs-keyword">return</span> func(user_data);
    }

    <span class="hljs-keyword">return</span> G_SOURCE_CONTINUE;
}
</code></pre>
<p>The logic here is as follows: this function is called at the monotonic time given to <code>g_source_set_ready_time</code> in the <code>check</code> function above, so we know at least <code>DEBOUNCE_US</code> time has passed since the handling of <em>those</em> events, <strong>but</strong> additional events may have been received in the meantime, reflected by an updated <code>self-&gt;last_event_time</code>. If at least <code>DEBOUNCE_US</code> time has elapsed since the last event, we call the user-provided callback function, and <code>g_source_set_ready_time(source, -1)</code> is used to stop future dispatching of this source until the <code>check</code> function detects pending events. This is necessary because <code>g_source_set_ready_time</code> will cause the source to be continuously dispatched if the time it was last given is in the past, which will inevitably be the case.</p>
<p>Finally, create the <code>GSourceFuncs</code> struct and initialize the custom GSource:</p>
<pre><code class="lang-c">GSourceFuncs inputsource_funcs = {
    <span class="hljs-literal">NULL</span>, <span class="hljs-comment">// prepare function</span>
    inputsource_check,
    inputsource_dispatch,
    <span class="hljs-literal">NULL</span>, <span class="hljs-comment">// finalize function</span>
    <span class="hljs-literal">NULL</span>,
    <span class="hljs-literal">NULL</span>,
};

GSource *source = g_source_new(&amp;inputsource_funcs, <span class="hljs-keyword">sizeof</span>(InputSource));
InputSource *self = (InputSource *)source;

self-&gt;last_event_time = <span class="hljs-number">0</span>;

<span class="hljs-comment">// Add the file descriptor.</span>
<span class="hljs-comment">// self-&gt;fd = g_source_add_unix_fd(source, ..., G_IO_IN);</span>

<span class="hljs-comment">// Set callback.</span>
<span class="hljs-comment">// g_source_set_callback(source, ...);</span>

<span class="hljs-comment">// Attach source to context.</span>
<span class="hljs-comment">// g_source_attach(source, ...);</span>
</code></pre>
<h2 id="heading-more-information">More information</h2>
<p><a target="_blank" href="https://web.archive.org/web/20200806195500/https://developer.gnome.org/gnome-devel-demos/unstable/custom-gsource.c.html.en">This tutorial</a> describes in greater detail the <code>check</code>, <code>dispatch</code>, and other functions that control the behavior of a custom GSource.</p>
<p>For a complete working example, refer to sessiond's debouncing implementation in <a target="_blank" href="https://github.com/jcrd/sessiond/blob/master/src/xsource.c">xsource.c</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Introducing sessiond]]></title><description><![CDATA[When I began using Linux on the desktop in ~2012, I experimented with the prevalent desktop environments (Gnome, KDE, etc.), but was quickly drawn into the realm of tiling window managers by their promise of increased productivity and customization. ...]]></description><link>https://blog.jamesreed.dev/sessiond</link><guid isPermaLink="true">https://blog.jamesreed.dev/sessiond</guid><category><![CDATA[Linux]]></category><category><![CDATA[Window Managers]]></category><category><![CDATA[Desktop Environments]]></category><category><![CDATA[systemd]]></category><dc:creator><![CDATA[James Reed]]></dc:creator><pubDate>Sun, 04 Apr 2021 00:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1662584978469/VzJ0_Vtn0.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When I began using Linux on the desktop in ~2012, I experimented with the prevalent desktop environments (Gnome, KDE, etc.), but was quickly drawn into the realm of tiling window managers by their promise of increased productivity and customization. While I believe these promises were fulfilled, tiling window managers were not without a significant shortcoming: they are solely window managers, lacking the integrated suite of software and applications that put the environment in desktop environments.</p>
<p>The most noticeable omission was proper <em>session management</em>, which facilitates, for instance: locking the screen or suspending the system after a period of inactivity. This was not a dealbreaker on a desktop computer, but using a laptop without these features felt like a dysfunctional hack.</p>
<p>It would be quite a few more years of exploring the universe of tiling window managers before deciding to address this issue myself, and with the broad adoption of <strong>systemd</strong> by the larger Linux ecosystem, it was easier than ever. Enter: <a target="_blank" href="https://github.com/jcrd/sessiond">sessiond</a>, a standalone session manager for Linux.</p>
<h2 id="heading-what-does-it-do">What does it do?</h2>
<p><strong>sessiond</strong> is a daemon for <strong>systemd</strong>-based Linux systems that interfaces with <strong>systemd-logind</strong> to provide the missing session management features to X11 window managers. Its primary responsibility is to monitor keyboard and mouse activity to determine when a session has become idle and to then act accordingly. It is capable of:</p>
<ul>
<li><p>locking the screen when idle and before suspending the system</p>
</li>
<li><p>dimming the screen's backlight when idle</p>
</li>
<li><p>triggering <strong>systemd</strong> targets for use by the window manager or end user</p>
</li>
<li><p>optionally managing DPMS settings</p>
</li>
<li><p>controlling keyboard and monitor backlight brightness</p>
</li>
<li><p>controlling audio sink volume and mute state</p>
</li>
</ul>
<blockquote>
<p>The audio sink interface is new in version <strong>0.6.0</strong>.</p>
</blockquote>
<p>It also provides a D-Bus service so that it may be integrated with modern window managers. For example, a window manager can prevent idling when a media player is open by interacting with the D-Bus methods.</p>
<p>It is designed to be zero-configuration, providing sensible defaults, but allows configuration if needed. See <a target="_blank" href="https://jcrd.github.io/sessiond/configuration/">here</a> for additional details.</p>
<h2 id="heading-how-do-i-use-it">How do I use it?</h2>
<p><strong>sessiond</strong> requires a Linux system utilizing <strong>systemd-logind</strong>. It may be possible to use <a target="_blank" href="https://github.com/elogind/elogind">elogind</a> but this has not been tested.</p>
<blockquote>
<p>This brief tutorial assumes basic knowledge of <strong><em>systemd</em></strong>-based Linux systems and the command-line.</p>
</blockquote>
<h3 id="heading-installation">Installation</h3>
<p>Currently, <strong>sessiond</strong> RPM packages are built for Fedora via <a target="_blank" href="https://copr.fedorainfracloud.org/coprs/jcrd/sessiond/">copr</a> and installable with the following commands:</p>
<pre><code class="lang-shell">dnf copr enable jcrd/sessiond
dnf install sessiond
</code></pre>
<p>If you're using Arch Linux, <strong>sessiond</strong> is available via the <a target="_blank" href="https://aur.archlinux.org/packages/sessiond">AUR</a> (thanks to <a target="_blank" href="https://github.com/denisoster">Denis Oster</a>)!</p>
<p>Use your preferred AUR helper or install it with:</p>
<pre><code class="lang-shell">git clone https://aur.archlinux.org/sessiond.git
cd sessiond
makepkg -si
</code></pre>
<p>I would like to see packages for other major distros in the future. Until <strong>sessiond</strong> achieves world domination, it is recommended to build from source by following <a target="_blank" href="https://jcrd.github.io/sessiond/building/">these instructions</a>.</p>
<h3 id="heading-setting-up-your-window-manager">Setting up your window manager</h3>
<p>The intended way to use <strong>sessiond</strong> with your window manager of choice is to create a custom <strong>systemd</strong> service in the <code>~/.config/systemd/user</code> directory. For example, below is a <code>awesome.service</code> file that runs the <a target="_blank" href="https://awesomewm.org/">Awesome</a> window manager:</p>
<pre><code class="lang-plaintext">[Unit]
Description=Awesome window manager
Requires=sessiond-session.target
After=sessiond.service
PartOf=graphical-session.target

[Service]
ExecStart=/usr/bin/awesome

[Install]
Alias=window-manager.service
</code></pre>
<p>The options in the <code>[Unit]</code> section ensures your window manager is only running alongside the <strong>sessiond</strong> daemon. The <code>Alias=</code> option in the <code>[Install]</code> section lets <strong>sessiond</strong> know this service is the window manager so the session will be stopped when it exits.</p>
<p>Next, enable the window manager service with <code>systemctl --user enable awesome.service</code>.</p>
<p>Now, select the <code>sessiond session</code> entry via your display manager or set it as the default in its configuration file. For example, if using <strong>lightdm</strong>, set <code>user-session=sessiond</code> in <code>/etc/lightdm/lightdm.conf</code>.</p>
<h3 id="heading-locking-the-session">Locking the session</h3>
<p><strong>sessiond</strong> wouldn't be of much use without a means of locking the screen. Create a service for your screen locker of choice in <code>~/.config/systemd/user</code>. For example, here is a <code>i3lock.service</code> that runs <strong>i3lock</strong> as the screen locker:</p>
<pre><code class="lang-plaintext">[Unit]
Description=Lock X session with i3lock
PartOf=graphical-session.target

[Service]
ExecStart=/usr/bin/i3lock
ExecStopPost=/usr/bin/sessionctl unlock

[Install]
WantedBy=graphical-lock.target
</code></pre>
<p>The <code>sessionctl unlock</code> command in <code>ExecStopPost</code> under the <code>[Service]</code> section notifies <strong>sessiond</strong> that the session has been unlocked when the service exits. Enable with <code>systemctl --user enable i3lock</code> so it's started upon triggering the <code>graphical-lock</code> target, which by default occurs when the session becomes inactive and before suspending the system.</p>
<h3 id="heading-other-services">Other services</h3>
<p>Additional services (e.g. a compositor) can be started with the session by creating <strong>systemd</strong> service files in <code>~/.config/systemd/user</code> containing:</p>
<pre><code class="lang-plaintext">[Unit]
PartOf=graphical-session.target
</code></pre>
<p>and:</p>
<pre><code class="lang-plaintext">[Install]
WantedBy=graphical-session.target
</code></pre>
<p>Enable such services with <code>systemctl --user enable &lt;service&gt;</code>.</p>
<h3 id="heading-inhibiting-idling">Inhibiting idling</h3>
<p>It's often desirable to prevent the session from idling while, for instance, watching a video. <code>sessiond-inhibit</code> comes to the rescue:</p>
<pre><code class="lang-shell">sessiond-inhibit -y 'movie night' mpv ...
</code></pre>
<p>This can be automated if your window manager supports client rules and some level of scripting.</p>
<h2 id="heading-more-information">More information</h2>
<p>For more information about <strong>sessiond</strong>, visit its <a target="_blank" href="https://jcrd.github.io/sessiond/">website</a>.</p>
<p>You should now be equipped with sufficient knowledge to go forth and manage your sessions!</p>
]]></content:encoded></item></channel></rss>