<?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: minnogit</title>
    <description>The latest articles on DEV Community by minnogit (@minnogit).</description>
    <link>https://dev.to/minnogit</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%2F673072%2Fc33fe002-ee6b-4e5b-b3af-2b2e113a2433.jpg</url>
      <title>DEV Community: minnogit</title>
      <link>https://dev.to/minnogit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/minnogit"/>
    <language>en</language>
    <item>
      <title>Updating an Android app without the Play Store: a manifest, an APK and a read-only share</title>
      <dc:creator>minnogit</dc:creator>
      <pubDate>Fri, 30 Oct 2026 15:00:57 +0000</pubDate>
      <link>https://dev.to/minnogit/updating-an-android-app-without-the-play-store-a-manifest-an-apk-and-a-read-only-share-83o</link>
      <guid>https://dev.to/minnogit/updating-an-android-app-without-the-play-store-a-manifest-an-apk-and-a-read-only-share-83o</guid>
      <description>&lt;p&gt;We hand out Android tablets to people who use our app in the field. Most of the devices are ours, nobody signs in with a personal Google account, and we do not want the app in a public store. Still, when a release is mandatory, every tablet has to pick it up, and whoever publishes the release should not need a developer's laptop and an adb cable.&lt;/p&gt;

&lt;p&gt;This article describes how we update the app without a marketplace: a small JSON manifest, a single APK, two authenticated endpoints on the backend, and a publishing path built on a read-only NFS share and a WebDAV folder. I keep the hostnames and paths generic on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the app is not in a store
&lt;/h2&gt;

&lt;p&gt;The app is built for police forces. It is part of a service we provide to them under contract, and it works together with our backend. A listing in a public store would put the APK within reach of anyone, and anyone who holds a copy can decompile it.&lt;/p&gt;

&lt;p&gt;Keeping the file out of a public listing does not make reverse engineering impossible. It narrows who can get the file in the first place. The real boundary stays on the backend, where every call is authenticated and scoped to a tenant.&lt;/p&gt;

&lt;p&gt;Some customers also want to use tablets they already own, which we do not manage. That rules out the approach of enrolling devices in a management system of our own, so we deliver the update ourselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I wanted from the design
&lt;/h2&gt;

&lt;p&gt;The fleet is small and known. Updates can be mandatory. The tablets already log in to our backend and carry a JWT, so the backend is the one place every device is willing to talk to. Adding a public static web server just for APKs would mean a second thing to secure, so I decided against it and let the backend serve the files.&lt;/p&gt;

&lt;p&gt;I also wanted the publishing side to be boring. One person uploads two files and the tablets do the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two files, one version at a time
&lt;/h2&gt;

&lt;p&gt;The share contains exactly two files: a manifest and the APK. There is no version history and no per-device targeting.&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.4.2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"force"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"note"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Mandatory update with fixes for the inspection form."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;48213377&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sha256"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"9f2c...e41a"&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 manifest has no URL field. An earlier idea was to put the download URL in the manifest, so the APK could carry its version in the file name. I dropped it. With a fixed file name, the manifest cannot point the app anywhere: the app downloads from the backend it is logged in to, and the file is always the same one. Having one fixed name was also easier to explain to the person who publishes.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;size&lt;/code&gt; and &lt;code&gt;sha256&lt;/code&gt; describe the APK. The app uses them to check that the download is complete before it tries to install anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens on the tablet
&lt;/h2&gt;

&lt;p&gt;After login the app calls &lt;code&gt;GET /api/app-version&lt;/code&gt; and compares the version in the manifest with the one it is running. If the manifest is newer and &lt;code&gt;force&lt;/code&gt; is true, the app blocks the normal screens until the update is installed. If &lt;code&gt;force&lt;/code&gt; is false, it offers the update and lets the user postpone.&lt;/p&gt;

&lt;p&gt;The download is &lt;code&gt;GET /api/app-apk&lt;/code&gt;, with the same bearer token as every other call. The response carries &lt;code&gt;Content-Length&lt;/code&gt;, which is what lets the app draw a real progress bar with percentage and megabytes. When the file is complete, the app compares size and sha256 with the manifest and hands the APK to the Android package installer.&lt;/p&gt;

&lt;p&gt;Android puts two conditions on this. The first is that the app needs the "install unknown apps" permission, which since Android 8 is granted per app, so someone has to allow it once on each tablet. The second is that an update must be signed with the same key as the installed version and must have a higher &lt;code&gt;versionCode&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The signature check is the real safety net. Whoever can write to the share cannot make a tablet install an APK signed with a different key. They could still make every tablet download a file, so the write credential deserves care, and the signing key stays off the server entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two endpoints
&lt;/h2&gt;

&lt;p&gt;Both endpoints sit behind the same middleware as the rest of the API: JWT validation and the tenant header. Both read from a directory given by one environment variable, &lt;code&gt;APP_RELEASES_PATH&lt;/code&gt;, so no path is hard-coded.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/api/app-version&lt;/code&gt; returns the manifest as it is, or a 404 if the file is missing. &lt;code&gt;/api/app-apk&lt;/code&gt; streams the APK with &lt;code&gt;Content-Type: application/vnd.android.package-archive&lt;/code&gt;, &lt;code&gt;Content-Length&lt;/code&gt; and a &lt;code&gt;Content-Disposition&lt;/code&gt; header, or a 404.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the files live
&lt;/h2&gt;

&lt;p&gt;The backend runs in a container on one machine, while the people who publish upload to another. I wanted a path between the two that the backend can only read.&lt;/p&gt;

