<?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: freerave</title>
    <description>The latest articles on DEV Community by freerave (@freerave).</description>
    <link>https://dev.to/freerave</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3563889%2F6ecf3060-63f8-46f1-9869-b61412ef894c.png</url>
      <title>DEV Community: freerave</title>
      <link>https://dev.to/freerave</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/freerave"/>
    <language>en</language>
    <item>
      <title>Making a file converter feel alive with QML</title>
      <dc:creator>freerave</dc:creator>
      <pubDate>Sat, 07 Nov 2026 20:28:43 +0000</pubDate>
      <link>https://dev.to/freerave/making-a-file-converter-feel-alive-with-qml-5gl6</link>
      <guid>https://dev.to/freerave/making-a-file-converter-feel-alive-with-qml-5gl6</guid>
      <description>&lt;p&gt;&lt;a href="https://dev.to/freerave/8-backends-one-engine-and-the-svg-bug-that-started-it-all-3ooo"&gt;Part 2&lt;/a&gt; was about DotMorph's conversion engine. This one is about the part people actually see: a two-ring radial menu that blooms around a file, written in QML and driven from Python with PySide6.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foa3bleniy3jtucnkf9bb.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foa3bleniy3jtucnkf9bb.gif" alt="The wheel opening, the Video fan blooming, GIF picked, progress, and a success burs" width="720" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why QML and not widgets
&lt;/h2&gt;

&lt;p&gt;A radial menu is all curves, glows and motion. In QWidgets that means hand-painting every frame with &lt;code&gt;QPainter&lt;/code&gt; and running your own animation timers. In QML, an animation is a property with a &lt;code&gt;Behavior&lt;/code&gt;, and the scene graph draws everything on the GPU. The Python side (a &lt;code&gt;Bridge&lt;/code&gt; QObject) exposes the file, the targets, progress and status, and QML just reacts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Drawing a ring sector
&lt;/h2&gt;

&lt;p&gt;My first attempt drew triangles instead of ring sectors, and the labels on top were invisible because of a colour-mixing bug. The version that works describes each sector as an &lt;strong&gt;SVG path&lt;/strong&gt;: the outer arc clockwise, a line inward, the inner arc back. QtQuick.Shapes renders it with the curve renderer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight qml"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ring sector as an SVG path: outer arc clockwise, inner arc back&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;pt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;deg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;deg&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cy&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&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="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;property&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;bigO&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sweepO&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;180&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bigI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sweepI&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;180&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="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;M &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;pt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;a0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; A &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;oR&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;oR&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; 0 &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;bigO&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; 1 &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;pt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;a0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;sweepO&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; L &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;pt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;iR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; A &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;iR&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;iR&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; 0 &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;bigI&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; 0 &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;pt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;iR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;sweepI&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;ShapePath&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;strokeColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sectorEdge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Qt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lighter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.35&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lift&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nl"&gt;fillGradient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;RadialGradient&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* sector colour → category colour, by s.lift */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kt"&gt;PathSvg&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;d&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 gaps between sectors are constant in &lt;em&gt;pixels&lt;/em&gt;, not degrees (&lt;code&gt;gapPx / radius&lt;/code&gt; converted to an angle), so the inner and outer rings line up visually even though their circumferences differ.&lt;/p&gt;

&lt;h2&gt;
  
  
  One number drives the hover
&lt;/h2&gt;

&lt;p&gt;Each sector has a single animated property, &lt;code&gt;lift&lt;/code&gt;: 0 when idle, about 0.55 when its category is open, 1 when hovered. Colour, outward push and outer radius all read from it, so they can never get out of sync:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight qml"&gt;&lt;code&gt;&lt;span class="nx"&gt;property&lt;/span&gt; &lt;span class="nx"&gt;real&lt;/span&gt; &lt;span class="nx"&gt;lift&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;busy&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hovered&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selected&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mf"&gt;0.55&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="kt"&gt;Behavior&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="nl"&gt;lift&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kt"&gt;NumberAnimation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;170&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nl"&gt;easing.type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Easing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OutCubic&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Translate&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;midRad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lift&lt;/span&gt;
    &lt;span class="nl"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;midRad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lift&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;theme.ms()&lt;/code&gt; scales every duration, and returns 0 when &lt;em&gt;reduce motion&lt;/em&gt; is on. That makes the accessibility setting a single switch instead of a hunt through fifty animations.&lt;/p&gt;

&lt;p&gt;The intro uses the same trick: every sector has a &lt;code&gt;reveal&lt;/code&gt; property that animates from 0 to 1 with a per-sector delay (&lt;code&gt;order * stagger&lt;/code&gt;). Scale, rotation and opacity are all functions of it, which gives you a staggered fly-out with a small spin. Labels counter-rotate (&lt;code&gt;rotation: -s.rotation&lt;/code&gt;) so the text stays upright while the sector spins in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The success burst
&lt;/h2&gt;

&lt;p&gt;When a conversion finishes, a check mark draws itself in the hub and particles fly out. That's QtQuick.Particles with an emitter that does nothing until asked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight qml"&gt;&lt;code&gt;&lt;span class="kt"&gt;ParticleSystem&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;particles&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;running&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;wheel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;motion&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kt"&gt;Emitter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;burst&lt;/span&gt;
    &lt;span class="nl"&gt;system&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;particles&lt;/span&gt;
    &lt;span class="nl"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;wheel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cx&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;wheel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;width&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="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="nl"&gt;emitRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nl"&gt;lifeSpan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;lifeSpanVariation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;350&lt;/span&gt;
    &lt;span class="nl"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;sizeVariation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;endSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="nl"&gt;velocity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;AngleDirection&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;angleVariation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;360&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;magnitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;magnitudeVariation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;160&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nl"&gt;acceleration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;PointDirection&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;240&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;&lt;code&gt;burst.burst(110)&lt;/code&gt; on success, and the slight downward acceleration makes it fall like confetti instead of expanding like a balloon. Failure gets a cross and a short &lt;code&gt;SequentialAnimation&lt;/code&gt; shake instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  0.1 seconds, thanks to a daemon
&lt;/h2&gt;

&lt;p&gt;Starting Python, importing PySide6 and compiling the QML takes about a second. That's fine for an app and terrible for a right-click menu. So DotMorph keeps a daemon running (started by XDG autostart) with Qt and the wheel already loaded.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe1kj8748w5qalv98xk3j.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe1kj8748w5qalv98xk3j.png" alt="Every entry point goes through a stdlib-only client to the daemon over a Unix socket" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The trick is that the client side imports &lt;strong&gt;no Qt at all&lt;/strong&gt;. &lt;code&gt;dotmorph FILE&lt;/code&gt; checks for the daemon's socket in &lt;code&gt;$XDG_RUNTIME_DIR&lt;/code&gt; and sends one line of JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;2.0&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;dict&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Send one request; returns the reply, or None when no daemon answers.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;socket_path&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AF_UNIX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SOCK_STREAM&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;sock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;sock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;settimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;sock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&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;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;sock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendall&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;makefile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readline&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&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;line&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;OSError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If nobody answers, it falls back to opening its own window. The wheel appears in about 0.1 s. Two bugs from building this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The daemon didn't answer.&lt;/strong&gt; Its socket server was created before the Qt event loop existed, so connections were accepted into the void.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long socket paths.&lt;/strong&gt; Unix socket paths are limited to 108 bytes. An unusual &lt;code&gt;$XDG_RUNTIME_DIR&lt;/code&gt; blew past that, so there's now a short private fallback path.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The hotkey that made the wheel shake
&lt;/h3&gt;

&lt;p&gt;Super+Shift+M opens the wheel with whatever files you copied. When I tested it, holding the keys made the wheel &lt;em&gt;shake&lt;/em&gt;. Key repeat fires about every 30 ms, each repeat sent a new "open", and each "open" restarted the bloom animation. The fix is a debounce in the daemon:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;repeat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_last_summon&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;SUMMON_DEBOUNCE&lt;/span&gt;   &lt;span class="c1"&gt;# 0.6 s
&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_last_summon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;visible&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repeat&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;paths&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_file&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_files&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;  &lt;span class="c1"&gt;# already showing these files: leave the wheel alone
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  A drag is not a click
&lt;/h3&gt;

&lt;p&gt;The wheel is a frameless window you can grab anywhere and move. Early on, releasing a drag over a sector &lt;em&gt;started a conversion&lt;/em&gt;. Now a press that moves past a small threshold becomes a move, never a click, and the wheel turns see-through while you move it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The edge dock
&lt;/h2&gt;

&lt;p&gt;Drag the wheel against the left or right edge of the screen and it turns into a slim glowing tab. Hover it, click it, or drag files over it, and a &lt;strong&gt;half wheel&lt;/strong&gt; slides out of the edge, like Android's edge panels. It folds back when the pointer leaves, after a conversion, or on Esc.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faxe9wd8372mv5gyc0tqj.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faxe9wd8372mv5gyc0tqj.gif" alt="Dragging the wheel to the edge: it becomes a tab, then a half wheel on hover" width="760" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The dock is a small state machine (&lt;code&gt;free → docked → peek&lt;/code&gt;) in Python that resizes and moves the window. QML switches the wheel into &lt;code&gt;half&lt;/code&gt; mode. Some details took more time than the feature itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mirroring.&lt;/strong&gt; On the right edge the half wheel is mirrored so the first category is still on top. The hidden half behind the screen edge must never react to clicks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sliding.&lt;/strong&gt; Dragging the tab up and down moves it along the edge; pulling it away gives you the full wheel back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory.&lt;/strong&gt; The side and height are saved, and the daemon brings the tab back at login.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Progress while folded.&lt;/strong&gt; A conversion that keeps running fills the tab from the bottom up.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wayland, honestly
&lt;/h2&gt;

&lt;p&gt;A menu that opens at a precise spot and stays above everything is exactly what Wayland doesn't let a normal client do. DotMorph runs the wheel through XWayland on Wayland sessions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;XDG_SESSION_TYPE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wayland&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;QT_QPA_PLATFORM&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&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="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;QT_QPA_PLATFORM&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;xcb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cost: it opens centred, because Wayland doesn't expose the pointer position, and it's X11 under the hood. A native version using the layer-shell protocol (KDE and wlroots compositors) is on the roadmap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Right to left
&lt;/h2&gt;

&lt;p&gt;The settings window and the options panel are fully translated to Arabic. For layout, QML does most of the work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight qml"&gt;&lt;code&gt;&lt;span class="nx"&gt;LayoutMirroring&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rtl&lt;/span&gt;
&lt;span class="nx"&gt;LayoutMirroring&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;childrenInherit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That mirrors anchors and layouts. Sliders and toggles needed manual flipping, and an Arabic font (Noto Kufi Arabic, or another Noto Arabic face) is picked automatically. Format names, commands and engine errors stay English on purpose.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyia08jj67amqgkb5ut68.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyia08jj67amqgkb5ut68.png" alt="Settings in English (dark) and Arabic (light, right to left)" width="800" height="272"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing a UI that moves
&lt;/h2&gt;

&lt;p&gt;The test suite loads the &lt;strong&gt;real QML&lt;/strong&gt; offscreen and drives it like a user would: arrow keys and Enter to pick a format, a drag that must not convert, the options panel, the dock modes, hit-testing on both edges. A message handler collects QML warnings, and the tests fail if any show up.&lt;/p&gt;

&lt;p&gt;The GIFs in this post come from the same idea. A script opens the real wheel, presses the keys, grabs every frame with &lt;code&gt;QQuickWindow.grabWindow()&lt;/code&gt;, and ffmpeg turns the frames into the GIF. One lesson from that: Qt's &lt;code&gt;offscreen&lt;/code&gt; platform has no OpenGL, so the Shapes simply weren't drawn. Running under Xvfb with Mesa's software OpenGL fixed it.&lt;/p&gt;

&lt;p&gt;In part 4, the last one: shipping all of this to Linux users five different ways from one signed git tag, and what AppImageHub's test taught me about "works on my machine".&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/kareem2099/DotMorph" rel="noopener noreferrer"&gt;kareem2099/DotMorph&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project page:&lt;/strong&gt; &lt;a href="https://dotsuite.dev/en/product/dotmorph" rel="noopener noreferrer"&gt;dotsuite.dev/en/product/dotmorph&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>qt</category>
      <category>python</category>
      <category>linux</category>
      <category>ui</category>
    </item>
    <item>
      <title>Rebuilding DotReminder: Glass UI, Reliable Reminders, and AI Drafts</title>
      <dc:creator>freerave</dc:creator>
      <pubDate>Thu, 05 Nov 2026 18:24:58 +0000</pubDate>
      <link>https://dev.to/freerave/rebuilding-dotreminder-glass-ui-reliable-reminders-and-ai-drafts-489m</link>
      <guid>https://dev.to/freerave/rebuilding-dotreminder-glass-ui-reliable-reminders-and-ai-drafts-489m</guid>
      <description>&lt;p&gt;A reminder app has a simple promise: help you remember something when it matters.&lt;/p&gt;

&lt;p&gt;That makes it an interesting app to redesign. A polished home screen is useful, but the experience also depends on what happens after you save a reminder, put the phone down, and leave the app alone.&lt;/p&gt;

&lt;p&gt;For DotReminder 0.0.7, I worked on both sides: a glass-inspired interface built with Jetpack Compose, and the scheduling, account, and AI flows behind it.&lt;/p&gt;

&lt;p&gt;DotReminder is an Android project from &lt;strong&gt;FreeRave (DotSuite)&lt;/strong&gt;. The source is available under &lt;strong&gt;Apache License 2.0&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/kareem2099/DotReminder" rel="noopener noreferrer"&gt;Explore DotReminder on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A visual language shared across screens
&lt;/h2&gt;

&lt;p&gt;The redesign started with the screens people use most: the reminder overview, the creation form, the calendar, and the AI conversation. It then extended to templates and account settings.&lt;/p&gt;

&lt;p&gt;The goal was a calmer interface with translucent-looking surfaces, rounded cards, clear grouping, and light and dark appearances. Those choices needed to feel consistent across the app, rather than becoming a separate styling exercise for every screen.&lt;/p&gt;

&lt;p&gt;Shared Compose components made that practical. Screens use the same glass surfaces and theme foundations, while their state and actions stay in ViewModels, controllers, and service or repository boundaries.&lt;/p&gt;

&lt;p&gt;That division also helped with screenshots: I could render the actual production screen content using local demonstration state, without signing into an account or writing sample reminders to Firestore.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdisymz0pezx1zovflh7s.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdisymz0pezx1zovflh7s.png" alt="DotReminder home overview, using demonstration reminders" width="800" height="1422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Saving a reminder is only the beginning
&lt;/h2&gt;

&lt;p&gt;One of the most useful findings during device review was also one of the simplest: a saved reminder could exist while notification permission was denied.&lt;/p&gt;

&lt;p&gt;The app needed to explain that dependency at the point where it mattered. The updated save flow checks notification access, blocked reminder channels, and precise-alarm access. If the user leaves to grant access, the form keeps its draft and validates the time again when they return.&lt;/p&gt;

&lt;p&gt;Reminder scheduling also has to survive more than the current process. Device-local alarm records now keep the information needed to restore registrations after reboot, an app update, or relevant clock and access changes, without first fetching the reminder from the network.&lt;/p&gt;

&lt;p&gt;Recurring reminders needed a related correction: after one occurrence, their next occurrence should be scheduled and shown as upcoming. A recurring task should not remain stuck in an overdue state just because its first date passed.&lt;/p&gt;

&lt;p&gt;Notification delivery and task completion remain separate concepts. A one-time reminder can sound and still appear overdue until the task is completed.&lt;/p&gt;

&lt;p&gt;There are still real device constraints. Permissions, notification-channel settings, Do Not Disturb, and power settings can affect alerts. I tested delivery on the review device, but that does not turn Android scheduling into a universal timing guarantee.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fae0z8hvcsii86sp00j7e.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fae0z8hvcsii86sp00j7e.png" alt="Calendar view with demonstration reminders" width="800" height="1422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  AI suggests; the user saves
&lt;/h2&gt;

&lt;p&gt;The assistant supports a different way to start a reminder. Someone can write:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Remind me to call Mum tomorrow at 6 pm.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The result is a draft, not an automatically saved task. The user reviews the title, date, time, and other details in the regular reminder form before saving.&lt;/p&gt;

&lt;p&gt;This keeps the same validation and permission flow for both manual entry and AI-assisted entry. It also gives the user a chance to correct an interpretation before it becomes a notification.&lt;/p&gt;

&lt;p&gt;The AI request path changed in this version too. Android calls an authenticated DotSuite gateway, while the provider key stays in the server environment. The gateway verifies the signed-in account's Firebase token, validates request sizes and shapes, and applies account and network usage limits.&lt;/p&gt;

&lt;p&gt;On the client, requests are tied to the account that started them. Signing out or switching accounts discards an old result, and cancelling a request cancels its network call.&lt;/p&gt;

&lt;p&gt;These are specific controls around credentials and requests. They do not make AI output inherently correct, which is why draft review remains part of the product.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr19dwxpm3jlkzu5llqrp.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr19dwxpm3jlkzu5llqrp.png" alt="AI conversation with an example reminder draft waiting for review" width="800" height="1422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Giving account controls clear meanings
&lt;/h2&gt;

&lt;p&gt;Account work surfaced another distinction that needed to be visible in the interface: biometric app lock and account MFA serve different purposes.&lt;/p&gt;

&lt;p&gt;Biometric app lock protects access to an existing signed-in session when returning to the app. Authenticator-based verification remains a separate account sign-in step for enrolled accounts.&lt;/p&gt;

&lt;p&gt;The privacy and deletion controls also needed plain wording. DotReminder provides a request pathway for deleting an account and its associated data. Support verifies ownership and processes verified requests within 30 days. Opening the request page or an email draft does not itself erase the account.&lt;/p&gt;

&lt;p&gt;These details are less visible than the home screen, but they shape whether people understand what an action will actually do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I wanted to keep maintainable
&lt;/h2&gt;

&lt;p&gt;The Android stack is Kotlin, Jetpack Compose, Hilt, Firebase, and DataStore. Version 0.0.7 targets Android 16 / API 36, while retaining Android 10 / API 29 as its minimum.&lt;/p&gt;

&lt;p&gt;For this update, the useful boundaries were concrete:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Screen rendering can accept demonstration state without live services.&lt;/li&gt;
&lt;li&gt;Reminder drafts pass through a shared review and save flow.&lt;/li&gt;
&lt;li&gt;Alarm restoration has device-local records it can use without a network fetch.&lt;/li&gt;
&lt;li&gt;Authentication challenges and biometric locking stay separate from visual components.&lt;/li&gt;
&lt;li&gt;Provider credentials belong in the backend environment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repository includes focused tests and review notes for these flows, along with the localized store artwork sources. The native UI currently uses English labels; the assistant supports Arabic and English requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the source and tell me what you would change
&lt;/h2&gt;

&lt;p&gt;This release is a useful checkpoint for the project: the screens share a visual language, and the less visible flows received attention alongside them.&lt;/p&gt;

&lt;p&gt;I'd like feedback on the reminder form, the calendar, and the AI draft review. What would make a reminder app easier for you to trust and use every day?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google Play:&lt;/strong&gt; &lt;a href="https://play.google.com/store/apps/details?id=com.FreeRave.remindme" rel="noopener noreferrer"&gt;DotReminder on Google Play&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/kareem2099/DotReminder" rel="noopener noreferrer"&gt;github.com/kareem2099/DotReminder&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Project:&lt;/strong&gt; &lt;a href="https://dotsuite.dev" rel="noopener noreferrer"&gt;DotSuite&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Support:&lt;/strong&gt; &lt;a href="mailto:support@dotsuite.dev"&gt;support@dotsuite.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>jetpackcompose</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Building DotScramble Part 4: A New UI in One Stylesheet, 7 Languages With Real Plurals, and the Bugs Only a Clean Machine Found</title>
      <dc:creator>freerave</dc:creator>
      <pubDate>Mon, 02 Nov 2026 19:27:44 +0000</pubDate>
      <link>https://dev.to/freerave/building-dotscramble-part-4-a-new-ui-in-one-stylesheet-7-languages-with-real-plurals-and-the-4618</link>
      <guid>https://dev.to/freerave/building-dotscramble-part-4-a-new-ui-in-one-stylesheet-7-languages-with-real-plurals-and-the-4618</guid>
      <description>&lt;h2&gt;
  
  
  DotScramble 1.5 rebuilt: one token-based QSS file for 16 themes, a canvas with Detect preview and Compare, five new languages with CLDR plurals, a privacy tool that refuses to fail silently, and four packaging bugs my own CI never saw.
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiety3i0nkxrm8i5prnhr.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiety3i0nkxrm8i5prnhr.gif" alt="DotScramble 1.5: Detect outlines the faces, Pixelation is picked, Apply hides them" width="600" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/freerave/building-dotscramble-part-3-wayland-wars-a-custom-file-picker-and-designing-ai-evasion-2mja"&gt;Part 3&lt;/a&gt; ended with the PySide6 migration and the design of AI Evasion. Since then DotScramble went through its biggest update yet: &lt;strong&gt;1.5&lt;/strong&gt;. Four things changed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The whole interface was rebuilt, and every color now comes from &lt;strong&gt;one stylesheet&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;It went from two languages to &lt;strong&gt;seven&lt;/strong&gt;, with plural forms that are actually correct.&lt;/li&gt;
&lt;li&gt;A batch of fixes, led by one rule: &lt;strong&gt;a privacy tool must never fail silently&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Releases are now built, tested and signed by &lt;strong&gt;GitHub Actions&lt;/strong&gt;, and a clean machine found four bugs my own CI had been hiding.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you haven't seen what DotScramble does, the &lt;a href="https://dev.to/freerave/i-built-a-linux-app-that-hides-faces-license-plates-and-gps-from-your-photos-3h97"&gt;tour&lt;/a&gt; covers it in five minutes.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The UI rebuild
&lt;/h2&gt;

&lt;p&gt;The 1.4 window had grown by accretion: a header, then a toolbar, then buttons that did the same thing as the toolbar, long radio lists, emoji as icons, and colors set in Python all over the place. Changing a theme meant hoping every widget had been told.&lt;/p&gt;

&lt;p&gt;1.5 has a single toolbar, a sidebar in four numbered steps (&lt;strong&gt;What to hide → How to hide it → Fine-tune → Metadata on save&lt;/strong&gt;), effect tiles instead of radio buttons, one primary &lt;strong&gt;Apply effect&lt;/strong&gt; button, and SVG icons that recolor with the theme.&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%2Fraw.githubusercontent.com%2Fkareem2099%2FDotScramble%2Fmain%2Fdocs%2Fscreenshots%2Fhero.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%2Fraw.githubusercontent.com%2Fkareem2099%2FDotScramble%2Fmain%2Fdocs%2Fscreenshots%2Fhero.png" alt="The new DotScramble window: toolbar, four-step sidebar, four faces pixelated" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  One stylesheet, sixteen themes
&lt;/h3&gt;

&lt;p&gt;The rule I set: &lt;strong&gt;widgets never set a color or a font in Python.&lt;/strong&gt; All styling lives in one file, &lt;code&gt;assets/themes/theme_template.qss&lt;/code&gt;, written with tokens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* {key} is a theme color, {key@NN} the same color at NN % opacity,
   {on:key} a readable text color on top of it */&lt;/span&gt;
