<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kir Axanov</title>
    <description>The latest articles on DEV Community by Kir Axanov (@axkira).</description>
    <link>https://dev.to/axkira</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F858749%2F5f0dc95f-89d2-4c6d-97e6-7cdd1aaaf43b.png</url>
      <title>DEV Community: Kir Axanov</title>
      <link>https://dev.to/axkira</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/axkira"/>
    <language>en</language>
    <item>
      <title>Android. Text-to-speech</title>
      <dc:creator>Kir Axanov</dc:creator>
      <pubDate>Sun, 15 Mar 2026 09:58:49 +0000</pubDate>
      <link>https://dev.to/axkira/android-text-to-speech-58le</link>
      <guid>https://dev.to/axkira/android-text-to-speech-58le</guid>
      <description>&lt;p&gt;&lt;em&gt;2026.03.15&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Hi!&lt;/p&gt;

&lt;h2&gt;
  
  
  Install apps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/woheller69/ttsengine" rel="noopener noreferrer"&gt;SherpaTTS&lt;/a&gt; as TTS engine itself (&lt;em&gt;has basic controls for reading text&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/drmfinlay/tts-util-app" rel="noopener noreferrer"&gt;TTS Util&lt;/a&gt; for more convenient TTS interface (&lt;em&gt;not a TTS engine&lt;/em&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also you'll need &lt;code&gt;adb&lt;/code&gt; installed on the desktop so you can copy model files from there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get voice models
&lt;/h2&gt;

&lt;p&gt;SherpaTTS has a list of models which you can download from HuggingFace from the app.&lt;/p&gt;

&lt;p&gt;If that list is not enough (&lt;em&gt;or returns errors, or you want to use your own model&lt;/em&gt;), you can sideload other models.&lt;/p&gt;

&lt;p&gt;Choose the model from e.g. &lt;a href="https://huggingface.co/spaces/k2-fsa/text-to-speech" rel="noopener noreferrer"&gt;HuggingFace&lt;/a&gt;, then search it on the &lt;a href="https://github.com/k2-fsa/sherpa-onnx/releases/tag/tts-models" rel="noopener noreferrer"&gt;release page&lt;/a&gt; and download needed files (&lt;em&gt;already in &lt;code&gt;onnx&lt;/code&gt; format&lt;/em&gt;).&lt;/p&gt;

&lt;p&gt;If you need to convert from Piper model see &lt;a href="https://k2-fsa.github.io/sherpa/onnx/tts/piper.html" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Extract downloaded archive and put those files into &lt;code&gt;yourModelName&lt;/code&gt; directory (&lt;em&gt;we will copy those to the device later&lt;/em&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;*.onnx&lt;/code&gt; renamed as &lt;code&gt;model.onnx&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tokens.txt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lang&lt;/code&gt; - create this file with 3-letter language code on the 1st line and model name on the 2nd&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Copy voice models to device
&lt;/h2&gt;

&lt;p&gt;Connect the smartphone via USB. Enable &lt;code&gt;USB Debugging&lt;/code&gt; and choose &lt;code&gt;File transfer&lt;/code&gt; in USB connection properties on the phone.&lt;/p&gt;

&lt;p&gt;List connected devices and make sure you got your phone there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb devices
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next steps should be done one by one for each model you want to copy.&lt;/p&gt;

&lt;p&gt;Models should be copied to &lt;code&gt;/sdcard/Android/data/org.woheller69.ttsengine/files/modelDir&lt;/code&gt;, so run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb push &amp;lt;path-to-model-files&amp;gt;/ /sdcard/Android/data/org.woheller69.ttsengine/files/modelDir
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can check the files via entering a shell into Android with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb shell
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After copying, open SherpaTTS app. It will register model files and put them where it needs. Then, you can repeat same steps to copy other models.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use via TTS Util
&lt;/h2&gt;

&lt;p&gt;Open &lt;code&gt;TTS Util&lt;/code&gt;, go to settings and select &lt;code&gt;TTS Engine&lt;/code&gt; (&lt;code&gt;SherpaTTS&lt;/code&gt;) and &lt;code&gt;TTS Language/Voice&lt;/code&gt;. Tune other settings as needed.&lt;/p&gt;

&lt;p&gt;Bye!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>android</category>
      <category>opensource</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>NixOS. Snikket. Self-host E2EE messenger with calls</title>
      <dc:creator>Kir Axanov</dc:creator>
      <pubDate>Mon, 02 Mar 2026 16:26:53 +0000</pubDate>
      <link>https://dev.to/axkira/nixos-snikket-self-host-e2ee-messenger-with-calls-3j0i</link>
      <guid>https://dev.to/axkira/nixos-snikket-self-host-e2ee-messenger-with-calls-3j0i</guid>
      <description>&lt;p&gt;&lt;em&gt;2026.03.02&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Hi!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://snikket.org/" rel="noopener noreferrer"&gt;Snikket&lt;/a&gt; is a modern federated messenger based on good ol' XMPP. It has attachments, group chats, audio / video messages and calls with end-to-end encryption and it is Docker / Podman ready.&lt;/p&gt;

&lt;p&gt;If you too have a NixOS server and desperately searching for a self-hosted instant messenger with E2EE, calls and a high chance to keep your sanity during configuring and deploying... then continue reading (:&lt;/p&gt;

&lt;h2&gt;
  
  
  DNS
&lt;/h2&gt;

&lt;p&gt;First of all, you should have a domain name.&lt;/p&gt;

&lt;p&gt;My server holds a lot of stuff, so I host Snikket on a subdomain, like &lt;code&gt;chat.example.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Besides &lt;code&gt;chat.example.com&lt;/code&gt; you'll also need these two A-type DNS records (&lt;em&gt;naming is up to you&lt;/em&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;groups.chat.example.com&lt;/code&gt; - for all things about group chats&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;share.chat.example.com&lt;/code&gt; - for files sharing&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  NixOS and OCI containers
&lt;/h2&gt;

&lt;p&gt;To be short, OCI (&lt;em&gt;Open Container Initiative&lt;/em&gt;) containers is what Docker has. Podman is a Docker replacement (&lt;em&gt;DYOR what you should choose&lt;/em&gt;) and also works with OCI.&lt;/p&gt;

&lt;p&gt;NixOS does not (&lt;em&gt;yet&lt;/em&gt;) have a nix module for Snikket, however there is wonderful &lt;a href="https://github.com/aksiksi/compose2nix" rel="noopener noreferrer"&gt;compose2nix&lt;/a&gt; project that turns any &lt;code&gt;docker-compose.yml&lt;/code&gt; into nix file which you can import to your server's configuration (&lt;em&gt;more about it later&lt;/em&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Configs
&lt;/h2&gt;

&lt;p&gt;Configs tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;your other nixos configs&amp;gt;
├── nginx.nix
├── snikket.nix
├── snikket
│   ├── copy-certs-job.nix
│   ├── copy-certs.sh
│   ├── docker-compose.yml
│   ├── snikket.conf
│   └── snikket.nix
&amp;lt;your other nixos configs&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;nginx.nix&lt;/code&gt; - general Nginx configuration shared between all deployed services on your server&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;snikket.nix&lt;/code&gt; - all the specific configs for Snikket, other than Docker configuration&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;snikket/docker-compose.yml&lt;/code&gt; - original Docker configuration for Snikket&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;snikket/snikket.conf&lt;/code&gt; - environment file for Snikket Docker configuration&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;snikket/snikket.nix&lt;/code&gt; - what &lt;code&gt;compose2nix&lt;/code&gt; generated from Docker configuration&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;snikket/copy-certs.sh&lt;/code&gt; - bash script to copy certificate files from Snikket container to Nginx directory&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;snikket/copy-certs-job.nix&lt;/code&gt; - systemd timer configuration that executes said bash script daily&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;nginx.nix&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nix"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;networking&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;firewall&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;allowedTCPPorts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="nv"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;nginx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;recommendedTlsSettings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;recommendedOptimisation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;recommendedGzipSettings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;recommendedProxySettings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;appendHttpConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;''&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;      proxy_headers_hash_max_size 1024;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;      proxy_headers_hash_bucket_size 128;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;    ''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;resolver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;ipv6&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nv"&gt;security&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;acme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;acceptTerms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;defaults&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"admin@example.com"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c"&gt;# &amp;lt;--- Change this.&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;snikket.nix&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Look at &lt;a href="https://snikket.org/service/help/setup/quickstart/" rel="noopener noreferrer"&gt;Quick-start&lt;/a&gt;, &lt;a href="https://snikket.org/service/help/advanced/firewall/" rel="noopener noreferrer"&gt;Firewall ports&lt;/a&gt; and &lt;a href="https://snikket.org/service/help/advanced/config/" rel="noopener noreferrer"&gt;Advanced configuration&lt;/a&gt; for details.&lt;/p&gt;

&lt;p&gt;For comments like &lt;em&gt;Keep in sync with &lt;code&gt;SNIKKET_DOMAIN&lt;/code&gt;&lt;/em&gt; see the &lt;code&gt;snikket/snikket.conf&lt;/code&gt; for said env variable.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;allowedUDPPortRanges&lt;/code&gt; pay attention to port range, see &lt;a href="https://snikket.org/service/help/advanced/firewall/#how-many-ports-does-the-turn-service-need" rel="noopener noreferrer"&gt;How many ports does the TURN service need?&lt;/a&gt; section.&lt;/p&gt;

&lt;p&gt;You can change &lt;code&gt;client_max_body_size&lt;/code&gt; value in &lt;code&gt;extraConfig = "client_max_body_size 104857616;";&lt;/code&gt; to whatever suits you, I left the defaults.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nix"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;lib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt;
  &lt;span class="nv"&gt;domain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"chat.example.com"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c"&gt;# Keep in sync with `SNIKKET_DOMAIN`.&lt;/span&gt;
  &lt;span class="nv"&gt;groups_domain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"groups.&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;domain&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nv"&gt;share_domain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"share.&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;domain&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nv"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5443&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c"&gt;# Keep in sync with `SNIKKET_TWEAK_HTTPS_PORT`.&lt;/span&gt;
&lt;span class="kn"&gt;in&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;imports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sx"&gt;./snikket/snikket.nix&lt;/span&gt;
    &lt;span class="sx"&gt;./snikket/copy-certs-job.nix&lt;/span&gt;
  &lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="nv"&gt;networking&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;firewall&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;allowedTCPPorts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="mi"&gt;5222&lt;/span&gt; &lt;span class="mi"&gt;5269&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt; &lt;span class="mi"&gt;3478&lt;/span&gt; &lt;span class="mi"&gt;3479&lt;/span&gt; &lt;span class="mi"&gt;5349&lt;/span&gt; &lt;span class="mi"&gt;5350&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="nv"&gt;allowedUDPPorts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="mi"&gt;3478&lt;/span&gt; &lt;span class="mi"&gt;3479&lt;/span&gt; &lt;span class="mi"&gt;5349&lt;/span&gt; &lt;span class="mi"&gt;5350&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="c"&gt;# Keep in sync with `SNIKKET_TWEAK_TURNSERVER_MIN_PORT` and `SNIKKET_TWEAK_TURNSERVER_MAX_PORT`.&lt;/span&gt;
    &lt;span class="nv"&gt;allowedUDPPortRanges&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;from&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;49152&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;to&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;49252&lt;/span&gt;&lt;span class="p"&gt;;}&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nv"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;nginx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;virtualHosts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;domain&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nv"&gt;enableACME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nv"&gt;forceSSL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="c"&gt;# Snikket manages ACME certs.&lt;/span&gt;
      &lt;span class="nv"&gt;acmeFallbackHost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"localhost:&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="kr"&gt;toString&lt;/span&gt; &lt;span class="nv"&gt;port&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="nv"&gt;locations&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="s2"&gt;"/"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;proxyPass&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://localhost:&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="kr"&gt;toString&lt;/span&gt; &lt;span class="nv"&gt;port&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;extraConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"client_max_body_size 104857616;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c"&gt;# 100MB + 16 bytes&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="nv"&gt;virtualHosts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;groups_domain&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nv"&gt;enableACME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nv"&gt;forceSSL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="c"&gt;# Snikket manages ACME certs.&lt;/span&gt;
      &lt;span class="nv"&gt;acmeFallbackHost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"localhost:&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="kr"&gt;toString&lt;/span&gt; &lt;span class="nv"&gt;port&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="nv"&gt;locations&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="s2"&gt;"/"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;proxyPass&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://localhost:&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="kr"&gt;toString&lt;/span&gt; &lt;span class="nv"&gt;port&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;extraConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"client_max_body_size 104857616;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c"&gt;# 100MB + 16 bytes&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="nv"&gt;virtualHosts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;share_domain&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nv"&gt;enableACME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nv"&gt;forceSSL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="c"&gt;# Snikket manages ACME certs.&lt;/span&gt;
      &lt;span class="nv"&gt;acmeFallbackHost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"localhost:&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="kr"&gt;toString&lt;/span&gt; &lt;span class="nv"&gt;port&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="nv"&gt;locations&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="s2"&gt;"/"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;proxyPass&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://localhost:&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="kr"&gt;toString&lt;/span&gt; &lt;span class="nv"&gt;port&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;extraConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"client_max_body_size 104857616;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c"&gt;# 100MB + 16 bytes&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;snikket/docker-compose.yml&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Download &lt;a href="https://snikket.org/service/resources/docker-compose.yml" rel="noopener noreferrer"&gt;docker-compose.yml&lt;/a&gt; from Snikket.&lt;/p&gt;

&lt;p&gt;Here's what it looks like (&lt;em&gt;listing it for full coverage, I've changed nothing&lt;/em&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.3"&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;snikket_proxy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;snikket-proxy&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;snikket/snikket-web-proxy:stable&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;snikket.conf&lt;/span&gt;
    &lt;span class="na"&gt;network_mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;host&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;snikket_data:/snikket&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;acme_challenges:/var/www/html/.well-known/acme-challenge&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unless-stopped"&lt;/span&gt;
  &lt;span class="na"&gt;snikket_certs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;snikket-certs&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;snikket/snikket-cert-manager:stable&lt;/span&gt;
    &lt;span class="na"&gt;network_mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;host&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;snikket.conf&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;snikket_data:/snikket&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;acme_challenges:/var/www/.well-known/acme-challenge&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unless-stopped"&lt;/span&gt;
  &lt;span class="na"&gt;snikket_portal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;snikket-portal&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;snikket/snikket-web-portal:stable&lt;/span&gt;
    &lt;span class="na"&gt;network_mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;host&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;snikket.conf&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unless-stopped"&lt;/span&gt;

  &lt;span class="na"&gt;snikket_server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;snikket&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;snikket/snikket-server:stable&lt;/span&gt;
    &lt;span class="na"&gt;network_mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;host&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;snikket_data:/snikket&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;snikket.conf&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unless-stopped"&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;acme_challenges&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;snikket_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;snikket/snikket.conf&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Be sure to keep in sync with &lt;code&gt;snikket.nix&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# The primary domain of your Snikket instance.
SNIKKET_DOMAIN=chat.example.com

# An email address where the admin can be contacted
# (also used to register your Let's Encrypt account to obtain certificates).
SNIKKET_ADMIN_EMAIL=admin@example.com

# For reverse-proxy.
SNIKKET_TWEAK_HTTP_PORT=5080
SNIKKET_TWEAK_HTTPS_PORT=5443

SNIKKET_TWEAK_TURNSERVER_MIN_PORT=49152
SNIKKET_TWEAK_TURNSERVER_MAX_PORT=49252
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;snikket/snikket.nix&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Generate this file with (&lt;em&gt;run in &lt;code&gt;snikket&lt;/code&gt; directory&lt;/em&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nix-shell &lt;span class="nt"&gt;-p&lt;/span&gt; compose2nix &lt;span class="nt"&gt;--run&lt;/span&gt; &lt;span class="s2"&gt;"compose2nix -inputs docker-compose.yml -output snikket.nix -project snikket"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You need to regenerate it in case &lt;code&gt;docker-compose.yml&lt;/code&gt; was changed.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;snikket/copy-certs.sh&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Snikket handles ACME with its own service with certbot, so we need to copy those files to Nginx directory.&lt;/p&gt;

&lt;p&gt;Also we need to rename key file and add a group read permissions to it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nv"&gt;DOMAIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"chat.example.com"&lt;/span&gt;
&lt;span class="nv"&gt;SOURCE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/var/lib/containers/storage/volumes/snikket_snikket_data/_data/letsencrypt/live/&lt;/span&gt;&lt;span class="nv"&gt;$DOMAIN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;TARGET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/var/lib/acme/&lt;/span&gt;&lt;span class="nv"&gt;$DOMAIN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-rL&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SOURCE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TARGET&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TARGET&lt;/span&gt;&lt;span class="s2"&gt;/privkey.pem"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TARGET&lt;/span&gt;&lt;span class="s2"&gt;/key.pem"&lt;/span&gt;
&lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; acme:nginx &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TARGET&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;chmod &lt;/span&gt;g+r &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TARGET&lt;/span&gt;&lt;span class="s2"&gt;/key.pem"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;snikket/copy-certs-job.nix&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Too lazy to figure out when to copy updated certs - just do it daily at 00:00 UTC:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nix"&gt;&lt;code&gt;&lt;span class="c"&gt;# Periodic job that copies cert files from Snikket container to nginx directory.&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;systemd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;timers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="s2"&gt;"snikket-copy-certs"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;wantedBy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"timers.target"&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
      &lt;span class="nv"&gt;timerConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;OnBootSec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"5m"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;OnCalendar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*-*-* 00:00:00 UTC"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;Unit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"snikket-copy-certs.service"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nv"&gt;systemd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="s2"&gt;"snikket-copy-certs"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kn"&gt;with&lt;/span&gt; &lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;bash&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="nv"&gt;serviceConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nv"&gt;Type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"oneshot"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nv"&gt;ExecStart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/path/to/copy-certs.sh"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c"&gt;# &amp;lt;--- Change this.&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Running
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Certs
&lt;/h3&gt;

&lt;p&gt;Check that all Snikket services are up and ready (&lt;em&gt;certs service can take a while to verify newly created certs&lt;/em&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status podman-snikket&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;podman-snikket*&lt;/code&gt; with &lt;code&gt;docker-snikket*&lt;/code&gt; if you've chosen the Docker backend for &lt;code&gt;compose2nix&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;snikket/copy-certs.sh&lt;/code&gt;. Now certs should be working.&lt;/p&gt;

&lt;p&gt;Restart Nginx for good measure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl restart nginx.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  First admin user
&lt;/h3&gt;

&lt;p&gt;To create your first user (&lt;em&gt;who is admin&lt;/em&gt;), run this command and follow the invitation link:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec &lt;/span&gt;snikket create-invite &lt;span class="nt"&gt;--admin&lt;/span&gt; &lt;span class="nt"&gt;--group&lt;/span&gt; default
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All other users must join Snikket by an invite link, which you can create in admin panel at &lt;code&gt;chat.example.com/admin/invitations&lt;/code&gt;. Invites can be one-time only and not restricted (&lt;em&gt;also you can choose user role and circle&lt;/em&gt;).&lt;/p&gt;

&lt;p&gt;Bye!&lt;/p&gt;

</description>
      <category>linux</category>
      <category>opensource</category>
      <category>privacy</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Pain &amp; suffering. Filenames on PocketBook</title>
      <dc:creator>Kir Axanov</dc:creator>
      <pubDate>Mon, 19 Jan 2026 14:09:17 +0000</pubDate>
      <link>https://dev.to/axkira/pain-suffering-filenames-on-pocketbook-3cpa</link>
      <guid>https://dev.to/axkira/pain-suffering-filenames-on-pocketbook-3cpa</guid>
      <description>&lt;p&gt;&lt;em&gt;2026.01.19&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Hi!&lt;/p&gt;

&lt;p&gt;I have a wonderful PocketBook ebook reader. But every &lt;em&gt;filename&lt;/em&gt; with non-ASCII characters turns into gibberish if it was copied from pc (&lt;em&gt;via USB&lt;/em&gt;). As a workaround I was copying a zip archive for then to unarchive it and all filenames were OK.&lt;/p&gt;

&lt;p&gt;Only after some embarrassing number of evenings lately (&lt;em&gt;playing with &lt;code&gt;rsync&lt;/code&gt;, &lt;code&gt;cp&lt;/code&gt;, &lt;code&gt;convmv&lt;/code&gt; and ALL AVAILABLE encodings&lt;/em&gt;) I've figured out the reason...&lt;br&gt;
I was mounting the ebook with &lt;code&gt;iocharset=iso-8859-1&lt;/code&gt; by default, but on the ebook itself its partitions were mounted with &lt;code&gt;iocharset=utf8&lt;/code&gt; (&lt;em&gt;with &lt;code&gt;codepage=437&lt;/code&gt; in both cases, btw&lt;/em&gt;). Bingo!&lt;/p&gt;

&lt;p&gt;How to mount with needed &lt;code&gt;iocharset&lt;/code&gt; (&lt;em&gt;&lt;code&gt;utf8&lt;/code&gt; here&lt;/em&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Like so: sudo mount /dev/sdc /mnt --options iocharset=utf8&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;mount /path/to/device /path/to/mountpoint &lt;span class="nt"&gt;--options&lt;/span&gt; &lt;span class="nv"&gt;iocharset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;utf8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If just mounting with &lt;code&gt;iocharset=utf8&lt;/code&gt; does not help and you want to actually find out the mount options you'll need a console for your ebook like &lt;a href="https://github.com/Alastor27/pbterm" rel="noopener noreferrer"&gt;PBterm&lt;/a&gt; or Konsole from vlasovsoft apps bundle (&lt;a href="https://wiki.vlasovsoft.net/doku.php?id=en:installation#pocketbook" rel="noopener noreferrer"&gt;docs&lt;/a&gt;, &lt;a href="https://pbchess.vlasovsoft.net/en/index.html" rel="noopener noreferrer"&gt;downloads&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;You can check current mount options with just &lt;code&gt;mount&lt;/code&gt; command (&lt;em&gt;search for your device and look at options in parenthesis&lt;/em&gt;). That's how I learned about wrong iocharset in the first place.&lt;/p&gt;

&lt;p&gt;Bye!&lt;/p&gt;

</description>
      <category>ebooks</category>
      <category>linux</category>
    </item>
    <item>
      <title>Configs. Fix any vertical mouse</title>
      <dc:creator>Kir Axanov</dc:creator>
      <pubDate>Fri, 19 Dec 2025 05:26:29 +0000</pubDate>
      <link>https://dev.to/axkira/configs-fix-any-vertical-mouse-3iol</link>
      <guid>https://dev.to/axkira/configs-fix-any-vertical-mouse-3iol</guid>
      <description>&lt;p&gt;&lt;em&gt;2025.12.19&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Hi!&lt;/p&gt;

&lt;p&gt;It's really dumb, but every vertical mouse I saw has one crucial mistake in its design - the sensor is misaligned and when you try to hold it the proper (&lt;em&gt;ergonomic!&lt;/em&gt;) way and move horizontally, it also moves up / down...&lt;/p&gt;

&lt;p&gt;But we can fix it with &lt;code&gt;xinput&lt;/code&gt;!&lt;/p&gt;

&lt;h2&gt;
  
  
  Steps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Identify your fancy mouse. Run the following command with the mouse unplugged and then plugged again to see which device you need (&lt;em&gt;get name after &lt;code&gt;↳&lt;/code&gt; and before &lt;code&gt;id=&lt;/code&gt;&lt;/em&gt;):&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xinput list
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Mine is called &lt;code&gt;USB OPTICAL MOUSE&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Prepare the Python3 environment for script in next step. You'll need the &lt;code&gt;scipy&lt;/code&gt; package.&lt;/p&gt;

&lt;p&gt;For NixOS just replace the shebang in this script with this one:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env nix-shell&lt;/span&gt;
&lt;span class="c"&gt;#!nix-shell -i python3 -p python3 python313Packages.scipy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Use python script below to experiment with rotation angle:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
# Set rotation matrix for mouse.
# Original: https://luator.de/linux/2022/03/05/xinput-rotate-mouse-axes.html
#
# Usage example:
# python3 rotate-mouse.py DEVICE_NAME ANGLE
#
# See all devices with:
# xinput list
#
# The script also prints the command for selected angle, so you can add it to autostart.
#
# Positive values result in clock-wise rotation.
# The rotation is always relative to the original orientation,
# so you can easily reset by setting the angle to 0.
&lt;/span&gt;
&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Use xinput to rotate the mouse axes by a given angle.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;scipy.spatial.transform&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Rotation&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;__doc__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;device&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Mouse name from `xinput list`.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;angle&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Rotation angle in degrees.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Construct rotation matrix from given angle (xinput expects an affine 2d
&lt;/span&gt;    &lt;span class="c1"&gt;# transformation but since we are only rotating, this is equivalent to a
&lt;/span&gt;    &lt;span class="c1"&gt;# simple 3d rotation matrix around the z-axis).
&lt;/span&gt;    &lt;span class="n"&gt;rot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Rotation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_euler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;angle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;degrees&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;as_matrix&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;matrix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatten&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;

    &lt;span class="n"&gt;xinput_cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;xinput&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;set-prop&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Coordinate Transformation Matrix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;matrix&lt;/span&gt;

    &lt;span class="n"&gt;xinput_str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;xinput set-prop &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Coordinate Transformation Matrix&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;    # Rotates mouse by &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;angle&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;°&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xinput_str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execvp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;xinput&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;xinput_cmd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Original script was taken from &lt;a href="https://luator.de/linux/2022/03/05/xinput-rotate-mouse-axes.html" rel="noopener noreferrer"&gt;Luator's Blog&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The script above also prints applied xinput command, which you can add to your autostart scripts to make it persistent after reboot.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;After finding the matrix, you can ditch created above python environment.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Bonus: InputLeap struggles
&lt;/h2&gt;

&lt;p&gt;I use &lt;a href="https://github.com/input-leap/input-leap" rel="noopener noreferrer"&gt;InputLeap&lt;/a&gt; to share my keyboard and mouse between my machines time to time, which apparently does not go well with such xinput changes (&lt;em&gt;leave a comment if I miss something&lt;/em&gt;). On clients the mouse comes stuck at the top right corner.&lt;/p&gt;

&lt;p&gt;So we need to reset our transformation matrix when we go to InputLeap client (&lt;em&gt;and struggle with not-so-ergonomic-mouse-with-misaligned-sensor, alas!&lt;/em&gt;).&lt;/p&gt;

&lt;p&gt;As it's mentioned in the script, you can reset the matrix by running it with zero angle, or straight with xinput:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xinput set-prop &lt;span class="s1"&gt;'YOUR_MOUSE_NAME'&lt;/span&gt; &lt;span class="s1"&gt;'Coordinate Transformation Matrix'&lt;/span&gt; 1.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So we just need to run this reset command when we go to client and run the rotation command when we are back on server.&lt;/p&gt;

&lt;p&gt;InputLeap has the &lt;code&gt;--screen-change-script&lt;/code&gt; argument, which you can use... but somehow applying any arguments to &lt;code&gt;input-leap&lt;/code&gt; on NixOS does nothing, it only respects the settings set up in its GUI.&lt;/p&gt;

&lt;p&gt;Thus, here is a uniform way to catch when InputLeap changes screens via monitoring its log file:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Open InputLeap GUI, go to &lt;code&gt;InputLeap&lt;/code&gt; top menu -&amp;gt; &lt;code&gt;Change Settings&lt;/code&gt; -&amp;gt; &lt;code&gt;Logging&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;set &lt;code&gt;Logging level&lt;/code&gt; to &lt;code&gt;info&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;check &lt;code&gt;Log to file&lt;/code&gt; and choose the log file you want to use (&lt;em&gt;i.e. &lt;code&gt;/tmp/inputleap.log&lt;/code&gt;&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;hit &lt;code&gt;OK&lt;/code&gt; and restart it&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Save and edit script below:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;change &lt;code&gt;LOG_FILE&lt;/code&gt; variable to the log file path which you set in InputLeap GUI&lt;/li&gt;
&lt;li&gt;change &lt;code&gt;YOUR_MOUSE_NAME&lt;/code&gt; with, you guessed it, your mouse name from &lt;code&gt;xinput list&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;change &lt;code&gt;ROTATE_CMD&lt;/code&gt; and &lt;code&gt;RESET_CMD&lt;/code&gt; with appropriate xinput commands found above&lt;/li&gt;
&lt;li&gt;change &lt;code&gt;SERVER_SCREEN&lt;/code&gt; to the name of InputLeap server's screen (&lt;em&gt;it's in &lt;code&gt;General&lt;/code&gt; -&amp;gt; &lt;code&gt;Screen name&lt;/code&gt; of InputLeap GUI settings from previous step&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;also, you can uncomment the line with &lt;code&gt;notify-send&lt;/code&gt; to see the screen name when you move between them
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# Rotates mouse based on current InputLeap screen.&lt;/span&gt;

&lt;span class="nv"&gt;LOG_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/tmp/inputleap.log"&lt;/span&gt;

&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;touch&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"INFO: switch from"&lt;/span&gt; &lt;span class="nt"&gt;--line-buffered&lt;/span&gt; | &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; line &lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;SCREEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$line&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/.*to "\(.*\)".*/\1/g'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="c"&gt;# notify-send -a InputLeap "Switched to $SCREEN"&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SCREEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="s2"&gt;"SERVER_SCREEN"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; ROTATE_CMD&lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; RESET_CMD&lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="k"&gt;esac&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run the script above (&lt;em&gt;add to autostart if needed&lt;/em&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Bye!&lt;/p&gt;

</description>
      <category>unixporn</category>
      <category>mouse</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Configs. Touchpad-like scroll with mouse</title>
      <dc:creator>Kir Axanov</dc:creator>
      <pubDate>Fri, 19 Dec 2025 04:10:06 +0000</pubDate>
      <link>https://dev.to/axkira/configs-touchpad-like-scroll-with-mouse-33o4</link>
      <guid>https://dev.to/axkira/configs-touchpad-like-scroll-with-mouse-33o4</guid>
      <description>&lt;p&gt;&lt;em&gt;2025.12.19&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Hi!&lt;/p&gt;

&lt;p&gt;Tired of fixing / blowing / replacing mouse wheel every now and then? Lovin' middle-button scroll in your favourite firefox fork and wanna use it everywhere?&lt;/p&gt;

&lt;p&gt;Let's make a touchpad-two-fingers-like-scrolling with a mouse!&lt;/p&gt;

&lt;p&gt;(&lt;em&gt;It works like this: you hold some button and drag the mouse up / down / left / right to scroll, the cursor remains in the same place.&lt;/em&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  General GNU/Linux
&lt;/h2&gt;

&lt;p&gt;Open / create &lt;code&gt;/etc/X11/xorg.conf.d/99-mouse.conf&lt;/code&gt; and add those lines to it (&lt;em&gt;this should work, but untested as I use NixOS&lt;/em&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Section "InputClass"
    Identifier "Mouse Defaults"
    MatchIsPointer "on"
    MatchDevicePath "/dev/input/event*"
    Driver "libinput"
    Option "ScrollMethod" "button"
    Option "ScrollButton" "button3"
EndSection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can replace &lt;code&gt;button3&lt;/code&gt; (&lt;em&gt;right button here&lt;/em&gt;) with the desired button to activate dragging scroll.&lt;br&gt;
To find out the exact button I use &lt;code&gt;xev&lt;/code&gt; util.&lt;/p&gt;
&lt;h2&gt;
  
  
  NixOS
&lt;/h2&gt;

&lt;p&gt;These are the lines to add to your NixOS config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nix"&gt;&lt;code&gt;&lt;span class="nv"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;libinput&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;mouse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;scrollMethod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"button"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nv"&gt;scrollButton&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bye!&lt;/p&gt;

</description>
      <category>unixporn</category>
      <category>mouse</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Shorts. Extra clipboard buffer with CopyQ</title>
      <dc:creator>Kir Axanov</dc:creator>
      <pubDate>Sat, 11 Oct 2025 04:01:17 +0000</pubDate>
      <link>https://dev.to/axkira/shorts-extra-clipboard-buffer-with-copyq-13l0</link>
      <guid>https://dev.to/axkira/shorts-extra-clipboard-buffer-with-copyq-13l0</guid>
      <description>&lt;p&gt;&lt;em&gt;2025.10.11&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Hi!&lt;/p&gt;

&lt;p&gt;Linux has two clipboard buffers: &lt;code&gt;primary&lt;/code&gt; for mouse selection and &lt;code&gt;clipboard&lt;/code&gt; for Ctrl+C / Ctrl+V.&lt;br&gt;
But sometimes that's not enough and you want to keep some text around to paste later (&lt;em&gt;like clipboard registers in Vim, but system-wide&lt;/em&gt;).&lt;br&gt;
We can make this possible with CopyQ clipboard manager, which allows us to have multiple tabs for our copied data.&lt;/p&gt;
&lt;h2&gt;
  
  
  DISCLAIMER
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;I’m talking about text only, other media types won’t work with the script below.&lt;/li&gt;
&lt;li&gt;In CopyQ settings check the &lt;code&gt;(1) Store clipboard&lt;/code&gt; and &lt;code&gt;(2) Store text selected using mouse&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F52is1oazboib7i90aqg1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F52is1oazboib7i90aqg1.png" alt="In CopyQ settings check the  raw `(1) Store clipboard` endraw  and  raw `(2) Store text selected using mouse` endraw " width="461" height="490"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;Here is a simple bash-script, that adds new tab and uses it as an extra clipboard buffer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# Allows to save text to extra buffer in CopyQ.&lt;/span&gt;
&lt;span class="c"&gt;# Bind `copy()` to Alt+C, `paste()` to Alt+V.&lt;/span&gt;
&lt;span class="c"&gt;# Alt+C copies selected text to extra buffer, current clipboard remains unchanged.&lt;/span&gt;
&lt;span class="c"&gt;# Alt+V copies latest item in extra buffer to clipboard (you still have to paste it manually with Ctrl+V, `copyq paste` is laggy).&lt;/span&gt;
&lt;span class="c"&gt;# If item in extra buffer is already in clipboard, then Alt+V removes it from clipboard (extra buffer still has it).&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="nv"&gt;CMD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;  &lt;span class="c"&gt;# One of: `copy`, `paste`&lt;/span&gt;
&lt;span class="nv"&gt;EXTRA_BUFFER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ExtraBuffer"&lt;/span&gt;

copy&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;selected&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;copyq selection&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;old&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;copyq &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  copyq tab &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$EXTRA_BUFFER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; add &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$selected&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  copyq remove 0
  copyq copy &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$old&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;paste&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;clipboard&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;copyq &lt;span class="nb"&gt;read &lt;/span&gt;0&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;extra&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;copyq tab &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$EXTRA_BUFFER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nb"&gt;read &lt;/span&gt;0&lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$clipboard&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$extra&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;  &lt;span class="c"&gt;# Restoring old copied item from clipboard.&lt;/span&gt;
    copyq remove 0
    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;old&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;copyq &lt;span class="nb"&gt;read &lt;/span&gt;0&lt;span class="si"&gt;)&lt;/span&gt;
    copyq copy &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$old&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;  &lt;span class="c"&gt;# Adding item from extra buffer to clipboard.&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;extra&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;copyq tab &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$EXTRA_BUFFER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nb"&gt;read &lt;/span&gt;0&lt;span class="si"&gt;)&lt;/span&gt;
    copyq write 0 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$extra&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    copyq copy &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$extra&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CMD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in
  &lt;/span&gt;copy&lt;span class="p"&gt;)&lt;/span&gt; copy&lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="nb"&gt;paste&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;paste&lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="k"&gt;esac&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can bind it in i3wm like so (&lt;em&gt;replace &lt;code&gt;SCRIPT_PATH&lt;/code&gt;&lt;/em&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;set $alt Mod1
bindsym $alt+C exec "bash SCRIPT_PATH copy"
bindsym $alt+V exec "bash SCRIPT_PATH paste"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bye!&lt;/p&gt;

</description>
      <category>bash</category>
      <category>unixporn</category>
    </item>
    <item>
      <title>Code. Gleam, SQLite and concurrent transactions walked to a bar</title>
      <dc:creator>Kir Axanov</dc:creator>
      <pubDate>Sat, 04 Oct 2025 11:10:16 +0000</pubDate>
      <link>https://dev.to/axkira/code-gleam-sqlite-and-concurrent-transactions-walked-to-a-bar-52nd</link>
      <guid>https://dev.to/axkira/code-gleam-sqlite-and-concurrent-transactions-walked-to-a-bar-52nd</guid>
      <description>&lt;p&gt;&lt;em&gt;2025.10.04&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Hi!&lt;/p&gt;

&lt;p&gt;Gleam + SQLite is a nice choice for a side-project.&lt;br&gt;
But when this project grows and demands parallel writes into the same DB, we can get stuck as SQLite does not support this out of the box.&lt;br&gt;
Nevertheless, we can achieve trully concurrent (&lt;em&gt;espetially write&lt;/em&gt;) transactions in SQLite if we:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use SQLite &lt;a href="https://sqlite.org/amalgamation.html" rel="noopener noreferrer"&gt;compiled&lt;/a&gt; from &lt;code&gt;begin-concurrent&lt;/code&gt; &lt;a href="https://www.sqlite.org/src/timeline?udc=1&amp;amp;ss=c&amp;amp;n=&amp;amp;y=ci&amp;amp;advm=0&amp;amp;rel=1&amp;amp;t=begin-concurrent" rel="noopener noreferrer"&gt;branch&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;use &lt;a href="https://sqlite.org/wal.html" rel="noopener noreferrer"&gt;WAL-mode&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;use own connection for each writer&lt;/li&gt;
&lt;li&gt;wrap the query into &lt;code&gt;begin concurrent transaction;&lt;/code&gt; - &lt;code&gt;commit;&lt;/code&gt; block&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  DISCLAIMER
&lt;/h2&gt;

&lt;p&gt;The actions below are semi-manual and require automation. Feel free to participate!&lt;/p&gt;
&lt;h2&gt;
  
  
  Replace SQLite in project
&lt;/h2&gt;

&lt;p&gt;I use SQLite via the &lt;a href="https://hexdocs.pm/sqlight/index.html" rel="noopener noreferrer"&gt;sqlight&lt;/a&gt; Gleam package, which uses &lt;code&gt;esqlite&lt;/code&gt; Erlang library under the hood, which has the SQLite embedded into it.&lt;br&gt;
So, all we have to do is to replace the embedded SQLite in &lt;code&gt;esqlite&lt;/code&gt; build directory.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find out the needed SQLite revision on &lt;code&gt;begin-concurrent&lt;/code&gt; branch &lt;a href="https://www.sqlite.org/src/timeline?udc=1&amp;amp;ss=c&amp;amp;n=&amp;amp;y=ci&amp;amp;advm=0&amp;amp;rel=1&amp;amp;t=begin-concurrent" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Download the archive from &lt;code&gt;Overview&lt;/code&gt; -&amp;gt; &lt;code&gt;Downloads&lt;/code&gt; -&amp;gt; &lt;code&gt;Tarball&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Unpack the archive (&lt;em&gt;replace the &lt;code&gt;REVISION&lt;/code&gt;&lt;/em&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;gzip&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; SQLite-REVISION.tar.gz
&lt;span class="nb"&gt;tar &lt;/span&gt;xvf SQLite-REVISION.tar
&lt;span class="nb"&gt;cd &lt;/span&gt;SQLite-REVISION
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Build the &lt;a href="https://sqlite.org/amalgamation.html" rel="noopener noreferrer"&gt;amalgamation&lt;/a&gt; (&lt;em&gt;needs &lt;code&gt;gnumake&lt;/code&gt; package installed&lt;/em&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;./configure &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; make sqlite3.c
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Build the project:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;gleam build
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Place generated &lt;code&gt;sqlite3.c&lt;/code&gt; and &lt;code&gt;sqlite3.h&lt;/code&gt; to &lt;code&gt;esqlite&lt;/code&gt; package directory (&lt;em&gt;replace &lt;code&gt;PROJECT_ROOT&lt;/code&gt;&lt;/em&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp &lt;/span&gt;sqlite3.c PROJECT_ROOT/build/packages/esqlite/c_src/sqlite3/
&lt;span class="nb"&gt;cp &lt;/span&gt;sqlite3.h PROJECT_ROOT/build/packages/esqlite/c_src/sqlite3/
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Remove previous build artifacts and rebuild (&lt;em&gt;replace &lt;code&gt;PROJECT_ROOT&lt;/code&gt;&lt;/em&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; PROJECT_ROOT/build/dev
gleam build
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Now SQLite should be compiled to &lt;code&gt;PROJECT_ROOT/build/dev/erlang/esqlite/priv/esqlite3_nif.so&lt;/code&gt; (&lt;em&gt;I like to put it into project's git too, you do you&lt;/em&gt;).&lt;/p&gt;&lt;/li&gt;

&lt;/ol&gt;

&lt;h3&gt;
  
  
  Verify installed version
&lt;/h3&gt;

&lt;p&gt;We can check the version of SQLite, which is used &lt;strong&gt;for the connection&lt;/strong&gt; to DB, with a query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;sqlite_version&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is a simple PoC:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import gleam/dynamic/decode
import sqlight


pub fn main() {
  use conn &amp;lt;- with_db("my-test-db.sqlite")

  "SELECT sqlite_version();"
  |&amp;gt; sqlight.query(on: conn, with: [], expecting: decode.dynamic)
  |&amp;gt; echo
}

fn with_db(
  db_path: String,
  do_something: fn(sqlight.Connection) -&amp;gt; Result(a, sqlight.Error),
) -&amp;gt; Result(a, sqlight.Error) {
  { "file:" &amp;lt;&amp;gt; db_path &amp;lt;&amp;gt; "?mode=rwc" }
  |&amp;gt; sqlight.open()
  |&amp;gt; try(fn(conn) {
    let result = do_something(conn)
    let _ = sqlight.close(conn)
    result
  })
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Enable WAL-mode
&lt;/h3&gt;

&lt;p&gt;To enable the WAL-mode &lt;strong&gt;for the entire DB&lt;/strong&gt; we use this &lt;a href="https://www.sqlite.org/pragma.html#pragma_journal_mode" rel="noopener noreferrer"&gt;pragma&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;PRAGMA&lt;/span&gt; &lt;span class="n"&gt;journal_mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WAL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can use the code from previous part, just replace the query.&lt;/p&gt;

&lt;p&gt;If you want to return to a default journal mode, then use those queries (&lt;em&gt;although I'm not sure you necessary need the &lt;a href="https://www.sqlite.org/pragma.html#pragma_wal_checkpoint" rel="noopener noreferrer"&gt;wal_checkpoint&lt;/a&gt; pragma here too&lt;/em&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;PRAGMA&lt;/span&gt; &lt;span class="n"&gt;wal_checkpoint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;PRAGMA&lt;/span&gt; &lt;span class="n"&gt;journal_mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Considerations
&lt;/h2&gt;

&lt;p&gt;Keep in mind the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;writing to a table in one job will break the other job reading the same table - use intermediate tables to update data&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CREATE&lt;/code&gt; and &lt;code&gt;DELETE&lt;/code&gt; queries lock the DB, so if you need some intermediate tables - create them before starting concurrent jobs&lt;/li&gt;
&lt;li&gt;single connection executes all queries in sequential order - use multiple connections for concurrent jobs&lt;/li&gt;
&lt;li&gt;long-running queries can cause &lt;code&gt;BusySnapshot&lt;/code&gt; error - try to split them to smaller operations / datasets&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Replace SQLite in NixOS
&lt;/h2&gt;

&lt;p&gt;It's a good idea to use the same SQLite system-wide, so here are the steps to install it from the same revision.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Clone &lt;code&gt;nixpkgs&lt;/code&gt; repo:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--depth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 https://github.com/NixOS/nixpkgs.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Find out the needed SQLite revision on &lt;code&gt;begin-concurrent&lt;/code&gt; branch &lt;a href="https://www.sqlite.org/src/timeline?udc=1&amp;amp;ss=c&amp;amp;n=&amp;amp;y=ci&amp;amp;advm=0&amp;amp;rel=1&amp;amp;t=begin-concurrent" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Download the archive from &lt;code&gt;Overview&lt;/code&gt; -&amp;gt; &lt;code&gt;Downloads&lt;/code&gt; -&amp;gt; &lt;code&gt;Tarball&lt;/code&gt;. Place it to &lt;code&gt;nixpkgs/pkgs/development/libraries/sqlite&lt;/code&gt; for convenience.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Edit &lt;code&gt;nixpkgs/pkgs/development/libraries/sqlite/default.nix&lt;/code&gt; to point to the downloaded archive (replace &lt;code&gt;REVISION&lt;/code&gt;, exact &lt;code&gt;url&lt;/code&gt; and &lt;code&gt;hash&lt;/code&gt; values may vary):&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- src = fetchurl {
-   url = "https://sqlite.org/2025/sqlite-autoconf-${archiveVersion version}.tar.gz";
-   hash = "sha256-hKYW/9MXOORZC2W6uzqeHvk3DzY4422yIO4Oc/itIVY=";
- };
&lt;/span&gt;&lt;span class="gi"&gt;+ src = ./SQLite-REVISION.tar.gz;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Build and install the package:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nix-build &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; sqlite
nix-env &lt;span class="nt"&gt;-i&lt;/span&gt; ./result-bin/
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Check the installed version:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nix-env &lt;span class="nt"&gt;-q&lt;/span&gt;  &lt;span class="c"&gt;# should list the sqlite package with the version from nixpkgs repo as we didn't change it&lt;/span&gt;
sqlite3 &lt;span class="nt"&gt;--version&lt;/span&gt;  &lt;span class="c"&gt;# should print the exact version which we downloaded&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Bye!&lt;/p&gt;

</description>
      <category>programming</category>
      <category>gleamlang</category>
      <category>sqlite</category>
    </item>
    <item>
      <title>Code. Gleam. Parallel tasks from pool</title>
      <dc:creator>Kir Axanov</dc:creator>
      <pubDate>Tue, 30 Sep 2025 13:55:26 +0000</pubDate>
      <link>https://dev.to/axkira/code-gleam-parallel-tasks-from-pool-2bjo</link>
      <guid>https://dev.to/axkira/code-gleam-parallel-tasks-from-pool-2bjo</guid>
      <description>&lt;p&gt;&lt;em&gt;2025.09.30&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Hi!&lt;/p&gt;

&lt;h2&gt;
  
  
  Usecase and terms
&lt;/h2&gt;

&lt;p&gt;Sometimes you want to split a heavy job into multiple pieces to run in parallel, but you cannot predict the amount of such smaller jobs.&lt;br&gt;
So, we need a function, that will tell when to stop spawning new jobs.&lt;br&gt;
Also, we will stop spawning &lt;em&gt;new&lt;/em&gt; jobs if we got any error (&lt;em&gt;already running jobs will continue until finished&lt;/em&gt;).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;job&lt;/strong&gt; is something, that you want to execute in parallel (&lt;em&gt;the smaller pieces&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;task&lt;/strong&gt; is any data, which is passed as argument to a job&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As an example, imagine that you need to import some huge amount of (&lt;em&gt;unlinked&lt;/em&gt;) time-based data. We can run, say, 20 parallel processes at once. Each process will take a time window for a day - that day's start timestamp is a &lt;em&gt;task&lt;/em&gt; for those processes (&lt;em&gt;jobs&lt;/em&gt;). We run those jobs until we import all the data or encounter any errors.&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;We will use a OTP-actor, which I named &lt;code&gt;ParaWorker&lt;/code&gt; (&lt;em&gt;parallel worker&lt;/em&gt;).&lt;/p&gt;

&lt;p&gt;Here are the types in &lt;code&gt;types.gleam&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import gleam/option.{ type Option }
import gleam/erlang/process


pub type ParaWorkerMsg(err) {
  TaskEnqueued
  TaskCompleted
  TaskFailed(error: err)
}

pub type ParaWorkerState(task, err) {
  ParaWorkerState(
    worker_name: process.Name(ParaWorkerMsg(err)),
    running_jobs: Int,
    next_task: Option(task),
    new_task: fn(task) -&amp;gt; Option(task),
    handle_task: fn(task) -&amp;gt; Result(Nil, err),
    errors: List(err),
    finish_subject: process.Subject(Result(Nil, List(err))),  // Where ParaWorker should send the results of handling all tasks.
  )
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here is the ParaWorker in &lt;code&gt;paraworker.gleam&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import gleam/string
import gleam/io
import gleam/otp/actor
import gleam/erlang/process
import gleam/result.{ try }
import gleam/option.{ type Option, Some, None }

import types as t


pub fn start(
  first_task: task,
  note: String,
  max_jobs: Int,
  new_task: fn(task) -&amp;gt; Option(task),
  handle_task: fn(task) -&amp;gt; Result(Nil, err),
) -&amp;gt; Result(Nil, List(err)) {
  case max_jobs {
    max_jobs if max_jobs &amp;lt;= 0 -&amp;gt; panic as "To start ParaWorker, `max_jobs` should be greater than 0!"
    _ -&amp;gt; Nil
  }

  let worker_name = process.new_name("parallel_worker")
  let finish_subject = process.new_subject()

  let _ = t.ParaWorkerState(
    worker_name:,
    running_jobs: 0,
    next_task: Some(first_task),
    new_task:,
    handle_task:,
    errors: [],
    finish_subject:,
  )
  |&amp;gt; actor.new()
  |&amp;gt; actor.on_message(handle_msg)
  |&amp;gt; actor.named(worker_name)
  |&amp;gt; actor.start()
  |&amp;gt; try(fn(_) { io.println("Started `" &amp;lt;&amp;gt; note &amp;lt;&amp;gt; "` ParaWorker.") |&amp;gt; Ok() })
  |&amp;gt; result.try_recover(fn(error) {
    io.println("Failed to start `" &amp;lt;&amp;gt; note &amp;lt;&amp;gt; "` ParaWorker.")
    Error(error)
  })

  start_handling(worker_name, max_jobs)
  let results = process.receive_forever(finish_subject)  // Waiting until all tasks are handled.

  io.println("Finished `" &amp;lt;&amp;gt; note &amp;lt;&amp;gt; "` ParaWorker with: `" &amp;lt;&amp;gt; string.inspect(results) &amp;lt;&amp;gt; "`.")
  results
}

fn start_handling(
  actor_name: process.Name(t.ParaWorkerMsg(err)),
  count: Int,
) -&amp;gt; Nil {
  actor_name
  |&amp;gt; process.named_subject()
  |&amp;gt; enqueue__loop(count)
}

fn enqueue__loop(
  subject: process.Subject(t.ParaWorkerMsg(err)),
  count: Int,
) -&amp;gt; Nil {
  case count {
    0 -&amp;gt; Nil
    count -&amp;gt; {
      process.send(subject, t.TaskEnqueued)
      enqueue__loop(subject, count - 1)
    }
  }
}

fn handle_msg(
  actor_state: t.ParaWorkerState(task, err),
  msg: t.ParaWorkerMsg(err),
) -&amp;gt; actor.Next(t.ParaWorkerState(task, err), t.ParaWorkerMsg(err)) {
  let t.ParaWorkerState(running_jobs:, next_task:, errors:, finish_subject:, ..) = actor_state

  case msg, running_jobs, next_task, errors {
    // New task is added for handling (during worker starting) - spawn new job and continue handling.
    t.TaskEnqueued, _, _, _ -&amp;gt; {
      start_job(actor_state)
      |&amp;gt; actor.continue()
    }

    // Finished the last job - stop and return errors if any.
    // `running_jobs` can't be 0, cause `first_task` is always `Some()`.
    _, 1, None, _ |  // No more tasks.
    t.TaskFailed(_), 1, _, _ |  // Got new error.
    _, 1, _, [_h, .._t] -&amp;gt; {  // Have any error.
      let results = case msg, errors {
        t.TaskFailed(error), errors -&amp;gt; Error([error, ..errors])
        _, [] -&amp;gt; Ok(Nil)
        _, errors -&amp;gt; Error(errors)
      }

      process.send(finish_subject, results)
      actor.stop()
    }

    // No errors found and have the next task - spawn new job and continue handling.
    t.TaskCompleted, _, Some(_), [] -&amp;gt; {
      t.ParaWorkerState(..actor_state, running_jobs: running_jobs - 1)
      |&amp;gt; start_job()
      |&amp;gt; actor.continue()
    }

    // A job returned error - wait till all running jobs finish.
    t.TaskFailed(error), running_jobs, _, errors -&amp;gt; {
      t.ParaWorkerState(..actor_state, running_jobs: running_jobs - 1, errors: [error, ..errors])
      |&amp;gt; actor.continue()
    }

    // No next task - wait till all running jobs finish.
    _, running_jobs, None, _ -&amp;gt; {
      t.ParaWorkerState(..actor_state, running_jobs: running_jobs - 1)
      |&amp;gt; actor.continue()
    }

    // Have some errors - wait till all running jobs finish.
    _, running_jobs, _, [_h, .._t] -&amp;gt; {
      t.ParaWorkerState(..actor_state, running_jobs: running_jobs - 1)
      |&amp;gt; actor.continue()
    }
  }
}

fn start_job(
  actor_state: t.ParaWorkerState(task, err),
) -&amp;gt; t.ParaWorkerState(task, err) {
  let t.ParaWorkerState(running_jobs:, next_task: task, new_task:, handle_task:, ..) = actor_state

  case task {
    Some(task) -&amp;gt; {
      let next_task = new_task(task)

      process.spawn(fn() {
        let msg = case handle_task(task) {
          Ok(_) -&amp;gt; t.TaskCompleted
          Error(error) -&amp;gt; t.TaskFailed(error)
        }

        actor_state.worker_name
        |&amp;gt; process.named_subject()
        |&amp;gt; process.send(msg)
      })

      t.ParaWorkerState(..actor_state, running_jobs: running_jobs + 1, next_task:)
    }
    None -&amp;gt; actor_state
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Usage example
&lt;/h2&gt;

&lt;p&gt;Here we count from 1 up to 10. First time without errors, second and third time with one error. Pay attention that already running jobs continue even after the error raised, but no new jobs started after. The third run shows that no new jobs started immediately after the error when we only use &lt;em&gt;one&lt;/em&gt; job at a time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import gleam/io
import gleam/int
import gleam/string
import gleam/erlang/process

import paraworker


pub fn main() {
  let new_task = fn(task: Int) {
    case task {
      task if task &amp;gt;= 10 -&amp;gt; None
      task -&amp;gt; Some(task + 1)
    }
  }

  let handle_task_ok = fn(task: Int) {
    io.println(timestamp() &amp;lt;&amp;gt; " Counted up to " &amp;lt;&amp;gt; int.to_string(task) &amp;lt;&amp;gt; ".")
    process.sleep(300)
    Ok(Nil)
  }

  let handle_task_err = fn(task: Int) {
    io.println(timestamp() &amp;lt;&amp;gt; " Counted up to " &amp;lt;&amp;gt; int.to_string(task) &amp;lt;&amp;gt; ".")
    process.sleep(300)
    case task {
      4 -&amp;gt; Error("1, 2, 3, 4... wait what... where is bleem?!")
      _ -&amp;gt; Ok(Nil)
    }
  }

  let _ = paraworker.start(1, "demo-ok", 3, new_task, handle_task_ok)
  io.println("")
  let _ = paraworker.start(1, "demo-error", 3, new_task, handle_task_err)
  io.println("")
  let _ = paraworker.start(1, "demo-error-single", 1, new_task, handle_task_err)
}

// Using erlang function for example's portability - replace by something like `birl` for convenience.
@external(erlang, "erlang", "timestamp")
fn erl_timestamp() -&amp;gt; #(Int, Int, Int)

fn timestamp() -&amp;gt; String {
  let erl_time = erl_timestamp()
  int.to_string(erl_time.0) &amp;lt;&amp;gt; int.to_string(erl_time.1) &amp;lt;&amp;gt; "." &amp;lt;&amp;gt; int.to_string(erl_time.2) |&amp;gt; string.slice(0, 3)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bye!&lt;/p&gt;

</description>
      <category>programming</category>
      <category>gleamlang</category>
    </item>
    <item>
      <title>Code. SQLite. Time tables with recursive CTE</title>
      <dc:creator>Kir Axanov</dc:creator>
      <pubDate>Fri, 08 Aug 2025 15:19:30 +0000</pubDate>
      <link>https://dev.to/axkira/code-sqlite-time-tables-with-recursive-cte-11md</link>
      <guid>https://dev.to/axkira/code-sqlite-time-tables-with-recursive-cte-11md</guid>
      <description>&lt;p&gt;&lt;em&gt;2025.08.08&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Hi!&lt;/p&gt;

&lt;p&gt;Assuming you know what time tables are and you need to have them in SQLite, here's how we can generate one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt;
  &lt;span class="n"&gt;cnt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;VALUES&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unixepoch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2025-01-01 00:00:00'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;    &lt;span class="c1"&gt;--&amp;gt; Start datetime.&lt;/span&gt;
    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;                               &lt;span class="c1"&gt;--&amp;gt; Period duration, in seconds.&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;cnt&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;unixepoch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2025-01-01 03:00:00'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;--&amp;gt; End datetime.&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;59&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;stop&lt;/span&gt;                                &lt;span class="c1"&gt;--&amp;gt; Also period duration, minus last second.&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;cnt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code above will generate 181 rows of &lt;code&gt;start - stop&lt;/code&gt; pairs since &lt;code&gt;2025-01-01 00:00:00&lt;/code&gt; till &lt;code&gt;2025-01-01 03:00:00&lt;/code&gt;. Each pair represents a 1-minute interval (&lt;em&gt;left end included, right end excluded&lt;/em&gt;). Obviously, you can change the interval to be anything, just don't forget to update it in both places.&lt;/p&gt;

&lt;p&gt;Full example with saving to an actual time table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;periods_1m&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;start&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stop&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt;
  &lt;span class="n"&gt;cnt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;VALUES&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unixepoch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2025-01-01 00:00:00'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;cnt&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;unixepoch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2025-01-01 03:00:00'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;periods_1m&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;59&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;cnt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Resources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.sqlite.org/lang_with.html#recursive_common_table_expressions" rel="noopener noreferrer"&gt;SQLite docs on &lt;code&gt;WITH&lt;/code&gt; clause and recursive CTE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.geekytidbits.com/date-range-table-sqlite/" rel="noopener noreferrer"&gt;blog post by Brady Holt&lt;/a&gt;, which pushed me to finally read the SQLite docs above (:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bye!&lt;/p&gt;

&lt;p&gt;P.S.&lt;br&gt;
Nice drawing canvas, Brady! Such touches make the web alive)&lt;/p&gt;

</description>
      <category>sqlite</category>
      <category>datetime</category>
      <category>cte</category>
    </item>
    <item>
      <title>Configs. Keyboard LED as layout indicator</title>
      <dc:creator>Kir Axanov</dc:creator>
      <pubDate>Wed, 12 Mar 2025 04:22:41 +0000</pubDate>
      <link>https://dev.to/axkira/configs-keyboard-led-as-layout-indicator-54nm</link>
      <guid>https://dev.to/axkira/configs-keyboard-led-as-layout-indicator-54nm</guid>
      <description>&lt;p&gt;&lt;em&gt;2025.03.12&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Hi!&lt;/p&gt;

&lt;p&gt;Using i3wm? Have a fancy keyboard with a nice CapsLock LED, but never actually press CapsLock (&lt;em&gt;not as a remapped Ctrl&lt;/em&gt;)?&lt;/p&gt;

&lt;p&gt;Me too! Let's convert it into a keyboard layout indicator then!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;You can use method described below for things other than keyboard layout and for other LEDs. Happy hacking!&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;i3&lt;/code&gt; - targeted window manager, we will use its IPC to subscribe to window change event&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;perl&lt;/code&gt; with &lt;code&gt;AnyEvent::I3&lt;/code&gt; - to communicate with i3 IPC&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bash&lt;/code&gt; with &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;sed&lt;/code&gt; and &lt;code&gt;xargs&lt;/code&gt; utils&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;brightnessctl&lt;/code&gt; - to control LED&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;xkb-switch&lt;/code&gt; - to monitor keyboard layout change event&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;i3-subscribe.pl&lt;/code&gt; - perl script for i3 IPC (&lt;a href="https://faq.i3wm.org/question/5721/how-do-i-subscribe-to-i3-events-using-bash-easily.1.html" rel="noopener noreferrer"&gt;source&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;toggle_capslock_led.sh&lt;/code&gt; - our bash script for toggling LED&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;i3ipc_capslock_led.sh&lt;/code&gt; - our bash script to start listening for window and layout changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;All scripts below are supposed to be put into &lt;code&gt;~/Scripts&lt;/code&gt; directory - change this path if you need to.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  General method
&lt;/h2&gt;

&lt;p&gt;We have a bash script, which checks current keyboard layout and turns the LED on when specified layout is active.&lt;/p&gt;

&lt;p&gt;We call this script:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;when the layout changes&lt;/li&gt;
&lt;li&gt;when we go to other window (&lt;em&gt;as each window has its own keyboard layout state&lt;/em&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the script (&lt;code&gt;toggle_capslock_led.sh&lt;/code&gt;) with some comments inside:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# Lights up capslock LED when specified layout is on.&lt;/span&gt;
&lt;span class="c"&gt;# Works for LED on all connected keyboards (e.g. laptop's and external one).&lt;/span&gt;

&lt;span class="c"&gt;# You can also set this to your specific input name, which can be found with: brightnessctl --list&lt;/span&gt;
&lt;span class="c"&gt;# LED_NAMES="input7::capslock"&lt;/span&gt;
&lt;span class="nv"&gt;LED_NAMES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;brightnessctl &lt;span class="nt"&gt;--list&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'::capslock'&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/Device .\(.*\). of.*/\1/g'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# The layout on which we want LED to be on.&lt;/span&gt;
&lt;span class="c"&gt;# Get available layout names with: xkb-switch -l&lt;/span&gt;
&lt;span class="nv"&gt;ON_LAYOUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"us"&lt;/span&gt;
&lt;span class="nv"&gt;CURRENT_LAYOUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;xkb-switch &lt;span class="nt"&gt;-p&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;VALUE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CURRENT_LAYOUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ON_LAYOUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;VALUE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LED_NAMES&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read &lt;/span&gt;name&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;brightnessctl &lt;span class="nt"&gt;--device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nv"&gt;$VALUE&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Talking to i3 IPC
&lt;/h2&gt;

&lt;p&gt;Below is the &lt;code&gt;i3-subscribe.pl&lt;/code&gt; script for talking to i3 with perl.&lt;/p&gt;

&lt;p&gt;To run it you will need to install &lt;code&gt;AnyEvent::I3&lt;/code&gt; for perl.&lt;/p&gt;

&lt;p&gt;If you are on NixOS - leave script as it is (&lt;em&gt;it uses nix-shell with perl540Packages.AnyEventI3&lt;/em&gt;).&lt;/p&gt;

&lt;p&gt;If you aren't on NixOS - replace first two lines with this one: &lt;code&gt;#!/usr/bin/env perl&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env nix-shell&lt;/span&gt;
&lt;span class="c1"&gt;#!nix-shell -i perl -p perl perl540Packages.AnyEventI3&lt;/span&gt;
&lt;span class="c1"&gt;# Subscribe to i3wm events via i3ipc.&lt;/span&gt;
&lt;span class="c1"&gt;# Source: https://faq.i3wm.org/question/5721/how-do-i-subscribe-to-i3-events-using-bash-easily.1.html&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# Usage example for bash:&lt;/span&gt;
&lt;span class="c1"&gt;# i3subscribe window workspace | while read -r event; do&lt;/span&gt;
&lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="c1"&gt;# done&lt;/span&gt;

&lt;span class="k"&gt;BEGIN&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="vg"&gt;$|&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;# flush \n&lt;/span&gt;

&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nv"&gt;strict&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nv"&gt;warnings&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Data::&lt;/span&gt;&lt;span class="nv"&gt;Dumper&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;AnyEvent::&lt;/span&gt;&lt;span class="nv"&gt;I3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nv"&gt;v5&lt;/span&gt;&lt;span class="mf"&gt;.10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$i3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;i3&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$i3&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;connect&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;recv&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nb"&gt;die&lt;/span&gt; &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Error connecting to i3&lt;/span&gt;&lt;span class="p"&gt;";&lt;/span&gt;

&lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$ev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vg"&gt;$_&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$dump&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vg"&gt;$_&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$i3&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="nv"&gt;$ev&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;@_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nv"&gt;say&lt;/span&gt; &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$ev&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="si"&gt;$msg&lt;/span&gt;&lt;span class="s2"&gt;-&amp;gt;{'change'}&lt;/span&gt;&lt;span class="p"&gt;";&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$dump&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;print&lt;/span&gt; &lt;span class="nv"&gt;Dumper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$msg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;recv&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;success&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;say&lt;/span&gt; &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Successfully subscribed to &lt;/span&gt;&lt;span class="si"&gt;$ev&lt;/span&gt;&lt;span class="s2"&gt;-event&lt;/span&gt;&lt;span class="p"&gt;";&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$nextArg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$nextArg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;say&lt;/span&gt; &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Subscribe to i3-events&lt;/span&gt;&lt;span class="p"&gt;";&lt;/span&gt;
    &lt;span class="nv"&gt;say&lt;/span&gt; &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Usage:   $0 workspace|output|mode|window|barconfig_update|binding [dump]&lt;/span&gt;&lt;span class="p"&gt;";&lt;/span&gt;
    &lt;span class="nv"&gt;say&lt;/span&gt; &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Example: $0 workspace dump window binding dump&lt;/span&gt;&lt;span class="p"&gt;";&lt;/span&gt;
    &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$nextArg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$arg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$nextArg&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$nextArg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$dump&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$nextArg&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nv"&gt;$nextArg&lt;/span&gt; &lt;span class="ow"&gt;eq&lt;/span&gt; &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dump&lt;/span&gt;&lt;span class="p"&gt;")&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$dump&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$nextArg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nv"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;("&lt;/span&gt;&lt;span class="si"&gt;$arg&lt;/span&gt;&lt;span class="p"&gt;",&lt;/span&gt; &lt;span class="nv"&gt;$dump&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nn"&gt;AE::&lt;/span&gt;&lt;span class="nv"&gt;cv&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;recv&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Toggle LED
&lt;/h2&gt;

&lt;p&gt;This is &lt;code&gt;i3ipc_capslock_led.sh&lt;/code&gt; which starts needed listeners - add this script to autostart and that's it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# Executes `toggle_capslock_led.sh` script on window change.&lt;/span&gt;
&lt;span class="c"&gt;# Source: https://faq.i3wm.org/question/5721/how-do-i-subscribe-to-i3-events-using-bash-easily.1.html&lt;/span&gt;

&lt;span class="c"&gt;# Run LED switcher on layout change&lt;/span&gt;
xkb-switch &lt;span class="nt"&gt;-W&lt;/span&gt; | xargs &lt;span class="nt"&gt;-I&lt;/span&gt;&lt;span class="o"&gt;{}&lt;/span&gt; ~/Scripts/toggle_capslock_led.sh &amp;amp;

&lt;span class="c"&gt;# Run LED switcher on window or workspace change&lt;/span&gt;
~/Scripts/i3-subscribe.pl window workspace | &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; event&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  ~/Scripts/toggle_capslock_led.sh
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bye!&lt;/p&gt;

</description>
      <category>unixporn</category>
      <category>keyboard</category>
      <category>xkb</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Code. Gleam. Named actors</title>
      <dc:creator>Kir Axanov</dc:creator>
      <pubDate>Fri, 07 Mar 2025 15:06:13 +0000</pubDate>
      <link>https://dev.to/axkira/code-named-actors-in-gleam-ji2</link>
      <guid>https://dev.to/axkira/code-named-actors-in-gleam-ji2</guid>
      <description>&lt;p&gt;&lt;em&gt;2025.03.07&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Hi!&lt;/p&gt;

&lt;h2&gt;
  
  
  DISCLAIMER
&lt;/h2&gt;

&lt;p&gt;This is a workaround!&lt;br&gt;
Totally not tested under any production load, you've been warned!&lt;/p&gt;
&lt;h2&gt;
  
  
  Problem
&lt;/h2&gt;

&lt;p&gt;We cannot have a global state in such an FP language as Gleam.&lt;/p&gt;

&lt;p&gt;So, when we want to send a message to some specific (&lt;em&gt;singleton&lt;/em&gt;) actor, we need to pass the reference to it as an argument. Every. Time.&lt;/p&gt;
&lt;h2&gt;
  
  
  Workaround
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Words
&lt;/h3&gt;

&lt;p&gt;So. Erlang has the pool of named processes, which we can query to get a process by its assigned unique name...&lt;/p&gt;

&lt;p&gt;But as of now it is &lt;a href="https://github.com/gleam-lang/otp?tab=readme-ov-file#limitations-and-known-issues" rel="noopener noreferrer"&gt;not type-checked by Gleam&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There is no support for named processes. They are untyped global mutable variables which may be uninitialized, more research is needed to find a suitable type safe alternative.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And all we can get is a process ID (&lt;em&gt;pid&lt;/em&gt;). Which is not what we need to &lt;a href="https://hexdocs.pm/gleam_otp/gleam/otp/actor.html#send" rel="noopener noreferrer"&gt;send a message&lt;/a&gt; to our actor.&lt;/p&gt;

&lt;p&gt;But! There is &lt;del&gt;hope&lt;/del&gt; workaround! While we are waiting for proper implementation of named actors.&lt;/p&gt;

&lt;p&gt;We &lt;em&gt;can&lt;/em&gt; send our message as a string to any process by its pid! All we need is to... &lt;strong&gt;&lt;em&gt;kill it&lt;/em&gt;&lt;/strong&gt;!&lt;/p&gt;

&lt;p&gt;Using &lt;a href="https://hexdocs.pm/gleam_erlang/gleam/erlang/process.html#send_abnormal_exit" rel="noopener noreferrer"&gt;abnormal exit&lt;/a&gt; we can send any string as an exit reason to our process. We trap[&lt;a href="https://hexdocs.pm/gleam_erlang/gleam/erlang/process.html#trap_exits" rel="noopener noreferrer"&gt;1&lt;/a&gt;][&lt;a href="https://hexdocs.pm/gleam_erlang/gleam/erlang/process.html#selecting_trapped_exits" rel="noopener noreferrer"&gt;2&lt;/a&gt;] this exit in our singleton actor (&lt;em&gt;as it is linked with the process we are killing&lt;/em&gt;) and parse received message from this string.&lt;/p&gt;
&lt;h3&gt;
  
  
  Diagrams
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiqlm23xtqwzxjyxkn04d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiqlm23xtqwzxjyxkn04d.png" alt="Actors and processes" width="800" height="255"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0r969f19evcarz4o075p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0r969f19evcarz4o075p.png" alt="Initialization steps" width="800" height="780"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbsd91pwomq8jcb9o5fed.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbsd91pwomq8jcb9o5fed.png" alt="What happens under the hood" width="800" height="2025"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Code
&lt;/h3&gt;

&lt;p&gt;Usage example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erlang"&gt;&lt;code&gt;&lt;span class="n"&gt;import&lt;/span&gt; &lt;span class="n"&gt;gleam&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;
&lt;span class="n"&gt;import&lt;/span&gt; &lt;span class="n"&gt;gleam&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;erlang&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;process&lt;/span&gt;

&lt;span class="n"&gt;import&lt;/span&gt; &lt;span class="n"&gt;named_actor&lt;/span&gt;


&lt;span class="n"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="nv"&gt;Just&lt;/span&gt; &lt;span class="n"&gt;echo&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;named_actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"my_singleton_actor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;Nil&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="nv"&gt;Send&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;you&lt;/span&gt; &lt;span class="n"&gt;actor&lt;/span&gt; &lt;span class="n"&gt;only&lt;/span&gt; &lt;span class="n"&gt;by&lt;/span&gt; &lt;span class="n"&gt;its&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;Waiting&lt;/span&gt; &lt;span class="n"&gt;is&lt;/span&gt; &lt;span class="n"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;just&lt;/span&gt; &lt;span class="n"&gt;for&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;demo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
  &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;named_actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"my_singleton_actor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"YOLO!!!1!"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="nv"&gt;Send&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;you&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
  &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;named_actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"my_singleton_actor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Hieee!!!"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="nv"&gt;Keep&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt; &lt;span class="n"&gt;running&lt;/span&gt;
  &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep_forever&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The whole code in &lt;code&gt;named_actor.gleam&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erlang"&gt;&lt;code&gt;&lt;span class="o"&gt;////&lt;/span&gt; &lt;span class="nv"&gt;A&lt;/span&gt; &lt;span class="n"&gt;workaround&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;have&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;named&lt;/span&gt; &lt;span class="n"&gt;actor&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="n"&gt;disposable&lt;/span&gt; &lt;span class="n"&gt;proxy&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="n"&gt;abnormal&lt;/span&gt; &lt;span class="n"&gt;exits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="o"&gt;////&lt;/span&gt; &lt;span class="nv"&gt;This&lt;/span&gt; &lt;span class="n"&gt;actor&lt;/span&gt; &lt;span class="n"&gt;is&lt;/span&gt; &lt;span class="n"&gt;implemented&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="nv"&gt;Nil&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;import&lt;/span&gt; &lt;span class="n"&gt;gleam&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;
&lt;span class="n"&gt;import&lt;/span&gt; &lt;span class="n"&gt;gleam&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;function&lt;/span&gt;
&lt;span class="n"&gt;import&lt;/span&gt; &lt;span class="n"&gt;gleam&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;dynamic&lt;/span&gt;
&lt;span class="n"&gt;import&lt;/span&gt; &lt;span class="n"&gt;gleam&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;otp&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;actor&lt;/span&gt;
&lt;span class="n"&gt;import&lt;/span&gt; &lt;span class="n"&gt;gleam&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;erlang&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;atom&lt;/span&gt;&lt;span class="p"&gt;.{&lt;/span&gt; &lt;span class="n"&gt;create_from_string&lt;/span&gt; &lt;span class="n"&gt;as&lt;/span&gt; &lt;span class="n"&gt;atom&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;import&lt;/span&gt; &lt;span class="n"&gt;gleam&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;erlang&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;process&lt;/span&gt;


&lt;span class="o"&gt;//---------------------------------------------------------------------------//&lt;/span&gt;
&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="nv"&gt;Constants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;                                                                &lt;span class="o"&gt;//&lt;/span&gt;
&lt;span class="o"&gt;//---------------------------------------------------------------------------//&lt;/span&gt;
&lt;span class="o"&gt;///&lt;/span&gt; &lt;span class="nv"&gt;How&lt;/span&gt; &lt;span class="n"&gt;much&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt; &lt;span class="n"&gt;is&lt;/span&gt; &lt;span class="n"&gt;given&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;actor&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="o"&gt;///&lt;/span&gt; &lt;span class="nv"&gt;In&lt;/span&gt; &lt;span class="n"&gt;milliseconds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;const&lt;/span&gt; &lt;span class="n"&gt;init_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;

&lt;span class="o"&gt;///&lt;/span&gt; &lt;span class="nv"&gt;How&lt;/span&gt; &lt;span class="n"&gt;much&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="n"&gt;between&lt;/span&gt; &lt;span class="n"&gt;tries&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;getting&lt;/span&gt; &lt;span class="n"&gt;named&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="o"&gt;///&lt;/span&gt; &lt;span class="nv"&gt;In&lt;/span&gt; &lt;span class="n"&gt;milliseconds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;const&lt;/span&gt; &lt;span class="n"&gt;send_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;


&lt;span class="o"&gt;//---------------------------------------------------------------------------//&lt;/span&gt;
&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="nv"&gt;Methods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;                                                                  &lt;span class="o"&gt;//&lt;/span&gt;
&lt;span class="o"&gt;//---------------------------------------------------------------------------//&lt;/span&gt;
&lt;span class="o"&gt;///&lt;/span&gt; &lt;span class="nv"&gt;Creates&lt;/span&gt; &lt;span class="n"&gt;an&lt;/span&gt; &lt;span class="n"&gt;actor&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;named&lt;/span&gt; &lt;span class="n"&gt;proxy&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="nb"&gt;send&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;that&lt;/span&gt; &lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nn"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nn"&gt;handle_msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;Dynamic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;Nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;Dynamic&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;StartError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_spec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;Spec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nn"&gt;init&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;init_actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nn"&gt;init_timeout&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt;
    &lt;span class="nn"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;Dynamic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;Nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;handle_msg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;init_actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;InitResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;Nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;Dynamic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;subject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_subject&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;trap_proxy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;exit_msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;ExitMessage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;Dynamic&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;start_proxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;exit_msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;Abnormal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;reason&lt;/span&gt;
        &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drop_left&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;cuts&lt;/span&gt; &lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="nv"&gt;Abnormal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"MSG"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;where&lt;/span&gt; &lt;span class="n"&gt;we&lt;/span&gt; &lt;span class="n"&gt;need&lt;/span&gt; &lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="nv"&gt;MSG&lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;
        &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drop_right&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;cuts&lt;/span&gt; &lt;span class="n"&gt;right&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="nv"&gt;Abnormal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"MSG"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;where&lt;/span&gt; &lt;span class="n"&gt;we&lt;/span&gt; &lt;span class="n"&gt;need&lt;/span&gt; &lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="nv"&gt;MSG&lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;
        &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;selector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_selector&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;selecting_trapped_exits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trap_proxy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;selecting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trap_exits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;start_proxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;Ready&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;Nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="o"&gt;///&lt;/span&gt; &lt;span class="nv"&gt;Starts&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;named&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;which&lt;/span&gt; &lt;span class="n"&gt;upon&lt;/span&gt; &lt;span class="n"&gt;exiting&lt;/span&gt; &lt;span class="n"&gt;sends&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;our&lt;/span&gt; &lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;start_proxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;Nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sleep_forever&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;atom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="nv"&gt;Nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="o"&gt;///&lt;/span&gt; &lt;span class="nv"&gt;Sends&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;named&lt;/span&gt; &lt;span class="nf"&gt;actor&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;by&lt;/span&gt; &lt;span class="n"&gt;killing&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="n"&gt;that&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="n"&gt;rough&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;
&lt;span class="o"&gt;///&lt;/span&gt; &lt;span class="nv"&gt;Sending&lt;/span&gt; &lt;span class="n"&gt;is&lt;/span&gt; &lt;span class="n"&gt;done&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="n"&gt;by&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="o"&gt;///&lt;/span&gt; &lt;span class="nv"&gt;Loop&lt;/span&gt; &lt;span class="n"&gt;does&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="n"&gt;until&lt;/span&gt; &lt;span class="n"&gt;succeeded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="o"&gt;///&lt;/span&gt; &lt;span class="nv"&gt;Each&lt;/span&gt; &lt;span class="n"&gt;iteration&lt;/span&gt; &lt;span class="n"&gt;has&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt; &lt;span class="n"&gt;inside&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="nb"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;Nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;send__loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nv"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;Nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;send__loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;Nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;named&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;atom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_alive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;True&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_abnormal_exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nv"&gt;False&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;send_timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="nf"&gt;send__loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;_&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;send_timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nf"&gt;send__loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do let me know, if you have some other way of doing this!&lt;/p&gt;

&lt;p&gt;Bye!&lt;/p&gt;

</description>
      <category>programming</category>
      <category>gleamlang</category>
    </item>
    <item>
      <title>NixOS. Managing secrets with sops-nix</title>
      <dc:creator>Kir Axanov</dc:creator>
      <pubDate>Fri, 21 Feb 2025 07:36:23 +0000</pubDate>
      <link>https://dev.to/axkira/nixos-managing-secrets-with-sops-nix-40e9</link>
      <guid>https://dev.to/axkira/nixos-managing-secrets-with-sops-nix-40e9</guid>
      <description>&lt;p&gt;&lt;em&gt;2025.02.21&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Hi! &lt;/p&gt;

&lt;p&gt;Here are some notes about &lt;em&gt;how to use secrets in NixOS configuration securely&lt;/em&gt;. As of now I use &lt;a href="https://github.com/Mic92/sops-nix" rel="noopener noreferrer"&gt;sops-nix&lt;/a&gt; with simple &lt;a href="https://github.com/FiloSottile/age" rel="noopener noreferrer"&gt;age&lt;/a&gt; keys (not generated from ssh keys).&lt;/p&gt;

&lt;h2&gt;
  
  
  Concept
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foeuhrdcvfbrjsw762uay.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foeuhrdcvfbrjsw762uay.png" alt=" " width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We will generate a file with all our secrets (e.g. &lt;code&gt;secrets.yaml&lt;/code&gt;). For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;my_secret_auth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;plain-text secret&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;secrets.yaml&lt;/code&gt; is stored encrypted. We can edit it with &lt;code&gt;sops secrets.yaml&lt;/code&gt; which decrypts it and opens your editor from the &lt;code&gt;$EDITOR&lt;/code&gt; environment variable.&lt;/p&gt;

&lt;p&gt;For this encryption / decryption sops uses an &lt;code&gt;age&lt;/code&gt; public and private keys - we generate those too.&lt;/p&gt;

&lt;p&gt;For those secrets to be picked up by nixos we register them in nixos config (&lt;em&gt;we can set secret's owner to allow access for needed service&lt;/em&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sops.secrets &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  my_secret_auth &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    owner &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"nginx"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the nixos configuration we now can use those secrets like so (&lt;em&gt;an example for nginx basic auth&lt;/em&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;basicAuthFile &lt;span class="o"&gt;=&lt;/span&gt; config.sops.secrets.my_secret_auth.path&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's basically it. Below are some details on how to setup all of this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local secrets with &lt;code&gt;nixos-rebuild&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;For use on a local machine. We will use &lt;a href="https://github.com/nmattia/niv" rel="noopener noreferrer"&gt;niv&lt;/a&gt; to install &lt;code&gt;sops-nix&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;1) Go to your configuration directory, init a &lt;code&gt;niv&lt;/code&gt; project with &lt;code&gt;sops-nix&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# where `configuration.nix` is located, adjust if needed&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; /etc/nixos

&lt;span class="c"&gt;# init `niv` with `sops-nix` before installing anything&lt;/span&gt;
nix-shell &lt;span class="nt"&gt;-p&lt;/span&gt; niv &lt;span class="nt"&gt;--run&lt;/span&gt; &lt;span class="s2"&gt;"niv init &amp;amp;&amp;amp; niv add Mic92/sops-nix"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2) Create the directory for age keys:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.config/sops/age
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3) Configure &lt;code&gt;sops&lt;/code&gt; and other stuff in &lt;code&gt;secrets.nix&lt;/code&gt; (&lt;em&gt;you can choose another name, don't forget to import this file from your configuration&lt;/em&gt;), replace &lt;code&gt;YOUR_USER&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;{&lt;/span&gt; pkgs, config, ... &lt;span class="o"&gt;}&lt;/span&gt;:

&lt;span class="o"&gt;{&lt;/span&gt;
  imports &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="p"&gt;(import ./nix/sources.nix).sops-nix&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/modules/sops"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  environment.systemPackages &lt;span class="o"&gt;=&lt;/span&gt; with pkgs&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;
    age sops
  &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  sops &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    defaultSopsFile &lt;span class="o"&gt;=&lt;/span&gt; ./secrets/secrets.yaml&lt;span class="p"&gt;;&lt;/span&gt;
    defaultSopsFormat &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"yaml"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    age.keyFile &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/home/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.users.users.YOUR_USER.name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/.config/sops/age/keys.txt"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;4) Apply configuration above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nixos-rebuild switch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;5) Generate an age key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;age-keygen &lt;span class="nt"&gt;-o&lt;/span&gt; ~/.config/sops/age/keys.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;6) Create &lt;code&gt;.sops.yaml&lt;/code&gt; and paste public key printed on the previous step (replace capslocked text), use arbitrarily username:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;USERNAME&lt;/span&gt; &lt;span class="s"&gt;PUBLIC_KEY&lt;/span&gt;
&lt;span class="na"&gt;creation_rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path_regex&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secrets/secrets.yaml$&lt;/span&gt;
    &lt;span class="na"&gt;key_groups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;*USERNAME&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;7) Register secrets to &lt;code&gt;secrets.nix&lt;/code&gt; from step 3:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;...
sops &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;# ...&lt;/span&gt;
  secrets &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    my_secret_auth &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      owner &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"nginx"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;8) Use your secrets in nixos configuration (&lt;em&gt;where the option expects a file path to the secret&lt;/em&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ...&lt;/span&gt;
basicAuthFile &lt;span class="o"&gt;=&lt;/span&gt; config.sops.secrets.my_secret_auth.path&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Remote secrets with &lt;code&gt;flake&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;For use on a remote machine.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;DISCLAIMER: I keep remote's configs locally and send them via &lt;code&gt;scp&lt;/code&gt; when updating it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;1) Add this to your flake:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;{&lt;/span&gt;
  inputs &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;# ...&lt;/span&gt;
    sops-nix.url &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"github:Mic92/sops-nix"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c"&gt;# &amp;lt;--&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  outputs &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;# ...&lt;/span&gt;
    sops-nix,  &lt;span class="c"&gt;# &amp;lt;--&lt;/span&gt;
    ...
  &lt;span class="o"&gt;}&lt;/span&gt;@inputs:  &lt;span class="c"&gt;# &amp;lt;--&lt;/span&gt;
  &lt;span class="o"&gt;{&lt;/span&gt;
    nixosConfigurations.your_flake_name &lt;span class="o"&gt;=&lt;/span&gt; nixpkgs.lib.nixosSystem &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="c"&gt;# ...&lt;/span&gt;
      specialArgs &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; inherit inputs&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c"&gt;# &amp;lt;--&lt;/span&gt;
      modules &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;
        &lt;span class="c"&gt;# ...&lt;/span&gt;
        sops-nix.nixosModules.sops  &lt;span class="c"&gt;# &amp;lt;--&lt;/span&gt;
      &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2) Create the directory for age keys:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.config/sops/age
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3) Configure &lt;code&gt;sops&lt;/code&gt; and other stuff in &lt;code&gt;secrets.nix&lt;/code&gt; (&lt;em&gt;you can choose another name, don't forget to import this file from your configuration&lt;/em&gt;), replace &lt;code&gt;YOUR_USER&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;{&lt;/span&gt; pkgs, config, inputs, ... &lt;span class="o"&gt;}&lt;/span&gt;:

&lt;span class="o"&gt;{&lt;/span&gt;
  imports &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; inputs.sops-nix.nixosModules.sops &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c"&gt;# &amp;lt;-- differs from the local setup&lt;/span&gt;

  environment.systemPackages &lt;span class="o"&gt;=&lt;/span&gt; with pkgs&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;
    age sops
  &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  sops &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    defaultSopsFile &lt;span class="o"&gt;=&lt;/span&gt; ./secrets/secrets.yaml&lt;span class="p"&gt;;&lt;/span&gt;
    defaultSopsFormat &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"yaml"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    age.keyFile &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/home/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.users.users.YOUR_USER.name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/.config/sops/age/keys.txt"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;4) Apply configuration above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# I'm copying the configuration via ssh and running update on a remote - you do you&lt;/span&gt;
nixos-rebuild switch &lt;span class="nt"&gt;--flake&lt;/span&gt; .#update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;5) Generate an age key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;age-keygen &lt;span class="nt"&gt;-o&lt;/span&gt; ~/.config/sops/age/keys.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;6) Create &lt;code&gt;.sops.yaml&lt;/code&gt; and paste public key printed on the previous step (replace capslocked text), use arbitrarily username:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;USERNAME&lt;/span&gt; &lt;span class="s"&gt;PUBLIC_KEY&lt;/span&gt;
&lt;span class="na"&gt;creation_rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path_regex&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secrets/secrets.yaml$&lt;/span&gt;
    &lt;span class="na"&gt;key_groups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;*USERNAME&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;7) Register secrets to &lt;code&gt;secrets.nix&lt;/code&gt; from step 3:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;...
sops &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  ...
  secrets &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    my_secret_auth &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      owner &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"nginx"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;8) Use your secrets in nixos configuration (&lt;em&gt;where the option expects a file path to the secret&lt;/em&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;...
basicAuthFile &lt;span class="o"&gt;=&lt;/span&gt; config.sops.secrets.my_secret_auth.path&lt;span class="p"&gt;;&lt;/span&gt;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;9) If you use sops on both local and remote, it's useful to copy age private key from the remote machine to your home &lt;code&gt;keys.txt&lt;/code&gt; (&lt;em&gt;so you'll have two keys there&lt;/em&gt;) - this way you can edit remote's &lt;code&gt;secrets.yaml&lt;/code&gt; locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/Mic92/sops-nix" rel="noopener noreferrer"&gt;sops-nix repo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=G5f6GC7SnhU" rel="noopener noreferrer"&gt;short tutorial from vimjoyer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/vimjoyer/sops-nix-video" rel="noopener noreferrer"&gt;commands from tutorial above&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://0xda.de/blog/2024/07/framework-and-nixos-sops-nix-secrets-management/#installing-sops-nix" rel="noopener noreferrer"&gt;nice article from dade&lt;/a&gt; about this topic and some more&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bye!&lt;/p&gt;

</description>
      <category>nixos</category>
      <category>sops</category>
      <category>security</category>
    </item>
  </channel>
</rss>