&lt;p&gt;On the file server, the directory is exported over NFSv4, read-only, to the private network of the application nodes. It is a dedicated export. The account that can write through WebDAV only ever reaches this directory and nothing else on the server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/exports on the file server
&lt;/span&gt;/&lt;span class="n"&gt;export&lt;/span&gt;/&lt;span class="n"&gt;releases&lt;/span&gt;   &lt;span class="m"&gt;10&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;/&lt;span class="m"&gt;16&lt;/span&gt;(&lt;span class="n"&gt;ro&lt;/span&gt;,&lt;span class="n"&gt;sync&lt;/span&gt;,&lt;span class="n"&gt;no_subtree_check&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the backend node the export is mounted read-only, and the container gets the same directory as a read-only bind mount at the same path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/fstab on the backend node
&lt;/span&gt;&lt;span class="n"&gt;fileserver&lt;/span&gt;:/&lt;span class="n"&gt;releases&lt;/span&gt;  /&lt;span class="n"&gt;mnt&lt;/span&gt;/&lt;span class="n"&gt;releases&lt;/span&gt;  &lt;span class="n"&gt;nfs4&lt;/span&gt;  &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;netdev&lt;/span&gt;,&lt;span class="n"&gt;auto&lt;/span&gt;,&lt;span class="n"&gt;nofail&lt;/span&gt;,&lt;span class="n"&gt;noatime&lt;/span&gt;,&lt;span class="n"&gt;ro&lt;/span&gt;,&lt;span class="n"&gt;tcp&lt;/span&gt;  &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${APP_RELEASES_PATH:-/mnt/releases}:${APP_RELEASES_PATH:-/mnt/releases}:ro"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default value after &lt;code&gt;:-&lt;/code&gt; makes the compose file start even when the variable has not been set yet. If the mount is missing, the directory on the host is empty and the endpoints answer with an error. I accepted that: the container still starts and the failure is visible.&lt;/p&gt;

&lt;p&gt;The directory is owned by the web server user, with mode 0755 for the folder and 0644 for the files. The backend reads them as "other", so there is no need to line up user ids across machines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Publishing with WebDAV
&lt;/h2&gt;

&lt;p&gt;The WebDAV folder is served by the Apache that already runs on the file server, with mod_dav and a dedicated account.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="nc"&gt;Alias&lt;/span&gt; /releases /srv/releases
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nl"&gt;Directory&lt;/span&gt;&lt;span class="sr"&gt; /srv/releases&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="nc"&gt;Dav&lt;/span&gt; &lt;span class="ss"&gt;On&lt;/span&gt;
    &lt;span class="nc"&gt;Options&lt;/span&gt; -Indexes
    &lt;span class="nc"&gt;AuthType&lt;/span&gt; &lt;span class="ss"&gt;Basic&lt;/span&gt;
    &lt;span class="nc"&gt;AuthName&lt;/span&gt; "Releases"
    &lt;span class="nc"&gt;AuthUserFile&lt;/span&gt; /etc/apache2/htpasswd_releases
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nl"&gt;RequireAll&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="nc"&gt;Require&lt;/span&gt; method GET HEAD &lt;span class="ss"&gt;OPTIONS&lt;/span&gt; PUT MOVE DELETE PROPFIND
        &lt;span class="nc"&gt;Require&lt;/span&gt; &lt;span class="ss"&gt;user&lt;/span&gt; publisher
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nl"&gt;RequireAll&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&amp;lt;/&lt;/span&gt;&lt;span class="nl"&gt;Directory&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details are worth explaining. &lt;code&gt;Require method&lt;/code&gt; sits inside a &lt;code&gt;RequireAll&lt;/code&gt; because several &lt;code&gt;Require&lt;/code&gt; lines at the same level are combined with OR, so a bare method restriction next to &lt;code&gt;Require user&lt;/code&gt; would let any authenticated user use any method. And the folder is never listed publicly: there is no directory index, and every request needs credentials.&lt;/p&gt;

&lt;p&gt;A plain &lt;code&gt;PUT&lt;/code&gt; is not atomic, and an APK is tens of megabytes. The backend can start reading a file that is still being written. So each file is uploaded under a temporary name and then renamed with &lt;code&gt;MOVE&lt;/code&gt;, which is a rename on the same filesystem. The order matters too. The APK goes first and the manifest last, because the manifest is what the tablets read to find out that a new version exists.&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;BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://files.example.com/releases

curl &lt;span class="nt"&gt;--netrc-file&lt;/span&gt; ~/.netrc &lt;span class="nt"&gt;-T&lt;/span&gt; app.apk &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;/app.apk.tmp"&lt;/span&gt;
curl &lt;span class="nt"&gt;--netrc-file&lt;/span&gt; ~/.netrc &lt;span class="nt"&gt;-X&lt;/span&gt; MOVE &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;/app.apk.tmp"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Destination: &lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;/app.apk"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Overwrite: T"&lt;/span&gt;

curl &lt;span class="nt"&gt;--netrc-file&lt;/span&gt; ~/.netrc &lt;span class="nt"&gt;-T&lt;/span&gt; version.json &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;/version.json.tmp"&lt;/span&gt;
curl &lt;span class="nt"&gt;--netrc-file&lt;/span&gt; ~/.netrc &lt;span class="nt"&gt;-X&lt;/span&gt; MOVE &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;/version.json.tmp"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Destination: &lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;/version.json"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Overwrite: T"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my tests the &lt;code&gt;PUT&lt;/code&gt; answered 201, the &lt;code&gt;MOVE&lt;/code&gt; answered 201 for a new file and 204 when it overwrote one, and a wrong password gave 401. The credentials live in a &lt;code&gt;.netrc&lt;/code&gt; file with mode 600, so the password never appears on the command line or in the shell history.&lt;/p&gt;

&lt;p&gt;I wrapped the four commands in a small script for Linux and one for Windows. It checks every status code, deletes the &lt;code&gt;.tmp&lt;/code&gt; file if a step fails, and at the end downloads both files again to compare their sha256 with the local ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two limits I know about
&lt;/h2&gt;

&lt;p&gt;Between the &lt;code&gt;MOVE&lt;/code&gt; of the APK and the &lt;code&gt;MOVE&lt;/code&gt; of the manifest there are a few seconds in which the new APK sits next to the old manifest. A tablet that starts a download in that window sees a sha256 that does not match and discards the file. It tries again at the next check, which is acceptable for a fleet that checks at every login.&lt;/p&gt;

&lt;p&gt;The other limit is that a release cannot be rolled back on devices that have already installed it, because Android refuses to install an APK with a lower &lt;code&gt;versionCode&lt;/code&gt;. The share keeps a single version, so a bad release is fixed by publishing a new build with a higher version, and the previous APK has to be kept by whoever builds it.&lt;/p&gt;

&lt;p&gt;There is also no staged rollout. Every tablet that checks after the manifest is replaced gets the new version, and for this fleet I prefer that to the extra machinery.&lt;/p&gt;

</description>
      <category>android</category>
      <category>devops</category>
      <category>backend</category>
      <category>nfs</category>
    </item>
    <item>
      <title>Terminator layouts from the command line: from two to five terminals</title>
      <dc:creator>minnogit</dc:creator>
      <pubDate>Thu, 29 Oct 2026 14:12:00 +0000</pubDate>
      <link>https://dev.to/minnogit/terminator-layouts-from-the-command-line-from-two-to-five-terminals-1ko9</link>
      <guid>https://dev.to/minnogit/terminator-layouts-from-the-command-line-from-two-to-five-terminals-1ko9</guid>
      <description>&lt;p&gt;One of the first things I do every morning at work is open a few terminals. I use &lt;a href="https://gnome-terminator.org/" rel="noopener noreferrer"&gt;Terminator&lt;/a&gt;, a terminal emulator for Linux that splits one window into several terminals. A saved arrangement of splits is called a layout, and Terminator can open a layout directly at startup.&lt;/p&gt;

&lt;p&gt;The first time, I built my layout from the preferences window. It works, but the generated configuration is full of fields you don't need (&lt;code&gt;uuid&lt;/code&gt;, &lt;code&gt;size&lt;/code&gt;, &lt;code&gt;position&lt;/code&gt;, &lt;code&gt;last_active_term&lt;/code&gt;). Writing layouts by hand turned out to be shorter and easier to change, and everything can be done from the terminal. Here are the layouts I tested with Terminator 2.1.6, from two to five terminals.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a layout works
&lt;/h2&gt;

&lt;p&gt;A layout is a tree:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;code&gt;Window&lt;/code&gt; at the root;&lt;/li&gt;
&lt;li&gt;splits inside it: &lt;code&gt;HPaned&lt;/code&gt; puts two children side by side, &lt;code&gt;VPaned&lt;/code&gt; stacks them one above the other;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Terminal&lt;/code&gt; as the leaves.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each element has a name (any name you like, as long as it is unique within the layout), a &lt;code&gt;type&lt;/code&gt; and a &lt;code&gt;parent&lt;/code&gt;. The two children of a split also have an &lt;code&gt;order&lt;/code&gt;: &lt;code&gt;0&lt;/code&gt; is the left or top one, &lt;code&gt;1&lt;/code&gt; the right or bottom one. Without other options each split is 50/50, and &lt;code&gt;ratio&lt;/code&gt; changes it.&lt;/p&gt;

&lt;p&gt;Layouts live in &lt;code&gt;~/.config/terminator/config&lt;/code&gt;, under the &lt;code&gt;[layouts]&lt;/code&gt; section. If you have never changed Terminator's preferences, the file doesn't exist yet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;terminator
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.config/terminator
nano ~/.config/terminator/config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The layouts below go under &lt;code&gt;[layouts]&lt;/code&gt;. If the file already exists, add them to that section instead of creating a second one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two terminals side by side
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+--------+------------------+
|        |                  |
|  left  |      right       |
|        |                  |
+--------+------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[layouts]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nn"&gt;[[two]&lt;/span&gt;&lt;span class="na"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nn"&gt;[[[window]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;Window&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;""&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[split]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;HPaned&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;window&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;ratio&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="s"&gt;0.3&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[left]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;Terminal&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;split&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;order&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="s"&gt;0&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[right]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;Terminal&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;split&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;order&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="s"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;ratio = 0.3&lt;/code&gt; the left terminal takes 30% of the width. Remove the line for two equal halves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two columns, the first split in two
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-------------+-------------+
|  top left   |             |
+-------------+    right    |
| bottom left |             |
+-------------+-------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nn"&gt;[[three]&lt;/span&gt;&lt;span class="na"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nn"&gt;[[[window]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;Window&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;""&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[columns]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;HPaned&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;window&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[left_split]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;VPaned&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;columns&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;order&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="s"&gt;0&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[top_left]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;Terminal&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;left_split&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;order&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="s"&gt;0&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[bottom_left]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;Terminal&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;left_split&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;order&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="s"&gt;1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[right]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;Terminal&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;columns&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;order&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="s"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first child of &lt;code&gt;columns&lt;/code&gt; is not a terminal but another split, and that is all it takes to nest them.&lt;/p&gt;

&lt;h2&gt;
  
  
  A grid of four
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-------------+-------------+
|             |             |
+-------------+-------------+
|             |             |
+-------------+-------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nn"&gt;[[grid]&lt;/span&gt;&lt;span class="na"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nn"&gt;[[[window]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;Window&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;""&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;maximised&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="s"&gt;True&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[columns]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;HPaned&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;window&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[left]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;VPaned&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;columns&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;order&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="s"&gt;0&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[right]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;VPaned&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;columns&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;order&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="s"&gt;1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[t1]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;Terminal&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;left&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;order&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="s"&gt;0&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[t2]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;Terminal&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;left&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;order&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="s"&gt;1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[t3]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;Terminal&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;right&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;order&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="s"&gt;0&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[t4]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;Terminal&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;right&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;order&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="s"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the layout I open every morning. &lt;code&gt;maximised = True&lt;/code&gt; makes the window fill the screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  A spiral of five
&lt;/h2&gt;

&lt;p&gt;This one follows a geometric rule: split the screen in two side by side, then split the second half again, alternating the direction each time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-----------+-----------------+
|           |        2        |
|           +--------+--------+
|     1     |        |   4    |
|           |   3    +--------+
|           |        |   5    |
+-----------+--------+--------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The terminals get smaller at each step, so the font gets smaller with them. The font belongs to a profile, and each terminal can use a different one. Profiles go under &lt;code&gt;[profiles]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[profiles]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nn"&gt;[[large]&lt;/span&gt;&lt;span class="na"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="py"&gt;font&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="s"&gt;Monospace 12&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="py"&gt;use_system_font&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="s"&gt;False&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nn"&gt;[[medium]&lt;/span&gt;&lt;span class="na"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="py"&gt;font&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="s"&gt;Monospace 10&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="py"&gt;use_system_font&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="s"&gt;False&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nn"&gt;[[small]&lt;/span&gt;&lt;span class="na"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="py"&gt;font&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="s"&gt;Monospace 8&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="py"&gt;use_system_font&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="s"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nn"&gt;[[spiral]&lt;/span&gt;&lt;span class="na"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nn"&gt;[[[window]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;Window&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;""&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[s1]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;HPaned&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;window&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[t1]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;Terminal&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;s1&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;order&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="s"&gt;0&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;profile&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="s"&gt;large&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[s2]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;VPaned&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;s1&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;order&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="s"&gt;1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[t2]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;Terminal&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;s2&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;order&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="s"&gt;0&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;profile&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="s"&gt;medium&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[s3]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;HPaned&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;s2&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;order&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="s"&gt;1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[t3]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;Terminal&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;s3&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;order&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="s"&gt;0&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;profile&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="s"&gt;medium&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[s4]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;VPaned&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;s3&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;order&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="s"&gt;1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[t4]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;Terminal&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;s4&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;order&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="s"&gt;0&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;profile&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="s"&gt;small&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[t5]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;Terminal&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;s4&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;order&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="s"&gt;1&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;profile&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="s"&gt;small&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each split is the second child (&lt;code&gt;order = 1&lt;/code&gt;) of the previous one, and the type alternates between &lt;code&gt;HPaned&lt;/code&gt; and &lt;code&gt;VPaned&lt;/code&gt;. The first terminal, the largest, uses &lt;code&gt;large&lt;/code&gt;, the next two &lt;code&gt;medium&lt;/code&gt; and the last two &lt;code&gt;small&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fonts and colors per terminal
&lt;/h2&gt;

&lt;p&gt;A profile can also change the font family and the colors, so the two smallest terminals can look different from the others. For them I use &lt;a href="https://terminus-font.sourceforge.net/" rel="noopener noreferrer"&gt;Terminus&lt;/a&gt;, a monospace font designed to stay sharp at small sizes, and a dark green background:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;fonts-terminus
fc-list : family | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; terminus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Debian the package installs the TTF version, and the family name is &lt;code&gt;Terminus (TTF)&lt;/code&gt;. With just &lt;code&gt;Terminus&lt;/code&gt;, Terminator doesn't find the font and falls back to another one with badly spaced letters, so check the exact name with &lt;code&gt;fc-list&lt;/code&gt;. The &lt;code&gt;small&lt;/code&gt; profile becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nn"&gt;[[small]&lt;/span&gt;&lt;span class="na"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="py"&gt;font&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="s"&gt;Terminus (TTF) 9&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="py"&gt;use_system_font&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="s"&gt;False&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="py"&gt;background_color&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="s"&gt;"#0f2a1d"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="py"&gt;foreground_color&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="s"&gt;"#9fe8b0"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A different background helps to recognize a terminal at a glance, for example the ones following logs. &lt;code&gt;foreground_color&lt;/code&gt; only changes plain text: the colors of the prompt (user in green, path in blue) come from bash's &lt;code&gt;PS1&lt;/code&gt;, not from Terminator.&lt;/p&gt;

&lt;h2&gt;
  
  
  Opening a layout
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terminator &lt;span class="nt"&gt;--list-layouts&lt;/span&gt;     &lt;span class="c"&gt;# check that Terminator reads them&lt;/span&gt;
terminator &lt;span class="nt"&gt;--layout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;grid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a Terminator window is already open, the new command doesn't start a new program: by default Terminator runs as a single instance, and the request goes to the instance already running. That instance still has the configuration it read at startup, so a layout added afterwards is unknown to it, and you get a window with a single terminal. Close all Terminator windows before trying a new layout, or start a separate instance with &lt;code&gt;-u&lt;/code&gt; (&lt;code&gt;--no-dbus&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terminator &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="nt"&gt;--layout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;grid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A command in each terminal
&lt;/h2&gt;

&lt;p&gt;Each terminal can start with a command, for example a connection to a server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[t1]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;Terminal&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;left&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;order&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="s"&gt;0&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;command&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="s"&gt;"ssh admin@server01; exec bash"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;; exec bash&lt;/code&gt; part matters: without it the terminal closes as soon as the command ends, for example when the SSH session drops. With it, you are left with a local shell in the same terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  A launcher for GNOME
&lt;/h2&gt;

&lt;p&gt;The command is already a single line, so in the terminal an alias is enough:&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;echo&lt;/span&gt; &lt;span class="s2"&gt;"alias tgrid='terminator --layout=grid'"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To open it from the GNOME menu or the dock, I create a &lt;code&gt;.desktop&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.local/share/applications
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/.local/share/applications/terminator-grid.desktop &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
[Desktop Entry]
Type=Application
Name=Terminator (grid)
Comment=Terminator with four terminals
Exec=terminator --layout=grid
Icon=terminator
Terminal=false
Categories=System;TerminalEmulator;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;desktop-file-validate ~/.local/share/applications/terminator-grid.desktop&lt;/code&gt; checks the file. The launcher then appears in the activities search under "Terminator (grid)", and from there you can pin it to the dock. For more layouts, one file per layout with a different &lt;code&gt;Name&lt;/code&gt; and &lt;code&gt;Exec&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  One launcher for all layouts
&lt;/h3&gt;

&lt;p&gt;With several layouts, one file each gets tedious. A &lt;code&gt;.desktop&lt;/code&gt; file can also declare actions: GNOME shows them when you right-click the icon, in the dock or in the applications list. So one launcher is enough, with an action for each layout, and a small script can generate it from Terminator's configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# Creates a GNOME launcher for Terminator with one action per configured layout.&lt;/span&gt;
&lt;span class="c"&gt;# The actions appear when you right-click the icon (dock or applications list).&lt;/span&gt;
&lt;span class="c"&gt;# Run it again after adding or removing a layout.&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;dest&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;XDG_DATA_HOME&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="p"&gt;/.local/share&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/applications/terminator-layouts.desktop"&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$dest&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;mapfile&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; layouts &amp;lt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;terminator &lt;span class="nt"&gt;--list-layouts&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s1"&gt;'^default$'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[Desktop Entry]"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Type=Application"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Name=Terminator layouts"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Comment=Terminator with a saved layout"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Exec=terminator -u"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Icon=terminator"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Terminal=false"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Categories=System;TerminalEmulator;"&lt;/span&gt;
  &lt;span class="c"&gt;# action identifiers may only contain letters, digits and hyphens&lt;/span&gt;
  &lt;span class="nv"&gt;ids&lt;/span&gt;&lt;span class="o"&gt;=()&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;l &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;layouts&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&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;do &lt;/span&gt;ids+&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$l&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'A-Za-z0-9-'&lt;/span&gt; &lt;span class="s1"&gt;'-'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Actions=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s;'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="p"&gt;!layouts[@]&lt;/span&gt;&lt;span class="k"&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;do
    &lt;/span&gt;&lt;span class="nv"&gt;l&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;layouts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;echo
    echo&lt;/span&gt; &lt;span class="s2"&gt;"[Desktop Action &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;]"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Name=&lt;/span&gt;&lt;span class="nv"&gt;$l&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Exec=terminator -u --layout=&lt;/span&gt;&lt;span class="nv"&gt;$l&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$dest&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

desktop-file-validate &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$dest&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Created &lt;/span&gt;&lt;span class="nv"&gt;$dest&lt;/span&gt;&lt;span class="s2"&gt; with &lt;/span&gt;&lt;span class="k"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;layouts&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; layouts: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;layouts&lt;/span&gt;&lt;span class="p"&gt;[*]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I saved it as &lt;code&gt;~/bin/terminator-layouts&lt;/code&gt;. A normal click opens a plain Terminator, a right-click lists the layouts. Every action uses &lt;code&gt;-u&lt;/code&gt;, so the layout opens even when another Terminator window is already running with an older configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  A dashboard layout
&lt;/h2&gt;

&lt;p&gt;The spiral turned out to be a good shape for a monitoring dashboard: one large terminal for the overview and smaller ones for details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;btop&lt;/code&gt;, the overview of CPU, memory, disks, network and processes, in the large terminal;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;atop&lt;/code&gt;, detailed system metrics, with one line per CPU core;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;wavemon&lt;/code&gt;, Wi-Fi link quality and signal level;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nvtop&lt;/code&gt;, the GPU (AMD, Intel or Nvidia);&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nload&lt;/code&gt;, network traffic in and out.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;btop atop wavemon nvtop nload fonts-unifont
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two more profiles. &lt;code&gt;retro&lt;/code&gt; uses &lt;a href="https://unifoundry.com/unifont/" rel="noopener noreferrer"&gt;GNU Unifont&lt;/a&gt;, a pixel font that covers almost all of Unicode, so btop's graphs keep every symbol and get an old-computer look. It stays sharp at 12 points, a multiple of its pixel grid. &lt;code&gt;tiny&lt;/code&gt; is the green Terminus profile at 7 points, for the two smallest terminals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nn"&gt;[[retro]&lt;/span&gt;&lt;span class="na"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="py"&gt;font&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="s"&gt;Unifont 12&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="py"&gt;use_system_font&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="s"&gt;False&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nn"&gt;[[tiny]&lt;/span&gt;&lt;span class="na"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="py"&gt;font&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="s"&gt;Terminus (TTF) 7&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="py"&gt;use_system_font&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="s"&gt;False&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="py"&gt;background_color&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="s"&gt;"#0f2a1d"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="py"&gt;foreground_color&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="s"&gt;"#9fe8b0"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The layout is the spiral, with a &lt;code&gt;title&lt;/code&gt; for each terminal and a command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nn"&gt;[[dashboard]&lt;/span&gt;&lt;span class="na"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nn"&gt;[[[window]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;Window&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;""&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;maximised&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="s"&gt;True&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[s1]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;HPaned&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;window&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[btop]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;Terminal&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;s1&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;order&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="s"&gt;0&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;profile&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="s"&gt;retro&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;title&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="s"&gt;btop&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;command&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="s"&gt;"btop; exec bash"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[s2]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;VPaned&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;s1&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;order&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="s"&gt;1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[atop]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;Terminal&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;s2&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;order&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="s"&gt;0&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;profile&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="s"&gt;small&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;title&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="s"&gt;atop&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;command&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="s"&gt;"atop; exec bash"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[s3]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;HPaned&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;s2&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;order&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="s"&gt;1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[wifi]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;Terminal&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;s3&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;order&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="s"&gt;0&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;profile&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="s"&gt;small&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;title&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="s"&gt;wifi&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;command&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="s"&gt;"wavemon; exec bash"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[s4]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;VPaned&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;s3&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;order&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="s"&gt;1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[gpu]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;Terminal&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;s4&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;order&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="s"&gt;0&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;profile&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="s"&gt;tiny&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;title&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="s"&gt;gpu&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;command&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="s"&gt;"nvtop; exec bash"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nn"&gt;[[[net]&lt;/span&gt;&lt;span class="na"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="py"&gt;type&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="s"&gt;Terminal&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;parent&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="s"&gt;s4&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;order&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="s"&gt;1&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;profile&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="s"&gt;tiny&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;title&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="s"&gt;network&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="py"&gt;command&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="s"&gt;"nload wlp2s0; exec bash"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things I learned while putting it together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the font size decides whether a program starts at all: &lt;code&gt;atop&lt;/code&gt; refuses to run in less than 60 columns by 24 lines, so it needs the larger top-right terminal or a small font;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nload&lt;/code&gt; without arguments picks the first network device, which on my laptop is an idle Docker bridge. &lt;code&gt;wlp2s0&lt;/code&gt; is my Wi-Fi interface: &lt;code&gt;ip -br link&lt;/code&gt; lists yours;&lt;/li&gt;
&lt;li&gt;without &lt;code&gt;title&lt;/code&gt;, some terminals show "None" in their title bar;&lt;/li&gt;
&lt;li&gt;not every console program behaves in Terminator. &lt;code&gt;s-tui&lt;/code&gt;, which I tried first for CPU temperature and frequency, filled its terminal with control sequences instead of graphs, so I left it out.&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%2F5z632654zmb96nov0vvx.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%2F5z632654zmb96nov0vvx.png" alt="The dashboard layout: btop, atop, wavemon, nvtop and nload" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Other tools
&lt;/h2&gt;

&lt;p&gt;Terminator splits a window on your desktop. If you also need splits on a remote server, or sessions that survive a disconnection, the tool is a terminal multiplexer like &lt;a href="https://github.com/tmux/tmux" rel="noopener noreferrer"&gt;tmux&lt;/a&gt; or &lt;a href="https://zellij.dev/" rel="noopener noreferrer"&gt;Zellij&lt;/a&gt;, which work inside any terminal, including over SSH. Zellij describes layouts in files, similar to what I did here.&lt;/p&gt;

</description>
      <category>terminal</category>
      <category>linux</category>
      <category>productivity</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Access control and cost monitoring for LLM models on AWS Bedrock</title>
      <dc:creator>minnogit</dc:creator>
      <pubDate>Tue, 27 Oct 2026 08:55:00 +0000</pubDate>
      <link>https://dev.to/minnogit/access-control-and-cost-monitoring-for-llm-models-on-aws-bedrock-20og</link>
      <guid>https://dev.to/minnogit/access-control-and-cost-monitoring-for-llm-models-on-aws-bedrock-20og</guid>
      <description>&lt;p&gt;I recently created an IAM user for an Android app that uses LLM models on Amazon Bedrock. I wanted two things: the app should be able to call only the models I picked, and I wanted to know at any time how much it was costing. This article describes how I set up both, and where the AWS documentation corrected a wrong idea I had.&lt;/p&gt;

&lt;h2&gt;
  
  
  The IAM user and the policy
&lt;/h2&gt;

&lt;p&gt;The user has a single policy, which allows invoking four models in &lt;code&gt;eu-west-1&lt;/code&gt; and nothing else:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BedrockInvokeSpecificModels"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&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="s2"&gt;"bedrock:InvokeModel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"bedrock:InvokeModelWithResponseStream"&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;"Resource"&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="s2"&gt;"arn:aws:bedrock:eu-west-1::foundation-model/qwen.qwen3-vl-235b-a22b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:bedrock:eu-west-1::foundation-model/google.gemma-3-4b-it"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:bedrock:eu-west-1::foundation-model/google.gemma-3-12b-it"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:bedrock:eu-west-1::foundation-model/google.gemma-3-27b-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;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;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BedrockGetModelInfo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&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="s2"&gt;"bedrock:GetFoundationModel"&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;"Resource"&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="s2"&gt;"arn:aws:bedrock:eu-west-1::foundation-model/qwen.qwen3-vl-235b-a22b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:bedrock:eu-west-1::foundation-model/google.gemma-3-4b-it"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:bedrock:eu-west-1::foundation-model/google.gemma-3-12b-it"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:bedrock:eu-west-1::foundation-model/google.gemma-3-27b-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;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="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;Each model is listed by ARN, so if I want to add one in the future I have to do it explicitly. You could use a wildcard to allow all of them, but for an app installed on a device I prefer a closed list.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tag on the user
&lt;/h2&gt;

&lt;p&gt;For costs, I added a tag to the user: key &lt;code&gt;bedrock&lt;/code&gt;, value &lt;code&gt;tablet&lt;/code&gt;. You can do it from the IAM console or from the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws iam tag-user &lt;span class="nt"&gt;--user-name&lt;/span&gt; &amp;lt;user-name&amp;gt; &lt;span class="nt"&gt;--tags&lt;/span&gt; &lt;span class="nv"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;bedrock,Value&lt;span class="o"&gt;=&lt;/span&gt;tablet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A second IAM user, which I use for something else, has the same &lt;code&gt;bedrock&lt;/code&gt; key with the value &lt;code&gt;inno&lt;/code&gt;. This way the same tag groups different users, and the values tell them apart.&lt;/p&gt;

&lt;p&gt;My initial plan was a bit different. I thought AWS couldn't attribute costs to an IAM user, and that the tag was the way around that. That is no longer true: Bedrock records the identity behind each call (IAM user or role), and that data reaches Cost Explorer without changing the policy or the app's code. For CUR 2.0 reports you need a new export instead, created in &lt;em&gt;Data Exports&lt;/em&gt; with the option "Include caller identity (IAM principal) allocation data", which adds the &lt;code&gt;line_item_iam_principal&lt;/code&gt; column. For this case Cost Explorer is enough, and I didn't create the export. The tag is still useful for another reason: it adds a grouping dimension, for example by team, application or device, that you can use in filters and budgets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Activating the tag as a cost allocation tag
&lt;/h2&gt;

&lt;p&gt;A tag on an IAM user doesn't show up in billing data by itself. It has to be activated:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open Billing and Cost Management, &lt;em&gt;Cost allocation tags&lt;/em&gt; section.&lt;/li&gt;
&lt;li&gt;Filter by type &lt;em&gt;IAM principal&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Select the &lt;code&gt;bedrock&lt;/code&gt; tag and press &lt;em&gt;Activate&lt;/em&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Activation is done from the organization's management account, or from a standalone account. From a member account the console returns the error &lt;code&gt;Cost allocation tags are managed at the payer account level&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In cost data the tag is called &lt;code&gt;iamPrincipal/bedrock&lt;/code&gt;. There are three limits to know before expecting numbers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the tag only appears after the user has made at least one call to Bedrock;&lt;/li&gt;
&lt;li&gt;activation is not retroactive, so earlier costs are not tagged;&lt;/li&gt;
&lt;li&gt;the tag can take up to 24 hours to appear in &lt;em&gt;Cost allocation tags&lt;/em&gt; after the first call and up to another 24 to activate, and cost data can then arrive with a further delay of up to 24 hours.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Looking at costs in Cost Explorer
&lt;/h2&gt;

&lt;p&gt;Once the data has arrived, in Cost Explorer I filter on the Amazon Bedrock service and group by the &lt;code&gt;iamPrincipal/bedrock&lt;/code&gt; tag. The finest level of detail is a cost per usage type per day, so you can't see the cost of a single request. For that you need Bedrock's invocation logs, which are a different path and which I haven't enabled.&lt;/p&gt;

&lt;h2&gt;
  
  
  The budget
&lt;/h2&gt;

&lt;p&gt;To avoid discovering an out-of-scale bill at the end of the month, I created a plan in AWS Budgets. In my case, spending on LLM calls is less predictable than spending on the other AWS resources I use, which is why I need the budget here more than elsewhere. I chose a custom cost budget with these parameters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;recurring monthly period, with a fixed amount;&lt;/li&gt;
&lt;li&gt;scope by tag: the &lt;code&gt;iamPrincipal/bedrock&lt;/code&gt; tag, with the values of the IAM users to watch (in my case &lt;code&gt;tablet&lt;/code&gt; and &lt;code&gt;inno&lt;/code&gt;);&lt;/li&gt;
&lt;li&gt;service filter: Amazon Bedrock;&lt;/li&gt;
&lt;li&gt;an alert at 80% of the amount, sent by email. It can also go to Slack, but I haven't set that up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tag filter also offers a value for untagged costs. That is what the service filter is for: without it, I expect that value to also count the untagged costs of the account's other services, though I haven't verified it. With both filters, the budget should count all Bedrock spending, tagged or not.&lt;/p&gt;

&lt;p&gt;If the untagged value is left out, the budget only sees tagged spending. An IAM user calling Bedrock without the tag stays out of the count and doesn't trigger the alert. Either way, it's worth checking Cost Explorer from time to time for untagged Bedrock spending and, if there is any, finding out which user generates it and tagging it.&lt;/p&gt;

&lt;p&gt;A budget only alerts: it doesn't stop the spending. If you need a real block, you have to set up an action tied to the threshold, or revoke the user's credentials.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things to keep in mind
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;If calls go through a gateway (LiteLLM, API Gateway or a homemade service), the cost is attributed to the gateway's role and not to the end user. To tell users apart, you need to pass &lt;em&gt;session tags&lt;/em&gt; when the gateway assumes the role.&lt;/li&gt;
&lt;li&gt;An IAM user's credentials are long-lived keys. If the Android app contains them, anyone who extracts the APK can use them: consider temporary credentials, which offer the same cost attribution options.&lt;/li&gt;
&lt;li&gt;Attribution by identity can be combined with &lt;em&gt;application inference profiles&lt;/em&gt; if you also need a split by application, but it requires changing the policy: you have to add the profiles' ARNs, as well as those of the models in the regions involved.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary of the steps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create an IAM user with a policy limited to the chosen models.&lt;/li&gt;
&lt;li&gt;Add a tag to the user (here &lt;code&gt;bedrock=tablet&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Activate the tag in &lt;em&gt;Cost allocation tags&lt;/em&gt;, type &lt;em&gt;IAM principal&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Wait for the first call and up to a couple of days to see the data in Cost Explorer.&lt;/li&gt;
&lt;li&gt;Create a monthly budget filtered on the tag.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/cost-mgmt-iam-principal-tracking.html" rel="noopener noreferrer"&gt;IAM principal cost attribution in Amazon Bedrock&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-prereq.html" rel="noopener noreferrer"&gt;Prerequisites for inference profiles&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/iam-principal-cost-allocation.html" rel="noopener noreferrer"&gt;IAM principal cost allocation in AWS Billing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>bedrock</category>
      <category>llm</category>
      <category>devops</category>
    </item>
    <item>
      <title>Da Markdown a PDF con tabelle larghe e diagrammi Mermaid</title>
      <dc:creator>minnogit</dc:creator>
      <pubDate>Mon, 26 Oct 2026 09:10:00 +0000</pubDate>
      <link>https://dev.to/minnogit/da-markdown-a-pdf-con-tabelle-larghe-e-diagrammi-mermaid-4cm</link>
      <guid>https://dev.to/minnogit/da-markdown-a-pdf-con-tabelle-larghe-e-diagrammi-mermaid-4cm</guid>
      <description>&lt;p&gt;Volevo trasformare in PDF alcuni file Markdown che contengono tabelle larghe e diagrammi Mermaid. Ho provato convertitori online, la stampa su PDF di CUPS e l'esportazione di VS Code, e ogni volta c'era qualcosa che non andava: le tabelle uscivano dalla pagina, o i diagrammi erano illeggibili o rotti. La combinazione che alla fine funziona è pandoc con il filtro &lt;code&gt;mermaid-filter&lt;/code&gt; e weasyprint come motore del PDF, senza LaTeX.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installazione
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;pandoc weasyprint
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; mermaid-filter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;mermaid-filter&lt;/code&gt; disegna i diagrammi con Mermaid dentro un Chromium lanciato da puppeteer. Se hai già Chrome, si può farglielo usare con la variabile &lt;code&gt;CHROME&lt;/code&gt;, come vedi nello script qui sotto.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un comando solo
&lt;/h2&gt;

&lt;p&gt;Il comando di pandoc da solo non basta, perché servono anche un foglio di stile e un file di configurazione per Mermaid, e dopo un po' non ricordavo più dove fossero. Ho messo tutto in uno script che si porta dietro il CSS e la configurazione e li scrive in una cartella temporanea. Si salva come &lt;code&gt;md2pdf&lt;/code&gt; in una cartella del &lt;code&gt;PATH&lt;/code&gt; e si rende eseguibile con &lt;code&gt;chmod +x&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# uso: md2pdf file.md [file.pdf]&lt;/span&gt;
&lt;span class="c"&gt;# CHROME=/percorso/chrome per far usare a mermaid-filter un Chrome già installato&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;realpath&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;realpath&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="k"&gt;:-${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="p"&gt;%.md&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="p"&gt;.pdf&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;work&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;mktemp&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;trap&lt;/span&gt; &lt;span class="s1"&gt;'rm -rf "$work"'&lt;/span&gt; EXIT

&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$work&lt;/span&gt;&lt;span class="s2"&gt;/pandoc.css"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;CSS&lt;/span&gt;&lt;span class="sh"&gt;'
@page { size: A4; margin: 1cm; }

body {
  font-size: 9pt;
  font-family: sans-serif;
  line-height: 1.5;
}

img {
  display: block;
  margin: 0 auto;
  max-width: 100%;
  height: auto;
}

table {
  width: 100%;
  table-layout: fixed;
  border-collapse: collapse;
  overflow-wrap: break-word;
}

td, th {
  border: 1px solid #ccc;
  padding: 6px;
  text-align: left;
  font-size: 8pt;
  overflow: hidden;
}
&lt;/span&gt;&lt;span class="no"&gt;CSS

&lt;/span&gt;&lt;span class="c"&gt;# Con htmlLabels false il testo dei nodi resta testo SVG: weasyprint non&lt;/span&gt;
&lt;span class="c"&gt;# disegna le etichette HTML (foreignObject) che Mermaid usa di default.&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"flowchart":{"htmlLabels":false}}'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$work&lt;/span&gt;&lt;span class="s2"&gt;/.mermaid-config.json"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CHROME&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'{"executablePath":"%s","args":["--no-sandbox"]}'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CHROME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$work&lt;/span&gt;&lt;span class="s2"&gt;/.puppeteer.json"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# mermaid-filter legge i due file di configurazione dalla cartella corrente&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$work&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;MERMAID_FILTER_FORMAT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;svg pandoc &lt;span class="nt"&gt;-F&lt;/span&gt; mermaid-filter &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-t&lt;/span&gt; html5 &lt;span class="nt"&gt;--pdf-engine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;weasyprint &lt;span class="nt"&gt;--css&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pandoc.css &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$in&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si usa così:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;md2pdf articolo.md              &lt;span class="c"&gt;# crea articolo.pdf&lt;/span&gt;
md2pdf articolo.md uscita.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Se il Markdown ha un front-matter YAML in testa, va tolto prima, altrimenti finisce nel testo del PDF.&lt;/p&gt;

&lt;h2&gt;
  
  
  Perché la configurazione di Mermaid
&lt;/h2&gt;

&lt;p&gt;Con &lt;code&gt;MERMAID_FILTER_FORMAT=svg&lt;/code&gt; i diagrammi restano vettoriali e si leggono bene anche ingranditi. Ma la prima volta che ho provato, i riquadri dei diagrammi erano vuoti: le forme c'erano, il testo no. Mermaid di default scrive le etichette dei nodi dentro un elemento HTML incorporato nell'SVG (&lt;code&gt;foreignObject&lt;/code&gt;), e per quanto ho capito weasyprint non lo disegna. La riga &lt;code&gt;htmlLabels: false&lt;/code&gt; nel file &lt;code&gt;.mermaid-config.json&lt;/code&gt; fa scrivere le etichette come normale testo SVG, e il problema sparisce. &lt;code&gt;mermaid-filter&lt;/code&gt; legge quel file dalla cartella corrente, per questo lo script entra nella cartella temporanea prima di lanciare pandoc.&lt;/p&gt;

&lt;p&gt;L'alternativa è lasciare il formato &lt;code&gt;png&lt;/code&gt; (il predefinito del filtro), ma i diagrammi non sono più vettoriali.&lt;/p&gt;

&lt;h2&gt;
  
  
  Perché il CSS
&lt;/h2&gt;

&lt;p&gt;Le parti del foglio di stile che contano sono poche:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@page&lt;/code&gt; con margini di 1 cm, per usare bene l'A4.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;table-layout: fixed&lt;/code&gt; e &lt;code&gt;overflow-wrap: break-word&lt;/code&gt;, che sono quello che tiene le tabelle larghe dentro la pagina: le colonne si dividono lo spazio in parti uguali e il testo lungo va a capo, anche nel mezzo di una parola se serve (un nome come &lt;code&gt;devto_synced_hash&lt;/code&gt; in una colonna stretta si spezza in due righe).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;text-align: left&lt;/code&gt; nelle celle: senza, il testo giustificato in colonne strette lascia spazi enormi tra le parole.&lt;/li&gt;
&lt;li&gt;il font a 9pt nel testo e 8pt nelle tabelle. Sono valori che ho scelto a occhio: con i 7pt con cui avevo cominciato il PDF si leggeva a fatica. Se hai tabelle con molte colonne puoi scendere ancora.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ho provato lo script su una tabella con otto colonne e su un'altra con percorsi e comandi lunghi, e in entrambi i casi sono rimaste dentro la pagina.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lanciarlo da VS Code
&lt;/h2&gt;

&lt;p&gt;Per non aprire il terminale ogni volta ho aggiunto un task di VS Code, cioè un comando salvato che parte dal menu. Va messo in &lt;code&gt;.vscode/tasks.json&lt;/code&gt; nel progetto, oppure in &lt;code&gt;tasks.json&lt;/code&gt; nella cartella di configurazione utente (&lt;code&gt;~/.config/Code/User/&lt;/code&gt; su Linux) per averlo in tutti i progetti:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tasks"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Markdown → PDF"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shell"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${env:HOME}/bin/md2pdf &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;${file}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&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;"problemMatcher"&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;"presentation"&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;"reveal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"silent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"panel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shared"&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="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="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;Con un file Markdown aperto si lancia da &lt;code&gt;Ctrl+Shift+P&lt;/code&gt;, scegliendo «Tasks: Run Task» e poi «Markdown → PDF». Il PDF compare nella stessa cartella del file. &lt;code&gt;${file}&lt;/code&gt; è il file aperto nell'editor e &lt;code&gt;${env:HOME}&lt;/code&gt; evita di dipendere dal &lt;code&gt;PATH&lt;/code&gt;, perché VS Code potrebbe non conoscere la cartella dove hai messo lo script. Per una scorciatoia da tastiera si aggiunge in &lt;code&gt;keybindings.json&lt;/code&gt;:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ctrl+alt+p"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"workbench.action.tasks.runTask"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Markdown → PDF"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"when"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"editorLangId == markdown"&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;h2&gt;
  
  
  Un limite dei diagrammi piccoli
&lt;/h2&gt;

&lt;p&gt;Un diagramma molto piccolo, per esempio con due soli nodi, viene ingrandito fino a occupare tutta la larghezza della pagina. &lt;code&gt;mermaid-filter&lt;/code&gt; lo salva con una larghezza fissa di 800 e weasyprint lo adatta alla pagina, mentre &lt;code&gt;max-height&lt;/code&gt; sulle immagini, nelle mie prove, non ha cambiato nulla. I diagrammi di un articolo vero hanno di solito più nodi e vengono della misura giusta, quindi per me non è un problema, ma è una cosa da sapere.&lt;/p&gt;

</description>
      <category>markdown</category>
      <category>pdf</category>
      <category>pandoc</category>
      <category>mermaid</category>
    </item>
    <item>
      <title>Rendere pubblico un repo: cosa ho controllato prima del primo push</title>
      <dc:creator>minnogit</dc:creator>
      <pubDate>Sat, 24 Oct 2026 10:24:38 +0000</pubDate>
      <link>https://dev.to/minnogit/rendere-pubblico-un-repo-cosa-ho-controllato-prima-del-primo-push-19nf</link>
      <guid>https://dev.to/minnogit/rendere-pubblico-un-repo-cosa-ho-controllato-prima-del-primo-push-19nf</guid>
      <description>&lt;p&gt;Questo articolo fa parte della serie «Un repo per i miei articoli». Per rendere pubblico lo strumento descritto in &lt;a href="https://dev.to/minnogit/tenere-gli-articoli-di-devto-in-git-e-accorgersi-quando-il-portale-cambia-1m5h"&gt;Tenere gli articoli di Dev.to in Git e accorgersi quando il portale cambia&lt;/a&gt;, &lt;a href="https://github.com/minnogit/devto-workbench" rel="noopener noreferrer"&gt;&lt;code&gt;devto-workbench&lt;/code&gt;&lt;/a&gt;, sono dovuto partire da un repo nuovo, perché in quello degli articoli ci sono le bozze. Qui racconto come convivono i due repo, cosa ho controllato prima del primo push e perché ho messo un hook di git che lo fa da solo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Perché non bastava cambiare la visibilità
&lt;/h2&gt;

&lt;p&gt;Su GitHub la visibilità appartiene all'intero repo, non a una cartella: un monorepo con una parte pubblica e una privata non esiste. Il problema vero però è la storia. Rendere pubblico il repo degli articoli avrebbe mostrato ogni versione di ogni bozza, comprese quelle poi riscritte o tagliate, e i messaggi dei commit. Prima era tutto in un solo repo, e solo una parte di quel contenuto poteva diventare pubblica:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TB
  subgraph prima["Un solo repo privato"]
    A["articles/ e assets/&amp;lt;br&amp;gt;articoli e bozze"]
    B["skill di scrittura&amp;lt;br&amp;gt;la mia voce"]
    C["web/&amp;lt;br&amp;gt;server e pagina"]
    D["scripts/&amp;lt;br&amp;gt;importazione"]
    E["skill di sync"]
  end
  A --&amp;gt; P["resta privato"]
  B --&amp;gt; P
  C --&amp;gt; U["va nel repo pubblico"]
  D --&amp;gt; U
  E --&amp;gt; U&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;La soluzione è separare i due repo: lo strumento in un repo pubblico, gli articoli in uno privato che include il primo come submodule. Un submodule è un repo dentro un altro, con un puntatore a un commit preciso: il repo privato non tiene una copia del codice, ricorda soltanto quale versione usa.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  subgraph priv["Repo privato"]
    A["articles/ e assets/"]
    B["skill di scrittura"]
    S["tools/devto-workbench&amp;lt;br&amp;gt;puntatore a un commit"]
  end
  subgraph pub["Repo pubblico devto-workbench"]
    C["web/, scripts/&amp;lt;br&amp;gt;skill di sync"]
  end
  S --&amp;gt;|"punta al commit"| C&lt;/code&gt;&lt;/pre&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git submodule add https://github.com/minnogit/devto-workbench.git tools/devto-workbench
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Il repo pubblico è nato con un solo commit iniziale, invece di estrarre i file dello strumento insieme alla loro storia. Estraendo la storia mi sarei portato dietro i messaggi dei commit che toccano anche gli articoli.&lt;/p&gt;

&lt;p&gt;Il prezzo sono due cose da ricordare. Chi clona il repo privato deve usare &lt;code&gt;git clone --recurse-submodules&lt;/code&gt; (oppure &lt;code&gt;git submodule update --init&lt;/code&gt; dopo un clone normale), altrimenti la cartella dello strumento resta vuota.&lt;/p&gt;

&lt;p&gt;La seconda riguarda le modifiche allo strumento. In entrambi i percorsi il repo privato continua a puntare alla versione vecchia finché non registra il nuovo commit con un commit suo:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  subgraph A["Percorso A: modifico dentro il submodule"]
    A1["modifico i file in&amp;lt;br&amp;gt;tools/devto-workbench"] --&amp;gt; A2["commit e push&amp;lt;br&amp;gt;vanno nel repo pubblico"] --&amp;gt; A3["nel repo privato:&amp;lt;br&amp;gt;commit del nuovo puntatore"]
  end
  subgraph B["Percorso B: modifico in un clone separato"]
    B1["modifico i file nel&amp;lt;br&amp;gt;clone del repo pubblico"] --&amp;gt; B2["commit e push&amp;lt;br&amp;gt;vanno nel repo pubblico"] --&amp;gt; B3["nel repo privato:&amp;lt;br&amp;gt;git submodule update --remote"] --&amp;gt; B4["nel repo privato:&amp;lt;br&amp;gt;commit del nuovo puntatore"]
  end
  A3 --&amp;gt; F["il repo privato usa&amp;lt;br&amp;gt;la versione nuova"]
  B4 --&amp;gt; F&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Nel percorso A si lavora dentro la cartella del submodule, con commit e push nel repo pubblico, e poi nel repo privato si fa il commit che aggiorna il puntatore. Nel percorso B si lavora in un clone separato del repo pubblico. Poi, nel repo privato, si scarica l'ultima versione dello strumento e si registra il nuovo puntatore:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git submodule update &lt;span class="nt"&gt;--remote&lt;/span&gt; tools/devto-workbench
git add tools/devto-workbench
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Aggiorna il submodule devto-workbench"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Se si dimentica &lt;code&gt;update --remote&lt;/code&gt;, la cartella resta alla versione vecchia anche se su GitHub c'è già quella nuova.&lt;/p&gt;

&lt;h2&gt;
  
  
  Controllare prima, non dopo
&lt;/h2&gt;

&lt;p&gt;Un segreto (una chiave API, un token, una password) si può cercare in due momenti: prima che il codice parta dal mio computer, oppure dopo che è arrivato su GitHub. Il secondo, da solo, su un repo pubblico arriva tardi.&lt;/p&gt;

&lt;p&gt;GitGuardian, collegato a GitHub come app, ascolta gli eventi allo stadio post-receive, cioè dopo che il codice è stato inviato, e descrive come risposta gli incidenti e gli avvisi. Su un repo pubblico questo significa che l'avviso arriva quando il segreto è già visibile a chiunque. Il riferimento è la pagina &lt;a href="https://docs.gitguardian.com/internal-monitoring/core-concepts/how-internal-monitoring-works" rel="noopener noreferrer"&gt;How Internal Monitoring works&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;GitHub ha una sua protezione sul push. Secondo la &lt;a href="https://docs.github.com/en/code-security/secret-scanning/introduction/about-push-protection" rel="noopener noreferrer"&gt;documentazione&lt;/a&gt; quella a livello di utente è attiva di default e vale per i repo pubblici su GitHub.com, mentre quella a livello di repo è disattivata di default.&lt;/p&gt;

&lt;p&gt;C'è poi un motivo pratico: se il segreto è già arrivato, ripulire costa molto. Riscrivere la storia con un force-push non basta, perché GitHub tiene un riferimento per ogni pull request e i vecchi commit restano raggiungibili dai loro indirizzi diretti. Per quanto ne so può ripulirli solo il supporto di GitHub.&lt;/p&gt;

&lt;p&gt;Servono quindi controlli sul computer, prima che il codice parta.&lt;/p&gt;

&lt;h2&gt;
  
  
  L'hook che lo fa da solo
&lt;/h2&gt;

&lt;p&gt;Ho aggiunto &lt;a href="https://docs.gitguardian.com/ggshield-docs/integrations/overview" rel="noopener noreferrer"&gt;ggshield&lt;/a&gt;, la riga di comando di GitGuardian, il servizio già collegato a GitHub. Si può lanciare a mano su tutta la storia:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ggshield secret scan repo &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La risposta è stata «No secrets have been found». Il passo che conta è farlo partire da solo. ggshield lo installa come hook di git:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ggshield &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--mode&lt;/span&gt; &lt;span class="nb"&gt;local&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; pre-commit
ggshield &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--mode&lt;/span&gt; &lt;span class="nb"&gt;local&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; pre-push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Il &lt;code&gt;pre-commit&lt;/code&gt; blocca il segreto prima che entri nella storia. Il &lt;code&gt;pre-push&lt;/code&gt; è la seconda barriera, subito prima dell'invio. Per aggirarli, secondo la documentazione del pre-commit, basta &lt;code&gt;git commit -n&lt;/code&gt; oppure &lt;code&gt;SKIP=ggshield git commit&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Provare l'hook con un token finto
&lt;/h3&gt;

&lt;p&gt;Per vedere se l'hook funziona conviene committare un token finto, ma non uno qualunque. Un &lt;code&gt;ghp_&lt;/code&gt; seguito da 36 caratteri casuali non viene riconosciuto: l'hook parte, risponde «No secrets have been found» e il commit passa. Un token finto con il checksum corretto invece viene riconosciuto come «GitHub Personal Access Token» (con validità «Invalid») e il commit è rifiutato. Se il commit di prova passa, va annullato con &lt;code&gt;git reset --hard&lt;/code&gt; e il reflog ripulito.&lt;/p&gt;

&lt;h3&gt;
  
  
  I limiti degli hook
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Sono locali e non vengono versionati: chi clona il repo non li ha.&lt;/li&gt;
&lt;li&gt;Il &lt;code&gt;pre-push&lt;/code&gt; di ggshield per default non scansiona i push con più di 50 commit (dalla documentazione, configurabile con &lt;code&gt;max-commits-for-hook&lt;/code&gt;). Con una storia lunga conviene lanciare a parte la scansione completa con &lt;code&gt;scan repo&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cosa un hook non vede
&lt;/h2&gt;

&lt;p&gt;Un hook cerca segreti. Non sa se nel repo c'è qualcosa che non dovrebbe esserci, e queste cose le ho controllate a mano:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Il server legge &lt;code&gt;articles/&lt;/code&gt; e &lt;code&gt;assets/&lt;/code&gt; dalla cartella da cui lo lancio e le crea se mancano, quindi un &lt;code&gt;git add -A&lt;/code&gt; distratto dal repo pubblico le metterebbe in un commit. Stanno nel &lt;code&gt;.gitignore&lt;/code&gt;, insieme a &lt;code&gt;.mcp.json&lt;/code&gt;: nel repo c'è un &lt;code&gt;.mcp.json.example&lt;/code&gt; con percorsi e segreti finti.&lt;/li&gt;
&lt;li&gt;Ogni commit porta nome ed email dell'autore, e su un repo pubblico li legge chiunque. Per il repo nuovo ho scelto l'indirizzo noreply, nella forma &lt;code&gt;&amp;lt;id&amp;gt;+&amp;lt;utente&amp;gt;@users.noreply.github.com&lt;/code&gt;, e l'ho impostato prima del primo push con &lt;code&gt;git commit --amend --reset-author --no-edit&lt;/code&gt;: dopo, cambiare l'autore vorrebbe dire riscrivere una storia già pubblicata.&lt;/li&gt;
&lt;li&gt;Durante le scansioni ggshield crea nella cartella corrente un file &lt;code&gt;.cache_ggshield&lt;/code&gt;, con dentro l'hash dei segreti trovati. Con un &lt;code&gt;git add -A&lt;/code&gt; distratto finirebbe nel commit, quindi va nel &lt;code&gt;.gitignore&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Uno scanner di segreti non dice se tra i file tracciati ci sono file di configurazione personali, file di terzi senza avviso di copyright o una licenza che manca. Per questo prima di pubblicare conviene leggere l'elenco con &lt;code&gt;git ls-files&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Il repo è pubblico e tutti i suoi commit hanno l'autore noreply.&lt;/p&gt;

</description>
      <category>git</category>
      <category>security</category>
      <category>github</category>
      <category>devops</category>
    </item>
    <item>
      <title>Un server MCP per una wiki in Markdown: stdio per modificare, HTTP per servire</title>
      <dc:creator>minnogit</dc:creator>
      <pubDate>Sat, 24 Oct 2026 08:36:12 +0000</pubDate>
      <link>https://dev.to/minnogit/un-server-mcp-per-una-wiki-in-markdown-stdio-per-modificare-http-per-servire-2nle</link>
      <guid>https://dev.to/minnogit/un-server-mcp-per-una-wiki-in-markdown-stdio-per-modificare-http-per-servire-2nle</guid>
      <description>&lt;p&gt;Questo articolo fa parte della serie «Un repo per i miei articoli». Riguarda il server MCP di una wiki LLM: una wiki di concetti tecnici in Markdown che un agente come Claude Code scrive, collega e interroga, con gli articoli pubblicati come fonti. Per farlo gli serve un'interfaccia con regole precise su cosa può leggere e cosa può scrivere, e per questo ho scritto &lt;a href="https://github.com/minnogit/wiki-mcp-api" rel="noopener noreferrer"&gt;&lt;code&gt;wiki-mcp-api&lt;/code&gt;&lt;/a&gt;, un server MCP in TypeScript costruito su &lt;code&gt;@mastra/mcp&lt;/code&gt;. Racconto come è fatto e perché lo stesso server si usa in due modi: in locale per modificare la wiki, via HTTP solo per servirla ad altri.&lt;/p&gt;

&lt;h2&gt;
  
  
  Due cartelle con ruoli diversi
&lt;/h2&gt;

&lt;p&gt;Il server non contiene nessuna wiki. Legge due cartelle indicate dall'ambiente: &lt;code&gt;WIKI_PATH&lt;/code&gt;, con le pagine, che si possono scrivere, e &lt;code&gt;RAW_PATH&lt;/code&gt;, con le fonti da cui nascono le pagine, in sola lettura. Una terza variabile, &lt;code&gt;WIKI_DOMAIN&lt;/code&gt;, è una riga che descrive di cosa parla quella wiki e finisce nelle istruzioni inviate al client. Con la stessa installazione posso così servire due wiki diverse, ciascuna con la sua configurazione.&lt;/p&gt;

&lt;p&gt;Il confine tra le due cartelle sta nel codice. &lt;code&gt;writePage&lt;/code&gt; risolve il percorso, controlla che resti dentro &lt;code&gt;WIKI_PATH&lt;/code&gt; e accetta solo file &lt;code&gt;.md&lt;/code&gt;. Le letture delle fonti passano da tool separati, che rifiutano percorsi assoluti e &lt;code&gt;..&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  I tool
&lt;/h2&gt;

&lt;p&gt;In stdio i tool sono undici:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;A cosa serve&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;wikiSearch&lt;/code&gt;, &lt;code&gt;wikiListPages&lt;/code&gt;, &lt;code&gt;wikiReadPage&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;cercare, elencare e leggere le pagine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;wikiGraph&lt;/code&gt;, &lt;code&gt;wikiStatus&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;grafo dei link e conteggi per tipo e stato&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;wikiWritePage&lt;/code&gt;, &lt;code&gt;wikiAppendLog&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;scrivere una pagina, aggiungere una voce al registro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;wikiListRaw&lt;/code&gt;, &lt;code&gt;wikiReadRaw&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;elencare e leggere le fonti&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;wikiChecksum&lt;/code&gt;, &lt;code&gt;wikiRawDiff&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;riconoscere una fonte già vista e vedere cosa è cambiato&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Scrivere senza rompere la wiki
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;wikiWritePage&lt;/code&gt; valida il frontmatter con &lt;code&gt;zod&lt;/code&gt; prima di scrivere: &lt;code&gt;tipo&lt;/code&gt; tra un elenco fisso, almeno un tag, &lt;code&gt;fonti&lt;/code&gt; come lista, &lt;code&gt;aggiornato&lt;/code&gt; nel formato &lt;code&gt;YYYY-MM-DD&lt;/code&gt; e &lt;code&gt;stato&lt;/code&gt; tra &lt;code&gt;bozza&lt;/code&gt;, &lt;code&gt;stabile&lt;/code&gt; e &lt;code&gt;da-rivedere&lt;/code&gt;. Le quattro pagine di servizio (&lt;code&gt;index&lt;/code&gt;, &lt;code&gt;log&lt;/code&gt;, &lt;code&gt;sources&lt;/code&gt;, &lt;code&gt;overview&lt;/code&gt;) sono esenti. Se il frontmatter non va, l'errore dice quale campo e perché, e l'agente può correggere da solo.&lt;/p&gt;

&lt;p&gt;Per le scritture concorrenti c'è un lock sul singolo file (&lt;code&gt;proper-lockfile&lt;/code&gt;). Evita che due scritture si mescolino, ma non risolve un conflitto tra due autori che scrivono cose diverse.&lt;/p&gt;

&lt;h2&gt;
  
  
  La ricerca
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;wikiSearch&lt;/code&gt; ordina per rilevanza con un punteggio in stile &lt;a href="https://it.wikipedia.org/wiki/Tf-idf" rel="noopener noreferrer"&gt;TF-IDF&lt;/a&gt;, con pesi per campo (il titolo conta più dei tag e i tag più del contenuto) e senza normalizzazione sulla lunghezza della pagina. Restituisce pochi risultati, 10 con una query e 50 senza: i client MCP troncano le risposte lunghe per proteggere il contesto, e un taglio casuale rischia di perdere proprio le pagine più rilevanti. Nei risultati ci sono metadati e uno snippet, non l'intero contenuto.&lt;/p&gt;

&lt;h2&gt;
  
  
  Aggiornare una fonte che è cambiata
&lt;/h2&gt;

&lt;p&gt;Le fonti sono file e cambiano: un articolo viene corretto, un documento ampliato. All'ingest, &lt;code&gt;wikiChecksum&lt;/code&gt; calcola lo sha256 del file e il registro &lt;code&gt;sources.md&lt;/code&gt; lo associa alle pagine che ne sono nate. Alla sessione successiva basta ricalcolare il checksum di ogni fonte: se è diverso da quello registrato, la fonte è cambiata. Il checksum individua quali fonti sono cambiate senza doverle rileggere tutte, e il confronto è sul contenuto e non sul nome, quindi una fonte rinominata non risulta nuova.&lt;/p&gt;

&lt;p&gt;Dice però solo che qualcosa è cambiato, non cosa. Se all'agente basta rileggere il file aggiornato per decidere cosa modificare nella wiki, spesso non si accorge delle differenze, soprattutto con file grandi, oppure le sottovaluta e non le riporta nelle pagine. Per questo &lt;code&gt;wikiRawDiff&lt;/code&gt; confronta la versione vecchia del file, quella che aveva il checksum registrato e che ritrova nella storia git di &lt;code&gt;raw/&lt;/code&gt;, con quella attuale, e restituisce le differenze. L'agente valuta solo quelle e aggiorna le pagine che le riguardano. Quando un diff di testo non c'è, ripiega sul confronto per checksum e sulla lettura della fonte. Serve quindi che &lt;code&gt;raw/&lt;/code&gt; stia in un repo git.&lt;/p&gt;

&lt;h2&gt;
  
  
  stdio e HTTP
&lt;/h2&gt;

&lt;p&gt;Il server ha due ingressi con scopi diversi. In stdio, lanciato dal client sulla stessa macchina, si usa per lavorare sulla wiki: espone tutti i tool, scrittura compresa. L'agente scrive le pagine come modifiche non committate del repo della wiki, che si rivedono e si committano a mano. Così ci sono le due garanzie che il server non dà da solo: l'attribuzione, perché il commit è di chi esegue l'agente, e un controllo umano, perché il diff si rivede prima di committare.&lt;/p&gt;

&lt;p&gt;In HTTP il server serve la conoscenza ad altri, quindi è di sola lettura. Richiede un token Bearer (senza &lt;code&gt;MCP_HTTP_TOKEN&lt;/code&gt; il processo non parte) e non gestisce TLS, che va messo a monte con un reverse proxy. Un client remoto non ha né attribuzione né controllo umano, e il lock evita solo la corruzione concorrente.&lt;/p&gt;

&lt;p&gt;Per lo stesso motivo da remoto si servono di default solo le pagine, cioè la conoscenza già elaborata. I tool sulle fonti (&lt;code&gt;wikiListRaw&lt;/code&gt;, &lt;code&gt;wikiReadRaw&lt;/code&gt;, &lt;code&gt;wikiRawDiff&lt;/code&gt;, &lt;code&gt;wikiChecksum&lt;/code&gt;) restano fuori, perché &lt;code&gt;raw/&lt;/code&gt; può contenere materiale non pensato per altri, come bozze. Si riattivano con &lt;code&gt;MCP_HTTP_EXPOSE_RAW=true&lt;/code&gt;, valore esatto, e all'avvio il server dice se le fonti sono esposte. I test coprono la differenza tra i due ingressi: per controllare che servissero li ho lanciati senza la modifica e falliscono.&lt;/p&gt;

&lt;h2&gt;
  
  
  Possibili miglioramenti
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;I valori di &lt;code&gt;tipo&lt;/code&gt; sono fissati nel codice: &lt;code&gt;concetto&lt;/code&gt;, &lt;code&gt;soggetto&lt;/code&gt;, &lt;code&gt;procedura&lt;/code&gt;, &lt;code&gt;normativa&lt;/code&gt;, &lt;code&gt;entita&lt;/code&gt; e &lt;code&gt;analisi&lt;/code&gt;. Per una wiki con altre categorie andrebbero resi configurabili.&lt;/li&gt;
&lt;li&gt;La ricerca non ha un indice: a ogni query il server controlla tutti i file e ricalcola il punteggio su tutte le pagine, e la cache evita solo di rileggere e riparsare i file non modificati. Non l'ho misurato su wiki grandi, dove un indice in memoria potrebbe servire.&lt;/li&gt;
&lt;li&gt;L'accesso HTTP ha un solo token condiviso e niente TLS.&lt;/li&gt;
&lt;li&gt;Il server stdio non ha una modalità di sola lettura: i tool di scrittura ci sono sempre.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;wikiRawDiff&lt;/code&gt; richiede &lt;code&gt;git&lt;/code&gt; sulla macchina che lancia il server, e per i PDF anche &lt;code&gt;pdftotext&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Il progetto ha una CI su GitHub Actions che lancia lint e test a ogni push su &lt;code&gt;main&lt;/code&gt; e a ogni pull request. Come ho reso pubblico lo strumento della serie, controllando i segreti prima del push, è raccontato in &lt;a href="https://dev.to/minnogit/rendere-pubblico-un-repo-cosa-ho-controllato-prima-del-primo-push-19nf"&gt;Rendere pubblico un repo: cosa ho controllato prima del primo push&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>typescript</category>
      <category>node</category>
      <category>security</category>
    </item>
    <item>
      <title>Tenere gli articoli di Dev.to in Git e accorgersi quando il portale cambia</title>
      <dc:creator>minnogit</dc:creator>
      <pubDate>Fri, 23 Oct 2026 08:45:00 +0000</pubDate>
      <link>https://dev.to/minnogit/tenere-gli-articoli-di-devto-in-git-e-accorgersi-quando-il-portale-cambia-1m5h</link>
      <guid>https://dev.to/minnogit/tenere-gli-articoli-di-devto-in-git-e-accorgersi-quando-il-portale-cambia-1m5h</guid>
      <description>&lt;p&gt;Scrivo su Dev.to da due posti: dall'editor del portale e da file Markdown in un repo Git, con l'aiuto di Claude Code, come racconto in &lt;a href="https://dev.to/minnogit/un-agente-ai-come-editor-dei-miei-articoli-devto-37c1"&gt;Un agente AI come editor dei miei articoli Dev.to&lt;/a&gt;. Il portale non tiene la storia delle modifiche, e con due punti di scrittura prima o poi uno dei due sovrascrive l'altro senza che nessuno se ne accorga. Per questo ho scritto uno strumento che tiene gli articoli come file, mi dice quando un lato è cambiato rispetto all'altro e non sceglie mai al posto mio quando sono cambiati entrambi. Si chiama &lt;a href="https://github.com/minnogit/devto-workbench" rel="noopener noreferrer"&gt;&lt;code&gt;devto-workbench&lt;/code&gt;&lt;/a&gt;, è in Python e usa solo la libreria standard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un file per articolo
&lt;/h2&gt;

&lt;p&gt;Ogni articolo è un file &lt;code&gt;articles/&amp;lt;id&amp;gt;-&amp;lt;slug&amp;gt;.md&lt;/code&gt;. Il front-matter ha una riga per campo, nella forma &lt;code&gt;chiave: &amp;lt;valore JSON&amp;gt;&lt;/code&gt;, così non serve nessuna libreria per leggerlo. Sotto c'è il &lt;code&gt;body_markdown&lt;/code&gt; esattamente come lo restituisce Dev.to:&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="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1234567&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Titolo"&lt;/span&gt;
&lt;span class="na"&gt;published&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;git"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;linux"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://dev.to/..."&lt;/span&gt;
&lt;span class="na"&gt;canonical_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://dev.to/..."&lt;/span&gt;
&lt;span class="na"&gt;cover_image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="na"&gt;devto_synced_hash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha256&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;del&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;come&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;era&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;su&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Dev.to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;all'ultimo&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;sync"&lt;/span&gt;
&lt;span class="na"&gt;stato&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;revisionata"&lt;/span&gt;
&lt;span class="na"&gt;reviewed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="s"&gt;Il testo dell'articolo...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;L'identificatore è l'&lt;code&gt;id&lt;/code&gt;, non il nome del file, che dipende dal titolo e può restare indietro. Il testo non va normalizzato: niente spazi tolti, niente markdown «ripulito». Il motivo è l'hash di cui parlo subito.&lt;/p&gt;

&lt;h2&gt;
  
  
  Perché un hash e non una data
&lt;/h2&gt;

&lt;p&gt;Per sapere se il portale è cambiato la prima idea è confrontare le date di modifica. Negli elenchi &lt;code&gt;/articles/me&lt;/code&gt; e &lt;code&gt;/articles/me/unpublished&lt;/code&gt; il campo &lt;code&gt;edited_at&lt;/code&gt; è vuoto, quindi non c'è niente da confrontare.&lt;/p&gt;

&lt;p&gt;Ho usato l'hash. &lt;code&gt;devto_synced_hash&lt;/code&gt; è lo sha256 del body remoto all'ultimo sync riuscito. Per ogni articolo lo strumento mette a confronto tre cose: l'hash del body locale, quello del body remoto e quello salvato. Da lì ricava lo stato:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stato&lt;/th&gt;
&lt;th&gt;Significato&lt;/th&gt;
&lt;th&gt;Cosa si fa&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;allineato&lt;/td&gt;
&lt;td&gt;nessuno ha cambiato nulla&lt;/td&gt;
&lt;td&gt;niente&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;remoto più recente&lt;/td&gt;
&lt;td&gt;è cambiato solo il portale&lt;/td&gt;
&lt;td&gt;pull&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;locale da pushare&lt;/td&gt;
&lt;td&gt;è cambiato solo il file&lt;/td&gt;
&lt;td&gt;push&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;conflitto&lt;/td&gt;
&lt;td&gt;sono cambiati entrambi, in modo diverso&lt;/td&gt;
&lt;td&gt;decido io&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;solo su Dev.to / solo locale&lt;/td&gt;
&lt;td&gt;manca un lato&lt;/td&gt;
&lt;td&gt;importo o creo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Non faccio merge automatici del testo. Su prosa un merge a righe mi sembra produrre risultati incoerenti, e in caso di conflitto preferisco guardare le due versioni e scegliere io.&lt;/p&gt;

&lt;h2&gt;
  
  
  La pagina web
&lt;/h2&gt;

&lt;p&gt;Dalla radice del repo con gli articoli:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 /percorso/di/devto-workbench/web/server.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;e poi &lt;code&gt;http://localhost:8787&lt;/code&gt;. Il server legge &lt;code&gt;articles/&lt;/code&gt; dalla cartella da cui lo lancio, oppure da &lt;code&gt;ARTICLES_DIR&lt;/code&gt;. Fa anche da proxy verso l'API di Dev.to: il browser da solo non può mandare l'header &lt;code&gt;api-key&lt;/code&gt; (lo blocca il CORS), quindi la chiave la aggiunge il server leggendola dall'ambiente.&lt;/p&gt;

&lt;p&gt;La pagina ha tre viste, elenco, card e kanban, e mostra per ogni articolo lo stato di sync. Un pulsante compare solo quando l'azione non è ambigua: «Scarica in locale» per un articolo solo su Dev.to, «Scarica aggiornamento» quando è cambiato solo il portale, «Manda online» quando è cambiato solo il file. In caso di conflitto non c'è nessun pulsante di scrittura, solo «Differenze», che mostra il confronto riga per riga, unificato o fianco a fianco.&lt;/p&gt;

&lt;h2&gt;
  
  
  Il kanban e il campo stato
&lt;/h2&gt;

&lt;p&gt;Il kanban ha quattro colonne: Bozza, Revisionata, Pronta, Pubblicato. Trascinare una card tra le prime tre scrive il campo &lt;code&gt;stato&lt;/code&gt; nel front-matter del file, senza chiamare Dev.to. La colonna Pubblicato non accetta il drop: pubblicare è un'azione a parte e non si fa trascinando una card.&lt;/p&gt;

&lt;p&gt;«Pubblicato» non ha un valore proprio in &lt;code&gt;stato&lt;/code&gt;: lo ricava il server da &lt;code&gt;published: true&lt;/code&gt;. Se fosse scritto in due posti, prima o poi i due direbbero cose diverse. &lt;code&gt;stato&lt;/code&gt; e &lt;code&gt;reviewed&lt;/code&gt; sono campi solo locali. Non vanno mai su Dev.to, non entrano nell'hash e si scrivono nel file solo se diversi dal valore di default, così spostare una card cambia una riga sola. Lo stato di sync resta un badge sulla card, indipendente dalla colonna.&lt;/p&gt;

&lt;h2&gt;
  
  
  Con Claude Code
&lt;/h2&gt;

&lt;p&gt;Gli articoli stanno in un repo privato che include il tool come submodule git. Nel repo del tool c'è una skill per Claude Code, &lt;code&gt;devto-articles&lt;/code&gt;, che descrive le operazioni in cui il controllo a hash evita di sovrascrivere una modifica altrui: creare un articolo nuovo, pull, push, conflitti, pubblicazione. Per parlare con Dev.to usa un server MCP, nel mio caso un fork di &lt;a href="https://github.com/minnogit/devto-mcp" rel="noopener noreferrer"&gt;&lt;code&gt;devto-mcp&lt;/code&gt;&lt;/a&gt;. Non contiene le norme di scrittura, che sono la mia voce e restano in un repo privato.&lt;/p&gt;

&lt;p&gt;La skill è testo, non codice: se l'agente non la segue, il controllo non scatta da solo. Per questo la pagina web fa lo stesso controllo per conto suo prima di ogni pull e push.&lt;/p&gt;

&lt;h2&gt;
  
  
  Esportare solo gli articoli pubblicati
&lt;/h2&gt;

&lt;p&gt;Volevo alimentare una wiki con gli articoli, senza esporre le bozze. &lt;code&gt;scripts/export_published.py&lt;/code&gt; copia in un'altra cartella solo gli articoli con &lt;code&gt;published: true&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 scripts/export_published.py &lt;span class="nt"&gt;--dest&lt;/span&gt; /percorso/altro-repo/raw/devto
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Il body resta identico byte per byte e il front-matter si riduce a &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt; e &lt;code&gt;tags&lt;/code&gt;. Il motivo è che il server della wiki che uso calcola il checksum sull'intero file: con &lt;code&gt;reviewed&lt;/code&gt; o &lt;code&gt;devto_synced_hash&lt;/code&gt; dentro, ogni articolo risulterebbe modificato a ogni sync. Una seconda esecuzione senza novità non scrive nulla, &lt;code&gt;--dry-run&lt;/code&gt; mostra cosa farebbe, &lt;code&gt;--prune&lt;/code&gt; toglie gli articoli esportati che non sono più pubblicati e la destinazione non può coincidere con la cartella degli articoli.&lt;/p&gt;

&lt;p&gt;Lo stesso si può fare dalla pagina web, lanciando il server con &lt;code&gt;EXPORT_DEST=/percorso/altro-repo/raw/devto&lt;/code&gt;. Il pulsante «Esporta pubblicati» mostra prima l'anteprima, poi esporta e stampa il comando per il commit nell'altro repo, che resta a mano. La destinazione si decide solo all'avvio: una richiesta alla pagina non può sceglierla.&lt;/p&gt;

&lt;p&gt;Lo script ha dei test con &lt;code&gt;unittest&lt;/code&gt;. Per controllare che i test servissero a qualcosa ho indebolito di proposito la regola sul &lt;code&gt;published&lt;/code&gt;, e quello giusto ha fallito.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cosa non fa
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Non fa merge del testo: in caso di conflitto sei tu a decidere.&lt;/li&gt;
&lt;li&gt;Non pubblica dalla pagina web. Non l'ho ancora implementato, e vorrei una conferma esplicita prima di mandare qualcosa online.&lt;/li&gt;
&lt;li&gt;L'ho provato solo su Linux con Python 3.14. Il codice usa la sintassi &lt;code&gt;X | None&lt;/code&gt;, quindi serve almeno la 3.10. Su Windows non l'ho provato.&lt;/li&gt;
&lt;li&gt;Lo script di esportazione esporta quello che dice il front-matter locale. La pagina, in anteprima, segnala invece gli articoli pubblicati sul portale che nel file sono ancora bozze e che l'esportazione salterebbe.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Questo articolo fa parte della serie «Un repo per i miei articoli». Nella serie c'è anche &lt;a href="https://dev.to/minnogit/un-server-mcp-per-una-wiki-in-markdown-stdio-per-modificare-http-per-servire-2nle"&gt;Un server MCP per una wiki in Markdown: stdio per modificare, HTTP per servire&lt;/a&gt;, sul server della wiki costruita dagli articoli pubblicati, e &lt;a href="https://dev.to/minnogit/rendere-pubblico-un-repo-cosa-ho-controllato-prima-del-primo-push-19nf"&gt;Rendere pubblico un repo: cosa ho controllato prima del primo push&lt;/a&gt;, su cosa controllare prima di rendere pubblico un repo.&lt;/p&gt;

&lt;p&gt;Il codice e il README sono su &lt;a href="https://github.com/minnogit/devto-workbench" rel="noopener noreferrer"&gt;github.com/minnogit/devto-workbench&lt;/a&gt;, con licenza MIT.&lt;/p&gt;

</description>
      <category>python</category>
      <category>git</category>
      <category>opensource</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Aprire una PR da fork verso il maintainer e i problemi che non mi aspettavo</title>
      <dc:creator>minnogit</dc:creator>
      <pubDate>Tue, 20 Oct 2026 12:52:31 +0000</pubDate>
      <link>https://dev.to/minnogit/aprire-una-pr-da-fork-verso-il-maintainer-e-i-problemi-che-non-mi-aspettavo-5e0j</link>
      <guid>https://dev.to/minnogit/aprire-una-pr-da-fork-verso-il-maintainer-e-i-problemi-che-non-mi-aspettavo-5e0j</guid>
      <description>&lt;p&gt;Ho un fork Python di un server MCP per Dev.to (&lt;a href="https://github.com/minnogit/devto-mcp" rel="noopener noreferrer"&gt;&lt;code&gt;devto-mcp&lt;/code&gt;&lt;/a&gt;, partito dal progetto pubblico &lt;a href="https://github.com/Arindam200/devto-mcp" rel="noopener noreferrer"&gt;&lt;code&gt;Arindam200/devto-mcp&lt;/code&gt;&lt;/a&gt;) che uso ogni giorno con Claude Code. Ci ho trovato dentro un paio di bug reali (uno rendeva impossibile aggiornare un articolo, mancava l'header di autenticazione sulla richiesta; l'altro faceva restituire 404 quando si cercava di leggere una propria bozza) e li ho risolti per me. A quel punto mi sono fatto la domanda che probabilmente si fa chiunque tocchi un fork per la prima volta: ha senso proporre queste fix al progetto originale?&lt;/p&gt;

&lt;p&gt;Non l'avevo mai fatto. Ecco cosa ho imparato facendolo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guardare il terreno prima di scrivere
&lt;/h2&gt;

&lt;p&gt;Prima ancora di pensare a come strutturare la PR, ho controllato lo stato del repository originale: l'ultimo push risale a maggio 2025, e le uniche due Pull Request aperte (maggio e luglio 2025) sono entrambe un badge promozionale scollegato dal codice, non una revisione reale. Anche l'unica PR mergiata in passato era un badge. Non è un quadro di manutenzione attiva: non mi aspettavo un merge, ma ho deciso di procedere comunque, come caso di studio.&lt;/p&gt;

&lt;p&gt;Prima di aprire una PR conviene guardare la cronologia delle altre, per farsi un'idea realistica di cosa aspettarsi.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guardare anche gli altri fork, non solo l'originale
&lt;/h2&gt;

&lt;p&gt;Un repository pubblico su GitHub ha spesso decine di fork, e conviene controllarli prima di scrivere la propria fix: qualcun altro potrebbe averla già risolta, magari meglio, o aver introdotto nel frattempo una regressione da non ripetere. Ho confrontato tutti i fork esistenti con l'originale: quasi tutti fermi al momento della creazione, uno solo con del lavoro reale sopra, ed è quello che ho letto con attenzione.&lt;/p&gt;

&lt;p&gt;Guardare il lavoro altrui serve a confermare che la propria soluzione è quella giusta, e a volte a raccogliere un'idea isolata da tenere in un backlog per dopo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Isolare le modifiche: perché non lavorare sul proprio main
&lt;/h2&gt;

&lt;p&gt;Fatta la ricerca, restava da preparare cosa proporre davvero. Qui è emerso un problema pratico: il mio branch &lt;code&gt;main&lt;/code&gt; locale, nel tempo, aveva accumulato anche cose mie personali (un file &lt;code&gt;TODO.md&lt;/code&gt;, note nel &lt;code&gt;README&lt;/code&gt; specifiche del mio fork), e nessuna di queste doveva finire nella PR verso l'originale. Ma ci sarebbe finita per forza, se avessi aperto la PR direttamente da lì.&lt;/p&gt;

&lt;p&gt;La soluzione più pulita: creare un branch dedicato a partire dal &lt;code&gt;main&lt;/code&gt; dell'upstream, non dal mio, e portarci sopra solo i commit che voglio proporre con &lt;code&gt;git cherry-pick&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git fetch upstream
git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; fix/draft-articles-auth upstream/main
git cherry-pick &amp;lt;hash-commit-1&amp;gt; &amp;lt;hash-commit-2&amp;gt;
git push origin fix/draft-articles-auth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Il vantaggio pratico: qualsiasi cosa io committi dopo sul mio &lt;code&gt;main&lt;/code&gt; (il &lt;code&gt;TODO.md&lt;/code&gt;, un README personalizzato) non tocca mai questo branch, a prescindere da quando la committo: il branch della PR resta «congelato» a cosa gli ho esplicitamente portato con &lt;code&gt;cherry-pick&lt;/code&gt;. L'unica accortezza è tenere i commit atomici: mai mescolare nello stesso commit una fix genuina con roba personale, altrimenti il cherry-pick selettivo smette di essere semplice.&lt;/p&gt;

&lt;p&gt;Un dettaglio da sapere se i commit non sono indipendenti tra loro (nel mio caso, il secondo riusa una funzione introdotta dal primo): non si possono isolare separatamente, ma si possono comunque proporre insieme in un'unica PR. L'alternativa, «impilare» due PR separate dove la seconda dipende dalla prima, ha senso solo se ci si aspetta che vengano revisionate in tempi diversi. Con un maintainer fermo da maggio 2025, quella complessità in più non ripaga: meglio una PR sola, ben descritta.&lt;/p&gt;

&lt;h2&gt;
  
  
  Il dettaglio che mi ha fatto perdere tempo: il branch tracciava il repo sbagliato
&lt;/h2&gt;

&lt;p&gt;Il push da riga di comando è andato a buon fine, e l'errore è comparso altrove, per cui sul momento non capivo da dove venisse. &lt;code&gt;main&lt;/code&gt; l'avevo ottenuto clonando il mio fork (&lt;code&gt;git clone .../minnogit/devto-mcp.git&lt;/code&gt;), e Git imposta di default il tracking sul remote da cui si clona: &lt;code&gt;origin/main&lt;/code&gt;, cioè il mio fork, dove ho sempre avuto permessi di scrittura. Il branch nuovo l'ho creato con &lt;code&gt;git checkout -b fix/draft-articles-auth upstream/main&lt;/code&gt;, e quel comando imposta il tracking su &lt;code&gt;upstream/main&lt;/code&gt;, dove non ho scrittura. Poi ho fatto &lt;code&gt;git push origin &amp;lt;branch&amp;gt;&lt;/code&gt; (senza &lt;code&gt;-u&lt;/code&gt;): il branch è arrivato sul mio fork, ma la configurazione di tracking è rimasta puntata a &lt;code&gt;upstream&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Da riga di comando questo non si vede, perché specifico sempre &lt;code&gt;origin&lt;/code&gt; come destinazione del push. È emerso aprendo VSCode: la sua interfaccia si fida della configurazione di tracking invece di guardare cosa esiste sul server, quindi mi proponeva di «sincronizzare» verso &lt;code&gt;upstream&lt;/code&gt;, e il push falliva.&lt;/p&gt;

&lt;p&gt;Fix, una riga:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git branch &lt;span class="nt"&gt;--set-upstream-to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;origin/fix/draft-articles-auth fix/draft-articles-auth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si poteva evitare con &lt;code&gt;git push -u origin fix/draft-articles-auth&lt;/code&gt;, che al primo push imposta il tracking su &lt;code&gt;origin&lt;/code&gt;, oppure creando il branch con &lt;code&gt;--no-track&lt;/code&gt;: &lt;code&gt;git checkout -b fix/draft-articles-auth --no-track upstream/main&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Aprire la PR: due percorsi diversi, un errore in comune
&lt;/h2&gt;

&lt;p&gt;Da riga di comando, &lt;code&gt;gh pr create&lt;/code&gt; accetta titolo e descrizione via flag (&lt;code&gt;--title&lt;/code&gt;, &lt;code&gt;--body-file&lt;/code&gt; per un testo su più righe salvato in un file):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh &lt;span class="nb"&gt;pr &lt;/span&gt;create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--repo&lt;/span&gt; Arindam200/devto-mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--base&lt;/span&gt; main &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--head&lt;/span&gt; minnogit:fix/draft-articles-auth &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--title&lt;/span&gt; &lt;span class="s2"&gt;"..."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--body-file&lt;/span&gt; pr-body.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Da VSCode, ho avuto un errore diverso e più subdolo: &lt;code&gt;No commits between main and fix/draft-articles-auth&lt;/code&gt;. La causa, per quanto ho capito: l'interfaccia aveva impostato come «base repository» il mio fork, non l'originale, e siccome il mio &lt;code&gt;main&lt;/code&gt; conteneva già gli stessi commit (i due fix li avevo cherry-pickati proprio da lì), il confronto risultava vuoto. Serve cambiare esplicitamente il repository di base, non solo il branch: nella schermata di creazione PR (o sul sito, dietro il link «compare across forks») vanno impostati due selettori distinti, repository di base (l'originale) e repository di confronto (il proprio fork).&lt;/p&gt;

&lt;h2&gt;
  
  
  Sulla lingua della descrizione
&lt;/h2&gt;

&lt;p&gt;Il repository originale (README, issue, le altre PR) è tutto in inglese. Ho scritto titolo e descrizione della PR in inglese, anche se i miei commit restavano in italiano (non li ho toccati, e va benissimo così: la storia dei commit non deve necessariamente combaciare con la lingua della conversazione attorno alla PR). Ha senso scrivere per il pubblico che effettivamente la leggerà.&lt;/p&gt;

&lt;h2&gt;
  
  
  Perché ho aperto la PR comunque
&lt;/h2&gt;

&lt;p&gt;Con un repository fermo da maggio 2025, la PR probabilmente non verrà mai guardata. Ho deciso di aprirla comunque: i commit erano già pronti e puliti, il costo marginale era basso, e anche se resta ignorata resta pubblica, quindi chi incontra lo stesso bug potrebbe trovarla, con la fix già pronta. Il mio fork intanto continua a funzionarmi perfettamente, a prescindere dall'esito.&lt;/p&gt;

&lt;p&gt;La PR è qui, se vuoi vedere com'è venuta: &lt;a href="https://github.com/Arindam200/devto-mcp/pull/4" rel="noopener noreferrer"&gt;Arindam200/devto-mcp#4&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>git</category>
      <category>opensource</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Un agente AI come editor dei miei articoli Dev.to</title>
      <dc:creator>minnogit</dc:creator>
      <pubDate>Sun, 18 Oct 2026 18:18:37 +0000</pubDate>
      <link>https://dev.to/minnogit/un-agente-ai-come-editor-dei-miei-articoli-devto-37c1</link>
      <guid>https://dev.to/minnogit/un-agente-ai-come-editor-dei-miei-articoli-devto-37c1</guid>
      <description>&lt;p&gt;Tengo tutti i miei articoli Dev.to (pubblicati e bozze) sotto controllo di versione in un repository Git privato, &lt;code&gt;devto-articles&lt;/code&gt;, e li scrivo e pubblico con l'aiuto di Claude Code, collegato a Dev.to tramite un server MCP, &lt;a href="https://github.com/minnogit/devto-mcp" rel="noopener noreferrer"&gt;&lt;code&gt;devto-mcp&lt;/code&gt;&lt;/a&gt; (un mio fork di &lt;a href="https://github.com/Arindam200/devto-mcp" rel="noopener noreferrer"&gt;&lt;code&gt;Arindam200/devto-mcp&lt;/code&gt;&lt;/a&gt;). Lo racconto perché l'agente lavora anche da editor, prima ancora che da postino verso Dev.to, e quello alza la qualità del testo finale più di quanto farebbe da solo un buon editor di testo.&lt;/p&gt;

&lt;h2&gt;
  
  
  L'agente come editor
&lt;/h2&gt;

&lt;p&gt;Concretamente, cosa fa in più rispetto a «scrivi il markdown e mandalo»: prima di tutto legge un paio di articoli già presenti nel repo per intonarsi a tono e struttura, invece di scrivere con la voce media di un modello linguistico. Poi applica una checklist esplicita contro i pattern tipici della prosa generata: via l'em-dash usato come connettore universale, via il contrasto fittizio «non è X, è Y» usato come effetto retorico invece che per correggere un vero equivoco del lettore, via le aperture e chiusure da manuale (tipo «Nel mondo di oggi...» o «In conclusione...»), via il lessico da traduzione automatica.&lt;/p&gt;

&lt;p&gt;Questo stesso articolo ne è un esempio concreto. La prima stesura conteneva oltre dieci em-dash e un paragrafo di chiusura costruito proprio sul contrasto fittizio segnalato sopra. Li ho fatti notare, l'agente ha riletto il testo e li ha corretti uno per uno, mostrandomi il diff prima di rimandarlo a Dev.to. Ecco una correzione, da una versione intermedia di questo articolo:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Prima: «Non è mai autopilota, e la differenza non è cosmetica: cambia chi decide cosa finisce pubblicato, anche quando non sono io a battere per primo le parole sulla tastiera.»&lt;/p&gt;

&lt;p&gt;Dopo: «In pratica chi decide cosa viene pubblicato sono io, anche quando le prime parole le scrive l'agente.»&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;La checklist che ha usato per farlo (sette divieti fissi e due cose da evitare, con le fonti da cui vengono) è ora parte permanente della skill che uso per scrivere, e si ripete a ogni articolo nuovo o rivisto, non solo su questo.&lt;/p&gt;

&lt;p&gt;Un altro aspetto: di solito l'articolo nuovo non parte da me che scrivo prosa da zero, ma da una sessione di lavoro già conclusa nella stessa conversazione, di cui l'agente ha già tutto il contesto (un bug risolto, una PR aperta). Le informazioni non le avevo mai messe per iscritto in forma di articolo: gli chiedo di scriverlo dopo, a lavoro finito, e il testo lo stende l'agente a partire da quello che abbiamo fatto insieme, non da una narrazione inventata.&lt;/p&gt;

&lt;p&gt;C'è poi una parte meno visibile ma altrettanto concreta: prima di sovrascrivere un file già sincronizzato con Dev.to, l'agente confronta lo stato locale con quello remoto e si ferma a chiedere se trova un disallineamento, invece di procedere alla cieca. Su un archivio che continuo a modificare anche a mano dal portale, questo controllo fa la differenza tra un assistente che aiuta a scrivere e uno che, ogni tanto, cancella silenziosamente qualcosa che avevi scritto altrove.&lt;/p&gt;

&lt;p&gt;Conviene essere precisi su cosa intendo con «editor». Dev.to distingue tre livelli di dichiarazione dell'uso dell'AI: scritto a mano, «AI-assisted» e «Fully Autonomous», quest'ultimo per i contenuti generati soprattutto da agenti o LLM. Questo articolo è dichiarato «AI-assisted». Anche quando lo stende materialmente l'agente a partire da una sessione di lavoro, il testo racconta cose che ho fatto davvero (bug risolti, decisioni prese insieme, comandi eseguiti) e non materiale inventato per l'occasione. Nessuna riga esce senza che io l'abbia riletta e senza una mia conferma esplicita prima della pubblicazione. In pratica chi decide cosa viene pubblicato sono io, anche quando le prime parole le scrive l'agente.&lt;/p&gt;

&lt;h2&gt;
  
  
  Perché tenere gli articoli in un repository
&lt;/h2&gt;

&lt;p&gt;Le motivazioni sono più d'una, e questa (avere un editor che lavora sugli stessi file con git, cronologia e diff) è la principale. Le altre:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Backup versionato indipendente da Dev.to: se il portale perde dati, cancella per errore una bozza o cambia formato, ho comunque una copia con tutta la storia delle modifiche, non legata alla disponibilità o alle scelte di un servizio terzo.&lt;/li&gt;
&lt;li&gt;Portabilità verso altre piattaforme: avere il testo grezzo in Markdown in un repository mio, senza dipendenze dal formato interno di Dev.to, rende più semplice riusare gli stessi articoli come base per pubblicarli altrove, in futuro, senza doverli prima esportare o estrarre dal portale.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Il meccanismo di sincronizzazione, in breve
&lt;/h2&gt;

&lt;p&gt;Questa parte è tecnica, ma serve a capire perché posso lasciare scrivere l'agente sui miei file senza temere di perdere lavoro: ogni file tiene traccia, nel front-matter, dell'hash SHA-256 del corpo dell'articolo così com'era su Dev.to all'ultimo sync riuscito (&lt;code&gt;devto_synced_hash&lt;/code&gt;). Prima di scrivere in un senso o nell'altro, l'agente ricalcola l'hash remoto attuale e lo confronta con quello salvato. Se combaciano, l'operazione è sicura. Se no, è un conflitto, e qui non c'è un &lt;code&gt;git merge&lt;/code&gt; che decide da solo: le due versioni mi vengono mostrate entrambe e la scelta su quale prevale resta mia. La riscrittura vera e propria, quando serve unire i due testi in un discorso coerente, la fa l'agente, ma solo dopo che gliel'ho chiesto e prima di rimandarla a Dev.to.&lt;/p&gt;

&lt;p&gt;Per un push, i passaggi in sequenza sono questi:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    actor Io
    participant Agente
    participant Repo as File nel repo
    participant Devto as Dev.to

    Io-&amp;gt;&amp;gt;Agente: correggi questo paragrafo
    Agente-&amp;gt;&amp;gt;Repo: modifica il file
    Agente-&amp;gt;&amp;gt;Repo: legge devto_synced_hash
    Agente-&amp;gt;&amp;gt;Devto: legge il body remoto
    Devto--&amp;gt;&amp;gt;Agente: body_markdown attuale
    Agente-&amp;gt;&amp;gt;Agente: calcola lo sha256 del body remoto
    alt gli hash coincidono
        Agente-&amp;gt;&amp;gt;Devto: update_article
        Agente-&amp;gt;&amp;gt;Repo: aggiorna devto_synced_hash e committa
    else gli hash sono diversi
        Agente-&amp;gt;&amp;gt;Io: mostra le due versioni
        Io-&amp;gt;&amp;gt;Agente: sceglie quale prevale
    end&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Un progetto per pubblicare su Dev.to da un repository Git, &lt;a href="https://github.com/maxime1992/dev-to-git" rel="noopener noreferrer"&gt;&lt;code&gt;dev-to-git&lt;/code&gt;&lt;/a&gt;, descrive solo la direzione dal repo verso il portale, e le proprietà dell'articolo si controllano da lì. Io modifico gli articoli anche a mano dal portale, quindi mi serve un controllo nelle due direzioni, non solo da qui verso Dev.to.&lt;/p&gt;

&lt;p&gt;In pratica, per un articolo nuovo l'agente non scrive prima un file provvisorio in &lt;code&gt;articles/&lt;/code&gt;. Scrive il body altrove e chiama &lt;code&gt;create_article&lt;/code&gt; per farlo esistere su Dev.to come bozza. Solo con la risposta in mano (id, url, hash del body riletto da remoto) scrive una volta sola il file definitivo in &lt;code&gt;articles/&amp;lt;id&amp;gt;-&amp;lt;slug&amp;gt;.md&lt;/code&gt; e lo committa nel mio repository. La pubblicazione su Dev.to e il commit nel repo sono due passaggi distinti, entrambi necessari.&lt;/p&gt;

&lt;p&gt;Un'alternativa altrettanto legittima è partire dal portale: creo io la bozza a mano nell'editor di Dev.to, e l'agente la importa nel repo con la stessa operazione di pull che usa per rilevare modifiche fatte da lì. Ogni modifica successiva ripete il controllo dell'hash prima di ripushare con &lt;code&gt;update_article&lt;/code&gt;. La pubblicazione (&lt;code&gt;published: true&lt;/code&gt;) è uno dei passaggi che l'agente conferma sempre esplicitamente prima di eseguire, perché è visibile pubblicamente e non banale da annullare in modo pulito.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cosa cambia davvero
&lt;/h2&gt;

&lt;p&gt;Scrivere in Markdown invece che nella textarea del portale è comodo, ma da solo si otterrebbe anche incollando poi il testo a mano. Il vantaggio vero è avere un editor che conosce la mia voce da un corpus reale di articoli precedenti, applica una checklist esplicita contro i tic della prosa generata prima di consegnare, e si ferma a chiedere invece di sovrascrivere alla cieca quando qualcosa non torna. La parte tecnica del sync rende tutto questo possibile senza rischiare di perdere lavoro. È infrastruttura di supporto, e il lavoro che conta è quello di scrittura descritto sopra.&lt;/p&gt;

</description>
      <category>git</category>
      <category>mcp</category>
      <category>writing</category>
      <category>ai</category>
    </item>
    <item>
      <title>Gestione repo script sul server</title>
      <dc:creator>minnogit</dc:creator>
      <pubDate>Thu, 25 Jun 2026 09:39:51 +0000</pubDate>
      <link>https://dev.to/minnogit/gestione-repo-script-sul-server-jj0</link>
      <guid>https://dev.to/minnogit/gestione-repo-script-sul-server-jj0</guid>
      <description>&lt;h1&gt;
  
  
  Come deployo gli script bash del mio repo git sul server
&lt;/h1&gt;

&lt;p&gt;Ho un repository git con decine di script bash che uso per la gestione del server (backup, restic, virtualhost, slack, deploy, ecc.).&lt;/p&gt;

&lt;p&gt;Alcuni script si richiamano tra loro. Devono quindi stare in una directory che sia nel PATH e che corrisponda ai path.&lt;/p&gt;

&lt;p&gt;La soluzione che ho adottato è questa:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/opt/scripts/              ← git clone del repo
/usr/local/sbin/script.sh  → symlink → /opt/scripts/script.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Perché funziona
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Il repo sta in &lt;code&gt;/opt/scripts/&lt;/code&gt;&lt;/strong&gt; — una directory dedicata, separata dai binari di sistema. Non si mescola con altro, non porta &lt;code&gt;.git/&lt;/code&gt; o file di configurazione in mezzo agli eseguibili.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I symlink stanno in &lt;code&gt;/usr/local/sbin/&lt;/code&gt;&lt;/strong&gt; — così gli script sono raggiungibili via PATH ed eventuali path hardcoded continuano a funzionare.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gli aggiornamenti sono semplici&lt;/strong&gt; — un &lt;code&gt;git pull&lt;/code&gt; in &lt;code&gt;/opt/scripts/&lt;/code&gt; aggiorna tutto, e i symlink puntano già ai file aggiornati.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Come si fa
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt;
git clone &amp;lt;url-del-repo&amp;gt; /opt/scripts
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x /opt/scripts/&lt;span class="k"&gt;*&lt;/span&gt;.sh
&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /opt/scripts/backup_all.sh /usr/local/sbin/
&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /opt/scripts/check_system_health.sh /usr/local/sbin/
&lt;span class="c"&gt;# ... e così via per ogni script che serve&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Perché non clono direttamente in &lt;code&gt;/usr/local/sbin/&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Perché nel repo ci sono anche file non eseguibili: &lt;code&gt;.git/&lt;/code&gt;, template &lt;code&gt;.tpl&lt;/code&gt;, file &lt;code&gt;.txt&lt;/code&gt;, script &lt;code&gt;.bat&lt;/code&gt; per Windows. Mettere tutto in &lt;code&gt;/usr/local/sbin/&lt;/code&gt; sporca il PATH e rischia interferenze con &lt;code&gt;git clean&lt;/code&gt; o &lt;code&gt;git status&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Con i symlink invece in &lt;code&gt;/usr/local/sbin/&lt;/code&gt; finiscono solo gli script che effettivamente servono, e il repo resta isolato e pulito in &lt;code&gt;/opt/scripts/&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>linux</category>
    </item>
    <item>
      <title>Come estrarre una singola tabella da un file dump MySQL</title>
      <dc:creator>minnogit</dc:creator>
      <pubDate>Tue, 19 May 2026 09:08:59 +0000</pubDate>
      <link>https://dev.to/minnogit/come-estrarre-una-singola-tabella-da-un-file-dump-mysql-35jm</link>
      <guid>https://dev.to/minnogit/come-estrarre-una-singola-tabella-da-un-file-dump-mysql-35jm</guid>
      <description>&lt;p&gt;A volte ho a disposizione solo il dump &lt;code&gt;.sql&lt;/code&gt; di un intero database e mi serve una tabella. Aprire un file di svariati gigabyte con un editor di testo è scomodo, e importare tutto su un server locale per recuperare poche righe è uno spreco. Basta un piccolo script che estrae solo la tabella che interessa, con i suoi dati.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo script
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# uso: estrai-tabella nome_tabella dump.sql &amp;gt; tabella.sql&lt;/span&gt;
&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-- Table structure for table &lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'
  BEGIN { header = 1 }
  /^\/\*!40103 SET TIME_ZONE=@OLD_TIME_ZONE/ { footer = 1; in_t = 0 }
  /^-- (Table structure for table|Temporary view structure for view|Final view structure for view|Dumping routines|Dumping events)/ { header = 0; in_t = ($0 == target) }
  (header || in_t || footer) &amp;amp;&amp;amp; !(in_t &amp;amp;&amp;amp; /^DROP TABLE/)
'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si usa così, con il nome della tabella e il dump generato da &lt;code&gt;mysqldump&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;estrai-tabella nome_tabella dump_completo.sql &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; nome_tabella.sql
mysql nome_database &amp;lt; nome_tabella.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Come funziona
&lt;/h2&gt;

&lt;p&gt;Un dump di &lt;code&gt;mysqldump&lt;/code&gt; è diviso in blocchi, ognuno introdotto da un commento come &lt;code&gt;-- Table structure for table&lt;/code&gt; seguito dal nome della tabella. Lo script stampa tre parti:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;l'intestazione, cioè le righe prima del primo blocco: impostano la codifica e disattivano il controllo delle chiavi esterne;&lt;/li&gt;
&lt;li&gt;il blocco della tabella scelta, dal suo commento fino al commento del blocco successivo, quindi struttura, dati e trigger;&lt;/li&gt;
&lt;li&gt;il piè di pagina, che rimette a posto le impostazioni cambiate dall'intestazione.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La riga &lt;code&gt;DROP TABLE IF EXISTS&lt;/code&gt; del blocco viene tolta. Prima usavo un intervallo di &lt;code&gt;sed&lt;/code&gt; tra &lt;code&gt;DROP TABLE&lt;/code&gt; e &lt;code&gt;UNLOCK TABLES&lt;/code&gt;, ma lasciava fuori l'intestazione e i trigger, che &lt;code&gt;mysqldump&lt;/code&gt; scrive dopo &lt;code&gt;UNLOCK TABLES&lt;/code&gt;. Con il &lt;code&gt;DROP&lt;/code&gt; dentro, importare quel file cancellava la tabella esistente, e con essa anche i suoi trigger, che il file non ricreava.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cosa succede all'import
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Se la tabella esiste già, l'import si ferma con l'errore &lt;code&gt;Table 'nome_tabella' already exists&lt;/code&gt; e non cancella nulla. Per sostituirla la cancello io prima con &lt;code&gt;DROP TABLE&lt;/code&gt;. Se è referenziata da una chiave esterna, anche il &lt;code&gt;DROP&lt;/code&gt; si ferma (&lt;code&gt;Cannot drop table ... referenced by a foreign key constraint&lt;/code&gt;) finché non disattivo i controlli con &lt;code&gt;SET FOREIGN_KEY_CHECKS=0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Le chiavi esterne della tabella vengono ricreate. Grazie all'intestazione, l'import funziona anche se la tabella a cui puntano non c'è ancora.&lt;/li&gt;
&lt;li&gt;I trigger della tabella vengono ricreati.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limiti
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Ho provato lo script con &lt;code&gt;mysqldump&lt;/code&gt; 9.7 e un server MySQL 8.4, importando una tabella con un trigger, una con una chiave esterna e una vuota in un database nuovo. Non l'ho provato con MariaDB né con versioni più vecchie di &lt;code&gt;mysqldump&lt;/code&gt;: i commenti di separazione dovrebbero essere gli stessi, ma è un'ipotesi.&lt;/li&gt;
&lt;li&gt;Se il dump è stato creato con &lt;code&gt;--skip-comments&lt;/code&gt;, i commenti che separano i blocchi non ci sono e lo script non trova la tabella.&lt;/li&gt;
&lt;li&gt;I trigger riportano un &lt;code&gt;DEFINER&lt;/code&gt;, cioè l'utente che li ha creati (nella mia prova &lt;code&gt;root@localhost&lt;/code&gt;). Se importo su un altro server, quell'utente deve esistere, altrimenti la creazione del trigger fallisce. Questo non l'ho provato.&lt;/li&gt;
&lt;li&gt;Viste e stored procedure non vengono estratte, anche se dipendono dalla tabella.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;awk&lt;/code&gt; legge il dump fino in fondo, anche dopo aver stampato il blocco. Su file molto grandi ci mette il tempo di una lettura completa, che non ho misurato.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mysql</category>
      <category>sql</category>
      <category>database</category>
      <category>bash</category>
    </item>
    <item>
      <title>Stabilizzazione dell'Infrastruttura: Gestire il Connection Pooling tra Proxy e Backend</title>
      <dc:creator>minnogit</dc:creator>
      <pubDate>Mon, 18 May 2026 13:58:20 +0000</pubDate>
      <link>https://dev.to/minnogit/stabilizzazione-dellinfrastruttura-gestire-il-connection-pooling-tra-proxy-e-backend-amg</link>
      <guid>https://dev.to/minnogit/stabilizzazione-dellinfrastruttura-gestire-il-connection-pooling-tra-proxy-e-backend-amg</guid>
      <description>&lt;p&gt;Nel mantenimento di architetture web complesse, l'ottimizzazione del dialogo tra il Proxy (nel nostro caso Apache) e i nodi di backend è un passaggio cruciale per garantire la fluidità dei servizi. Recentemente abbiamo analizzato una situazione di saturazione delle risorse che ci ha permesso di approfondire la gestione dei socket TCP, il comportamento dei pool di connessione e la complessa natura dei timeout multilivello.&lt;/p&gt;

&lt;p&gt;In questo articolo vedremo come abbiamo identificato il problema, gli strumenti di monitoraggio utilizzati e come configurare correttamente i timeout per proteggere il proxy, senza penalizzare i portali che richiedono elaborazioni lunghe.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. I primi segnali: Errori di Fork
&lt;/h2&gt;

&lt;p&gt;Il punto di partenza della nostra analisi è stata una serie di messaggi nei log di Apache che indicavano difficoltà nel gestire nuovi processi, non un crash improvviso:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[error] (11)Resource temporarily unavailable: apr_thread_create: unable to create worker thread&lt;/code&gt;&lt;br&gt;
&lt;code&gt;[error] (11)Resource temporarily unavailable: fork: avocado_create: pool_task&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Questi errori di fork compaiono quando il server raggiunge i limiti imposti dal sistema operativo o dalla configurazione dell'MPM (Multi-Processing Module). I worker esistenti rimangono occupati troppo a lungo, impedendo la creazione di nuovi thread per servire le nuove richieste in ingresso.&lt;/p&gt;


&lt;h2&gt;
  
  
  2. Monitoraggio e Analisi del Traffico
&lt;/h2&gt;

&lt;p&gt;Per capire perché Apache stesse saturando i worker, abbiamo utilizzato alcuni comandi shell per analizzare lo stato delle connessioni di rete in tempo reale.&lt;/p&gt;
&lt;h3&gt;
  
  
  Analisi quantitativa per backend
&lt;/h3&gt;

&lt;p&gt;Per osservare come variava il carico verso un nodo specifico (nel nostro caso il &lt;code&gt;.106&lt;/code&gt;), abbiamo utilizzato un monitoraggio continuo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;watch &lt;span class="nt"&gt;-n&lt;/span&gt; 1 &lt;span class="s2"&gt;"netstat -atpn | grep 172.31.41.106 | wc -l"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Questo comando ci ha permesso di notare che il numero di connessioni non scalava verso il basso come previsto, indicando una persistenza anomala dei socket.&lt;/p&gt;

&lt;h3&gt;
  
  
  Analisi della distribuzione delle connessioni
&lt;/h3&gt;

&lt;p&gt;Un altro comando fondamentale è stato quello per mappare la provenienza del traffico e identificare eventuali anomalie nella distribuzione dei client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;netstat &lt;span class="nt"&gt;-ntu&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $5}'&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;: &lt;span class="nt"&gt;-f1&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;L'analisi dettagliata ha rivelato un numero elevato di connessioni in stato &lt;code&gt;CLOSE_WAIT&lt;/code&gt;. Questo stato indica che il backend ha già terminato la sua parte di connessione, ma il proxy non ha ancora chiuso il socket lato suo. Un vero e proprio "limbo" TCP.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Disaccoppiare i Timeout: Applicazione vs Infrastruttura
&lt;/h2&gt;

&lt;p&gt;Per risolvere questo scenario, è fondamentale fare chiarezza sulla differenza tra due concetti spesso confusi:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Timeout Applicativo:&lt;/strong&gt; È il tempo richiesto dal codice sorgente per completare un'operazione (es. una query complessa o la generazione di un file pesante). Un timeout applicativo troppo lungo espone il server al pericolo di elaborazioni infinite, mantenendo il backend occupato e saturando le risorse computazionali.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timeout Infrastrutturale (o di Rete):&lt;/strong&gt; È il tempo massimo concesso per la trasmissione dei dati e la gestione dei socket tra i vari nodi della rete.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Il problema esaminato in questa analisi riguardava specificamente il timeout della comunicazione infrastrutturale tra il proxy e l'applicazione, ovvero i socket rimasti orfani che bloccavano i thread di Apache. L'implementazione applicativa interna al backend è ininfluente: che si tratti di un'architettura o di un'altra, se la gestione del socket TCP lato proxy fallisce, l'infrastruttura si satura.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Timeout per route: cosa si può e cosa no
&lt;/h2&gt;

&lt;p&gt;Durante le indagini abbiamo esplorato la possibilità di creare configurazioni di timeout differenziate a seconda della route (URL path), in modo da isolare i percorsi più lenti da quelli più veloci.&lt;/p&gt;

&lt;p&gt;Il limite che abbiamo incontrato è più circoscritto di quanto sembrasse: dentro un singolo &lt;code&gt;balancer://&lt;/code&gt; il path non seleziona il membro. A decidere quale &lt;code&gt;BalancerMember&lt;/code&gt; serve la richiesta è l'algoritmo di bilanciamento (&lt;code&gt;lbmethod&lt;/code&gt;), non l'URL richiesto, quindi differenziare i timeout per route agendo sui membri dello stesso cluster non può funzionare per costruzione.&lt;/p&gt;

&lt;p&gt;Non è invece vero che il timeout non sia granularizzabile: &lt;code&gt;timeout&lt;/code&gt; è un parametro documentato di worker/BalancerMember (&lt;em&gt;"Socket timeout in seconds. The number of seconds Apache httpd waits for data sent by / to the backend"&lt;/em&gt;, con default pari a &lt;code&gt;ProxyTimeout&lt;/code&gt;), e la configurazione che adottiamo più avanti ne è la prova, dato che imposta &lt;code&gt;connectiontimeout&lt;/code&gt; sul singolo membro. Per differenziare davvero per route servono worker distinti: balancer separati indirizzati da &lt;code&gt;ProxyPass&lt;/code&gt; diversi, oppure &lt;code&gt;ProxyPass&lt;/code&gt; verso URL di backend distinti.&lt;/p&gt;

&lt;p&gt;C'è però un'insidia che rende l'operazione fragile, e che spiega perché molti tentativi sembrano non avere effetto: il worker sharing. I worker sono identificati dall'URL di backend, e se l'URL di uno è prefisso di quello di un worker definito dopo, il secondo non viene creato e i suoi parametri vengono ignorati, con un warning nell'error log. La documentazione indica il rimedio: ordinare le definizioni partendo dagli URL più lunghi.&lt;/p&gt;

&lt;p&gt;Nel nostro caso abbiamo comunque applicato la strategia in modo uniforme all'intero blocco proxy, perché non dipende dall'ordine delle direttive e regge meglio le modifiche successive.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. La Soluzione: Il Ruolo di &lt;code&gt;connectiontimeout&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;In molti dei nostri portali è indispensabile mantenere timeout globali alti per consentire l'esecuzione di script applicativi lunghi ed elaborazioni pesanti. Abbassare indiscriminatamente il timeout del proxy avrebbe interrotto servizi legittimi.&lt;/p&gt;

&lt;p&gt;La soluzione definitiva che ha azzerato i &lt;code&gt;CLOSE_WAIT&lt;/code&gt; e risolto gli errori di fork, pur mantenendo il supporto agli script lunghi, è stata il passaggio alla sintassi &lt;code&gt;balancer://&lt;/code&gt; sfruttando il parametro &lt;strong&gt;&lt;code&gt;connectiontimeout&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Configurazione applicata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nl"&gt;Proxy&lt;/span&gt;&lt;span class="sr"&gt; balancer://frontoffice&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="c"&gt;# node04 - Configurazione ottimizzata&lt;/span&gt;
    &lt;span class="nc"&gt;BalancerMember&lt;/span&gt; http://172.31.41.106:8080 route=4 connectiontimeout=2 retry=10 hcmethod=TCP hcinterval=5 hcpasses=2 hcfails=1

    &lt;span class="nc"&gt;ProxySet&lt;/span&gt; lbmethod=bybusyness
    &lt;span class="nc"&gt;ProxySet&lt;/span&gt; stickysession=ROUTEID
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nl"&gt;Proxy&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;
&lt;span class="nc"&gt;ProxyPass&lt;/span&gt; "/" "balancer://frontoffice/"
&lt;span class="nc"&gt;ProxyPassReverse&lt;/span&gt; "/" "balancer://frontoffice/"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Perché questa configurazione è risolutiva?
&lt;/h3&gt;

&lt;p&gt;Il segreto sta nella separazione tra il timeout di risposta globale e il timeout di connessione iniziale:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;connectiontimeout=2&lt;/code&gt; (La vera protezione)&lt;/strong&gt;: Questo parametro agisce solo sulla fase di handshake TCP iniziale. Se il backend è congestionato, bloccato o non risponde entro 2 secondi, Apache desiste immediatamente e libera il proprio worker. Questo previene l'accumulo di processi appesi in Apache che causavano gli errori di fork.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mantenimento degli script lunghi&lt;/strong&gt;: Una volta stabilita la connessione nei primi 2 secondi, il proxy concede all'applicazione tutto il tempo configurato nel timeout globale per terminare le sue elaborazioni pesanti.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health Checks attivi (&lt;code&gt;hcinterval=5&lt;/code&gt;)&lt;/strong&gt;: Apache interroga il backend ogni 5 secondi. Se un nodo fallisce i controlli TCP, viene temporaneamente isolato dal cluster, evitando di inviare traffico a un server saturo.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  6. Conclusioni e Manutenzione Futura
&lt;/h2&gt;

&lt;p&gt;La risoluzione degli errori di fork ci ha ricordato che la stabilità di un'infrastruttura richiede di configurare correttamente i limiti della rete, senza per questo sacrificare le necessità dell'applicazione (come le elaborazioni lunghe).&lt;/p&gt;

&lt;p&gt;Per evitare derive configurative e mantenere l'uniformità su tutta la flotta di server, la distribuzione di queste direttive &lt;code&gt;BalancerMember&lt;/code&gt; e la gestione dei relativi VirtualHost verrà centralizzata e gestita tramite Puppet Server. L'automazione è l'unica via per garantire che queste best practice rimangano stabili e documentate nel tempo.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cheat Sheet di Emergenza per il futuro:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Isolamento rapido:&lt;/strong&gt; &lt;code&gt;netstat -atpn | grep CLOSE_WAIT&lt;/code&gt; per contare i socket orfani.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analisi client:&lt;/strong&gt; &lt;code&gt;netstat -ntu | awk '{print $5}' ...&lt;/code&gt; per verificare la distribuzione degli IP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regola d'oro:&lt;/strong&gt; Separare sempre il tempo concesso all'applicazione per elaborare i dati dal tempo concesso alla rete per aprire la connessione (&lt;code&gt;connectiontimeout&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>devops</category>
      <category>linux</category>
    </item>
  </channel>
</rss>