&lt;span class="nt"&gt;QPushButton&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;variant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"primary"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;&lt;span class="n"&gt;accent_cyan&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="nt"&gt;border&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="err"&gt;1&lt;/span&gt;&lt;span class="nt"&gt;px&lt;/span&gt; &lt;span class="nt"&gt;solid&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;accent_cyan&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="nt"&gt;color&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="py"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;accent_cyan&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="nt"&gt;font-weight&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="err"&gt;700&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;QPushButton&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;variant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"primary"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="nd"&gt;:hover&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;&lt;span class="n"&gt;accent_cyan&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="m"&gt;85&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The theme manager fills the tokens from the active theme in &lt;code&gt;themes.json&lt;/code&gt; with one regex:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;_TOKEN_RE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\{(on:)?([a-z_]+)(?:@(\d{1,3}))?\}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_resolve_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alpha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&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;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="c1"&gt;# pick white or near-black, whichever reads better on this color
&lt;/span&gt;        &lt;span class="n"&gt;white&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_calculate_contrast_ratio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#ffffff&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;black&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_calculate_contrast_ratio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#000000&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#ffffff&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;white&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;black&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#111111&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;alpha&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_hex_to_rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&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;rgba(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alpha&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Widgets only expose hooks: an &lt;code&gt;objectName&lt;/code&gt; for unique pieces (&lt;code&gt;#Toolbar&lt;/code&gt;, &lt;code&gt;#Sidebar&lt;/code&gt;), a &lt;code&gt;variant&lt;/code&gt; property for button flavours, a &lt;code&gt;role&lt;/code&gt; for text styles. &lt;code&gt;{on:key}&lt;/code&gt; turned out to be the most useful token: text on an accent-colored button is readable in all 16 themes without a single per-theme override. The image picker, the metadata dialogs and the batch window, which used to carry their own inline styles, now use the same file, so dialogs finally follow the theme too.&lt;/p&gt;

&lt;h3&gt;
  
  
  A canvas you can trust
&lt;/h3&gt;

&lt;p&gt;The old preview was a scaled label. The new canvas has mouse-wheel zoom around the cursor, pan, fit / 100 %, and two features that change how the app feels:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Detect preview&lt;/strong&gt; (&lt;code&gt;Ctrl+F&lt;/code&gt;) outlines every region before anything is touched. Clicking one toggles it:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;  &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_detection_at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pos&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;index&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_skipped&lt;/span&gt; &lt;span class="o"&gt;^=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;          &lt;span class="c1"&gt;# click again to include it back
&lt;/span&gt;      &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;detectionsChanged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you press Apply, only the regions still outlined are hidden. In a group photo, &lt;em&gt;you&lt;/em&gt; decide who stays visible.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compare&lt;/strong&gt; (&lt;code&gt;Ctrl+Shift+C&lt;/code&gt;) draws the original on one side of a draggable divider and the result on the other, by clipping the "before" image to the divider's position.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Snapping to half the screen
&lt;/h3&gt;

&lt;p&gt;A small bug with a lesson. On GNOME, dragging the window to the screen edge did nothing. The window refused to tile because its &lt;strong&gt;minimum width was 1,095 px&lt;/strong&gt;: the toolbar with all its labels was that wide, and Qt adds up minimum sizes. Half of a 1920 px screen is 960.&lt;/p&gt;

&lt;p&gt;The fix has two parts. The toolbar no longer reports a minimum width (&lt;code&gt;QSizePolicy.Policy.Ignored&lt;/code&gt;), and an event filter switches it to icon-only buttons when there isn't room for the labels:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;toolbar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setSizePolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QSizePolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ignored&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;QSizePolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fixed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_set_toolbar_compact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;compact&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;style&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Qt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToolButtonStyle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToolButtonIconOnly&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;compact&lt;/span&gt;
             &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;Qt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToolButtonStyle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToolButtonTextBesideIcon&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;button&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_labelled_tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setToolButtonStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The window's minimum is now 640 × 480, and it snaps like any other app.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Seven languages, and plurals that are actually right
&lt;/h2&gt;

&lt;p&gt;1.5 adds &lt;strong&gt;Persian&lt;/strong&gt; (right to left), &lt;strong&gt;Spanish&lt;/strong&gt;, &lt;strong&gt;Russian&lt;/strong&gt;, &lt;strong&gt;German&lt;/strong&gt; and &lt;strong&gt;Turkish&lt;/strong&gt; to English and Arabic, and starts in your system language when it's one of them. Every screen is translated, including the factory metadata presets. Those got stable IDs (&lt;code&gt;strip_all&lt;/code&gt;, &lt;code&gt;strip_gps&lt;/code&gt;…) and translated display names, so presets users saved under their own names keep working.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft2bfpc9mebwa9qje46jp.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft2bfpc9mebwa9qje46jp.png" alt="DotScramble in Arabic, right to left, on the light theme" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The interesting part was counting. My first translations dodged numbers ("Processed: 3") because "3 files" is easy in English and wrong almost everywhere else:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Arabic&lt;/strong&gt; has six plural forms: ملف واحد / ملفين / 3 ملفات / 11 ملفًا…&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Russian&lt;/strong&gt; has three: 1 файл / 3 файла / 5 файлов, and 21 is "one" again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persian&lt;/strong&gt; uses the same form for 0 and 1.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So a counted string in the locale files is now a dict of CLDR plural forms, and &lt;code&gt;get(key, count=n)&lt;/code&gt; picks one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"detected"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"one"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="s2"&gt;"{shown} of {count} region will be hidden — click it to skip it"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"other"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{shown} of {count} regions will be hidden — click one to skip it"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rules are small functions, straight from CLDR:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_plural_ar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&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;str&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;n&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;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;zero&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&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="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;one&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;two&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;n&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;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;few&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;n&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;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;many&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;other&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_plural_ru&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&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;str&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;n&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;n&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="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;one&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;n&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;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;few&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;many&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One subtlety: in "3 of 4 regions", the noun agrees with the &lt;strong&gt;total&lt;/strong&gt;, not with the number shown, so the &lt;em&gt;total&lt;/em&gt; has to pick the form. 105 tests now pin the rules and every counted string in every language.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. A privacy tool must never fail silently
&lt;/h2&gt;

&lt;p&gt;This one scared me. Updating dependencies pulled in &lt;strong&gt;OpenCV 5&lt;/strong&gt;, which no longer ships the Haar cascade files that free-tier face detection loads. The detectors returned an empty list, which looks exactly like "no faces found". The result: &lt;em&gt;"0 regions processed"&lt;/em&gt;, a success message, and &lt;strong&gt;the face still visible&lt;/strong&gt; in the saved photo.&lt;/p&gt;

&lt;p&gt;For most apps, "found nothing" is a fine default. For an app whose whole job is hiding faces, it's the worst possible outcome. Now the engine can tell the two apart:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@staticmethod&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;missing_cascades&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Cascade files `mode` needs but that can&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t be loaded. The detect_* methods
    return [] in that case, which is indistinguishable from &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nothing found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:
    callers must check this first so a face is never left visible silently.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;HAAR_CASCADES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;_get_cascade&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The window shows an error before detecting, batch processing marks the file as &lt;strong&gt;failed&lt;/strong&gt; instead of "done", and &lt;code&gt;requirements.txt&lt;/code&gt; pins &lt;code&gt;opencv-contrib-python&amp;lt;5&lt;/code&gt; with a comment explaining why.&lt;/p&gt;

&lt;p&gt;The same principle fixed a few quieter bugs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live preview stacked the effect on itself.&lt;/strong&gt; Each slider move blurred the already-blurred image, so the preview looked stronger than what you'd save. It now always re-applies to the original.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redo worked only once in a row&lt;/strong&gt;: redo saved its result as a new history step, and saving a step clears the redo stack. Redo now saves with &lt;code&gt;clear_redo=False&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A startup error crashed the app&lt;/strong&gt; (a core dump, because the error dialog was built before &lt;code&gt;QApplication&lt;/code&gt; existed) instead of telling you what was wrong.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Activation: "Failed to fetch"
&lt;/h3&gt;

&lt;p&gt;When the DotSuite dashboard moved to &lt;code&gt;dotsuite.dev&lt;/code&gt;, Pro activation broke with a bare &lt;em&gt;"Failed to fetch"&lt;/em&gt; in the browser. Activation works through a small local HTTP server the app runs on &lt;code&gt;127.0.0.1&lt;/code&gt;: the website sends the licence key to it. Two things had changed underneath it. The new origin wasn't in the server's CORS allow-list, and Chrome's &lt;strong&gt;Private Network Access&lt;/strong&gt; now sends a preflight before a public site may talk to &lt;code&gt;localhost&lt;/code&gt;, which the server has to answer explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;origin&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ALLOWED_ORIGINS&lt;/span&gt;
        &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Access-Control-Request-Private-Network&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="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Access-Control-Allow-Private-Network&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;true&lt;/span&gt;&lt;span class="sh"&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 second half of the bug was a 404 from a licence server that had moved. Two behaviour changes came with the fix: a rejected activation now shows the server's reason instead of a status code, and a Pro licence is no longer removed when the server simply can't be reached. Only an explicit rejection revokes it.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Shipping it: four bugs only a clean machine found
&lt;/h2&gt;

&lt;p&gt;Until 1.4, releases were built on my laptop. That's how 1.4.1 ended up needing a much newer glibc than most users have: PyInstaller binaries need a glibc at least as new as the machine that built them.&lt;/p&gt;

&lt;p&gt;In 1.5, pushing a tag runs one workflow on &lt;strong&gt;Ubuntu 22.04&lt;/strong&gt; (glibc 2.35): tests → build the AppImage, &lt;code&gt;.deb&lt;/code&gt;, Arch package and tarball → inspect the binaries → smoke-test the AppImage → write &lt;code&gt;SHA256SUMS&lt;/code&gt; → GPG-sign every file → publish. The built-in updater installs an update only after the checksum &lt;strong&gt;and&lt;/strong&gt; the signature check out against a pinned key fingerprint. And &lt;code&gt;version_policy.json&lt;/code&gt; in the repo lets an old, broken version show an "Update required" screen with a download button. It never blocks offline use.&lt;/p&gt;

&lt;p&gt;That pipeline was green from day one. Then real machines found four bugs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 1: Tesseract crashed, but only in the packages
&lt;/h3&gt;

&lt;p&gt;Text and single-word detection said &lt;em&gt;"Tesseract not found"&lt;/em&gt; in the AppImage and the &lt;code&gt;.deb&lt;/code&gt;, yet &lt;code&gt;tesseract&lt;/code&gt; was installed. Running it with the bundle's library path showed why:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tesseract: /tmp/_MEI…/libstdc++.so.6: version `GLIBCXX_3.4.32' not found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PyInstaller's bootloader points &lt;code&gt;LD_LIBRARY_PATH&lt;/code&gt; at the bundle so the app finds its own libraries. Every program the app &lt;em&gt;starts&lt;/em&gt; inherits that, so the system Tesseract loaded the bundle's older &lt;code&gt;libstdc++&lt;/code&gt; from Ubuntu 22.04 and died. The dynamic loader reads the variable once at startup, so the app can safely put the user's value back for its children:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;restore_child_library_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;environ&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;environ&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;environ&lt;/span&gt;
    &lt;span class="n"&gt;frozen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;frozen&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&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;frozen&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;frozen&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;frozen&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;linux&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="n"&gt;original&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LD_LIBRARY_PATH_ORIG&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# saved by the bootloader
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;original&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LD_LIBRARY_PATH&lt;/span&gt;&lt;span class="sh"&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;original&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LD_LIBRARY_PATH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That was 1.5.1.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 2: the AppImage couldn't open a window
&lt;/h3&gt;

&lt;p&gt;I submitted DotScramble to &lt;a href="https://www.appimagehub.com/p/2362477/" rel="noopener noreferrer"&gt;AppImageHub&lt;/a&gt;, whose test runs your AppImage on a clean Ubuntu 22.04 and takes a screenshot. It failed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cannot load library …/platforms/libqxcb.so: libxkbcommon-x11.so.0: cannot open shared object file
This application failed to start because no Qt platform plugin could be initialized.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Qt's X11 plugin needs five libraries (&lt;code&gt;libxkbcommon-x11&lt;/code&gt;, &lt;code&gt;libxcb-icccm&lt;/code&gt;, &lt;code&gt;libxcb-keysyms&lt;/code&gt;, &lt;code&gt;libxcb-shape&lt;/code&gt;, &lt;code&gt;libxcb-xkb&lt;/code&gt;) that PyInstaller only bundles if they're installed &lt;strong&gt;on the build machine&lt;/strong&gt;. They weren't, PyInstaller skipped them with a warning nobody read, and my smoke test passed because it ran Qt with the &lt;code&gt;offscreen&lt;/code&gt; platform, which never loads the X11 plugin. On my desktop, those libraries happen to be installed, so I never saw it.&lt;/p&gt;

&lt;p&gt;I reproduced their environment in Docker before changing anything: same failure, then &lt;code&gt;Application initialized successfully&lt;/code&gt; once those five libraries were present. The fix was to install them on the builder. The real change was in the workflow: it now &lt;strong&gt;lists every library the X11 plugin links&lt;/strong&gt; (&lt;code&gt;readelf -d&lt;/code&gt;) and fails if one isn't in the binary, and it &lt;strong&gt;runs the AppImage on X11 in a clean &lt;code&gt;ubuntu:22.04&lt;/code&gt; container&lt;/strong&gt; before publishing. Both checks failed on the old build and passed on 1.5.4, and AppImageHub's test turned green.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 3: the check that almost broke the AppImage
&lt;/h3&gt;

&lt;p&gt;To verify the AppImage's embedded update information (so AppImageUpdate and Gear Lever can update it), my first draft of the check read the ELF section with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;objcopy &lt;span class="nt"&gt;--dump-section&lt;/span&gt; .upd_info&lt;span class="o"&gt;=&lt;/span&gt;/dev/stdout DotScramble-Linux-x86_64.AppImage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With no output file, &lt;code&gt;objcopy&lt;/code&gt; &lt;strong&gt;rewrites the input in place&lt;/strong&gt;, and it drops everything after the ELF part, which in an AppImage is the entire SquashFS image. I only noticed because the next step, extracting the AppImage, failed locally with &lt;em&gt;"This doesn't look like a squashfs image"&lt;/em&gt;. In CI it would have published a broken file. &lt;code&gt;readelf --string-dump=.upd_info&lt;/code&gt; only reads.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 4: right metadata, wrong file name
&lt;/h3&gt;

&lt;p&gt;AppImageHub builds its listing from the AppStream file inside the AppImage. I shipped it under the current AppStream name, &lt;code&gt;….metainfo.xml&lt;/code&gt;. Their scripts only read &lt;code&gt;*.appdata.xml&lt;/code&gt; for the summary and description, so the listing would have been blank. Same content, different name: 1.5.3.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bugs I Hit and What They Actually Were
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Actual cause&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Window won't snap to half the screen&lt;/td&gt;
&lt;td&gt;toolbar's labels made the minimum width 1,095 px&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"0 regions processed", face still visible&lt;/td&gt;
&lt;td&gt;OpenCV 5 dropped the Haar cascade files; empty list looked like "no faces"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Preview stronger than the saved image&lt;/td&gt;
&lt;td&gt;live preview re-applied the effect to its own output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Failed to fetch" on activation&lt;/td&gt;
&lt;td&gt;new origin missing from CORS, plus Chrome's Private Network Access preflight&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Tesseract not found" in packages only&lt;/td&gt;
&lt;td&gt;children inherited the bundle's &lt;code&gt;LD_LIBRARY_PATH&lt;/code&gt; and its older &lt;code&gt;libstdc++&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AppImage quits on a clean system&lt;/td&gt;
&lt;td&gt;five X11 libraries missing from the build; smoke test ran &lt;code&gt;offscreen&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AppImage corrupted by its own check&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;objcopy&lt;/code&gt; without an output file rewrites the input&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Honest Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Still &lt;strong&gt;Linux x86_64 only&lt;/strong&gt;, glibc 2.35+. No Windows or macOS builds yet.&lt;/li&gt;
&lt;li&gt;The five new translations are new. They follow CLDR, but a native speaker will find phrasing to fix, and corrections are very welcome.&lt;/li&gt;
&lt;li&gt;The theme system covers Qt widgets. The canvas overlays read colors through &lt;code&gt;qproperty-*&lt;/code&gt; hooks, so a theme can't yet change, say, the detection outline's thickness.&lt;/li&gt;
&lt;li&gt;AI Evasion works as before: slow on the CPU, and tested against MediaPipe only.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One stylesheet with tokens beats colors scattered across Python.&lt;/strong&gt; &lt;code&gt;{on:key}&lt;/code&gt; alone removed a whole class of unreadable-button bugs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plurals are data, not string formatting.&lt;/strong&gt; Six Arabic forms don't fit in an f-string.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;In a privacy tool, "found nothing" and "couldn't look" must never look the same.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your CI is a machine you configured.&lt;/strong&gt; Test on one you didn't: a clean container found two bugs that every green CI run had missed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Read the tool's man page before pointing it at your release artifact.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;DotScramble 1.5.4 is out: AppImage, &lt;code&gt;.deb&lt;/code&gt;, Arch package and tarball, all GPG-signed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/kareem2099/DotScramble" rel="noopener noreferrer"&gt;kareem2099/DotScramble&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Download:&lt;/strong&gt; &lt;a href="https://github.com/kareem2099/DotScramble/releases/latest" rel="noopener noreferrer"&gt;latest release&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Also on:&lt;/strong&gt; &lt;a href="https://www.appimagehub.com/p/2362477/" rel="noopener noreferrer"&gt;AppImageHub&lt;/a&gt; · &lt;a href="https://www.opendesktop.org/p/2362477/" rel="noopener noreferrer"&gt;OpenDesktop&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project page:&lt;/strong&gt; &lt;a href="https://dotsuite.dev/en/product/dotscramble" rel="noopener noreferrer"&gt;dotsuite.dev/en/product/dotscramble&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>pyside6</category>
      <category>linux</category>
      <category>opensource</category>
    </item>
    <item>
      <title>8 backends, one engine, and the SVG bug that started it all</title>
      <dc:creator>freerave</dc:creator>
      <pubDate>Fri, 30 Oct 2026 14:28:36 +0000</pubDate>
      <link>https://dev.to/freerave/8-backends-one-engine-and-the-svg-bug-that-started-it-all-3ooo</link>
      <guid>https://dev.to/freerave/8-backends-one-engine-and-the-svg-bug-that-started-it-all-3ooo</guid>
      <description>&lt;p&gt;How DotMorph turns Pillow, librsvg, poppler, Ghostscript, ImageMagick, ffmpeg, LibreOffice and pandoc into one conversion engine, and why its SVG rendering moved from ImageMagick to librsvg over ctypes.&lt;/p&gt;




&lt;p&gt;In &lt;a href="https://dev.to/freerave/i-built-a-radial-menu-that-converts-any-file-on-linux-4c4m"&gt;part 1&lt;/a&gt; I introduced DotMorph, a radial-menu file converter for Linux. This post is about the part you don't see: the engine that decides &lt;em&gt;how&lt;/em&gt; a file gets converted, and the bug that made me rewrite it.&lt;/p&gt;

&lt;h2&gt;
  
  
  It started with a broken PNG
&lt;/h2&gt;

&lt;p&gt;The first version of DotMorph sent most image work to ImageMagick. Then I converted an SVG logo to PNG and got this (left):&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuhkrz50tgorjth5eevl7.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuhkrz50tgorjth5eevl7.png" alt="The same SVG rendered by ImageMagick's MSVG renderer (gradients, dashes, clip-path and text broken) and by librsvg (correct)" width="799" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Same file, two renderers. ImageMagick's built-in SVG renderer (MSVG) dropped the gradients, the dashed stroke and the clip-path, spread &lt;code&gt;letter-spacing&lt;/code&gt; across the whole word and stacked a &lt;code&gt;&amp;lt;tspan&amp;gt;&lt;/code&gt; on top of its line. On the logo that started this, I measured the damage against Chromium's rendering of the same file: a &lt;strong&gt;mean pixel difference of 8.09&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The fix was obvious once I said it out loud: every GNOME desktop already ships the renderer GNOME itself uses, &lt;strong&gt;librsvg&lt;/strong&gt;. After switching, the difference against Chromium dropped to &lt;strong&gt;0.43&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But "just call a different tool for SVG" exposed the real problem: the app was a pile of &lt;code&gt;if extension == ...&lt;/code&gt; branches. Adding a renderer meant touching everything. So I rebuilt the engine around one idea.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backends declare, the registry decides
&lt;/h2&gt;

&lt;p&gt;Every converter is a &lt;code&gt;Backend&lt;/code&gt;. It doesn't get called by name; it &lt;em&gt;declares&lt;/em&gt; what it needs and what it can do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Requirement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;  &lt;span class="c1"&gt;# executable on PATH, "py:module" (Python package) or "lib:name" (shared library)
&lt;/span&gt;    &lt;span class="n"&gt;packages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# package manager -&amp;gt; package
&lt;/span&gt;    &lt;span class="n"&gt;purpose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;present&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&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;bool&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;py:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;importlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;util&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_spec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:])&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lib:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ctypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;util&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_library&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:])&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;shutil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;which&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A backend lists its requirements (&lt;code&gt;ffmpeg&lt;/code&gt;, &lt;code&gt;py:PIL&lt;/code&gt;, &lt;code&gt;lib:rsvg-2&lt;/code&gt;…) and returns the pairs it can handle right now, as &lt;code&gt;{source format: {targets}}&lt;/code&gt;. The registry builds the live table from whatever is installed at this moment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@lru_cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxsize&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_table&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;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&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;Backend&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&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;Backend&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;backend&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;backends&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;pairs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# a broken tool must never take the whole app down
&lt;/span&gt;            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actions&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;slot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&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;action&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;backend&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things fall out of this for free:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Priority is just order.&lt;/strong&gt; Backends are registered best-first (Pillow, poppler, Ghostscript, librsvg, ImageMagick, ffmpeg, LibreOffice, pandoc), and &lt;code&gt;setdefault&lt;/code&gt; means the first one that can do a conversion owns it. SVG → PNG goes to librsvg; ImageMagick still handles PSD, XCF, HEIC and JPEG XL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The menu never lies.&lt;/strong&gt; The wheel shows &lt;code&gt;actions_for(format)&lt;/code&gt;, so a conversion only appears if a backend can do it on &lt;em&gt;this&lt;/em&gt; machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing tools become advice.&lt;/strong&gt; Each requirement knows its package name per package manager, so &lt;code&gt;dotmorph doctor&lt;/code&gt; and the error messages can say &lt;code&gt;sudo dnf install poppler-utils&lt;/code&gt; instead of "command not found". &lt;code&gt;formats --missing&lt;/code&gt; even tells you how many conversions installing a tool would unlock.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On my machine that table holds 440+ conversions across 53 formats. On a minimal install it might be 60, and that's fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  One conversion, start to finish
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgryiehtdpj6zvbcs2xa4.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgryiehtdpj6zvbcs2xa4.png" alt="The five steps of a conversion: detect, plan, workdir, run, finalize" width="800" height="286"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Whether it comes from the wheel, the CLI, a file-manager quick action or a watched folder, every conversion goes through the same path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Safe output
&lt;/h3&gt;

&lt;p&gt;The rule: &lt;strong&gt;you never see a half-written file, and nothing gets overwritten.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@contextmanager&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;workdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&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;Iterator&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Hidden scratch dir inside the output dir: same filesystem, so the final move is a
    rename, big videos don&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t fill a tmpfs /tmp, and half-written files never show up.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TemporaryDirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.dotmorph-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;finalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;overwrite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&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;Path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Move a finished temp file to its final name. The name is picked under a lock so
    parallel jobs never claim the same &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name (1).ext&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="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;_finalize_lock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;final&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;overwrite&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;shutil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;move&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;tmp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;final&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scratch folder lives &lt;em&gt;next to the output&lt;/em&gt;, not in &lt;code&gt;/tmp&lt;/code&gt;. That keeps the final move a cheap rename on the same filesystem, and a 4 GB video doesn't fill a RAM-backed &lt;code&gt;/tmp&lt;/code&gt;. Batches run in parallel, so picking &lt;code&gt;photo (1).jpg&lt;/code&gt; happens under a lock. Otherwise two jobs can both decide that name is free.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cancel that actually cancels
&lt;/h3&gt;

&lt;p&gt;Clicking the centre of the wheel cancels. That sounds trivial until the tool you're running has children: LibreOffice spawns helpers, and ffmpeg flushes its encoder for seconds after SIGTERM. Every tool starts in its own session (&lt;code&gt;start_new_session=True&lt;/code&gt;), and cancel kills the whole process group:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_kill_group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Popen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Partial output is thrown away anyway, so don't wait long for a graceful exit
&lt;/span&gt;    &lt;span class="c1"&gt;# (ffmpeg flushing its encoder on SIGTERM can take seconds).
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;grace&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SIGTERM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SIGKILL&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="k"&gt;try&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;killpg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proc&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;sig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ProcessLookupError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;PermissionError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;proc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;grace&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TimeoutExpired&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cancel went from about 2 seconds to about 0.5.&lt;/p&gt;

&lt;h3&gt;
  
  
  Honest progress
&lt;/h3&gt;

&lt;p&gt;ffmpeg reports progress with &lt;code&gt;-progress pipe:1&lt;/code&gt;, and poppler gets one tick per rendered page. Batches count files. Some jobs can't report anything, and one of them taught me to stop faking it. Making a GIF runs a palette pass that reads the whole video &lt;em&gt;before&lt;/em&gt; writing a single frame, so the bar sat at 0% the entire time. Now, when a tool can't report progress, the wheel shows &lt;strong&gt;elapsed seconds&lt;/strong&gt; instead of a fake percentage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Small things that matter
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stream copy first.&lt;/strong&gt; MOV → MP4 with the same codec is a remux: seconds instead of minutes, and lossless. If the container refuses the codec, it falls back to a transcode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encoders are probed&lt;/strong&gt;, not assumed: libx264 → openh264 → mpeg4, VP9 → VP8. Distros ship very different ffmpeg builds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;compress&lt;/code&gt; never makes things bigger.&lt;/strong&gt; If the "compressed" file is larger, you get the original back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LibreOffice gets a private profile&lt;/strong&gt;, so a conversion works while you have LibreOffice open, and those jobs are serialised automatically.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Back to SVG: librsvg without a new dependency
&lt;/h2&gt;

&lt;p&gt;I didn't want to add a Python binding (and its build headaches) just for SVG. librsvg and cairo are already on practically every Linux desktop, so DotMorph calls them directly with &lt;code&gt;ctypes&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_Libs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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;ctypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CDLL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ctypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CDLL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;util&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_library&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="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rsvg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rsvg-2&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;librsvg-2.so.2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cairo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cairo&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;libcairo.so.2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rsvg&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rsvg_handle_new_from_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;restype&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vp&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rsvg_handle_new_from_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argtypes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ctypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c_char_p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;perr&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rsvg_handle_render_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;restype&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rsvg_handle_render_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argtypes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;vp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;POINTER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_Rect&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;perr&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Declaring &lt;code&gt;restype&lt;/code&gt; and &lt;code&gt;argtypes&lt;/code&gt; for every function isn't optional. Without them ctypes assumes &lt;code&gt;int&lt;/code&gt;, and on 64-bit a pointer gets silently truncated. That gives you crashes that only show up sometimes.&lt;/p&gt;

&lt;p&gt;Two bonuses came with it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vector PDF.&lt;/strong&gt; Rendering onto a cairo &lt;em&gt;PDF surface&lt;/em&gt; instead of an image surface gives a real vector PDF. Text stays selectable text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The right size.&lt;/strong&gt; The old path rendered SVGs 1.56× larger than their declared size, because it reused the 150 dpi default meant for PDF pages (150 / 96 = 1.56). SVG now has its own default of 96 dpi, so a 480×300 SVG becomes a 480×300 PNG. &lt;code&gt;--dpi 192&lt;/code&gt; gives you 2×.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the backend's requirement is simply &lt;code&gt;lib:rsvg-2&lt;/code&gt;. If it's missing, ImageMagick quietly takes over SVG again, because it's next in line in the registry.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell myself at the start
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Make capabilities data, not code paths.&lt;/strong&gt; Once backends declared what they do, the menu, the CLI, &lt;code&gt;doctor&lt;/code&gt;, the shell completions and the docs all came from the same table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compare against a reference.&lt;/strong&gt; A screenshot showed the SVG bug; a number (8.09 → 0.43 against Chromium) proved the fix and keeps "looks fine to me" out of the discussion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't fake progress.&lt;/strong&gt; An honest timer beats a frozen percentage.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In &lt;a href="https://dev.to/freerave/making-a-file-converter-feel-alive-with-qml-5gl6"&gt;part 3&lt;/a&gt; we go to the other side: the QML wheel, GPU-drawn ring sectors, particles, and how a daemon makes it open in 0.1 s.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/kareem2099/DotMorph" rel="noopener noreferrer"&gt;kareem2099/DotMorph&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project page:&lt;/strong&gt; &lt;a href="https://dotsuite.dev/en/product/dotmorph" rel="noopener noreferrer"&gt;dotsuite.dev/en/product/dotmorph&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>linux</category>
      <category>opensource</category>
      <category>architecture</category>
    </item>
    <item>
      <title>I built a Linux app that hides faces, license plates and GPS from your photos</title>
      <dc:creator>freerave</dc:creator>
      <pubDate>Wed, 28 Oct 2026 12:28:51 +0000</pubDate>
      <link>https://dev.to/freerave/i-built-a-linux-app-that-hides-faces-license-plates-and-gps-from-your-photos-3h97</link>
      <guid>https://dev.to/freerave/i-built-a-linux-app-that-hides-faces-license-plates-and-gps-from-your-photos-3h97</guid>
      <description>&lt;h2&gt;
  
  
  DotScramble: blur, pixelate or black-bar faces, plates and text, and strip the EXIF/GPS hidden in the file, before you share a photo. Local, open source, Linux.
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq5ltxy40mkhyzv9olljm.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq5ltxy40mkhyzv9olljm.gif" alt="DotScramble comparing a group photo before and after: the divider sweeps across and the four faces are pixelated" width="720" height="720"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Open a photo. Pick what to hide. Compare. Save.&lt;/p&gt;

&lt;p&gt;That is &lt;strong&gt;DotScramble&lt;/strong&gt;, a photo privacy studio for Linux. It blurs, pixelates or black-bars &lt;strong&gt;faces, eyes, license plates and text&lt;/strong&gt;, and on save it strips or replaces the &lt;strong&gt;EXIF and GPS data&lt;/strong&gt; hidden in the file. Everything runs on your machine. It's open source (Apache 2.0) and part of &lt;a href="https://dotsuite.dev/en" rel="noopener noreferrer"&gt;DotSuite&lt;/a&gt;, my collection of developer tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why?
&lt;/h2&gt;

&lt;p&gt;A photo you're about to post usually shows more than you meant to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the faces of people who never agreed to be online,&lt;/li&gt;
&lt;li&gt;a car's number plate, a name on a badge, an address on a parcel,&lt;/li&gt;
&lt;li&gt;and, invisible in the picture itself, the &lt;strong&gt;exact GPS location&lt;/strong&gt;, the phone model and the time it was taken.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The usual options on Linux are a full image editor (select every face by hand, then remember the metadata is still there) or an online "blur faces" site, which means uploading the very photo you're trying to protect. I wanted one small app that does the whole job locally, shows you exactly what it's about to hide, and is honest when it can't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it hides, and how
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What:&lt;/strong&gt; faces, eyes, full bodies, license plates, any text it can read (OCR), &lt;strong&gt;one specific word&lt;/strong&gt; (a name, a phone number), regions you draw by hand, or the whole image.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How:&lt;/strong&gt; seven effects (blur, pixelation, black bar, gradient, mosaic, frosted glass, oil paint) with strength, block size and opacity controls.&lt;/p&gt;

&lt;p&gt;Two things make it feel safe to use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Detect preview.&lt;/strong&gt; Before anything changes, every region it found is outlined. Click one to skip it, so with several people in a photo you protect exactly the ones you choose. The status bar says &lt;em&gt;"3 of 4 regions will be hidden"&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compare.&lt;/strong&gt; A before / after split view with a draggable divider (that's the GIF at the top).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkf6rrz6cg6tkonmgs2yq.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkf6rrz6cg6tkonmgs2yq.gif" alt="Detect outlines the faces, Pixelation is picked, Apply hides them" width="600" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It also refuses to pretend. If detection can't run (say, OpenCV's cascade files are missing), you get an error, never a "done" with the face still visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The metadata you can't see
&lt;/h2&gt;

&lt;p&gt;Every photo from a phone carries EXIF data: GPS coordinates, camera make and model, timestamps, sometimes a device fingerprint. DotScramble handles it on save:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep&lt;/strong&gt; it,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strip&lt;/strong&gt; GPS, camera and device tags,&lt;/li&gt;
&lt;li&gt;or replace them with believable &lt;strong&gt;decoys&lt;/strong&gt; (three presets, or set each field yourself).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before the file is written, a report lists every field that will be kept, removed or replaced, so there are no surprises.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI Evasion
&lt;/h2&gt;

&lt;p&gt;A blurred face is unreadable to people, but face &lt;em&gt;detectors&lt;/em&gt; can often still find it, and a face software can find can be indexed and matched. &lt;strong&gt;AI Evasion&lt;/strong&gt; adds a small, optimised pattern of noise to the face region (a black-box SPSA optimisation against the detector) until the detector no longer finds a face there.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid&lt;/strong&gt; blurs the face for people &lt;em&gt;and&lt;/em&gt; hides it from the detector.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invisible&lt;/strong&gt; leaves the face looking the same, and only fools the detector.&lt;/li&gt;
&lt;li&gt;It takes about 30–90 seconds per face on the CPU, and it reports the result honestly: complete, partial (with the evasion rate) or failed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To be clear about the limits: the pattern is tuned against the detector DotScramble itself uses (MediaPipe). It is &lt;strong&gt;not&lt;/strong&gt; a guarantee against every face recognition system, so treat it as an extra layer on top of a visible effect, not a replacement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Also in the box
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Batch processing&lt;/strong&gt; for whole folders, with a per-file error report.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zoom and pan&lt;/strong&gt;, live preview, presets, undo and redo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;7 languages&lt;/strong&gt;: English, العربية, فارسی, Español, Русский, Deutsch and Türkçe, with right-to-left layouts and correct plural forms in every one of them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;16 themes&lt;/strong&gt;; every dialog follows the one you pick.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signed updates&lt;/strong&gt;: the built-in updater checks the checksum &lt;em&gt;and&lt;/em&gt; the GPG signature before installing anything.&lt;/li&gt;
&lt;/ul&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft2bfpc9mebwa9qje46jp.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft2bfpc9mebwa9qje46jp.png" alt="DotScramble in Arabic, right to left, on the light theme" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Free and Pro
&lt;/h2&gt;

&lt;p&gt;Everything you need to hide faces is free: all seven effects, Detect preview, Compare, zoom, face and eye detection (OpenCV Haar cascades), manual regions and batches of three images.&lt;/p&gt;

&lt;p&gt;The optional &lt;strong&gt;Pro&lt;/strong&gt; plan (DotScramble Max on &lt;a href="https://dotsuite.dev/en/product/dotscramble" rel="noopener noreferrer"&gt;dotsuite.dev&lt;/a&gt;) adds AI face ovals and eye bars (MediaPipe), full bodies, license plates, text and single-word detection, AI Evasion, the metadata tools and unlimited batches.&lt;/p&gt;

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

&lt;p&gt;Grab one of these from the &lt;a href="https://github.com/kareem2099/DotScramble/releases/latest" rel="noopener noreferrer"&gt;releases page&lt;/a&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;# AppImage: any distribution, no install&lt;/span&gt;
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x DotScramble-Linux-x86_64.AppImage &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./DotScramble-Linux-x86_64.AppImage

&lt;span class="c"&gt;# Debian, Ubuntu, Kali, Mint&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; ./DotScramble-Linux-x86_64.deb

&lt;span class="c"&gt;# Arch, Manjaro&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;pacman &lt;span class="nt"&gt;-U&lt;/span&gt; DotScramble-Linux-x86_64.pkg.tar.zst

&lt;span class="c"&gt;# optional, for the text and word modes&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;tesseract-ocr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also a portable &lt;code&gt;.tar.gz&lt;/code&gt;, and it runs from source with Python 3.10+. Every release ships &lt;code&gt;SHA256SUMS&lt;/code&gt; and a detached GPG signature for each package, so you can check what you downloaded:&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;sha256sum&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; SHA256SUMS &lt;span class="nt"&gt;--ignore-missing&lt;/span&gt;
gpg &lt;span class="nt"&gt;--import&lt;/span&gt; DotScramble-signing-key.asc
gpg &lt;span class="nt"&gt;--verify&lt;/span&gt; DotScramble-Linux-x86_64.AppImage.asc DotScramble-Linux-x86_64.AppImage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DotScramble is also on &lt;a href="https://www.appimagehub.com/p/2362477/" rel="noopener noreferrer"&gt;AppImageHub&lt;/a&gt; and &lt;a href="https://www.opendesktop.org/p/2362477/" rel="noopener noreferrer"&gt;OpenDesktop&lt;/a&gt;. The AppImage carries update information, so AppImageUpdate and Gear Lever can update it too.&lt;/p&gt;

&lt;h2&gt;
  
  
  What goes online
&lt;/h2&gt;

&lt;p&gt;Your photos never leave your computer. The app only connects to GitHub (to check for updates and the supported-version policy), to dotsuite.dev (to activate or re-check Pro), and, the first time you use a Pro AI feature and only if you agree, to Google's storage to download the MediaPipe face model (about 8 MB).&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Linux x86_64 only&lt;/strong&gt;, and the packages need &lt;strong&gt;glibc 2.35+&lt;/strong&gt; (Ubuntu 22.04, Debian 12, Fedora 36 and newer). Older systems can run it from source. No Windows or macOS builds yet.&lt;/li&gt;
&lt;li&gt;The text and word modes need &lt;strong&gt;Tesseract&lt;/strong&gt; installed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Evasion is slow&lt;/strong&gt; (30–90 s per face on the CPU) and is tested against one detector, not all of them.&lt;/li&gt;
&lt;li&gt;Save AI-evaded images as &lt;strong&gt;PNG&lt;/strong&gt;: JPEG compression can weaken the pattern.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;If you try it, I'd love to know what you hide most, and what's missing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/kareem2099/DotScramble" rel="noopener noreferrer"&gt;kareem2099/DotScramble&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project page:&lt;/strong&gt; &lt;a href="https://dotsuite.dev/en/product/dotscramble" rel="noopener noreferrer"&gt;dotsuite.dev/en/product/dotscramble&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Also on:&lt;/strong&gt; &lt;a href="https://www.appimagehub.com/p/2362477/" rel="noopener noreferrer"&gt;AppImageHub&lt;/a&gt; · &lt;a href="https://www.opendesktop.org/p/2362477/" rel="noopener noreferrer"&gt;OpenDesktop&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>privacy</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I built a radial menu that converts any file on Linux</title>
      <dc:creator>freerave</dc:creator>
      <pubDate>Sat, 24 Oct 2026 10:31:27 +0000</pubDate>
      <link>https://dev.to/freerave/i-built-a-radial-menu-that-converts-any-file-on-linux-4c4m</link>
      <guid>https://dev.to/freerave/i-built-a-radial-menu-that-converts-any-file-on-linux-4c4m</guid>
      <description>&lt;h2&gt;
  
  
  DotMorph: right-click a file, a wheel blooms, pick a format. 440+ local conversions for images, PDF, SVG, video, audio and Office files, from your file manager, a hotkey or a screen edge.
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8vcd8odij1dyin5io0o2.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8vcd8odij1dyin5io0o2.gif" alt="The DotMorph wheel opening on a video, blooming the Video fan, converting to GIF and finishing with a particle burst" width="720" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Right-click a file. A wheel blooms around it. Pick a format. Done.&lt;/p&gt;

&lt;p&gt;That is the whole idea behind &lt;strong&gt;DotMorph&lt;/strong&gt;, a file converter for Linux that lives in a radial menu instead of a window. It does 440+ conversions (images, PDF, SVG, video, audio, Office documents, Markdown) and everything happens on your machine. It's free, open source (Apache 2.0) and part of &lt;a href="https://dotsuite.dev/en" rel="noopener noreferrer"&gt;DotSuite&lt;/a&gt;, my collection of developer tools.&lt;/p&gt;

&lt;p&gt;This is the first of four posts. This one is the tour; the next three are about how it's built.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why another converter?
&lt;/h2&gt;

&lt;p&gt;Converting a file on Linux usually goes one of three ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You remember (or search for) the right &lt;code&gt;ffmpeg&lt;/code&gt; / &lt;code&gt;magick&lt;/code&gt; / &lt;code&gt;libreoffice --headless&lt;/code&gt; incantation.&lt;/li&gt;
&lt;li&gt;You install a big GUI app, open it, find the file again, pick a format from a dropdown.&lt;/li&gt;
&lt;li&gt;You upload your file to some random website and hope it isn't keeping a copy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On Windows there's a nice pattern for this: converters that sit in the right-click menu. I wanted that on Linux, and I wanted it to feel like a &lt;em&gt;tool&lt;/em&gt;, not a form. So: a wheel that appears where you are, shows only what makes sense for the file you picked, and gets out of the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it converts
&lt;/h2&gt;

&lt;p&gt;DotMorph doesn't reimplement codecs. It drives the tools that are already the best at each job, and only offers what your machine can actually do:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Files&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;th&gt;Done by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Images&lt;/td&gt;
&lt;td&gt;PNG, JPG, WebP, AVIF, HEIC, GIF, TIFF, ICO, PSD, XCF, JPEG XL&lt;/td&gt;
&lt;td&gt;Pillow, ImageMagick&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SVG&lt;/td&gt;
&lt;td&gt;→ PNG, WebP, … and &lt;strong&gt;vector PDF&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;librsvg (via ctypes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PDF&lt;/td&gt;
&lt;td&gt;→ images (one per page), → text, compress, merge&lt;/td&gt;
&lt;td&gt;poppler, Ghostscript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Video&lt;/td&gt;
&lt;td&gt;MP4, MKV, WebM, MOV, AVI, … ↔ each other, → GIF, → audio&lt;/td&gt;
&lt;td&gt;ffmpeg&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audio&lt;/td&gt;
&lt;td&gt;MP3, M4A, OGG, Opus, FLAC, WAV, AAC&lt;/td&gt;
&lt;td&gt;ffmpeg&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Documents&lt;/td&gt;
&lt;td&gt;DOCX, ODT, PPTX, XLSX, … → PDF and between Office formats&lt;/td&gt;
&lt;td&gt;LibreOffice (headless)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Text&lt;/td&gt;
&lt;td&gt;Markdown, HTML, reST, Org, EPUB ↔ DOCX, ODT, EPUB, PDF&lt;/td&gt;
&lt;td&gt;pandoc&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Plus tools that keep the format: &lt;strong&gt;compress&lt;/strong&gt; (never returns a bigger file), &lt;strong&gt;resize&lt;/strong&gt;, &lt;strong&gt;rotate&lt;/strong&gt;, &lt;strong&gt;strip metadata&lt;/strong&gt; (EXIF, GPS, tags), &lt;strong&gt;mute&lt;/strong&gt; a video, &lt;strong&gt;merge&lt;/strong&gt; images and PDFs into one PDF.&lt;/p&gt;

&lt;p&gt;On a machine with everything installed that's 53 formats, 6 tools and 440+ conversions. If a tool is missing, its conversions simply don't show up, and &lt;code&gt;dotmorph doctor&lt;/code&gt; tells you which package to install for apt, dnf, pacman or zypper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Many ways in, one wheel
&lt;/h2&gt;

&lt;p&gt;The wheel opens from wherever you happen to be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;File manager right-click&lt;/strong&gt;: Nautilus, Thunar, Dolphin and Nemo, with a &lt;em&gt;DotMorph&lt;/em&gt; submenu and quick actions (To PNG, To PDF, To MP4, Compress…) that run without any window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open with DotMorph&lt;/strong&gt; in any app that lists launchers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A global hotkey&lt;/strong&gt; (Super+Shift+M): copy files in your file manager, press it, the wheel opens with those files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The edge dock&lt;/strong&gt;: drag the wheel against the side of the screen and it turns into a slim tab. Hover it and half a wheel slides out, like Android's edge panels. Drop files on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watched folders&lt;/strong&gt;: &lt;code&gt;dotmorph watch ~/Downloads --to webp&lt;/code&gt; converts new files as they land (inotify, run as a systemd user service).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The terminal&lt;/strong&gt;, for everything above and more.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu209dmjm1ua6nm56hjsd.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu209dmjm1ua6nm56hjsd.gif" alt="Dragging the wheel to the left edge: it docks as a tab, then opens as a half wheel on hover" width="760" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks to a small background daemon, the wheel opens in about &lt;strong&gt;0.1 s&lt;/strong&gt; instead of the ~1 s it takes to start Qt. Post 3 is about how that works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The command line is a first-class citizen
&lt;/h2&gt;

&lt;p&gt;Everything the wheel does, the CLI does too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotmorph convert holiday/&lt;span class="k"&gt;*&lt;/span&gt;.heic &lt;span class="nt"&gt;--to&lt;/span&gt; jpg              &lt;span class="c"&gt;# batch, in parallel&lt;/span&gt;
dotmorph convert talk.mov &lt;span class="nt"&gt;--to&lt;/span&gt; mp4                    &lt;span class="c"&gt;# same codec? stream copy, seconds&lt;/span&gt;
dotmorph convert report.docx slides.pptx &lt;span class="nt"&gt;--to&lt;/span&gt; pdf
dotmorph convert photos/&lt;span class="k"&gt;*&lt;/span&gt;.jpg &lt;span class="nt"&gt;--preset&lt;/span&gt; web            &lt;span class="c"&gt;# WebP, ≤1920 px, quality 80, no metadata&lt;/span&gt;
dotmorph convert scan.pdf &lt;span class="nt"&gt;--to&lt;/span&gt; compress
dotmorph convert diagram.svg &lt;span class="nt"&gt;--to&lt;/span&gt; pdf                 &lt;span class="c"&gt;# stays vector&lt;/span&gt;
dotmorph formats video.mp4                            &lt;span class="c"&gt;# what can this file become?&lt;/span&gt;
dotmorph doctor                                       &lt;span class="c"&gt;# what's installed, what's missing&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get a live progress bar, Ctrl-C cancels cleanly, existing files are never overwritten (you get &lt;code&gt;name (1).ext&lt;/code&gt;), and half-written files never appear because every job works in a hidden folder and finishes with a rename.&lt;/p&gt;

&lt;h2&gt;
  
  
  Settings, in English and Arabic
&lt;/h2&gt;

&lt;p&gt;There's a separate settings window: themes (dark, light, glass), pinned and hidden formats, the hotkey recorder, file-manager integration, watch folders, and a page that lists every engine with a "copy install command" button. The whole interface is also available in Arabic, laid out right to left.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyia08jj67amqgkb5ut68.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyia08jj67amqgkb5ut68.png" alt="The settings window in English (dark theme) and Arabic (light theme, right to left)" width="800" height="272"&gt;&lt;/a&gt;&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pipx &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s1"&gt;'dotmorph[gui]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or grab the &lt;strong&gt;AppImage&lt;/strong&gt; (one file, no install, and AppImageUpdate can update it), the &lt;strong&gt;.deb&lt;/strong&gt;, or the &lt;strong&gt;Arch package&lt;/strong&gt; from the &lt;a href="https://github.com/kareem2099/DotMorph/releases" rel="noopener noreferrer"&gt;releases page&lt;/a&gt;. Every file is checksummed and GPG-signed.&lt;/p&gt;

&lt;p&gt;DotMorph is also on &lt;a href="https://www.appimagehub.com/p/2380213/" rel="noopener noreferrer"&gt;AppImageHub&lt;/a&gt; and &lt;a href="https://www.opendesktop.org/p/2380213/" rel="noopener noreferrer"&gt;OpenDesktop&lt;/a&gt;, and in the &lt;a href="https://appimage.github.io" rel="noopener noreferrer"&gt;AppImage community catalog&lt;/a&gt; that stores like AppImagePool read from.&lt;/p&gt;

&lt;p&gt;Then, optionally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotmorph integrate          &lt;span class="c"&gt;# right-click menus&lt;/span&gt;
dotmorph daemon &lt;span class="nt"&gt;--install&lt;/span&gt;   &lt;span class="c"&gt;# instant wheel + edge dock at login&lt;/span&gt;
dotmorph hotkey             &lt;span class="c"&gt;# Super+Shift+M&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It needs Python 3.10+ and works on glibc 2.28 and newer (Ubuntu 20.04, Debian 11, RHEL 8 and later).&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;On Wayland the wheel runs through &lt;strong&gt;XWayland&lt;/strong&gt;, so it can stay on top and be placed. A native layer-shell version is planned.&lt;/li&gt;
&lt;li&gt;The hotkey is registered automatically on &lt;strong&gt;GNOME and Xfce&lt;/strong&gt;; on other desktops &lt;code&gt;dotmorph hotkey&lt;/code&gt; prints the command to bind yourself.&lt;/li&gt;
&lt;li&gt;The AppImage is &lt;strong&gt;x86_64 only&lt;/strong&gt; for now.&lt;/li&gt;
&lt;li&gt;LibreOffice conversions run &lt;strong&gt;one at a time&lt;/strong&gt; (a limitation of its profile).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next in this series
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;This post&lt;/strong&gt;: the tour.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The engine&lt;/strong&gt;: eight backends behind one interface, safe output, cancelling, and the SVG bug that started the whole rewrite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The wheel&lt;/strong&gt;: QML, GPU-drawn ring sectors, particles, a 0.1 s daemon and the edge dock.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shipping it&lt;/strong&gt;: PyPI, AppImage, .deb, Arch and AUR from one signed tag, and why the AppImage went from 109 MB to 73 MB.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you try it, I'd love to hear what you convert most, and what's missing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/kareem2099/DotMorph" rel="noopener noreferrer"&gt;kareem2099/DotMorph&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project page:&lt;/strong&gt; &lt;a href="https://dotsuite.dev/en/product/dotmorph" rel="noopener noreferrer"&gt;dotsuite.dev/en/product/dotmorph&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Also on:&lt;/strong&gt; &lt;a href="https://pypi.org/project/dotmorph/" rel="noopener noreferrer"&gt;PyPI&lt;/a&gt; · &lt;a href="https://www.appimagehub.com/p/2380213/" rel="noopener noreferrer"&gt;AppImageHub&lt;/a&gt; · &lt;a href="https://www.opendesktop.org/p/2380213/" rel="noopener noreferrer"&gt;OpenDesktop&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>python</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
    <item>
      <title>DotShare AI 3.5.2: From Git Tags to Technical Articles You Can Review</title>
      <dc:creator>freerave</dc:creator>
      <pubDate>Thu, 08 Oct 2026 10:51:14 +0000</pubDate>
      <link>https://dev.to/freerave/dotshare-ai-352-from-git-tags-to-technical-articles-you-can-review-3lik</link>
      <guid>https://dev.to/freerave/dotshare-ai-352-from-git-tags-to-technical-articles-you-can-review-3lik</guid>
      <description>&lt;h2&gt;
  
  
  A deep dive into DotShare 3.5.2: release-aware AI articles, verified code excerpts, Dev.to live preview, and safer credential handling.
&lt;/h2&gt;

&lt;p&gt;An AI release article can sound convincing while describing code that never shipped.&lt;/p&gt;

&lt;p&gt;The failure is often subtle: a changelog comes from one version, source code comes from today's checkout, and an example gets rewritten into something that looks cleaner but no longer matches the implementation. By the time the article reaches the publishing screen, the facts have already drifted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DotShare AI 3.5.2 makes release selection, implementation evidence, and article review part of the writing workflow.&lt;/strong&gt; It adds a dedicated Dev.to article experience, strengthens the generation pipeline, and revises how credentials move between the extension host, its webviews, and the CLI.&lt;/p&gt;

&lt;p&gt;The work behind this update spans &lt;strong&gt;65 changed or new files&lt;/strong&gt;, including implementation, tests, documentation, dependency manifests, and build configuration. The useful result is what those changes let a developer do: choose a version, generate a technical draft from that version, inspect its formatting and code, and publish with a clearer understanding of what was checked.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes in the daily workflow?
&lt;/h2&gt;

&lt;p&gt;DotShare already brings social and blog publishing into the editor. Version 3.5.2 expands the technical writing side of that workflow:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Behavior in this update&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Release selection&lt;/td&gt;
&lt;td&gt;Choose a committed release, current committed work at HEAD, or tracked uncommitted changes explicitly.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Implementation context&lt;/td&gt;
&lt;td&gt;Read release material from one resolved commit and include source excerpts with file and line information.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Article generation&lt;/td&gt;
&lt;td&gt;Generate a Markdown draft with metadata, validate it, and use a bounded repair pass when needed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code examples&lt;/td&gt;
&lt;td&gt;Verify excerpts against supplied source or documentation; flag and omit remaining unverifiable examples when the rest of the draft passes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dev.to review&lt;/td&gt;
&lt;td&gt;Preview the title, description, tags, and rendered body together.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Saved credentials&lt;/td&gt;
&lt;td&gt;Show &lt;strong&gt;🔒 Key Saved &amp;amp; Active&lt;/strong&gt;, with an empty replacement field available on demand.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLI configuration&lt;/td&gt;
&lt;td&gt;Store supported platform credentials in the OS credential store and keep preferences in the configuration file.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The developer still owns the final editorial decision. The checks make specific problems detectable; they do not establish the truth of every sentence an AI writes.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A release article needs a consistent version of the project
&lt;/h2&gt;

&lt;p&gt;The first architectural change is the release boundary.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ReleaseRepository&lt;/code&gt; resolves the selected Git reference to a commit. That snapshot is then used for metadata, changelog content, release documents, source files, diffs, commits, and dependency comparisons. Reading these inputs from the same snapshot prevents a tagged release article from accidentally describing later local edits.&lt;/p&gt;

&lt;p&gt;The distinction becomes visible in the UI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Committed release&lt;/strong&gt; describes the selected tagged version.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Current committed work (HEAD)&lt;/strong&gt; can describe committed changes beyond the latest tag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uncommitted changes&lt;/strong&gt; describes tracked local edits as work in progress.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, if a repository contains a &lt;code&gt;v2.1.2&lt;/code&gt; tag and additional edits in the checkout, selecting &lt;code&gt;v2.1.2&lt;/code&gt; keeps those edits out of the release article. Choosing HEAD or uncommitted changes communicates a different scope.&lt;/p&gt;

&lt;p&gt;The collector also explains incomplete history. A first tagged release without a previous release tag can include the complete committed tree. In an untagged repository, the committed comparison can be limited to the latest commit. Warnings help the author understand the material behind the draft instead of guessing from the version label.&lt;/p&gt;

&lt;p&gt;Context is refreshed before generation, so an earlier modal preview is not silently treated as the final evidence set.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Workspace search follows the topic and the release boundary
&lt;/h2&gt;

&lt;p&gt;A useful technical article needs more than the last few commit messages.&lt;/p&gt;

&lt;p&gt;The context service can search Git history for terms from the selected topic. It combines commit-message matching with Git pickaxe searches for relevant code changes. Keyword extraction handles English and Arabic input, filtering conversational words so the search can focus on technical terms.&lt;/p&gt;

&lt;p&gt;The same selected boundary also governs dependency comparisons. DotShare recognizes manifests for several ecosystems:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ecosystem&lt;/th&gt;
&lt;th&gt;Manifest&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;&lt;code&gt;package.json&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Cargo.toml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pyproject.toml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;&lt;code&gt;go.mod&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That lets an article distinguish a dependency upgrade in the chosen release from an unrelated edit in the current checkout.&lt;/p&gt;

&lt;p&gt;Documentation adds another layer. Version-specific release files and relevant architecture or security documents can explain why a change exists. Multiline changelog notes are preserved so their meaning does not collapse into a handful of isolated bullets.&lt;/p&gt;

&lt;p&gt;The collector limits document and source budgets and reports truncation. A truncated README is a visible limitation, not evidence that the entire document was reviewed. Architecture notes provide background; roadmap items do not establish that a feature has shipped. A verification document can also contain checks that remain unfinished, and the article should retain that distinction.&lt;/p&gt;

&lt;p&gt;Active editor context is optional. A selected fragment can help identify the topic, but a local selection is labeled as a working-tree hint when writing about a committed release. It does not replace source read from the selected commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Code examples come from implementation excerpts
&lt;/h2&gt;

&lt;p&gt;Technical articles are especially vulnerable to invented examples. A plausible function name or a convincing security check is easy to generate and hard to spot during a quick read.&lt;/p&gt;

&lt;p&gt;DotShare now extracts source excerpts around added executable lines. The extractor looks for enclosing declarations and supplies the file path, line range, and whether the excerpt is partial. Ranking uses the release terms, identifiers, and changed code, while supporting helper or lifecycle methods can give a change the context it needs.&lt;/p&gt;

&lt;p&gt;This is bounded source extraction using language-aware heuristics. It is not a full compiler analysis of every supported language.&lt;/p&gt;

&lt;p&gt;The collector also reduces prompt noise by excluding material such as lockfiles, binary files, minified bundles, and test code from implementation examples. Python handling distinguishes executable changes from docstrings and test-oriented material.&lt;/p&gt;

&lt;p&gt;After generation, a separate verifier checks fenced examples. Implementation examples must match a contiguous excerpt from the supplied evidence. Display normalization allows line-ending differences and a uniform dedent while preserving internal indentation, string literals, and the actual statements.&lt;/p&gt;

&lt;p&gt;Reconstructing a function, inserting explanatory comments into its body, or joining unrelated pieces into a new example does not satisfy that rule.&lt;/p&gt;

&lt;p&gt;Documentation examples and shell commands have their own evidence rules. Commands must be supported by supplied documentation or fit a narrow recognized checkout workflow. Changing a fence label to &lt;code&gt;text&lt;/code&gt; does not provide a route around provenance checking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code is optional.&lt;/strong&gt; An article that explains a supported change clearly can be more useful than one padded with an invented implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Validation and repair have a defined stopping point
&lt;/h2&gt;

&lt;p&gt;The generation flow has three main stages: create a draft, check detectable problems, and repair when necessary.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ReleaseContentValidator&lt;/code&gt; checks problems including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Empty output and unfinished code fences.&lt;/li&gt;
&lt;li&gt;Unsupported fenced code examples.&lt;/li&gt;
&lt;li&gt;URLs outside the exact selected call-to-action links.&lt;/li&gt;
&lt;li&gt;Source paths that do not appear in supplied evidence.&lt;/li&gt;
&lt;li&gt;Certain numeric claims about tests or diff statistics without supporting material.&lt;/li&gt;
&lt;li&gt;Missing or invalid Dev.to metadata.&lt;/li&gt;
&lt;li&gt;A generated draft that incorrectly sets &lt;code&gt;published: true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Release prose that mixes in uncommitted work or exposes internal writing instructions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The service makes &lt;strong&gt;at most two generation attempts&lt;/strong&gt;. A validation problem can trigger one repair using the same evidence. The previous generated draft is identified as untrusted text rather than new evidence.&lt;/p&gt;

&lt;p&gt;There is also a bounded recovery path for a response that reaches its output limit. A retry with a larger budget uses the remaining attempt; it does not start an unlimited series of retries and repairs.&lt;/p&gt;

&lt;p&gt;Full Dev.to articles begin with a 6,000-token generation budget. The output-limit recovery can increase that budget up to 12,000 tokens. Technical deep dives and shorter posts use smaller budgets appropriate to their format. These are response budgets, not promises of an exact article length.&lt;/p&gt;

&lt;h3&gt;
  
  
  When an example still cannot be verified
&lt;/h3&gt;

&lt;p&gt;Earlier behavior could end the entire generation flow with a code-validation error even when most of the article was usable.&lt;/p&gt;

&lt;p&gt;The revised behavior is more precise. If the second attempt still contains closed, unverifiable code blocks, DotShare can omit those blocks and validate the remaining article again. When that article passes, the author receives a visible warning such as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Omitted 1 code example(s) that could not be verified against the supplied evidence. Review the surrounding explanation before publishing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A short standalone introduction to an omitted example can be removed with it. Other prose remains available for review.&lt;/p&gt;

&lt;p&gt;The warning matters: removing a code block does not prove the neighboring explanation is correct. An unfinished fence, an unsupported URL, or another unresolved blocking error still prevents a successful result.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Provider completion is part of article quality
&lt;/h2&gt;

&lt;p&gt;A successful HTTP request does not necessarily contain a complete article.&lt;/p&gt;

&lt;p&gt;This update strengthens response handling across OpenAI, Claude, Gemini, DeepSeek, and xAI. Empty text is rejected, and provider-specific completion signals are checked before returning a draft.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Completion handling&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;Detect incomplete Responses API output and distinguish output-limit truncation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude&lt;/td&gt;
&lt;td&gt;Combine text blocks and detect token-limit stops or refusal.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini&lt;/td&gt;
&lt;td&gt;Check candidate finish reasons, including output-token exhaustion.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek&lt;/td&gt;
&lt;td&gt;Check completion finish reasons and reject empty content.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;xAI&lt;/td&gt;
&lt;td&gt;Check completion finish reasons and reject empty content.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Generation requests have explicit timeouts, and SDK retry settings are bounded where configured. The author gets an error for an incomplete response instead of a truncated article presented as finished.&lt;/p&gt;

&lt;p&gt;Live model discovery remains the foundation introduced in the previous release. The improvements here build on it with stronger response handling and safer reuse of saved keys. Provider/model request identifiers also help prevent an older asynchronous response from replacing a newer selection in the UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Dev.to articles arrive as editable Markdown
&lt;/h2&gt;

&lt;p&gt;The Dev.to workflow now has a dedicated &lt;strong&gt;DotShare AI Article&lt;/strong&gt; action and a &lt;strong&gt;Full Article (Dev.to)&lt;/strong&gt; writing tone. The result is Markdown with YAML frontmatter, ready to inspect and edit.&lt;/p&gt;

&lt;p&gt;DotShare parses the metadata into separate publisher fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Title.&lt;/li&gt;
&lt;li&gt;Description.&lt;/li&gt;
&lt;li&gt;Tags, normalized and limited to four.&lt;/li&gt;
&lt;li&gt;Optional cover image, canonical URL, and series when supplied.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The frontmatter is separated from the article body so the editor does not show metadata as prose. Generated content also receives publishing cleanup, including redundant opening titles and trailing hashtag lines where appropriate.&lt;/p&gt;

&lt;p&gt;The article title belongs in the title field. Body sections can start at &lt;code&gt;##&lt;/code&gt;, which keeps the hierarchy consistent with Dev.to's article page. This follows the conventions in the &lt;a href="https://dev.to/p/editor_guide/"&gt;DEV Editor Guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Canonical URLs receive special treatment: a repository URL is not automatically used as the article's canonical URL. The repository identifies the project; a canonical URL identifies the original publication of the same article. Authors can add that field when it actually applies.&lt;/p&gt;

&lt;p&gt;The publisher retains both draft and publishing choices. Generating text is separate from deciding to publish it.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Live Preview includes the whole article header
&lt;/h2&gt;

&lt;p&gt;Reviewing only the Markdown body misses part of the reader's first impression.&lt;/p&gt;

&lt;p&gt;The updated Live Preview displays &lt;strong&gt;title, description, tags, then the article body&lt;/strong&gt;, using the current editor values. Changes to the metadata fields update the preview without another AI request. Direction-aware text also helps with Arabic and mixed-language content.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;marked&lt;/code&gt; renders the body and &lt;code&gt;DOMPurify&lt;/code&gt; sanitizes the resulting HTML. The title, description, and tag labels are inserted as text rather than interpreted as HTML. The preview supports familiar Markdown structures such as headings, lists, tables, blockquotes, and fenced code blocks, with code styling suitable for review.&lt;/p&gt;

&lt;p&gt;The UI includes word count, character count, and estimated reading time. They help authors judge the size of a draft without treating length as a measure of correctness.&lt;/p&gt;

&lt;p&gt;Related webview changes escape content in thread composers, media thumbnails, draft cards, and scheduled-post attributes. Custom publish-button labels also survive UI refreshes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Links respect the editor's decision
&lt;/h3&gt;

&lt;p&gt;Web links appear blue and can be clicked during review. The webview sends an opening request to the extension host, which accepts ordinary HTTP or HTTPS URLs and calls the editor's external-link API.&lt;/p&gt;

&lt;p&gt;This is the implementation in &lt;code&gt;src/services/PreviewLinkService.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;openArticlePreviewLink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getPreviewWebUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&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="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Respect VS Code's result; a refusal must not trigger an alternate opener.&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;vscode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openExternal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vscode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If opening is declined or throws an error, the action stops. There is no alternate opener. Local resources and command URLs are not accepted as external article links. In-article anchors are handled within the preview.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Saved keys stay on the extension host
&lt;/h2&gt;

&lt;p&gt;Credential storage needed a review across the full workflow: platform configuration, AI model selection, saved API sets, migration, logging, and cleanup.&lt;/p&gt;

&lt;p&gt;The extension uses &lt;code&gt;context.secrets&lt;/code&gt; for sensitive values. That includes the Dev.to API key and the supported AI-provider keys. The browser-based webviews receive saved-status information instead of stored key values.&lt;/p&gt;

&lt;p&gt;The status helper in &lt;code&gt;src/security/credentials.ts&lt;/code&gt; returns booleans:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;credentialStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;vscode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SecretStorage&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;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;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;ALL_SECRET_KEYS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;status&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 UI shows &lt;strong&gt;🔒 Key Saved &amp;amp; Active&lt;/strong&gt;. It does not populate the old key into the field, even as a masked value. &lt;strong&gt;Change saved key&lt;/strong&gt; reveals an empty replacement input, and leaving it blank retains the saved value.&lt;/p&gt;

&lt;p&gt;Newly typed replacements necessarily pass through the input while the user is editing them. The important separation is that stored keys are not sent back from the extension host to fill the webview. Successful save acknowledgements clear replacement fields; closing the AI dialog discards pending replacements.&lt;/p&gt;

&lt;p&gt;Saved AI keys can be reused for model discovery and generation without retyping them. A successful model-list request alone is not treated as proof that a new key was saved, and save failures do not receive a success acknowledgement.&lt;/p&gt;

&lt;p&gt;Saved API configurations follow the same design. The host activates the selected credentials, while the UI gets metadata such as the configuration name and whether credentials are available. Snapshots are created from credentials already held on the host.&lt;/p&gt;

&lt;h3&gt;
  
  
  A saved key is scoped to its editor
&lt;/h3&gt;

&lt;p&gt;VS Code and a separate editor application such as Antigravity have separate application storage. A key saved by DotShare in VS Code is not automatically available in DotShare running inside another editor.&lt;/p&gt;

&lt;p&gt;The same key can be entered and saved in that editor separately. An empty saved-key status there does not, by itself, show that VS Code lost its stored key.&lt;/p&gt;

&lt;p&gt;The extension relies on the host's SecretStorage implementation and its available storage backend. VS Code documents its API in the &lt;a href="https://code.visualstudio.com/api/references/vscode-api#SecretStorage" rel="noopener noreferrer"&gt;SecretStorage reference&lt;/a&gt; and Linux keyring configuration in its &lt;a href="https://code.visualstudio.com/docs/configure/settings-sync#_troubleshooting-keychain-issues" rel="noopener noreferrer"&gt;credential-storage troubleshooting guidance&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Migration and cleanup cover the credential lifecycle
&lt;/h2&gt;

&lt;p&gt;Moving future writes into secure storage is only part of a migration. Older plaintext copies can remain behind.&lt;/p&gt;

&lt;p&gt;The update migrates saved API configurations for every supported platform, including Dev.to. When a newer secure copy already exists, it is preserved and the stale legacy copy is removed. When no secure copy exists, the secure write precedes deletion of the old state.&lt;/p&gt;

&lt;p&gt;Migration also handles the previous Discord webhook storage name and legacy API keys inside saved model-selection state. Model preferences can remain as preferences without retaining the key in ordinary state.&lt;/p&gt;

&lt;p&gt;Local credential clearing covers platform keys, AI keys, GitHub and DotShare account secrets, saved configuration lists, relevant model preferences, and remembered cloud-sync consent. The UI receives cleanup notifications so the displayed state can follow the host.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local cleanup is not cloud revocation.&lt;/strong&gt; Credentials previously uploaded for scheduling must be revoked separately through the cloud service. That distinction is now stated in the user-facing security documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. The CLI stores credentials in the OS credential store
&lt;/h2&gt;

&lt;p&gt;The extension's storage changes also exposed a separate CLI concern: a preferences file should not double as a credential vault.&lt;/p&gt;

&lt;p&gt;The CLI now uses &lt;code&gt;@napi-rs/keyring&lt;/code&gt; through a dedicated credential-store adapter. Its supported Telegram and LinkedIn credentials are stored under the DotShare CLI service in the native OS credential store.&lt;/p&gt;

&lt;p&gt;The JSON configuration file contains preferences such as the server URL and default platforms. It is written through a temporary file and renamed, with a restrictive file mode requested on platforms that honor it. Credentials are read from the credential store when a command needs them.&lt;/p&gt;

&lt;p&gt;For older configurations, the migration writes credentials to the native store before replacing the configuration with metadata. An existing secure value takes precedence over an older file copy. If keyring access fails, migration stops without erasing the legacy file or falling back to a new plaintext credential write.&lt;/p&gt;

&lt;p&gt;The setter in &lt;code&gt;src/cli/credential-store.ts&lt;/code&gt; makes the failure behavior explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;rememberSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DotShare CLI&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;setPassword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Could not save credentials in the OS keyring. No plaintext fallback was used.&lt;/span&gt;&lt;span class="dl"&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;CLI logging is separated from VS Code-dependent logging, so command-line diagnostics can use redaction without requiring the editor runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Logs and cloud sync have narrower, clearer behavior
&lt;/h2&gt;

&lt;p&gt;This update removes sensitive logging of complete OAuth callback URLs and credential values from the reviewed paths. A shared redaction layer remembers secrets obtained from or written to SecretStorage and masks known values and common token patterns in diagnostic messages. Snippet sanitization also reduces the chance of common credential formats entering external AI requests.&lt;/p&gt;

&lt;p&gt;Git commands use &lt;code&gt;execFile&lt;/code&gt; with structured argument arrays. That avoids shell interpretation of topic text and path arguments and improves handling of platform-specific quoting. It is a concrete protection for Git subprocess calls, not a general claim that all inputs are safe.&lt;/p&gt;

&lt;p&gt;Cloud scheduling has an explicit boundary as well. Credential synchronization checks saved user consent and requires HTTPS before reading local credentials for upload. Consent text explains that the server receives credentials to run scheduled posts.&lt;/p&gt;

&lt;p&gt;The security documentation no longer presents unverified backend encryption claims as guarantees. Local SecretStorage protects the local workflow; it does not establish how a deployed server stores its copy.&lt;/p&gt;

&lt;p&gt;Redaction and migration also have practical limits. Pattern matching cannot identify every possible secret, historical logs are not automatically rewritten, and removing a local copy does not rotate a token that was previously exposed.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. The saved-key controls now follow the editor theme
&lt;/h2&gt;

&lt;p&gt;The saved-state design should look deliberate, too.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Change saved key&lt;/strong&gt; control now has a dedicated CSS class in both the sidebar and the AI modal. It uses VS Code theme colors, rounded borders, a hover state, and a visible keyboard-focus outline. Hidden controls stay hidden through the CSS rule as well as their UI state.&lt;/p&gt;

&lt;p&gt;That fixes the unstyled native button previously shown beside the saved-key badge and makes credential replacement fit the rest of the extension.&lt;/p&gt;

&lt;h2&gt;
  
  
  13. Build and test changes support the release
&lt;/h2&gt;

&lt;p&gt;The update targets VS Code &lt;code&gt;^1.90.0&lt;/code&gt;, with matching API type definitions. Build, webview type-checking, lint configuration, dependency manifests, and packaging exclusions were adjusted alongside the implementation. Development artifacts and source-only material are excluded from the packaged extension where configured.&lt;/p&gt;

&lt;p&gt;The automated suite was rerun for this article: &lt;strong&gt;157 tests passed across 17 suites&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The coverage includes release boundaries and evidence extraction, generation validation and repair, provider completion signals, preview-link handling, credential status and migration, AI-key behavior, redaction, cloud-consent checks, and CLI configuration migration.&lt;/p&gt;

&lt;p&gt;Those tests exercise deterministic behavior and mocked integrations. Native keyring persistence across an editor restart, live provider responses, Dev.to publishing, and deployed backend behavior remain separate integration checks. The passing suite is evidence for the tested implementation paths, not a production security certification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install or download DotShare
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://marketplace.visualstudio.com/items?itemName=FreeRave.dotshare" rel="noopener noreferrer"&gt;Install from the Visual Studio Marketplace&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://open-vsx.org/extension/freerave/dotshare" rel="noopener noreferrer"&gt;Install from Open VSX&lt;/a&gt; in editors that use that registry.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://marketplace.visualstudio.com/_apis/public/gallery/publishers/FreeRave/vsextensions/dotshare/latest/vspackage" rel="noopener noreferrer"&gt;Download the latest available VSIX from the Marketplace&lt;/a&gt; for manual installation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To install the downloaded file, open the Extensions view, select &lt;strong&gt;Install from VSIX…&lt;/strong&gt; from its menu, and choose the package. Check that the installed version is &lt;strong&gt;3.5.2&lt;/strong&gt; to use the behavior described in this article; the latest version available can differ between registries while an update is being published.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the article workflow on a real release
&lt;/h2&gt;

&lt;p&gt;Start with a project that has useful release notes and a committed version to describe:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Save an AI-provider key and choose an available model in DotShare.&lt;/li&gt;
&lt;li&gt;Open the Dev.to Article Publisher and select &lt;strong&gt;DotShare AI Article&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Choose the release scope and version deliberately.&lt;/li&gt;
&lt;li&gt;Enable the relevant changelog and Git sources, and add an optional topic or editor hint.&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Full Article (Dev.to)&lt;/strong&gt;, language, selected links, and whether to include verified code excerpts.&lt;/li&gt;
&lt;li&gt;Generate the draft and inspect any context or validation warnings.&lt;/li&gt;
&lt;li&gt;Review the title, description, tags, links, and body in Live Preview.&lt;/li&gt;
&lt;li&gt;Save a Dev.to draft, make the final editorial edits, and publish when ready.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;DotShare 3.5.2 puts the version, the evidence, and the draft in the same workflow. It helps authors spend their review time on the actual explanation: what changed, why it matters, and what the implementation supports.&lt;/p&gt;

&lt;p&gt;The code is available in the &lt;a href="https://github.com/kareem2099/DotShare" rel="noopener noreferrer"&gt;DotShare repository&lt;/a&gt;. Which part of writing release articles costs you the most time: finding the changes, explaining the implementation, or preparing the final post?&lt;/p&gt;

</description>
      <category>vscode</category>
      <category>ai</category>
      <category>opensource</category>
      <category>security</category>
    </item>
    <item>
      <title>DotGhostBoard 2.1 'Leviathan' — Cryptographic Vault Backups, Secret Expiry &amp; Password History (Part 2)</title>
      <dc:creator>freerave</dc:creator>
      <pubDate>Tue, 06 Oct 2026 13:20:28 +0000</pubDate>
      <link>https://dev.to/freerave/dotghostboard-21-leviathan-cryptographic-vault-backups-secret-expiry-password-history-part-3pk8</link>
      <guid>https://dev.to/freerave/dotghostboard-21-leviathan-cryptographic-vault-backups-secret-expiry-password-history-part-3pk8</guid>
      <description>&lt;h2&gt;
  
  
  A deep-dive into DotGhostBoard 2.1's security layer: AES-256-GCM .vault backup packages with AAD header authentication, CSPRNG password generation with keyspace entropy, secret expiration tracking, and encrypted version history with safe revert.
&lt;/h2&gt;

&lt;p&gt;Most local password managers have a backup problem: export to unencrypted CSV and you have a plaintext credential dump sitting on your disk. Use proprietary cloud sync and you've handed your secrets to a third party. Neither is acceptable.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;&lt;a href="https://dev.to/freerave/dotghostboard-21-leviathan-smart-clipboard-tagging-contextual-actions-on-linux-part-1-5gb8"&gt;Part 1&lt;/a&gt;&lt;/strong&gt;, we covered the productivity layer of DotGhostBoard 2.1 — smart auto-tagging and contextual clipboard actions running locally in under 1ms for typical payloads (&amp;lt; 5KB).&lt;/p&gt;

&lt;p&gt;This part is about security architecture: what happens when you need to safely move, store, expire, and rotate credentials.&lt;/p&gt;

&lt;p&gt;Here's what we're covering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Standalone &lt;code&gt;.vault&lt;/code&gt; Encrypted Backup Packages (with AAD header authentication)&lt;/li&gt;
&lt;li&gt;CSPRNG Password &amp;amp; Token Generator with Live Keyspace Entropy&lt;/li&gt;
&lt;li&gt;Secret Expiry Tracking &amp;amp; Dynamic Warning Badges&lt;/li&gt;
&lt;li&gt;Encrypted Password Version History with Safe Revert&lt;/li&gt;
&lt;li&gt;Vault-to-Dashboard Plaintext Sweep &amp;amp; Its Practical Boundaries&lt;/li&gt;
&lt;li&gt;Unified UX Polish &amp;amp; Ergonomics&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. Encrypted Vault Backups: The &lt;code&gt;.vault&lt;/code&gt; Package Format
&lt;/h2&gt;

&lt;p&gt;The goal was simple: export your entire Vault as a single portable file that contains &lt;strong&gt;zero unencrypted secret bytes&lt;/strong&gt; — safe to copy to a USB drive, upload to a personal server, or store offsite.&lt;/p&gt;

&lt;p&gt;The result is the &lt;code&gt;.vault&lt;/code&gt; format (&lt;code&gt;core/security/vault/backup.py&lt;/code&gt;), a compact binary package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+---------------+---------------+---------------+---------------+---------------+
| Magic Header  | Version       | Salt          | Nonce         | Payload + Tag |
| "DGBV"        | 0x01          | 16 bytes      | 12 bytes      | Ciphertext +  |
| (4 bytes)     | (1 byte)      | os.urandom    | AES-GCM       | 16B GCM Tag   |
+---------------+---------------+---------------+---------------+---------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Cryptographic Stack
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Implementation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cipher&lt;/td&gt;
&lt;td&gt;AES-256-GCM (authenticated encryption with AAD)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key derivation&lt;/td&gt;
&lt;td&gt;PBKDF2-HMAC-SHA256, 100,000 rounds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Salt&lt;/td&gt;
&lt;td&gt;16-byte random, per-export (&lt;code&gt;os.urandom(16)&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nonce&lt;/td&gt;
&lt;td&gt;12-byte random, per-export (&lt;code&gt;os.urandom(12)&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Associated Data (AAD)&lt;/td&gt;
&lt;td&gt;5-byte header (&lt;code&gt;MAGIC + VERSION&lt;/code&gt;) authenticated into the GCM tag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authentication&lt;/td&gt;
&lt;td&gt;128-bit GCM tag — tampering with ciphertext, nonce, or header fails cleanly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here's the core export logic, lightly abbreviated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# core/security/vault/backup.py (simplified)
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cryptography.hazmat.primitives.kdf.pbkdf2&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PBKDF2HMAC&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cryptography.hazmat.primitives&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashes&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cryptography.hazmat.primitives.ciphers.aead&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AESGCM&lt;/span&gt;

&lt;span class="n"&gt;MAGIC&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DGBV&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;VERSION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\x01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;KDF_ITER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100_000&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;export_vault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;passphrase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;salt&lt;/span&gt;  &lt;span class="o"&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;urandom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;nonce&lt;/span&gt; &lt;span class="o"&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;urandom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;header_aad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MAGIC&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;VERSION&lt;/span&gt;

    &lt;span class="c1"&gt;# Derive 256-bit key from passphrase
&lt;/span&gt;    &lt;span class="n"&gt;kdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PBKDF2HMAC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hashes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SHA256&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;salt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;iterations&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;KDF_ITER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;derive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;passphrase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="c1"&gt;# Serialize payload entirely in-memory — never touches disk
&lt;/span&gt;    &lt;span class="n"&gt;plaintext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ensure_ascii&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# AES-256-GCM: encrypt payload and authenticate format header via AAD
&lt;/span&gt;    &lt;span class="n"&gt;ciphertext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AESGCM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plaintext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;header_aad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Return complete package (16-byte GCM tag is appended to ciphertext by AESGCM)
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;header_aad&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;salt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;nonce&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ciphertext&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And importing reverses the process — with AEAD integrity verification happening &lt;em&gt;before&lt;/em&gt; any payload parsing or database operations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;import_vault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;passphrase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MAGIC&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Not a valid .vault file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&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="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unsupported .vault backup version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;header_aad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ciphertext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;

    &lt;span class="n"&gt;kdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PBKDF2HMAC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hashes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SHA256&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;salt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;iterations&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;KDF_ITER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;derive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;passphrase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="c1"&gt;# Decrypt and authenticate: raises InvalidTag if ciphertext, passphrase,
&lt;/span&gt;    &lt;span class="c1"&gt;# or header bytes (MAGIC/VERSION) were tampered with
&lt;/span&gt;    &lt;span class="n"&gt;plaintext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AESGCM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ciphertext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;header_aad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plaintext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why Associated Authenticated Data (AAD)?
&lt;/h3&gt;

&lt;p&gt;Passing &lt;code&gt;MAGIC + VERSION&lt;/code&gt; as AAD cryptographically binds the cleartext header to the payload's 128-bit authentication tag. Without AAD, an attacker could flip the version byte undetected by the cipher until application-level parsing. With AAD, modifying even a single bit in the header or ciphertext triggers an immediate &lt;code&gt;InvalidTag&lt;/code&gt; rejection by the cipher.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Gets Exported
&lt;/h3&gt;

&lt;p&gt;Everything: active secrets, categories, creation/update timestamps, expiration dates, and the last 3 encrypted version history entries. Full Vault state in one file.&lt;/p&gt;

&lt;p&gt;Built-in duplicate skipping (&lt;code&gt;overwrite_existing=False&lt;/code&gt;) on import means re-importing an old backup won't create duplicates in your live Vault.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;On PBKDF2 iteration count:&lt;/strong&gt; 100,000 rounds is a responsive baseline across diverse Linux hardware, including low-power ARM boards. Argon2id support and user-configurable iteration scaling are on the v2.2 roadmap.&lt;/p&gt;
&lt;/blockquote&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl55vaetpohpdzyfb3my6.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl55vaetpohpdzyfb3my6.png" alt="DotGhostBoard 2.1 Encrypted Vault Backup Dialog" width="799" height="401"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 1: Standalone .vault backup export dialog showing PBKDF2 passphrase derivation, AES-256-GCM encryption parameters, and live Vault drawer state.&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  2. CSPRNG Password &amp;amp; Token Generator
&lt;/h2&gt;

&lt;p&gt;Generating credentials inside a clipboard manager means you stay in one tool. No tab-switching to a web generator, no pasting between windows.&lt;/p&gt;

&lt;p&gt;The generator is embedded directly inside the Add/Edit Secret dialog (&lt;code&gt;ui/vault/secret_dialog.py&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;[ ⚡ Generate Password / Token ]
├── CSPRNG Source: Python secrets module (/dev/urandom)
├── Character Pools: [✓] a-z  [✓] A-Z  [✓] 0-9  [✓] Symbols
├── Length Slider: 8 ──────────●─────── 64  (default: 20)
├── Token Presets: Hex | UUIDv4 | Base64 | Bearer | URL-Safe
└── Entropy Meter: [████████████████████] 131.1 bits — Fort Knox
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Entropy Calculation
&lt;/h3&gt;

&lt;p&gt;Instead of an arbitrary "password strength checklist," we display &lt;strong&gt;theoretical keyspace entropy&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;H = L × log₂(N)
&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;L&lt;/code&gt; = character length&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;N&lt;/code&gt; = active pool size (e.g., a-z + A-Z + 0-9 + symbols = 94 characters)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ui/vault/secret_dialog.py (simplified)
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;keyspace_entropy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&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;pool_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&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;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Theoretical bits of entropy for a uniformly random credential.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pool_size&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pool_size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;entropy_label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&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;str&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;bits&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Weak&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;bits&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Moderate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;bits&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Strong&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Fort Knox&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The meter updates live as you drag the slider or toggle character pools. A 20-character password using the full 94-character pool yields &lt;strong&gt;131.1 bits&lt;/strong&gt; of keyspace entropy (20 × log₂(94)) — comfortably above the 80–128-bit range generally considered strong for random credentials.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Keyspace entropy across the stack:&lt;/strong&gt; Both the auto-tagger in Part 1 and the generator here use the same keyspace entropy formula (H = L × log₂(N)). The difference is in application: the auto-tagger &lt;em&gt;estimates&lt;/em&gt; the character pool of existing clipboard text to detect high-entropy tokens, while the generator &lt;em&gt;computes&lt;/em&gt; the exact strength of newly generated passwords from user-selected pools.&lt;/p&gt;
&lt;/blockquote&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgfxt41u34xsh6bvwvtsj.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgfxt41u34xsh6bvwvtsj.png" alt="DotGhostBoard 2.1 Cryptographic Password Generator &amp;amp; Live Entropy Meter" width="503" height="695"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 2: Built-in CSPRNG Password &amp;amp; Token Generator showing character pool toggles, 20-character length slider, and live keyspace entropy evaluation (Strong ~131 bits).&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  3. Secret Expiry Tracking &amp;amp; Dynamic Badges
&lt;/h2&gt;

&lt;p&gt;Temporary credentials — staging tokens, contractor access keys, short-lived API secrets — have an expiration date. Most password managers ignore this.&lt;/p&gt;

&lt;p&gt;DotGhostBoard 2.1 tracks expiry directly in the Vault model (&lt;code&gt;core/security/vault/models.py&lt;/code&gt;) and surfaces it visually on each secret card (&lt;code&gt;ui/vault/secret_card.py&lt;/code&gt;).&lt;/p&gt;
&lt;h3&gt;
  
  
  Setting an Expiry Date
&lt;/h3&gt;

&lt;p&gt;In the Add/Edit dialog, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pick a &lt;strong&gt;preset duration&lt;/strong&gt;: 30 days, 90 days, 180 days, 1 year.&lt;/li&gt;
&lt;li&gt;Or pick an &lt;strong&gt;exact date&lt;/strong&gt; via a &lt;code&gt;QDateEdit&lt;/code&gt; calendar picker.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The date is stored as an ISO 8601 timestamp string in SQLite. The expiry logic lives in a shared mixin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# core/security/vault/models.py
&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_ExpiryMixin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nd"&gt;@property&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_expired&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&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;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;exp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tzinfo&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;exp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tzinfo&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;exp&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Corrupted timestamp: treated as non-expiring rather than raising in the UI
&lt;/span&gt;            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="nd"&gt;@property&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;days_remaining&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&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;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;exp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tzinfo&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;exp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tzinfo&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exp&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;VaultItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_ExpiryMixin&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;VaultSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_ExpiryMixin&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both &lt;code&gt;VaultItem&lt;/code&gt; (full DB record) and &lt;code&gt;VaultSummary&lt;/code&gt; (UI listing) inherit the same expiry logic through the mixin — written once, tested once.&lt;/p&gt;

&lt;h3&gt;
  
  
  Visual Status Badges
&lt;/h3&gt;

&lt;p&gt;The secret card renders the appropriate badge at render time:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Badge&lt;/th&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Appearance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;⛔ EXPIRED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;is_expired == True&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Red badge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;⚠️ 0d–7d left&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;days_remaining &amp;lt;= 7&lt;/code&gt; (e.g., &lt;code&gt;⚠️ 3d left&lt;/code&gt;, or &lt;code&gt;⚠️ 0d left&lt;/code&gt; when less than 24 hours remain)&lt;/td&gt;
&lt;td&gt;Amber badge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;⏳ 2027-03-01&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Active, &amp;gt; 7 days remaining&lt;/td&gt;
&lt;td&gt;Blue pill&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;(none)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;No expiry set&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzjkxkbbwfx97vo7gvikd.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzjkxkbbwfx97vo7gvikd.png" alt="DotGhostBoard 2.1 Secret Expiry Status Badges" width="395" height="306"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 3: Secret cards displaying dynamic status badges including expired credentials and upcoming renewal countdowns.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Encrypted Password Version History &amp;amp; Safe Revert
&lt;/h2&gt;

&lt;p&gt;Rotating a production credential is risky. If the new key fails, you need the old one — immediately. DotGhostBoard 2.1 solves this with &lt;strong&gt;Encrypted Version History&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How It Works
&lt;/h3&gt;

&lt;p&gt;Every time you update an existing secret, the current ciphertext is archived before the new one is written:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# core/security/vault/service.py (simplified)
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;update_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item_id&lt;/span&gt;&lt;span class="p"&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;secret_text&lt;/span&gt;&lt;span class="p"&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;save_history&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_require_unlocked&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;save_history&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# 1. Fetch current encrypted record before replacing
&lt;/span&gt;        &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item_id&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;existing&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ciphertext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# 2. Archive previous ciphertext to history (capped at 3 newest)
&lt;/span&gt;            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_history_entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ciphertext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_entries&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="c1"&gt;# 3. Encrypt and store the new secret version
&lt;/span&gt;    &lt;span class="n"&gt;new_ciphertext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secret_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ciphertext&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;new_ciphertext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Max 3 versions&lt;/strong&gt; — capped in SQLite, avoiding unbounded storage growth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encrypted at rest&lt;/strong&gt; — history entries use the same DEK as the live secret. Plaintext never hits disk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decrypted in memory only&lt;/strong&gt; — when you open the History Viewer, decryption happens in RAM and is discarded on close.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The History Viewer (📜) &amp;amp; Safe Revert
&lt;/h3&gt;

&lt;p&gt;Clicking the &lt;code&gt;📜&lt;/code&gt; icon on any secret card opens the modal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Version index and creation timestamp for each entry.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;👁️ Reveal&lt;/code&gt; / &lt;code&gt;🙈 Hide&lt;/code&gt; toggle with monospace masking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copy&lt;/strong&gt; button — protected by DotGhostBoard's 30-second automatic clipboard scrub.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;↺ Revert to this&lt;/strong&gt; — restores the selected version as the active secret.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Behind the scenes, revert calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret_text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;plaintext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;save_history&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This neatly preserves the active secret you just replaced into history, allowing you to toggle back and forth safely without data loss. When a secret is deleted, SQLite cascades and purges all historical entries in the same transaction.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fph2t177u2a1giemvqig2.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fph2t177u2a1giemvqig2.png" alt="DotGhostBoard 2.1 Encrypted Password Version History &amp;amp; Safe Revert" width="559" height="661"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 4: Password version history modal showing archived credential states, reveal toggle, and one-click atomic revert.&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  5. Vault-to-Dashboard Plaintext Sweep
&lt;/h2&gt;

&lt;p&gt;Here's a subtle security gap:&lt;/p&gt;

&lt;p&gt;You add a secret to the Vault. But before you did, you copied it to your clipboard — meaning an unencrypted copy was recorded in your general clipboard history.&lt;/p&gt;

&lt;p&gt;Later, you delete the secret from the Vault. If the clipboard history remains untouched, your secret is gone from the encrypted vault but still sitting in plaintext in the general history database.&lt;/p&gt;

&lt;p&gt;DotGhostBoard 2.1 closes this gap with a &lt;strong&gt;Coordinated Plaintext Sweep&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ui/vault/vault_controller.py (simplified)
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;delete_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&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;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# 1. Temporarily decrypt secret in-memory to discover history traces
&lt;/span&gt;    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;plaintext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reveal_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;plaintext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. Delete from encrypted Vault database
&lt;/span&gt;    &lt;span class="n"&gt;success&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 3. If deleted, sweep matching plaintext entries from clipboard history
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;success&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;plaintext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;history_matches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_history_repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plaintext&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;entry&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;history_matches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_history_repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;history_item_removed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;  &lt;span class="c1"&gt;# removes card from UI
&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;secrets_changed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;success&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;history_item_removed&lt;/code&gt; signal coordinates the UI: the Vault controller emits it, and the HistoryController receives it to cleanly remove the card from the UI. No tight coupling, no shared database transactions — just Qt signals.&lt;/p&gt;

&lt;h3&gt;
  
  
  Practical Boundary: Exact Match vs. Substrings
&lt;/h3&gt;

&lt;p&gt;The sweep performs an exact match (&lt;code&gt;find_by_content(plaintext)&lt;/code&gt;). If you copied the credential with trailing whitespace, a trailing newline, or embedded inside a larger terminal command line (e.g. &lt;code&gt;curl -H "Authorization: Bearer &amp;lt;key&amp;gt;"&lt;/code&gt;), the exact sweep will not catch the enclosing entry.&lt;/p&gt;

&lt;p&gt;We deliberately chose this conservative exact match: running broad substring searches across the entire clipboard history could easily result in destructive false-positive deletions of unrelated notes or commands.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Unified Design System &amp;amp; UX Polish
&lt;/h2&gt;

&lt;p&gt;Security tools only work if developers actually use them. Friction kills adoption. In 2.1, we focused on tightening common interactions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Component Unification (&lt;code&gt;PasswordInputWidget&lt;/code&gt;):&lt;/strong&gt; We extracted password input handling and the visibility toggle into a single reusable widget. Both the session &lt;code&gt;LockScreen&lt;/code&gt; and the &lt;code&gt;VaultUnlockDialog&lt;/code&gt; now share identical keyboard behavior, input styling, and state management rather than maintaining divergent Qt implementations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drawer Ergonomics:&lt;/strong&gt; The slide-out Vault drawer (&lt;code&gt;ui/vault/vault_panel.py&lt;/code&gt;) was restructured into a clear two-row toolbar (title/status row + action controls row) with an expanded 380px width for better readability. First-time users see an empty-state onboarding card outlining shortcuts and clipboard auto-scrub policies rather than a blank panel.&lt;/li&gt;
&lt;/ul&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fysuuvwbuew71rimd2yod.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fysuuvwbuew71rimd2yod.png" alt="DotGhostBoard 2.1 Vault Drawer &amp;amp; Action Toolbar" width="380" height="1008"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 5: Expanded 380px Vault drawer featuring a two-row action toolbar and structured onboarding empty-state.&lt;/em&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgibhaox0g71dmc63kj8t.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgibhaox0g71dmc63kj8t.png" alt="DotGhostBoard 2.1 Unified PasswordInputWidget" width="519" height="315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 6: Unified PasswordInputWidget on the Session Lock screen with integrated eye toggle and focus handling.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing &amp;amp; Code Quality
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;523 automated tests. 100% passing.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;======================== 523 passed in ~100s ========================
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test matrix covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cryptographic roundtrips with AAD tampering verification&lt;/li&gt;
&lt;li&gt;SQLite transaction isolation for version history and cascade deletes&lt;/li&gt;
&lt;li&gt;UI event loops via headless Xvfb (tested against both Openbox and Qtile 0.36.0)&lt;/li&gt;
&lt;li&gt;Expiry logic edge cases (timezone-aware vs. naive datetimes, malformed strings)&lt;/li&gt;
&lt;li&gt;Plaintext sweep signal propagation across the Vault ↔ Dashboard boundary&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Zero Flake8 errors. &lt;code&gt;ui/dashboard.py&lt;/code&gt; enforces a hard &lt;code&gt;&amp;lt;= 500 LOC&lt;/code&gt; limit — all specialized logic delegates to controllers and mixins.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next &amp;amp; Known Limitations
&lt;/h2&gt;

&lt;p&gt;A few honest limitations, on the record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wayland compatibility:&lt;/strong&gt; DotGhostBoard relies on X11/EWMH for clipboard capture and hotkeys. We're investigating &lt;code&gt;wlr-data-control&lt;/code&gt; protocol support for Wayland. No ETA yet, but it's our most-requested architecture evolution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KDF hardening:&lt;/strong&gt; 100,000 PBKDF2 rounds is our current baseline. User-configurable iteration counts (targeting OWASP's 600,000 recommendation) and Argon2id evaluation are on the v2.2 roadmap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backup format version negotiation:&lt;/strong&gt; v2.1 introduces the single-byte version header; upcoming releases will support forward-compatible schema migrations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Substring Plaintext Sweeps:&lt;/strong&gt; Exploring optional flagged heuristics for wiping shell history snippets containing expired tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLI Vault queries:&lt;/strong&gt; Scriptable, authenticated &lt;code&gt;dotghost vault list&lt;/code&gt; and &lt;code&gt;dotghost vault get &amp;lt;title&amp;gt;&lt;/code&gt; commands are planned for v2.2.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📦 Try DotGhostBoard 2.1
&lt;/h2&gt;

&lt;p&gt;100% free and open-source — &lt;strong&gt;Apache-2.0 License.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/kareem2099/DotGhostBoard" rel="noopener noreferrer"&gt;kareem2099/DotGhostBoard&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenDesktop / KDE Store:&lt;/strong&gt; &lt;a href="https://www.opendesktop.org/p/2353623/" rel="noopener noreferrer"&gt;DotGhostBoard on OpenDesktop&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/kareem2099/DotGhostBoard.git
&lt;span class="nb"&gt;cd &lt;/span&gt;DotGhostBoard

python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

python3 main.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Keyboard Shortcuts
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Shortcut&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Ctrl+Alt+V&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Summon / hide Dashboard (migrates to active workspace)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Ctrl+Alt+Space&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Floating Spotlight Quick Search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Ctrl+Shift+V&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Open / close The Vault encrypted drawer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Ctrl+F&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Focus search bar inside Dashboard&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;Questions about the vault format, the key derivation choices, or the history revert logic? Drop them below — happy to go deeper on any of it. 👻🔒&lt;/p&gt;

</description>
      <category>linux</category>
      <category>opensource</category>
      <category>python</category>
      <category>security</category>
    </item>
    <item>
      <title>DotGhostBoard 2.1 'Leviathan' — Smart Clipboard Tagging &amp; Contextual Actions on Linux (Part 1)</title>
      <dc:creator>freerave</dc:creator>
      <pubDate>Fri, 02 Oct 2026 20:59:46 +0000</pubDate>
      <link>https://dev.to/freerave/dotghostboard-21-leviathan-smart-clipboard-tagging-contextual-actions-on-linux-part-1-5gb8</link>
      <guid>https://dev.to/freerave/dotghostboard-21-leviathan-smart-clipboard-tagging-contextual-actions-on-linux-part-1-5gb8</guid>
      <description>&lt;h2&gt;
  
  
  How we built a rules-based clipboard intelligence engine for Linux — local, deterministic, sub-millisecond for typical payloads (&amp;lt; 5KB), zero AI, zero telemetry. Auto-tagging, contextual smart actions, and a tight pipeline architecture explained.
&lt;/h2&gt;

&lt;p&gt;You copy a minified JSON blob from a terminal log. To actually read it, you paste it into an editor, run a formatter, then copy the result back. You copy an API key from a config file and your clipboard manager stores it as plain text, alongside screenshots and grocery lists, until you manually hunt it down and secure it.&lt;/p&gt;

&lt;p&gt;Most clipboard tools treat every copied item the same: inert text in a database. For anyone who spends time in terminals, editors, and browser devtools, that uniformity creates constant small frictions that accumulate fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DotGhostBoard 2.1 "Leviathan"&lt;/strong&gt; takes a different approach. The clipboard understands what it's holding and surfaces the right action immediately — format this JSON, open that URL, secure this credential.&lt;/p&gt;

&lt;p&gt;This is a two-part series on how we built it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Part 1 (you're here):&lt;/strong&gt; Rules-based Smart Auto-Tagging, Contextual Smart Actions, and the pipeline behind them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/freerave/dotghostboard-21-leviathan-cryptographic-vault-backups-secret-expiry-password-history-part-3pk8"&gt;Part 2&lt;/a&gt;:&lt;/strong&gt; Cryptographic &lt;code&gt;.vault&lt;/code&gt; Backups, CSPRNG Password Generator, Secret Expiry, and Encrypted Version History.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Design Constraint: Local, Deterministic, Fast
&lt;/h2&gt;

&lt;p&gt;The obvious shortcut for text classification is an LLM or a cloud API. For a clipboard manager, that's a non-starter on three counts.&lt;/p&gt;

&lt;p&gt;Your clipboard routinely holds API keys, internal paths, private tokens, and proprietary code snippets. Sending that to any remote service — even a local on-device model — introduces a trust boundary that shouldn't exist in a tool people actually rely on for security. Beyond that, loading inference libraries adds hundreds of MB of idle RAM, and any latency between &lt;code&gt;Ctrl+C&lt;/code&gt; and the item appearing in history is enough to make the tool feel broken.&lt;/p&gt;

&lt;p&gt;Our constraint was concrete: &lt;strong&gt;every classification must run locally, produce deterministic results, and complete in under 1ms for typical clipboard payloads (&amp;lt; 5KB), scaling to ~1.4ms worst-case at the 8KB classification cap.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The approach: pre-compiled regular expressions, structural heuristics, and a pool-based keyspace entropy estimate — no opaque models.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Auto-Tagger: How Classification Actually Works
&lt;/h2&gt;

&lt;p&gt;The engine lives in &lt;code&gt;core/security/auto_tagger.py&lt;/code&gt;. It runs synchronously on every new clipboard item before storage and UI rendering.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Captured Text ──► Pre-compiled Patterns &amp;amp; Heuristics ──► Tags ──► Storage &amp;amp; UI
                  ├── URL regex                       ──► #link
                  ├── JSON fast-path + json.loads     ──► #json
                  ├── Code keywords + bracket density ──► #code
                  ├── Known prefixes + entropy gate   ──► #secret
                  ├── IPv4 regex + IPv6 regex         ──► #ip
                  ├── RFC-like email regex            ──► #email
                  ├── Unix / Windows path regex       ──► #path
                  └── Hex length check {32,64}        ──► #hash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All patterns compile once at module load time. The function is pure — no I/O, no Qt, no database access — which makes it fast to call and trivial to test.&lt;/p&gt;

&lt;p&gt;Here's the real implementation, abbreviated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# core/security/auto_tagger.py
&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;

&lt;span class="c1"&gt;# ── Compiled patterns (module-level — compiled once, reused forever) ──────────
&lt;/span&gt;
&lt;span class="n"&gt;_RE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https?://[^\s\"&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&amp;gt;]{3,}|ftp://[^\s\"&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&amp;gt;]{3,}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IGNORECASE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;_RE_EMAIL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;_RE_IPV4&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;                             &lt;span class="c1"&gt;# validates 0-255 per octet
&lt;/span&gt;    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;_RE_IPV6&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\b(?:[0-9a-fA-F]{1,4}:){2,7}[0-9a-fA-F]{1,4}\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Pragmatic range matching MD5 (32), SHA-1 (40), SHA-256 (64); strict lengths planned for v2.2
&lt;/span&gt;&lt;span class="n"&gt;_RE_HEX_HASH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\b[0-9a-fA-F]{32,64}\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Known credential prefixes — the first line of secret detection
&lt;/span&gt;&lt;span class="n"&gt;_RE_SECRET_PATTERNS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ghp_[A-Za-z0-9]{36,}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;                          &lt;span class="c1"&gt;# GitHub PAT
&lt;/span&gt;    &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AKIA[0-9A-Z]{16}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;                               &lt;span class="c1"&gt;# AWS Access Key
&lt;/span&gt;    &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk-[A-Za-z0-9]{32,}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;                           &lt;span class="c1"&gt;# OpenAI / Stripe
&lt;/span&gt;    &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;xox[baprs]-[A-Za-z0-9\-]{10,}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;                 &lt;span class="c1"&gt;# Slack tokens
&lt;/span&gt;    &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eyJ[A-Za-z0-9\-_]{10,}\.[A-Za-z0-9\-_]{10,}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;# JWT
&lt;/span&gt;    &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----&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="c1"&gt;# (Note: _RE_PATH, _CODE_KEYWORDS, and _CODE_BRACKETS are omitted here for brevity)
&lt;/span&gt;
&lt;span class="c1"&gt;# ── Pool-based keyspace entropy estimate ──────────────────────────────────────
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_entropy_bits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Estimate theoretical keyspace entropy: H = len * log₂(pool_size).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ascii_lowercase&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;26&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ascii_uppercase&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;26&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;digits&lt;/span&gt;           &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
    &lt;span class="c1"&gt;# Any non-alphanumeric character (symbols, punctuation, or non-ASCII) adds 32
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ascii_letters&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;digits&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# ── Main classification function ──────────────────────────────────────────────
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;detect_tags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&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="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&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;_RE_URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#link&lt;/span&gt;&lt;span class="sh"&gt;"&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;_RE_EMAIL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;  &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#email&lt;/span&gt;&lt;span class="sh"&gt;"&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;_RE_IPV4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;_RE_IPV6&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#ip&lt;/span&gt;&lt;span class="sh"&gt;"&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;_RE_PATH&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#path&lt;/span&gt;&lt;span class="sh"&gt;"&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;_RE_HEX_HASH&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;                       &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#hash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Secret detection: known prefix first, entropy gate as fallback
&lt;/span&gt;    &lt;span class="n"&gt;is_secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&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;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_RE_SECRET_PATTERNS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;is_secret&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt; &lt;span class="ow"&gt;and&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="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;_entropy_bits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;is_secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#secret&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# JSON: only attempt parse if it looks like an object or array
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lstrip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&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="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="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONDecodeError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;pass&lt;/span&gt;

    &lt;span class="c1"&gt;# Code: multi-line with language keywords or dense bracket patterns
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;40&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;_CODE_KEYWORDS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;_CODE_BRACKETS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromkeys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# deduplicate, preserve order
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Secret Detection: Two Layers
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;#secret&lt;/code&gt; classifier uses a two-step approach:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1 — Known prefixes.&lt;/strong&gt; Patterns like &lt;code&gt;ghp_&lt;/code&gt;, &lt;code&gt;AKIA&lt;/code&gt;, &lt;code&gt;eyJ&lt;/code&gt; (JWT header) are unambiguous. Match any of them and the tag is assigned immediately, no entropy calculation needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2 — Entropy gate.&lt;/strong&gt; For everything else: single-line, no spaces, at least 16 characters, and a pool-based keyspace entropy ≥ 90 bits. The 90-bit threshold was tuned empirically to catch random high-entropy tokens, API keys, and strong bearer credentials while skipping short identifiers, dictionary words, and common camelCase variable names.&lt;/p&gt;

&lt;p&gt;Both conditions guard against a common pitfall: pure Shannon entropy on raw text produces too many false positives on long sentences or dense code snippets. The entropy function here is &lt;code&gt;H = len × log₂(pool_size)&lt;/code&gt; — a measure of the &lt;em&gt;theoretical keyspace&lt;/em&gt;, not character frequency distribution.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note on non-ASCII characters:&lt;/strong&gt; Because &lt;code&gt;any(c not in string.ascii_letters + string.digits ...)&lt;/code&gt; catches non-ASCII characters (e.g. Arabic script, accented letters, emoji), unicode text will trigger the &lt;code&gt;+32&lt;/code&gt; symbols branch. This heuristic is tuned primarily for ASCII credentials; multilingual script entropy is handled gracefully without throwing exceptions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Known Limitations
&lt;/h3&gt;

&lt;p&gt;Honest engineering requires talking about edge cases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Long Paths tagged as &lt;code&gt;#secret&lt;/code&gt;:&lt;/strong&gt; A Unix path like &lt;code&gt;/home/kareem/StudioProjects/DotGhostBoard/main.py&lt;/code&gt; currently receives both &lt;code&gt;#path&lt;/code&gt; and &lt;code&gt;#secret&lt;/code&gt; — it satisfies the entropy gate because it has no spaces and mixes uppercase, lowercase, digits, and &lt;code&gt;/&lt;/code&gt;. We've logged this as a known limitation; v2.2 will add &lt;code&gt;#path&lt;/code&gt; as an explicit exclusion condition for the entropy fallback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hashes flagged as &lt;code&gt;#secret&lt;/code&gt;:&lt;/strong&gt; A 32-character hex string (like an MD5 digest) uses digits + lowercase (&lt;code&gt;pool = 36&lt;/code&gt;), yielding &lt;code&gt;32 × log₂(36) ≈ 165 bits&lt;/code&gt;, which clears the ≥ 90-bit gate. Consequently, MD5 and SHA digests receive both &lt;code&gt;#hash&lt;/code&gt; and &lt;code&gt;#secret&lt;/code&gt;; a dedicated check to prioritize &lt;code&gt;#hash&lt;/code&gt; alone is slated for v2.2.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  2. What the Tag Enables: Contextual Smart Actions
&lt;/h2&gt;

&lt;p&gt;Tags are displayed as colored pill chips on each card. More importantly, they drive &lt;strong&gt;Contextual Smart Actions&lt;/strong&gt; — buttons that appear dynamically in the card toolbar based on what was detected.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tag&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;#link&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;🔗 Open Link&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;QDesktopServices.openUrl()&lt;/code&gt; — no copy-paste to browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;#json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;{ }&lt;/code&gt; Format JSON&lt;/td&gt;
&lt;td&gt;Parses, re-serializes with 2-space indent, writes back to clipboard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;#email&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✉ Compose&lt;/td&gt;
&lt;td&gt;Constructs a &lt;code&gt;mailto:&lt;/code&gt; URI, hands off to default mail client&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;#ip&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;📡 Copy IP&lt;/td&gt;
&lt;td&gt;Extracts just the IP from surrounding log noise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;#secret&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;🛡 → Vault&lt;/td&gt;
&lt;td&gt;Opens Vault drawer, prefills secret dialog, purges plaintext from history&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here's the JSON formatter — the action developers use most:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ui/widgets/item_card.py (simplified)
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_on_format_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_raw_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;formatted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ensure_ascii&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;QApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clipboard&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;setText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;formatted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_flash_badge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;✓ Formatted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONDecodeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_flash_badge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;✗ Invalid JSON&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One button. Minified JSON in, readable JSON back in your clipboard. No editor, no extra window.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Facfb25eq5b5pldkhxpkp.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Facfb25eq5b5pldkhxpkp.png" alt="DotGhostBoard 2.1 Smart Auto-Tagging &amp;amp; Contextual Actions Live UI" width="800" height="216"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 1: Auto-detected tags (&lt;code&gt;#link&lt;/code&gt;, &lt;code&gt;#ip&lt;/code&gt;, &lt;code&gt;#code&lt;/code&gt;) and dynamic contextual smart actions (&lt;code&gt;🔗 Open Link&lt;/code&gt;, &lt;code&gt;📡 Copy IP&lt;/code&gt;) on clipboard cards.&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  3. Pipeline Architecture
&lt;/h2&gt;

&lt;p&gt;The classifier only stays fast because it runs inside a well-isolated pipeline:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb7mwvntrvq6rrmbusbb2.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb7mwvntrvq6rrmbusbb2.png" alt="DotGhostBoard 2.1 Ingestion &amp;amp; Classification Pipeline" width="800" height="382"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 2: The 4-phase sequential classification and dispatch pipeline.&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;[System Clipboard] ──► ClipboardWatcher (background polling / events)
                             │
                             ▼
                    ClipboardPipeline (pure policy layer)
                      ├── App filter      (skip blacklisted sources)
                      ├── Self-paste guard (skip our own copy events)
                      ├── AutoTagger      (sync, pure, no I/O)
                      └── Duplicate &amp;amp; pin policy
                             │
                             ▼
                    HistoryService ──► SQLite (PRAGMA secure_delete)
                             │
                             ▼
                    HistoryController ──► CardsView (lazy render)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;AutoTagger&lt;/code&gt; has zero dependencies on Qt, SQLite, or any network code. That isolation means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The UI thread is never blocked by classification.&lt;/li&gt;
&lt;li&gt;Unit tests need no display server or mocked widgets.&lt;/li&gt;
&lt;li&gt;You can swap, extend, or replace tagging rules without touching controllers or storage.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. Measured Latency
&lt;/h2&gt;

&lt;p&gt;We wanted hard performance guarantees rather than assumptions. Here are &lt;code&gt;timeit&lt;/code&gt; measurements on typical clipboard payloads running on an &lt;strong&gt;Intel Core i5-8350U @ 1.70GHz (Python 3.14.7, Linux x86_64)&lt;/strong&gt;, 1,000 iterations each:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxa88jjpqkrr6fvqc6da0.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxa88jjpqkrr6fvqc6da0.png" alt="DotGhostBoard 2.1 Classification Latency Benchmarks" width="800" height="382"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 3: Microsecond-level classification benchmark measurements across typical clipboard payloads.&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;Payload                      min µs    median µs    max µs
──────────────────────────────────────────────────────────
URL                             8.2          8.8      10.7
GitHub PAT                      6.5          6.8      10.1
IPv4 address                    7.1          7.5      12.7
MD5 hash                        7.1          7.2       7.6
JSON (small, ~80 chars)        14.1         14.4      16.1
Code snippet                   20.3         20.4      21.4
Plain English text             14.8         15.2      15.3
JSON (1KB payload)             80.3         81.8      82.6
Worst-case dense (7.8KB)     1380.0       1410.6    1460.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "&amp;lt; 1ms" guarantee holds for typical clipboard payloads (&amp;lt; 5KB), with small items (URLs, hashes, JSON) processing in 8–82 µs. At the 8KB cap, worst-case dense tokens (7.8KB of unspaced text forcing character-by-character entropy evaluation) measure ~1.4ms. Payloads exceeding 8KB automatically bypass the entropy gate to ensure the UI loop never hitches on huge paste events.&lt;/p&gt;




&lt;h2&gt;
  
  
  Coming Up in Part 2
&lt;/h2&gt;

&lt;p&gt;The productivity layer is half the story. The other half is what happens to sensitive data after it's detected.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;&lt;a href="https://dev.to/freerave/dotghostboard-21-leviathan-cryptographic-vault-backups-secret-expiry-password-history-part-3pk8"&gt;Part 2&lt;/a&gt;&lt;/strong&gt;, we cover the security architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔐 &lt;strong&gt;Standalone &lt;code&gt;.vault&lt;/code&gt; encrypted backups&lt;/strong&gt; — AES-256-GCM packages with AAD header authentication, zero plaintext on disk, PBKDF2 key derivation.&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;CSPRNG Password &amp;amp; Token Generator&lt;/strong&gt; — Live keyspace entropy meter calibrated to 131.1 bits.&lt;/li&gt;
&lt;li&gt;⏳ &lt;strong&gt;Secret Expiry Tracking&lt;/strong&gt; — Calendar-based expiry with &lt;code&gt;⛔ EXPIRED&lt;/code&gt; and &lt;code&gt;⚠️ 3d left&lt;/code&gt; badges.&lt;/li&gt;
&lt;li&gt;📜 &lt;strong&gt;Encrypted Version History&lt;/strong&gt; — Last 3 versions retained, one-click atomic revert.&lt;/li&gt;
&lt;li&gt;🧹 &lt;strong&gt;Coordinated Plaintext Sweep&lt;/strong&gt; — Purging matching unencrypted history records upon Vault deletion.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📦 Try DotGhostBoard 2.1
&lt;/h2&gt;

&lt;p&gt;100% free and open-source — &lt;strong&gt;Apache-2.0 License.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/kareem2099/DotGhostBoard" rel="noopener noreferrer"&gt;kareem2099/DotGhostBoard&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenDesktop / KDE Store:&lt;/strong&gt; &lt;a href="https://www.opendesktop.org/p/2353623/" rel="noopener noreferrer"&gt;DotGhostBoard on OpenDesktop&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/kareem2099/DotGhostBoard.git
&lt;span class="nb"&gt;cd &lt;/span&gt;DotGhostBoard

python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

python3 main.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Keyboard Shortcuts
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Shortcut&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Ctrl+Alt+V&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Summon / hide Dashboard (migrates to active workspace)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Ctrl+Alt+Space&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Floating Spotlight Quick Search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Ctrl+Shift+V&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Open / close The Vault encrypted drawer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;Questions about the classification heuristics, the entropy threshold choice, or the action pipeline? Drop them in the comments. 👻&lt;/p&gt;

</description>
      <category>linux</category>
      <category>opensource</category>
      <category>python</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Building an Automated, API-Driven Stats &amp; Version Synchronizer for VS Code Extensions with 24-Hour Smart Caching (dotUniverse v1.2.0)</title>
      <dc:creator>freerave</dc:creator>
      <pubDate>Mon, 28 Sep 2026 14:01:29 +0000</pubDate>
      <link>https://dev.to/freerave/building-an-automated-api-driven-stats-version-synchronizer-for-vs-code-extensions-with-24-hour-4inm</link>
      <guid>https://dev.to/freerave/building-an-automated-api-driven-stats-version-synchronizer-for-vs-code-extensions-with-24-hour-4inm</guid>
      <description>&lt;h2&gt;
  
  
  How I built a dual-path synchronization architecture for 7 VS Code extensions using VS Marketplace &amp;amp; Open VSX APIs, 24-hour client caching, and daily zero-dependency GitHub Actions.
&lt;/h2&gt;

&lt;p&gt;As an open-source creator building a growing ecosystem of developer tools, one repetitive chore kept draining my time: &lt;strong&gt;keeping metrics and version numbers in sync&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;My portfolio, &lt;a href="https://universe.dotsuite.dev/" rel="noopener noreferrer"&gt;&lt;strong&gt;dotUniverse&lt;/strong&gt;&lt;/a&gt;, showcases 20+ open-source tools—including 7 VS Code extensions published across both the &lt;strong&gt;Visual Studio Marketplace&lt;/strong&gt; and the &lt;strong&gt;Eclipse Open VSX Registry&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Every time I bumped a release or checked how many developers installed &lt;a href="https://github.com/kareem2099/codetune" rel="noopener noreferrer"&gt;CodeTune&lt;/a&gt;, &lt;a href="https://github.com/kareem2099/dotcommand" rel="noopener noreferrer"&gt;dotcommand&lt;/a&gt;, or &lt;a href="https://github.com/kareem2099/DotShare" rel="noopener noreferrer"&gt;DotShare&lt;/a&gt;, I had to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log into multiple web dashboards.&lt;/li&gt;
&lt;li&gt;Manually calculate cross-platform sums.&lt;/li&gt;
&lt;li&gt;Edit HTML cards, version badges, and repository README tables by hand.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Manual upkeep is tedious, error-prone, and quickly falls behind. I wanted an &lt;strong&gt;automated, dual-path synchronization architecture&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;At Runtime:&lt;/strong&gt; Visitors see fresh, API-backed numbers and versions fetched directly from store APIs, cached intelligently on the client for 24 hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;At Build/Repository Time:&lt;/strong&gt; Static fallbacks in &lt;code&gt;index.html&lt;/code&gt; and markdown tables in &lt;code&gt;README.md&lt;/code&gt; are automatically kept fresh via a scheduled, zero-dependency GitHub Action.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is a deep dive into how I built this system using pure modern JavaScript, 24-hour client-side caching, and GitHub Actions CI/CD.&lt;/p&gt;




&lt;h2&gt;
  
  
  🏛️ The Architecture: Dual-Path Synchronization
&lt;/h2&gt;

&lt;p&gt;Instead of treating this as a simple "fetch data in JavaScript" problem, I designed a &lt;strong&gt;dual-path synchronization pipeline&lt;/strong&gt; sharing a single source of truth:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                  ┌──────────────────────────────┐
                  │ Visual Studio Marketplace    │
                  │   &amp;amp; Open VSX Registry APIs   │
                  └──────────────┬───────────────┘
                                 │
             ┌───────────────────┴───────────────────┐
             │                                       │
      [Runtime Path]                           [Build/CI Path]
             │                                       │
             ▼                                       ▼
  extension-stats.js                     sync-extension-stats.mjs
             │                                       │
      ┌──────┴──────┐                         ┌──────┴──────┐
      ▼             ▼                         ▼             ▼
  24h Cache     Fresh DOM                 index.html    README.md
 (localStorage) (No Flicker)              (Fallback)    (Markdown)
                                                     │
                                                     ▼
                                              Git Commit &amp;amp; Push
                                              (Only on Changes)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This decoupled design guarantees that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;First-time and repeat visitors&lt;/strong&gt; get an instant render from static markup or &lt;code&gt;localStorage&lt;/code&gt; without waiting for API responses to render the initial content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search engine crawlers&lt;/strong&gt; and developers browsing the GitHub repository see accurate static numbers even without executing client-side JavaScript.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🏗️ The Challenge: Two Stores, Fragmented Metrics
&lt;/h2&gt;

&lt;p&gt;Tracking extensions across the VS Code ecosystem is fragmented:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;VS Code Marketplace&lt;/strong&gt;: Owned by Microsoft. Its web interface displays a single "Installs" counter, but its underlying gallery API exposes separate metrics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open VSX Registry&lt;/strong&gt;: The vendor-neutral marketplace used by VSCodium, Gitpod, and Eclipse Theia. It provides a clean REST API with different metric structures.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Crucially, &lt;strong&gt;both APIs support CORS with &lt;code&gt;access-control-allow-origin: *&lt;/code&gt;&lt;/strong&gt;, allowing modern browsers to query them directly without requiring a proxy server.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔍 Layer 1: Querying Store APIs Directly
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Batch-Querying Visual Studio Marketplace
&lt;/h3&gt;

&lt;p&gt;Instead of making 7 separate HTTP requests, Microsoft's Marketplace API allows querying multiple extensions in a single &lt;code&gt;POST&lt;/code&gt; request to &lt;code&gt;_apis/public/gallery/extensionquery&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchVsMarketplace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;extensions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="na"&gt;criteria&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;extensions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ext&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;filterType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vsId&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
    &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="na"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;914&lt;/span&gt; &lt;span class="c1"&gt;// Requests statistics, versions, and metadata&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Accept&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json;api-version=3.0-preview.1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ext&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;results&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="nx"&gt;extensions&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statistics&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="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statisticName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Empirical verification: Microsoft Marketplace UI displays (install + downloadCount)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;installs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;install&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="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;downloadCount&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="c1"&gt;// The API returns the latest release first in versions[0]&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;versions&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="nx"&gt;version&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;extensionName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&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="nx"&gt;installs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;map&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;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Empirical Verification on Marketplace Installs:&lt;/strong&gt; Microsoft's API returns two separate download metrics: &lt;code&gt;install&lt;/code&gt; (active installed instances) and &lt;code&gt;downloadCount&lt;/code&gt; (package downloads/updates). Through empirical cross-referencing between the API response and the live web pages for all 7 extensions, the "Installs" figure shown on the Marketplace product page aligned with &lt;code&gt;(stats.install + stats.downloadCount)&lt;/code&gt;. This was observed across the seven extensions tested during development; it should be treated as an empirical observation rather than a guaranteed API contract.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  2. Querying Open VSX Registry
&lt;/h3&gt;

&lt;p&gt;Open VSX provides an open REST endpoint at &lt;code&gt;https://open-vsx.org/api/{namespace}/{extension}&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchOpenVsx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;extensions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ovsxExts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;extensions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ovsxId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;promises&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ovsxExts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ext&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;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://open-vsx.org/api/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ovsxId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&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="na"&gt;downloads&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;downloadCount&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="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Open VSX fetch failed for &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&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;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allSettled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;promises&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;map&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;
  
  
  ⚡ Layer 2: The Client-Side Module (&lt;code&gt;extension-stats.js&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://github.com/kareem2099/dotuniverse/tree/main/js/modules" rel="noopener noreferrer"&gt;dotUniverse's modular architecture&lt;/a&gt;, we created &lt;code&gt;js/modules/extension-stats.js&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why a 24-Hour Cache is Essential
&lt;/h3&gt;

&lt;p&gt;Firing external API requests on every page refresh is bad engineering—it wastes bandwidth, risks third-party rate limits, and causes text flicker.&lt;/p&gt;

&lt;p&gt;Because developer tools are typically released over days or weeks, a &lt;strong&gt;24-hour cache TTL&lt;/strong&gt; (&lt;code&gt;24 * 60 * 60 * 1000&lt;/code&gt;) is a practical default: it reduces unnecessary API requests while keeping the displayed statistics reasonably fresh:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ExtensionStats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;CACHE_KEY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dotuniverse_ext_stats_v1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;CACHE_TTL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 24 hours (1 day)&lt;/span&gt;

  &lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. Instantly apply cached data to DOM (avoids visible flicker)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCached&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="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;applyToDOM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Fetch in background only when the 24-hour window expires&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="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CACHE_TTL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refresh&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="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Clean DOM Mutation via Semantic Attributes
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;index.html&lt;/code&gt;, each extension card includes a semantic &lt;code&gt;data-ext-name&lt;/code&gt; identifier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"ext-card"&lt;/span&gt; &lt;span class="na"&gt;data-ext-name=&lt;/span&gt;&lt;span class="s"&gt;"codetune"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"ext-header"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"ext-name"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"prefix"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Code&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"suffix"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Tune&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"ext-version"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;v1.3.0&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  ...
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"ext-stores"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"store-badge vs"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"store-info"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"store-name"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;VS Marketplace&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"store-dl"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;737 installs&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"store-badge ovsx"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"store-info"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"store-name"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Open VSX&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"store-dl"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;1,500 downloads&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When fresh metrics arrive:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;.ext-version&lt;/code&gt; updates to the store's current version (e.g. &lt;code&gt;v1.3.0&lt;/code&gt;, &lt;code&gt;v2.2.1&lt;/code&gt;, &lt;code&gt;v3.5.1&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Store badges update with formatted counts (e.g., &lt;code&gt;1,040 installs&lt;/code&gt;, &lt;code&gt;6,000 downloads&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The grand total (&lt;code&gt;19,500+&lt;/code&gt;) is calculated and smoothly updated in both the header stats and the hero banner.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🤖 Layer 3: Automated Daily CI/CD Synchronization
&lt;/h2&gt;

&lt;p&gt;To keep the repository &lt;code&gt;README.md&lt;/code&gt; and static HTML in sync for users without JavaScript or search bots, we created a zero-dependency Node script: &lt;code&gt;scripts/sync-extension-stats.mjs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Running this script fetches live data, parses &lt;code&gt;index.html&lt;/code&gt; and &lt;code&gt;README.md&lt;/code&gt;, updates markdown tables, Shields.io badges, and prints a formatted terminal overview:&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="nv"&gt;$ &lt;/span&gt;node scripts/sync-extension-stats.mjs

🔄 Fetching live extension stats from VS Marketplace and Open VSX...

📊 Live Extension Stats:
┌─────────┬──────────────┬──────────┬────────────────┬──────────┬─────────────────┐
│ &lt;span class="o"&gt;(&lt;/span&gt;index&lt;span class="o"&gt;)&lt;/span&gt; │ Extension    │ Version  │ VS Marketplace │ Open VSX │ Total Downloads │
├─────────┼──────────────┼──────────┼────────────────┼──────────┼─────────────────┤
│ 0       │ &lt;span class="s1"&gt;'CodeTune'&lt;/span&gt;   │ &lt;span class="s1"&gt;'v1.3.0'&lt;/span&gt; │ &lt;span class="s1"&gt;'734'&lt;/span&gt;          │ &lt;span class="s1"&gt;'1,486'&lt;/span&gt;  │ &lt;span class="s1"&gt;'2,220'&lt;/span&gt;         │
│ 1       │ &lt;span class="s1"&gt;'dotcommand'&lt;/span&gt; │ &lt;span class="s1"&gt;'v2.0.1'&lt;/span&gt; │ &lt;span class="s1"&gt;'778'&lt;/span&gt;          │ &lt;span class="s1"&gt;'970'&lt;/span&gt;    │ &lt;span class="s1"&gt;'1,748'&lt;/span&gt;         │
│ 2       │ &lt;span class="s1"&gt;'DotEnvy'&lt;/span&gt;    │ &lt;span class="s1"&gt;'v2.2.1'&lt;/span&gt; │ &lt;span class="s1"&gt;'758'&lt;/span&gt;          │ &lt;span class="s1"&gt;'2,173'&lt;/span&gt;  │ &lt;span class="s1"&gt;'2,931'&lt;/span&gt;         │
│ 3       │ &lt;span class="s1"&gt;'DotShare'&lt;/span&gt;   │ &lt;span class="s1"&gt;'v3.5.1'&lt;/span&gt; │ &lt;span class="s1"&gt;'1,040'&lt;/span&gt;        │ &lt;span class="s1"&gt;'5,987'&lt;/span&gt;  │ &lt;span class="s1"&gt;'7,027'&lt;/span&gt;         │
│ 4       │ &lt;span class="s1"&gt;'DotFetch'&lt;/span&gt;   │ &lt;span class="s1"&gt;'v2.1.1'&lt;/span&gt; │ &lt;span class="s1"&gt;'470'&lt;/span&gt;          │ &lt;span class="s1"&gt;'2,766'&lt;/span&gt;  │ &lt;span class="s1"&gt;'3,236'&lt;/span&gt;         │
│ 5       │ &lt;span class="s1"&gt;'DotReadme'&lt;/span&gt;  │ &lt;span class="s1"&gt;'v1.2.0'&lt;/span&gt; │ &lt;span class="s1"&gt;'364'&lt;/span&gt;          │ &lt;span class="s1"&gt;'1,719'&lt;/span&gt;  │ &lt;span class="s1"&gt;'2,083'&lt;/span&gt;         │
│ 6       │ &lt;span class="s1"&gt;'DotSense'&lt;/span&gt;   │ &lt;span class="s1"&gt;'v1.3.0'&lt;/span&gt; │ &lt;span class="s1"&gt;'257'&lt;/span&gt;          │ &lt;span class="s1"&gt;'0'&lt;/span&gt;      │ &lt;span class="s1"&gt;'257'&lt;/span&gt;           │
└─────────┴──────────────┴──────────┴────────────────┴──────────┴─────────────────┘

🏆 Grand Total Downloads: 19,502 &lt;span class="o"&gt;(&lt;/span&gt;Marketplace: 4,401, Open VSX: 15,101&lt;span class="o"&gt;)&lt;/span&gt;

✅ index.html updated successfully.
✅ README.md updated successfully.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To run this hands-free, we added &lt;code&gt;.github/workflows/update-extension-stats.yml&lt;/code&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Auto Update Extension Stats&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Runs everyday at midnight UTC&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# Allows manual trigger from GitHub UI&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;sync-stats&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Sync extension downloads and versions&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node scripts/sync-extension-stats.mjs&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Commit and push changes if updated&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;git config --global user.name "github-actions[bot]"&lt;/span&gt;
          &lt;span class="s"&gt;git config --global user.email "github-actions[bot]@users.noreply.github.com"&lt;/span&gt;
          &lt;span class="s"&gt;git add index.html README.md&lt;/span&gt;
          &lt;span class="s"&gt;if git diff --staged --quiet; then&lt;/span&gt;
            &lt;span class="s"&gt;echo "No changes detected."&lt;/span&gt;
          &lt;span class="s"&gt;else&lt;/span&gt;
            &lt;span class="s"&gt;git commit -m "chore: auto-update extension downloads and versions [skip ci]"&lt;/span&gt;
            &lt;span class="s"&gt;git push&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Clean Commit History:&lt;/strong&gt; The &lt;code&gt;git diff --staged --quiet&lt;/code&gt; check ensures that GitHub Actions only commits when marketplace metrics or versions actually change. This keeps the Git history clean from empty daily noise.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🚀 The Bigger Picture: Building in Public
&lt;/h2&gt;

&lt;p&gt;Automating tool statistics ties directly into how I build in public. Alongside extensions, dotUniverse features an open &lt;strong&gt;Growth Challenge Tracker&lt;/strong&gt; across 9 creator and developer platforms.&lt;/p&gt;

&lt;p&gt;Here is the current state for &lt;strong&gt;October 2026&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Current (Now)&lt;/th&gt;
&lt;th&gt;Target (Goal)&lt;/th&gt;
&lt;th&gt;Remaining&lt;/th&gt;
&lt;th&gt;Progress&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🚀 &lt;strong&gt;dev.to&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3,986&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4,200&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;+214&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;95%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;💼 &lt;strong&gt;LinkedIn&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;709&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;800&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;+91&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;89%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🎵 &lt;strong&gt;TikTok&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;37&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;60&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;+23&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;62%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;▶️ &lt;strong&gt;YouTube&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;30&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;60&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;+30&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;50%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✍️ &lt;strong&gt;Medium&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;+4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;60%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;📘 &lt;strong&gt;Facebook&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;15&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;+5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;75%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;📸 &lt;strong&gt;Instagram&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;+3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;70%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;𝕏 &lt;strong&gt;X / Twitter&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;18&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;+2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;90%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🦋 &lt;strong&gt;Bluesky&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;21&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;30&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;+9&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;70%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On dev.to specifically, we recently passed &lt;strong&gt;15,400+ total views&lt;/strong&gt;, &lt;strong&gt;93 published posts&lt;/strong&gt;, &lt;strong&gt;97 reactions&lt;/strong&gt;, and &lt;strong&gt;67 comments&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Because dev.to is currently at 3,986 / 4,200, we explicitly commented out the celebratory &lt;code&gt;15-DAY RECORD&lt;/code&gt; badge in the HTML until the 4,200 mark is officially crossed. Milestones are meaningful only when they are genuine!&lt;/p&gt;




&lt;h2&gt;
  
  
  💡 Key Engineering Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Leverage Dual-Path Synchronization&lt;/strong&gt;: Combining client-side background sync with scheduled CI/CD ensures that both web visitors and static repository viewers get consistently updated extension statistics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explore Public API Capabilities&lt;/strong&gt;: Both the Visual Studio Marketplace and Open VSX support CORS directly. Before introducing a custom serverless proxy, inspect the network headers first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Respect the Client Cache&lt;/strong&gt;: A 24-hour &lt;code&gt;localStorage&lt;/code&gt; TTL reduces visible flickering, avoids API-dependent initial rendering on repeat visits, respects third-party rate limits, and matches the realistic release frequency of developer tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conditional CI/CD Commits&lt;/strong&gt;: Always check &lt;code&gt;git diff --staged --quiet&lt;/code&gt; before committing automated bot changes to keep your repository history concise and meaningful.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🔗 Project Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🌐 &lt;strong&gt;Live Website&lt;/strong&gt;: &lt;a href="https://universe.dotsuite.dev/" rel="noopener noreferrer"&gt;universe.dotsuite.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;GitHub Repository&lt;/strong&gt;: &lt;a href="https://github.com/kareem2099/dotuniverse" rel="noopener noreferrer"&gt;kareem2099/dotuniverse&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;VS Code Extensions&lt;/strong&gt;: Search for &lt;code&gt;FreeRave&lt;/code&gt; on VS Code Marketplace or Open VSX!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;How do you automate metrics across your open-source repositories? Do you rely on static shields or live client-side queries? Let's discuss in the comments below!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>vscode</category>
      <category>opensource</category>
    </item>
    <item>
      <title>The VSIX Packaging Trap: How I Eliminated Embedded Client Secrets from a VS Code Extension</title>
      <dc:creator>freerave</dc:creator>
      <pubDate>Fri, 25 Sep 2026 15:04:51 +0000</pubDate>
      <link>https://dev.to/freerave/the-vsix-packaging-trap-how-i-eliminated-embedded-client-secrets-from-a-vs-code-extension-2650</link>
      <guid>https://dev.to/freerave/the-vsix-packaging-trap-how-i-eliminated-embedded-client-secrets-from-a-vs-code-extension-2650</guid>
      <description>&lt;h2&gt;
  
  
  A .vsix is just a ZIP archive of plain JavaScript. Here is the post-mortem of how our build-time secrets failed, how we designed dynamic device registration with OS SecretStorage, and how we solved the 'scanner scanning the scanner' paradox.
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: Distributing a client-server VS Code extension means accepting a hard truth: &lt;strong&gt;a &lt;code&gt;.vsix&lt;/code&gt; file is just a ZIP archive containing JavaScript that can be extracted and inspected.&lt;/strong&gt; This article breaks down how our initial attempt to inject build-time backend secrets was scrapped, how we engineered an anonymous dynamic device handshake with OS-level &lt;code&gt;SecretStorage&lt;/code&gt;, and how we resolved the bizarre paradox of static security scanners flagging our own secret detection patterns.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Illusion of the Extension Package
&lt;/h2&gt;

&lt;p&gt;When developers distribute software through the VS Code Marketplace or the Open VSX Registry, it is easy to unconsciously think of the &lt;code&gt;.vsix&lt;/code&gt; file as a compiled, opaque binary.&lt;/p&gt;

&lt;p&gt;It is not.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;.vsix&lt;/code&gt; package is an Open Packaging Conventions (OPC) archive — an ordinary ZIP file containing your extension’s manifest (&lt;code&gt;package.json&lt;/code&gt;), assets, and compiled JavaScript output (&lt;code&gt;out/&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Anyone can verify this with standard shell utilities:&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;# Rename and extract any published extension&lt;/span&gt;
&lt;span class="nb"&gt;cp &lt;/span&gt;DotEnvy-2.1.0.vsix DotEnvy.zip
unzip &lt;span class="nt"&gt;-q&lt;/span&gt; DotEnvy.zip &lt;span class="nt"&gt;-d&lt;/span&gt; extracted/
&lt;span class="nb"&gt;cd &lt;/span&gt;extracted/extension/out

&lt;span class="c"&gt;# Search for sensitive variables&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"SHARED_SECRET"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you injected an API key, an HMAC signing secret, or a database token into your client code at compile time, &lt;strong&gt;you just published that secret to the entire world.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Context: Why Did We Need a Secret in the First Place?
&lt;/h2&gt;

&lt;p&gt;In &lt;a&gt;Part 1 of this series&lt;/a&gt;, I explained how we built &lt;strong&gt;Aegis&lt;/strong&gt; — a 4-layer AI secret detection pipeline for &lt;a href="https://marketplace.visualstudio.com/items?itemName=FreeRave.dotenvy" rel="noopener noreferrer"&gt;DotEnvy&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The first three layers (Regex, Community Blacklist, and Entropy Gating) execute locally inside the editor process. But Layer 4 requires invoking our Python microservice (&lt;code&gt;aegis.dotsuite.dev&lt;/code&gt;) on Railway to run contextual neural classification.&lt;/p&gt;

&lt;p&gt;To protect the backend against unauthorized abuse, spam, and denial-of-service, we required incoming HTTP requests to be signed using &lt;strong&gt;HMAC-SHA256&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;$$\text{Signature} = \text{HMAC-SHA256}(\text{SharedSecret}, \text{Timestamp} + "." + \text{Payload})$$&lt;/p&gt;

&lt;p&gt;This design introduced our fundamental dilemma:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How does an open-source, publicly distributed client authenticate with a protected backend service without shipping a hardcoded secret in its package?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Attempt 1: The Build-Time Secret Trap (v2.1.0)
&lt;/h2&gt;

&lt;p&gt;Our initial approach relied on build-time environment variable substitution. In our deployment pipeline, a Node script injected a secret before invoking &lt;code&gt;vsce package&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// scripts/build-with-env.js (Flawed Approach)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PRODUCTION_EXTENSION_SECRET&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;out/utils/llmAnalyzer.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;REPLACE_AT_BUILD_TIME&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;out/utils/llmAnalyzer.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While this kept the secret out of our public git repository, it was an architectural disaster:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The VSIX Leak&lt;/strong&gt;: As demonstrated above, extracting the &lt;code&gt;.vsix&lt;/code&gt; exposed the secret immediately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated Scanner Flags&lt;/strong&gt;: Automated CI/CD bots (GitHub Secret Scanning, Trufflehog, CodeQL) automatically inspect release assets. A high-entropy hex string inside compiled JavaScript triggers immediate alerts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single Point of Compromise&lt;/strong&gt;: If one user decompiles the extension and leaks the shared secret, the backend can no longer trust &lt;em&gt;any&lt;/em&gt; extension client without rotating the key and breaking every installed instance worldwide.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We scrapped this design entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  Attempt 2: Zero Embedded Client Secrets &amp;amp; Dynamic Device Handshake (v2.1.2)
&lt;/h2&gt;

&lt;p&gt;We established a strict architectural rule: &lt;strong&gt;Zero Embedded / Static Client Secrets&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Not a single secret, token, or private key may exist in the source code, build scripts, or packaged &lt;code&gt;.vsix&lt;/code&gt; bundle.&lt;/p&gt;

&lt;p&gt;Instead, we designed an anonymous, zero-touch dynamic device registration handshake:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────────────────────────────────┐           ┌────────────────────────────────────────┐
│ DotEnvy Client (First Activation)      │           │ Aegis Backend (aegis.dotsuite.dev)     │
└───────────────────┬────────────────────┘           └───────────────────┬────────────────────┘
                    │                                                    │
                    │ 1. POST /extension/register                        │
                    │    { machine_id: hash(vscode.env.machineId) }      │
                    ├───────────────────────────────────────────────────►│ Strict IP Rate Limiter
                    │                                                    │ (Max 10 per hour per IP)
                    │                                                    │
                    │                                                    │ 2. Backend generates unique
                    │                                                    │    cryptographic client_secret
                    │ 3. Returns per-device secret                       │    and stores active record
                    │◄───────────────────────────────────────────────────┤
                    │                                                    │
                    ▼                                                    ▼
          Stored Exclusively in                                Subsequent requests signed via
         OS-level SecretStorage                                HMAC-SHA256(timestamp + body)
       (macOS Keychain / Windows                               with per-machine rate limiting
      Credential Manager / libsecret)                                 (30 req/min)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  1. Anonymous Environment Identity
&lt;/h3&gt;

&lt;p&gt;When DotEnvy launches for the first time, it generates a non-reversible identifier from VS Code's environment API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;getMachineId&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;vscode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;machineId&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;anonymous-machine&lt;/span&gt;&lt;span class="dl"&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;&lt;code&gt;vscode.env.machineId&lt;/code&gt; provides a pseudonymous environment identifier that lets the backend maintain per-client state and rate limits without requiring a user account.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Ephemeral Dynamic Registration
&lt;/h3&gt;

&lt;p&gt;The extension calls &lt;code&gt;/extension/register&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Defense in Depth&lt;/strong&gt;: The registration endpoint is protected by strict IP-based rate limiting (maximum 10 registrations per hour per IP) to prevent Sybil registration floods.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unique Credential Issuance&lt;/strong&gt;: The backend generates an independent 256-bit cryptographically random secret (&lt;code&gt;client_secret&lt;/code&gt;), persists the mapping in PostgreSQL, and returns it over TLS.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. OS-Level SecretStorage
&lt;/h3&gt;

&lt;p&gt;The extension never writes the returned secret to disk, &lt;code&gt;.env&lt;/code&gt; files, or standard workspace settings. It stores the credential directly inside VS Code's &lt;code&gt;SecretStorage&lt;/code&gt; API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;setSharedSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&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;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dotenvy.llm.sharedSecret&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sharedSecret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;secret&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;Under the hood, VS Code delegates &lt;code&gt;SecretStorage&lt;/code&gt; to native operating system keyrings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;macOS&lt;/strong&gt;: Apple Keychain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows&lt;/strong&gt;: Windows Credential Manager&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linux&lt;/strong&gt;: Secret Service API (gnome-keyring / KWallet)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Authenticated Request Signing
&lt;/h3&gt;

&lt;p&gt;For all subsequent requests, DotEnvy signs its payloads using HMAC-SHA256:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;signRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sharedSecret&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="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signature&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;On the backend, Aegis:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validates the timestamp against a &lt;strong&gt;5-minute sliding window&lt;/strong&gt; to prevent replay attacks.&lt;/li&gt;
&lt;li&gt;Looks up the &lt;code&gt;client_secret&lt;/code&gt; corresponding to the &lt;code&gt;X-Machine-ID&lt;/code&gt; header.&lt;/li&gt;
&lt;li&gt;Uses Python's constant-time comparison &lt;code&gt;hmac.compare_digest&lt;/code&gt; to prevent timing attacks.&lt;/li&gt;
&lt;li&gt;Applies a machine-level rate limit (30 requests/minute).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a specific client secret is ever compromised on a developer's local machine, revoking that single record does not disrupt any other user.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Automated Gatekeeper: CI/CD Pre-Publish Validation
&lt;/h2&gt;

&lt;p&gt;To ensure no developer or automated script ever accidentally reintroduces an embedded secret, we added an automated pre-publish scanner to &lt;a&gt;&lt;code&gt;scripts/build-with-env.js&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// scripts/build-with-env.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;outDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&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="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;..&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;out&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;scanDir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;flagged&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fullPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&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="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;f&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="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;statSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fullPath&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isDirectory&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;flagged&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;scanDir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fullPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fullPath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fullPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;// Check for illegal embedded secret patterns&lt;/span&gt;
            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/const&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+embeddedSecret&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*=&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;"'&lt;/span&gt;&lt;span class="se"&gt;][&lt;/span&gt;&lt;span class="sr"&gt;A-Za-z0-9_-&lt;/span&gt;&lt;span class="se"&gt;]{20,}[&lt;/span&gt;&lt;span class="sr"&gt;"'&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`❌ Security Alert: Hardcoded embeddedSecret found in &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;fullPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;!`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nx"&gt;flagged&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="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;flagged&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;scanDir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outDir&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="nx"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`\n❌ Pre-publish check failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; leaked secret(s) found in out/!`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&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="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;✅ Security check passed: 0 embedded secrets found in compiled out/ bundle.&lt;/span&gt;&lt;span class="dl"&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;This verification script runs automatically as part of &lt;code&gt;npm run package&lt;/code&gt;. If a secret is detected, the build halts with an exit code of &lt;code&gt;1&lt;/code&gt;, aborting the release pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  The "Scanner Scanning the Scanner" Paradox (v2.1.3)
&lt;/h2&gt;

&lt;p&gt;Once we eliminated embedded secrets, we collided with an ironic roadblock: &lt;strong&gt;automated static security scanners started blocking our extension releases.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;DotEnvy is a security extension designed to detect leaked credentials. Its pattern registry naturally contains regular expressions for popular API tokens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ v2.1.0: Literal regex prefixes&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;KNOWN_SECRET_PATTERNS&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AWS Access Key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/AKIA&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;0-9A-Z&lt;/span&gt;&lt;span class="se"&gt;]{16}&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Stripe Live Key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/sk_live_&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;0-9a-zA-Z&lt;/span&gt;&lt;span class="se"&gt;]{24,}&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GitHub Token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="na"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/ghp_&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;a-zA-Z0-9&lt;/span&gt;&lt;span class="se"&gt;]{36}&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Google API Key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/AIza&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;0-9A-Za-z&lt;/span&gt;&lt;span class="se"&gt;\-&lt;/span&gt;&lt;span class="sr"&gt;_&lt;/span&gt;&lt;span class="se"&gt;]{35}&lt;/span&gt;&lt;span class="sr"&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;When GitHub’s push protection and marketplace upload scanners inspected our compiled output, their naive string-matching heuristics triggered on our own code:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Warning: Leaked AWS Access Key pattern detected in out/utils/llmAnalyzer.js"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The scanners couldn't differentiate between code &lt;em&gt;containing&lt;/em&gt; a secret and code &lt;em&gt;designed to detect&lt;/em&gt; a secret.&lt;/p&gt;

&lt;h3&gt;
  
  
  Avoiding Static Scanner False Positives via Runtime Pattern Construction
&lt;/h3&gt;

&lt;p&gt;To resolve these false positives without weakening our detection capabilities, we refactored the pattern registry to assemble sensitive token prefixes dynamically at runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ v2.1.3: Assembled at runtime — clean scanner bill of health&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;KNOWN_SECRET_PATTERNS&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AWS Access Key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
        &lt;span class="na"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RegExp&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;KIA&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[0-9A-Z]{16}&lt;/span&gt;&lt;span class="dl"&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="dl"&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Stripe Live Key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="na"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RegExp&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_live_&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[0-9a-zA-Z]{24,}&lt;/span&gt;&lt;span class="dl"&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="dl"&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GitHub Token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    
        &lt;span class="na"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RegExp&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;g&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hp_&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[a-zA-Z0-9]{36}&lt;/span&gt;&lt;span class="dl"&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="dl"&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Google API Key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
        &lt;span class="na"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RegExp&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AI&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;za&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[0-9A-Za-z&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;-_]{35}&lt;/span&gt;&lt;span class="dl"&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="dl"&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;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Engineering Note&lt;/strong&gt;: This runtime string assembly is &lt;strong&gt;not an evasion technique against security controls&lt;/strong&gt;; it is a pragmatic packaging workaround to prevent regex-based linters from confusing detection definitions with genuine credential leaks. Inside Node's V8 engine, the compiled &lt;code&gt;RegExp&lt;/code&gt; objects are identical and execute at native engine speed.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Build Hygiene &amp;amp; VSIX Purity
&lt;/h2&gt;

&lt;p&gt;Our final hardening step addressed package purity:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Automatic Pre-Build Purging&lt;/strong&gt;: We updated our &lt;code&gt;package.json&lt;/code&gt; scripts to mandate &lt;code&gt;rm -rf out&lt;/code&gt; before every invocation of &lt;code&gt;tsc&lt;/code&gt;. Stale, uncompiled test artifacts from previous development sessions can never accidentally be swept into a production &lt;code&gt;.vsix&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aggressive &lt;code&gt;.vscodeignore&lt;/code&gt; Hardening&lt;/strong&gt;: We audited and excluded all non-production files:

&lt;ul&gt;
&lt;li&gt;Development linter configs (&lt;code&gt;eslint.config.mjs&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Internal test runners and mock fixtures&lt;/li&gt;
&lt;li&gt;Temporary extraction folders (&lt;code&gt;extracted-vsix/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Documentation drafts and article markdowns&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The resulting &lt;code&gt;.vsix&lt;/code&gt; bundle contains solely the minified runtime code and localized UI assets.&lt;/p&gt;




&lt;h2&gt;
  
  
  Checklist for VS Code Extension Developers
&lt;/h2&gt;

&lt;p&gt;If you are building a VS Code extension that interacts with a private backend, keep these rules in mind:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;th&gt;Bad Practice ❌&lt;/th&gt;
&lt;th&gt;Good Practice ✅&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Client Secrets&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Injecting API keys in build scripts&lt;/td&gt;
&lt;td&gt;Zero embedded secrets; dynamic device handshake&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Local Storage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Saving tokens in &lt;code&gt;workspaceState&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Storing credentials in native OS &lt;code&gt;SecretStorage&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Request Signing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Sending plain machine IDs&lt;/td&gt;
&lt;td&gt;HMAC-SHA256 with timestamp sliding window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scanner Hygiene&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hardcoded token regex literals&lt;/td&gt;
&lt;td&gt;Dynamic runtime pattern construction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Package Hygiene&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Relying on default package contents&lt;/td&gt;
&lt;td&gt;Automated &lt;code&gt;rm -rf out&lt;/code&gt; + strict &lt;code&gt;.vscodeignore&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Links &amp;amp; Verification
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🔗 &lt;strong&gt;VS Code Marketplace&lt;/strong&gt;: &lt;a href="https://marketplace.visualstudio.com/items?itemName=FreeRave.dotenvy" rel="noopener noreferrer"&gt;marketplace.visualstudio.com/items?itemName=FreeRave.dotenvy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🌐 &lt;strong&gt;Open VSX Registry&lt;/strong&gt;: &lt;a href="https://open-vsx.org/extension/freerave/dotenvy" rel="noopener noreferrer"&gt;open-vsx.org/extension/freerave/dotenvy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;GitHub Repository&lt;/strong&gt;: &lt;a href="https://github.com/kareem2099/dotenvy" rel="noopener noreferrer"&gt;github.com/kareem2099/dotenvy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📖 &lt;strong&gt;Read Part 1&lt;/strong&gt;: &lt;a href="https://dev.to/freerave/dotenvy-aegis-building-a-4-layer-ai-secret-detection-pipeline-for-vs-code-115o"&gt;DotEnvy Aegis: Building a 4-Layer AI Secret Detection Pipeline for VS Code&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📝 &lt;strong&gt;Full Changelog&lt;/strong&gt;: &lt;a href="https://github.com/kareem2099/dotenvy/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Written by FreeRave. If you've encountered packaging headaches or static scanner false alarms in your extensions, feel free to reach out and share your experience.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>vscode</category>
      <category>security</category>
      <category>typescript</category>
      <category>architecture</category>
    </item>
    <item>
      <title>DotEnvy Aegis: Building a 4-Layer AI Secret Detection Pipeline for VS Code</title>
      <dc:creator>freerave</dc:creator>
      <pubDate>Thu, 24 Sep 2026 20:38:11 +0000</pubDate>
      <link>https://dev.to/freerave/dotenvy-aegis-building-a-4-layer-ai-secret-detection-pipeline-for-vs-code-115o</link>
      <guid>https://dev.to/freerave/dotenvy-aegis-building-a-4-layer-ai-secret-detection-pipeline-for-vs-code-115o</guid>
      <description>&lt;h2&gt;
  
  
  How I architected a multi-layer secret scanner combining regex, truncated composite community hashing, Shannon entropy gating, and a 35-feature neural classifier — cutting LLM overhead by ~80%.
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: DotEnvy Aegis introduces a 4-layer secret detection pipeline (Regex → Truncated Composite Community Blacklist → Shannon Entropy Gate → Neural Context Classifier), migrates encryption keys to the OS keychain, auto-authorizes backups with project master keys, and syncs hashes to an anti-poisoning community blacklist on Railway — all without transmitting raw secret payloads.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What is DotEnvy?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=FreeRave.dotenvy" rel="noopener noreferrer"&gt;DotEnvy&lt;/a&gt; is an open-source VS Code extension designed to bring enterprise-grade security to local &lt;code&gt;.env&lt;/code&gt; file management:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Encrypted Variable Management&lt;/strong&gt; (AES-256-GCM, PBKDF2 with 310,000 iterations)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-User Envelope Encryption&lt;/strong&gt; (each teammate decrypts using individual credentials)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud Synchronization&lt;/strong&gt; (native Doppler, Vercel, and AWS integrations)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI-Powered Secret Detection&lt;/strong&gt; (the focus of this article)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Git Pre-Commit Hooks&lt;/strong&gt; (blocking credential leaks before commits land in git history)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment Analytics, Timeline, &amp;amp; Diff&lt;/strong&gt; panels&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The extension communicates with &lt;strong&gt;Aegis&lt;/strong&gt; (&lt;code&gt;python-llm&lt;/code&gt;), a custom Python microservice deployed on Railway that runs contextual neural classification on candidate secrets.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: One Layer is Never Enough
&lt;/h2&gt;

&lt;p&gt;In our earliest experimental prototype, secret detection was a blunt instrument: every candidate token triggered an HTTP request to the backend model.&lt;/p&gt;

&lt;p&gt;This naive architecture fell apart under real-world development workloads:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Latency Overhead&lt;/strong&gt;: Every network round-trip added 200–450ms of latency, quickly degrading editor responsiveness during live typing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Availability Dependency&lt;/strong&gt;: If the network dropped or the backend restarted, secret detection failed silently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The False Positive/Negative Dilemma&lt;/strong&gt;: 

&lt;ul&gt;
&lt;li&gt;A high-entropy random string (like an image hash or UUID) looks like a secret to entropy analyzers, but is harmless.&lt;/li&gt;
&lt;li&gt;A low-entropy or structured token looks harmless to statistical math, but may grant full database admin rights.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Context matters. Regex patterns matter. Math matters. Community intelligence matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Aegis solves this by decoupling detection into an L1 → L4 pipeline: four independent, complementary layers where each layer filters out noise before invoking the more expensive layer downstream.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The L1–L4 Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Candidate: (secretValue, contextLine, variableName)
                     │
                     ▼
┌──────────────────────────────────────────┐
│  L1: Regex Pattern Engine                │ ← Sub-millisecond, in-process
│  Matches well-known vendor prefixes      │   High-confidence match → HIGH
└────────────────────┬─────────────────────┘
                     │ no match
                     ▼
┌──────────────────────────────────────────┐
│  L2: Community Blacklist Cache           │ ← O(1) in-memory Set&amp;lt;string&amp;gt;
│  Truncated composite hash lookup         │   Known leaked hash → HIGH
└────────────────────┬─────────────────────┘
                     │ not in blacklist
                     ▼
┌──────────────────────────────────────────┐
│  L3: Shannon Entropy Gate                │ ← Pure mathematical calculation
│  Normalized entropy &amp;lt; 3.5 → LOW (skip)   │   Filters ~80% of candidate tokens
└────────────────────┬─────────────────────┘
                     │ entropy ≥ 3.5
                     ▼
┌──────────────────────────────────────────┐
│  L4: Contextual Neural Classifier        │ ← Remote FastAPI service
│  35-feature vector + attention layers    │   Deep semantic risk analysis
└──────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In our current workload benchmarks across typical codebases, &lt;strong&gt;approximately 20% of extracted candidates reach L4&lt;/strong&gt;. The remaining ~80% are resolved in-memory with sub-millisecond execution times, saving substantial network bandwidth and cloud compute.&lt;/p&gt;




&lt;h3&gt;
  
  
  Layer 1: In-Process Regex Engine (Instant Confidence)
&lt;/h3&gt;

&lt;p&gt;Known secret formats (like AWS access keys, Stripe live tokens, and GitHub personal access tokens) follow strict, deterministic grammars. Running an AI model to identify a string that starts with &lt;code&gt;AKIA&lt;/code&gt; followed by 16 alphanumeric characters is wasteful.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;KNOWN_SECRET_PATTERNS&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AWS Access Key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RegExp&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;KIA&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[0-9A-Z]{16}&lt;/span&gt;&lt;span class="dl"&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="dl"&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Stripe Live Key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RegExp&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_live_&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[0-9a-zA-Z]{24,}&lt;/span&gt;&lt;span class="dl"&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="dl"&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GitHub Token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="na"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RegExp&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;g&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hp_&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[a-zA-Z0-9]{36}&lt;/span&gt;&lt;span class="dl"&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="dl"&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Google API Key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RegExp&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AI&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;za&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[0-9A-Za-z&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;-_]{35}&lt;/span&gt;&lt;span class="dl"&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="dl"&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;&lt;em&gt;An L1 match immediately assigns &lt;code&gt;high&lt;/code&gt; risk, bypasses all subsequent layers, and triggers an asynchronous hash report to the community blacklist.&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Layer 2: Community Blacklist via Truncated Composite Hashing
&lt;/h3&gt;

&lt;p&gt;When a secret is identified and verified across the community, other developers should benefit instantly without having to re-analyze it through the LLM. However, sharing leaked secrets raises an immediate privacy question: &lt;strong&gt;How do you cross-reference secrets without transmitting sensitive values over the network?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;L2 utilizes &lt;strong&gt;truncated composite hashing&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;hashEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;variableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Only capture the variable name and the first 8 characters of the value&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&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="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;variableName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&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="mi"&gt;16&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;h4&gt;
  
  
  Privacy Nuance &amp;amp; Threat Model
&lt;/h4&gt;

&lt;p&gt;We deliberately avoid claiming that this hashing scheme is an "absolute privacy shield":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What it accomplishes&lt;/strong&gt;: The full secret is never transmitted across the wire or stored in community databases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The engineering trade-off&lt;/strong&gt;: For very short secrets or known formats, truncated prefixes could theoretically be susceptible to targeted dictionary guessing. We treat this composite hash as an effective deduplication fingerprint for collaborative defense, not a one-way cryptographic commitment for sensitive data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On extension activation, DotEnvy pulls the promoted blacklist hashes into an in-memory &lt;code&gt;Set&amp;lt;string&amp;gt;&lt;/code&gt;. Any candidate lookup is an $O(1)$ memory check with zero network overhead.&lt;/p&gt;

&lt;h4&gt;
  
  
  Server-Side Anti-Poisoning Architecture
&lt;/h4&gt;

&lt;p&gt;To prevent malicious clients from poisoning the blacklist or whitelisting actual credentials, submissions must pass a server-side reputation pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client Submits Hash
        │
        ▼
Staging Queue (Requires 3 distinct environments + reputation weighting)
        │
        ▼
Server-Side Verification (Backend re-evaluates risk on the pattern)
        │
        ▼
Promoted to Community Blacklist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Threat Model Note&lt;/strong&gt;: The 3-environment quorum and historical accuracy weighting are designed to &lt;strong&gt;raise the operational cost of poisoning attacks&lt;/strong&gt;, rather than claiming Byzantine Sybil-proof consensus. High-accuracy reporters gain vote weight, while repeated false reporters are banned.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Layer 3: Shannon Entropy Gate
&lt;/h3&gt;

&lt;p&gt;Shannon entropy quantifies the unpredictability and information density of a string:&lt;/p&gt;

&lt;p&gt;$$H(X) = -\sum_{i=1}^{n} P(x_i) \log_2 P(x_i)$$&lt;/p&gt;

&lt;p&gt;In our feature extraction pipeline, Shannon entropy is calculated and normalized into a standard feature scalar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;features&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extractFeatures&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secretValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;variableName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entropy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;features&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;8.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Denormalize f[7]&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entropy&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;3.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;low&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Skip expensive L4 inference entirely&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plain text words and standard identifiers (&lt;code&gt;const databaseHost = "localhost"&lt;/code&gt;) exhibit low entropy ($\approx 2.1 - 2.8$). Cryptographic secrets and API tokens (&lt;code&gt;sk_live_9xQ8...&lt;/code&gt;) typically exhibit entropy values above $4.2$. &lt;/p&gt;

&lt;p&gt;Filtering out low-entropy candidates at L3 eliminates the vast majority of false alarms before a single byte touches the network.&lt;/p&gt;




&lt;h3&gt;
  
  
  Layer 4: Contextual Neural Classifier (Aegis)
&lt;/h3&gt;

&lt;p&gt;Tokens that survive L1–L3 enter L4. This layer evaluates a &lt;strong&gt;35-dimensional feature vector&lt;/strong&gt; capturing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Text Morphology (6)&lt;/strong&gt;: Uppercase ratio, digit ratio, symbol density, length normalization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entropy Gradients (3)&lt;/strong&gt;: Shannon entropy, bigram entropy, trigram entropy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pattern Signals (3)&lt;/strong&gt;: Base64 padding, hex character sets, character transition frequency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context Semantics (5)&lt;/strong&gt;: Surrounding keywords (&lt;code&gt;Bearer&lt;/code&gt;, &lt;code&gt;Authorization&lt;/code&gt;, &lt;code&gt;db_pass&lt;/code&gt;, &lt;code&gt;private_key&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identifier Conventions (4)&lt;/strong&gt;: &lt;code&gt;ALL_CAPS&lt;/code&gt; naming, presence of &lt;code&gt;SECRET&lt;/code&gt;/&lt;code&gt;TOKEN&lt;/code&gt;/&lt;code&gt;KEY&lt;/code&gt; substrings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structural Separators (4)&lt;/strong&gt;: Dot delimiters (JWTs), underscores, dashes, colons.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Derived Interactions (10)&lt;/strong&gt;: Non-linear threshold flags combining entropy with context sensitivity.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# python-llm/src/model.py
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomLLM&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Neural risk classifier with self-attention and backpropagation.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ModelConfig&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DenseLayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hidden_dim&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attention&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MultiHeadSelfAttention&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hidden_dim&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_heads&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;classifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FeedForward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hidden_dim&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;num_classes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;optimizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Adam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;learning_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.001&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At its current stage, the classifier is trained on an &lt;strong&gt;experimental curated dataset of 112+ labeled secret and non-secret samples&lt;/strong&gt;. It runs backpropagation via Adam optimization, with model weights persisted in a PostgreSQL database surviving service redeploys.&lt;/p&gt;




&lt;h2&gt;
  
  
  Important Philosophy: Risk Signals, Not a Security Oracle
&lt;/h2&gt;

&lt;p&gt;A vital design principle behind Aegis:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;No layer is treated as an absolute security oracle.&lt;/strong&gt; &lt;br&gt;
The pipeline generates heuristic risk classifications, not mathematical proofs of secrecy. L1 provides deterministic pattern matching for known vendor formats, while entropy and neural inference provide weighted signals to aid human developers in reviewing their environments.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When the remote model is unreachable or rate limits are reached, the extension seamlessly falls back to local heuristic scoring without crashing or blocking the developer.&lt;/p&gt;




&lt;h2&gt;
  
  
  OS-Level Key Security: SecretStorage Migration
&lt;/h2&gt;

&lt;p&gt;A secret detector is useless if the extension itself stores encryption keys insecurely. &lt;/p&gt;

&lt;p&gt;In earlier versions, master keys lived in VS Code’s &lt;code&gt;workspaceState&lt;/code&gt; (essentially unencrypted JSON storage). In &lt;strong&gt;Aegis&lt;/strong&gt;, all master encryption keys and PBKDF2 salts were migrated to VS Code’s OS-level &lt;strong&gt;&lt;code&gt;SecretStorage&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;macOS&lt;/strong&gt;: Apple Keychain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows&lt;/strong&gt;: Windows Credential Manager&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linux&lt;/strong&gt;: Secret Service API / gnome-keyring
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;ensureMasterKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;vscode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ExtensionContext&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secretsKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SECRET_STORAGE_KEY_PREFIX&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// 1. Check OS SecretStorage&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secretsKey&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="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Zero-touch migration from legacy workspaceState&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;legacyKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workspaceState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secretsKey&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="nx"&gt;legacyKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secretsKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;legacyKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workspaceState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secretsKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Secure cleanup&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;legacyKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Cryptographically secure 256-bit key generation&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secretsKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;key&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 migration is completely transparent to the user, executing silently upon extension activation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Test Verification
&lt;/h2&gt;

&lt;p&gt;Both the TypeScript extension and the Python backend are verified by end-to-end test suites:&lt;/p&gt;

&lt;h3&gt;
  
  
  DotEnvy Extension (VS Code)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🧪 Running Complete DotEnvy Test Suite

✅ [Encryption Algorithms &amp;amp; PBKDF2]           8/8 passed
✅ [L1–L4 Multi-Layer AI Scanner &amp;amp; HMAC]      4/4 passed  
✅ [Master Key Lifecycle, Password Migration]  9/9 passed

🎉 21/21 TESTS PASSED
npm run compile   → 0 errors
npm run lint      → 0 warnings
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Aegis Backend Service (FastAPI / PyTorch)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✅ Feature extractor: 35 features verified - OK
✅ Neural model architecture - loss=1.1534
✅ Forward pass evaluation - prediction: high
✅ Full analyze pipeline execution - confidence: medium
✅ Database round-trip test - weights save/load OK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Next Challenge: Shipping the Detector
&lt;/h2&gt;

&lt;p&gt;Designing a 4-layer AI scanner was only half the journey. &lt;/p&gt;

&lt;p&gt;When we attempted to package this extension into a &lt;code&gt;.vsix&lt;/code&gt; bundle and distribute it through the VS Code Marketplace and Open VSX, we collided with two massive engineering road hazards:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The VSIX Trap&lt;/strong&gt;: How our attempt to authenticate extension requests with our backend accidentally created a critical secret leak vector.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Scanner Paradox&lt;/strong&gt;: Why static analysis bots accused DotEnvy of leaking secrets simply because it contained the regex patterns needed to detect them!&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;👉 &lt;strong&gt;Read the complete post-mortem in Part 2&lt;/strong&gt;:&lt;br&gt;&lt;br&gt;
&lt;strong&gt;&lt;a href="https://dev.to/freerave/the-vsix-packaging-trap-how-i-eliminated-embedded-client-secrets-from-a-vs-code-extension-2650"&gt;The VSIX Packaging Trap: How I Eliminated Embedded Client Secrets from a VS Code Extension&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Links &amp;amp; Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🔗 &lt;strong&gt;VS Code Marketplace&lt;/strong&gt;: &lt;a href="https://marketplace.visualstudio.com/items?itemName=FreeRave.dotenvy" rel="noopener noreferrer"&gt;marketplace.visualstudio.com/items?itemName=FreeRave.dotenvy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🌐 &lt;strong&gt;Open VSX Registry&lt;/strong&gt;: &lt;a href="https://open-vsx.org/extension/freerave/dotenvy" rel="noopener noreferrer"&gt;open-vsx.org/extension/freerave/dotenvy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;GitHub Repository&lt;/strong&gt;: &lt;a href="https://github.com/kareem2099/dotenvy" rel="noopener noreferrer"&gt;github.com/kareem2099/dotenvy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐍 &lt;strong&gt;Backend Health&lt;/strong&gt;: &lt;code&gt;https://aegis.dotsuite.dev/health&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;📝 &lt;strong&gt;Changelog&lt;/strong&gt;: &lt;a href="https://github.com/kareem2099/dotenvy/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Written by FreeRave. Contributions and community feedback are always welcome.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>vscode</category>
      <category>security</category>
      <category>typescript</category>
      <category>python</category>
    </item>
  </channel>
</rss>
