<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Software Development by mm-dev on mm-dev</title>
    <link>https://mm-dev.rocks/</link>
    <description>Recent content in Software Development by mm-dev on mm-dev</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-gb</language><atom:link href="https://mm-dev.rocks/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Proper Dotted Borders</title>
      <link>https://mm-dev.rocks/posts/proper-dotted-borders/</link>
      <pubDate>Mon, 01 Sep 2025 07:46:58 +0100</pubDate>
      <guid>https://mm-dev.rocks/posts/proper-dotted-borders/</guid>
      <description>&lt;ul class=&#39;gallery single-item&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_489R_1&#39; /&gt;
      &lt;label for=&#39;item_489R_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/proper-dotted-borders/css-dotted-dashed-issues.webp&#39; alt=&#39;Issues with CSS dashed and dotted border styles&#39; /&gt;
        &lt;span&gt;Issues with CSS dashed and dotted border styles&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CSS has dotted and dashed borders. They&amp;rsquo;re fine but don&amp;rsquo;t always look the way you might expect or prefer.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;border-style: dotted&lt;/code&gt; dots are round (on my site I wanted square)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;border-style: dashed&lt;/code&gt; dashes are nice and sharp but on corners the dashes become L shapes&lt;/li&gt;
&lt;li&gt;Both dots and dashes are drawn inconsistently depending on how thick they are.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;9-slice-scaling&#34;&gt;9-Slice Scaling&lt;/h2&gt;
&lt;ul class=&#39;gallery single-item&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_DyCh_1&#39; /&gt;
      &lt;label for=&#39;item_DyCh_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/proper-dotted-borders/9-slice-numbered.webp&#39; alt=&#39;Green dotted border image with 9 slices overlaid, slices 2, 4, 6 &amp;#43; 8 would repeat to fill the edges of their image&#39; /&gt;
        &lt;span&gt;Green dotted border image with 9 slices overlaid, slices 2, 4, 6 &amp;#43; 8 would repeat to fill the edges of their image&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;9-slice scaling is a technique for defining how images scale that&amp;rsquo;s been around since at least the days of Macromedia Flash. It&amp;rsquo;s typically used when we want to scale a border and avoid the ugly distortion we&amp;rsquo;d encounter if we were to just naively stretch an image.&lt;/p&gt;
&lt;p&gt;An image is sliced up into 9 pieces, each piece being used to draw a specific edge or corner of a border. The top, bottom, left and right edges stretch (or repeat) to fit the content, but the corners are not scaled so they don&amp;rsquo;t distort. When it&amp;rsquo;s all put together we get a nice clean border.&lt;/p&gt;
&lt;p&gt;9-slicing was a common way of achieving rounded corners on the web before we had &lt;code&gt;border-radius&lt;/code&gt;. In those days it required assembling all of the image slices into a &lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt; or similar, then using that element as a background for the thing we wanted bordered. Hacky, but it worked and if you wanted rounded corners that didn&amp;rsquo;t look like hockey sticks before 2015, it&amp;rsquo;s what you did.&lt;/p&gt;
&lt;h3 id=&#34;css-border-image&#34;&gt;CSS &lt;code&gt;border-image&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;CSS now has &lt;a href = &#34;https://developer.mozilla.org/en-US/docs/Web/CSS/border-image&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;border-image&lt;/a&gt; which lets us use the 9-slice method properly. It depends on a specific, static image though, meaning if we want borders of different colours we have to create a separate image for each color. Particularly annoying while we&amp;rsquo;re experimenting with different colours.&lt;/p&gt;
&lt;h2 id=&#34;sass-can-change-the-fill-colour-of-an-svg&#34;&gt;SASS can Change the Fill Colour of an SVG&lt;/h2&gt;
&lt;p&gt;We can get around this by using an SVG image and encoding it as a Data URI (ie &lt;code&gt;url(&amp;quot;data:image/svg...&amp;quot;)&lt;/code&gt;), only instead of the usual fill colour inside the SVG, we use placeholder text eg &lt;code&gt;FILLCOLOR&lt;/code&gt;. We can then use SASS to find/replace that string with whichever colour we want.&lt;/p&gt;
&lt;h3 id=&#34;string-replacement&#34;&gt;String replacement&lt;/h3&gt;
&lt;p&gt;SASS doesn&amp;rsquo;t directly have a string replacement function but it&amp;rsquo;s simple to add one. I found one from &lt;a href = &#34;https://css-tricks.com/snippets/sass/str-replace-function/&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;CSS Tricks&lt;/a&gt; (I had to update it a little because SASS string function names have changed since it was written).&lt;/p&gt;
&lt;p&gt;As you can see SASS has &lt;code&gt;string.index()&lt;/code&gt;, which is just like &lt;code&gt;String.prototype.lastIndexOf()&lt;/code&gt; in JavaScript (it gives us the character position/index of the matching string). SASS also has &lt;code&gt;string.length()&lt;/code&gt;, so getting from those two functions to a &lt;code&gt;str-replace&lt;/code&gt; is straightforward.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-scss&#34; data-lang=&#34;scss&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;@use&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;sass:string&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;/// Replace `$search` with `$replace` in `$string`
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;/// @author Kitty Giraudel
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;@function&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt; str-replace&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$string&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$search&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$replace&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nv&#34;&gt;$index&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;index&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$string&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$search&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;@if&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$index&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nv&#34;&gt;$string&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;slice&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$string&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$index&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;nv&#34;&gt;$replace&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;nf&#34;&gt;str-replace&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;slice&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$string&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$index&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;length&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$search&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nv&#34;&gt;$search&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$replace&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;@return&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;getting-the-coloured-image&#34;&gt;Getting the coloured image&lt;/h3&gt;
&lt;p&gt;We&amp;rsquo;ll stick to hex colours, because the URI encoding for those is really simple: the &lt;code&gt;#&lt;/code&gt; just has to be converted to &lt;code&gt;%23&lt;/code&gt;, so eg &lt;code&gt;#ed397d&lt;/code&gt; becomes &lt;code&gt;%23ed397d&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The SVG data is stored in a SASS variable &lt;code&gt;$dotted-border-image-enc&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Using the &lt;code&gt;str-replace&lt;/code&gt; function from above we can create a &lt;code&gt;border-image-colored&lt;/code&gt; function which takes a hex colour, inserts it into &lt;code&gt;$dotted-border-image-enc&lt;/code&gt;, and returns a &lt;code&gt;url(&amp;quot;data:image/svg...&amp;quot;)&lt;/code&gt; which can be used as our &lt;code&gt;border-image-source&lt;/code&gt; in CSS.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-scss&#34; data-lang=&#34;scss&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// SVG string, note the `style=&amp;#39;fill:FILLCOLOR;`
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$dotted-border-image-enc&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;data:image/svg+xml,%3Csvg xmlns=&amp;#39;http://www.w3.org/2000/svg&amp;#39; width=&amp;#39;90&amp;#39; height=&amp;#39;90&amp;#39;%3E%3Cg style=&amp;#39;fill:FILLCOLOR&amp;#39;%3E%3Cpath d=&amp;#39;M10 10h10v10H10zM70 10h10v10H70zM40 40h10v10H40zM40 10h10v10H40zM10 70h10v10H10zM40 70h10v10H40zM70 70h10v10H70zM70 40h10v10H70zM10 40h10v10H10z&amp;#39;/%3E%3C/g%3E%3C/svg%3E&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;@function&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt; border-image-colored&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$color_hex&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;c1&#34;&gt;// Replace hex colour with url-encoded colour
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;  &lt;span class=&#34;nv&#34;&gt;$enc-color&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;str-replace&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$color_hex&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;#&amp;#34;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;%23&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nv&#34;&gt;$img-url&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;str-replace&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nv&#34;&gt;$dotted-border-image-enc&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s2&#34;&gt;&amp;#34;FILLCOLOR&amp;#34;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nv&#34;&gt;$enc-color&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;@return&lt;/span&gt; &lt;span class=&#34;sx&#34;&gt;url($img-url)&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;getting-our-sliced-image-border&#34;&gt;Getting our sliced image border&lt;/h2&gt;
&lt;p&gt;Now we have a coloured image we can slice it up and apply it to borders. We&amp;rsquo;ll make a SASS &lt;code&gt;@mixin&lt;/code&gt; that we can re-use wherever we want a dotted border.&lt;/p&gt;
&lt;p&gt;The CSS &lt;code&gt;border-image-*&lt;/code&gt; properties interact with each other in ways which can be confusing. I&amp;rsquo;ve used values here that worked with my image but check the &lt;a href = &#34;https://developer.mozilla.org/en-US/docs/Web/CSS/border-image&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;MDN docs for border-image&lt;/a&gt; and play around.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A solid border is drawn for a split second while the border image loads. To prevent a flicker, we set &lt;code&gt;border-color&lt;/code&gt; to match the background.&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-scss&#34; data-lang=&#34;scss&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;@mixin&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt; dotted-border&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$color_hex&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;na&#34;&gt;border-style&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;ni&#34;&gt;solid&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;na&#34;&gt;border-width&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;30&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;px&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;na&#34;&gt;border-image-width&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;30&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;px&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;na&#34;&gt;border-image-slice&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;30&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;na&#34;&gt;border-image-repeat&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;ni&#34;&gt;round&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;c1&#34;&gt;// Prevent flash of foreground colour by setting the initial colour to match the page background
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;  &lt;span class=&#34;na&#34;&gt;border-color&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;ni&#34;&gt;white&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;c1&#34;&gt;// Call the function to get the SVG Data URI
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;  &lt;span class=&#34;na&#34;&gt;border-image-source&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;border-image-colored&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;#{&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$color_hex&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;allowing-for-optional-border-sides&#34;&gt;Allowing for optional border sides&lt;/h3&gt;
&lt;p&gt;Sometimes we might want to only show the border on specific sides of its element so we&amp;rsquo;ll give the mixin a &lt;code&gt;$sides...&lt;/code&gt; argument. This means &amp;ldquo;take all remaining arguments (after &lt;code&gt;$color_hex&lt;/code&gt;) and make them members of a list called &lt;code&gt;$sides&lt;/code&gt;&amp;rdquo;. If &lt;code&gt;$sides&lt;/code&gt; is not provided we&amp;rsquo;ll draw the full border.&lt;/p&gt;
&lt;p&gt;It might look more complicated but the main difference is instead of setting eg &lt;code&gt;border-style: solid&lt;/code&gt; as a whole, we set &lt;code&gt;border-right-style: solid&lt;/code&gt; and so on, depending on which sides we want to draw.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-scss&#34; data-lang=&#34;scss&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;@mixin&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt; dotted-border&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$color_hex&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$sides&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;...&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;c1&#34;&gt;// biw = border image width
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;  &lt;span class=&#34;nv&#34;&gt;$biw_top&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;initial&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nv&#34;&gt;$biw_left&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;initial&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nv&#34;&gt;$biw_right&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;initial&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nv&#34;&gt;$biw_bottom&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;initial&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nv&#34;&gt;$border_width&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;30&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;px&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nv&#34;&gt;$border_image_width&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;30&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;px&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;c1&#34;&gt;// Ensure we&amp;#39;re starting clean
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;  &lt;span class=&#34;na&#34;&gt;border-style&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;ni&#34;&gt;none&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;@if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;list&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;length&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$sides&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;// For each specified border, set its style and width ready to receive the image
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;    &lt;span class=&#34;k&#34;&gt;@each&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$side&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;in&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$sides&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;k&#34;&gt;@if&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$side&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;left&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;na&#34;&gt;border-left-style&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;ni&#34;&gt;solid&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;na&#34;&gt;border-left-width&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;si&#34;&gt;#{&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$border_width&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nv&#34;&gt;$biw_left&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;si&#34;&gt;#{&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$border_image_width&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;@else if&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$side&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;right&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;na&#34;&gt;border-right-style&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;ni&#34;&gt;solid&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;na&#34;&gt;border-right-width&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;si&#34;&gt;#{&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$border_width&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nv&#34;&gt;$biw_right&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;si&#34;&gt;#{&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$border_image_width&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;@else if&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$side&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;top&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;na&#34;&gt;border-top-style&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;ni&#34;&gt;solid&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;na&#34;&gt;border-top-width&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;si&#34;&gt;#{&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$border_width&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nv&#34;&gt;$biw_top&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;si&#34;&gt;#{&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$border_image_width&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;@else if&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$side&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;bottom&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;na&#34;&gt;border-bottom-style&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;ni&#34;&gt;solid&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;na&#34;&gt;border-bottom-width&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;si&#34;&gt;#{&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$border_width&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nv&#34;&gt;$biw_bottom&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;si&#34;&gt;#{&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$border_image_width&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;na&#34;&gt;border-image-width&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;si&#34;&gt;#{&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$biw_top&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;si&#34;&gt;#{&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$biw_right&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;si&#34;&gt;#{&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$biw_bottom&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;si&#34;&gt;#{&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$biw_left&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;@else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;// No `$sides` specified so default to drawing all sides
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;    &lt;span class=&#34;na&#34;&gt;border-style&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;ni&#34;&gt;solid&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;na&#34;&gt;border-width&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;si&#34;&gt;#{&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$border_width&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;na&#34;&gt;border-image-width&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;si&#34;&gt;#{&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$border_image_width&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;c1&#34;&gt;// Prevent flash of foreground colour by setting the initial colour to match the page background
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;  &lt;span class=&#34;c1&#34;&gt;// (pretending the page is white for this example)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;  &lt;span class=&#34;na&#34;&gt;border-color&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;ni&#34;&gt;white&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;na&#34;&gt;border-image-repeat&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;ni&#34;&gt;round&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;na&#34;&gt;border-image-source&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;border-image-colored&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;#{&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$color_hex&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;c1&#34;&gt;// This should match the size of your corner slices but you can experiement with it
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;  &lt;span class=&#34;na&#34;&gt;border-image-slice&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;30&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Call it like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-scss&#34; data-lang=&#34;scss&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;p&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;@include&lt;/span&gt;&lt;span class=&#34;nd&#34;&gt; dotted-border&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mh&#34;&gt;#ed397d&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;ni&#34;&gt;top&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;ni&#34;&gt;bottom&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Which will convert into:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-css&#34; data-lang=&#34;css&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;p&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;border-style&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;none&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;border-style&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;solid&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;border-image-width&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;30&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;px&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;border-color&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;white&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;border-image-repeat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;round&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;border-image-source&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;url&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;data:image/svg+xml,%3Csvg xmlns=&amp;#39;http://www.w3.org/2000/svg&amp;#39; width=&amp;#39;90&amp;#39; height=&amp;#39;90&amp;#39;%3E%3Cg style=&amp;#39;fill:%23ed397d&amp;#39;%3E%3Cpath d=&amp;#39;M10 10h10v10H10zM70 10h10v10H70zM40 40h10v10H40zM40 10h10v10H40zM10 70h10v10H10zM40 70h10v10H40zM70 70h10v10H70zM70 40h10v10H70zM10 40h10v10H10z&amp;#39;/%3E%3C/g%3E%3C/svg%3E&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;border-image-slice&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;30&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p style=&#34;border-style: solid; border-image-width: 30px; border-color: white; border-image-repeat: round; border-image-source: url(&amp;quot;data:image/svg+xml,%3Csvg xmlns=&#39;http://www.w3.org/2000/svg&#39; width=&#39;90&#39; height=&#39;90&#39;%3E%3Cg style=&#39;fill:%23ed397d&#39;%3E%3Cpath d=&#39;M10 10h10v10H10zM70 10h10v10H70zM40 40h10v10H40zM40 10h10v10H40zM10 70h10v10H10zM40 70h10v10H40zM70 70h10v10H70zM70 40h10v10H70zM10 40h10v10H10z&#39;/%3E%3C/g%3E%3C/svg%3E&amp;quot;); border-image-slice: 30;border-image-outset: 15px; text-align: center; padding: 1em; margin-top: var(--section-gap);&#34;&gt;and this is what our custom &lt;code&gt;border-image&lt;/code&gt; looks like&lt;/p&gt;
&lt;p style=&#34;border-style: dashed; border-color: #ed397d; border-width: 8px; text-align: center; padding: 1em; margin-top: var(--section-gap);&#34;&gt;compared to CSS &lt;code&gt;border-style: dashed&lt;/code&gt;...&lt;/p&gt;
&lt;p style=&#34;border-style: dotted; border-color: #ed397d; border-width: 12px; text-align: center; padding: 1em; margin-top: var(--section-gap);&#34;&gt;... or CSS &lt;code&gt;border-style: dotted&lt;/code&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Hugo Component Pattern</title>
      <link>https://mm-dev.rocks/posts/hugo-component-pattern/</link>
      <pubDate>Thu, 28 Aug 2025 13:42:44 +0100</pubDate>
      <guid>https://mm-dev.rocks/posts/hugo-component-pattern/</guid>
      <description>&lt;p&gt;For this example we&amp;rsquo;ll make a block to display user testimonials.&lt;/p&gt;
&lt;p&gt;The testimonials might be shown in more than one place on the site, so it&amp;rsquo;s important that the data be stored in a single place (otherwise when testimonials change in future we&amp;rsquo;ll need to change it everywhere manually, risking missing some instances).&lt;/p&gt;
&lt;h3 id=&#34;create-data-file-datatestimonialstoml&#34;&gt;Create data file: &lt;code&gt;/data/testimonials.toml&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;For this project the testimonials require a name, location and a little bit of text. To keep it super simple the name and location will be displayed in a single element, we&amp;rsquo;ll use just 2 fields, &lt;code&gt;heading&lt;/code&gt; and &lt;code&gt;body&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Saving the TOML to a dedicated &lt;code&gt;testimonials.toml&lt;/code&gt; file in the &lt;code&gt;/data&lt;/code&gt; directory makes it easy to get at from a partial.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-toml&#34; data-lang=&#34;toml&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# /data/testimonials.toml&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;[[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;testimonials&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;heading&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Alan Best, Brighton&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;body&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;A professional, trustworthy service.&amp;#34;&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;[[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;testimonials&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;heading&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Christine Danes, London&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;body&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Couldn&amp;#39;t ask for more!&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;[[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;testimonials&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;heading&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Elliot Fox, Bristol&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;body&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;I couldn&amp;#39;t be happier!&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;TOML syntax is weird with the repeating &lt;code&gt;[[testimonials]]&lt;/code&gt;. &lt;strong&gt;For comparison, this is the same data as JSON&lt;/strong&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;testimonials&amp;#34;&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nt&#34;&gt;&amp;#34;heading&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Alan Best, Brighton&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nt&#34;&gt;&amp;#34;body&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;A professional, trustworthy service.&amp;#34;&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;},&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nt&#34;&gt;&amp;#34;heading&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Christine Danes, London&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nt&#34;&gt;&amp;#34;body&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Couldn&amp;#39;t ask for more!&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;},&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nt&#34;&gt;&amp;#34;heading&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Elliot Fox, Bristol&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nt&#34;&gt;&amp;#34;body&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;I couldn&amp;#39;t be happier!&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;create-partial-layoutspartialstestimonialshtml&#34;&gt;Create partial: &lt;code&gt;/layouts/partials/testimonials.html&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;/data/testimonials.toml&lt;/code&gt; file we just created automatically becomes accessible in code as &lt;code&gt;.Site.Data.testimonials&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We want to output the data as an HTML unordered list (&lt;code&gt;&amp;lt;ul&amp;gt;&lt;/code&gt;). So we loop through the data, creating a list item (&lt;code&gt;&amp;lt;li&amp;gt;&lt;/code&gt;) for each testimonial.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go-html-template&#34; data-lang=&#34;go-html-template&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;&amp;lt;!-- /layouts/partials/testimonials.html --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;ul&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;{{-&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;range&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;na&#34;&gt;.Site.Data.testimonials&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;cp&#34;&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;cp&#34;&gt;{{-&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;range&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;na&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;cp&#34;&gt;-}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;li&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;h3&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;cp&#34;&gt;{{-&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;index&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;na&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;heading&amp;#34;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;cp&#34;&gt;-}}&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;h3&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;cp&#34;&gt;{{-&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;index&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;na&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;body&amp;#34;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;cp&#34;&gt;-}}&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;li&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;cp&#34;&gt;{{-&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;end&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;cp&#34;&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;{{-&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;end&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;cp&#34;&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;ul&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To explain the two nested &lt;code&gt;range&lt;/code&gt; blocks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The outer &lt;code&gt;{{- range .Site.Data.testimonials -}}&lt;/code&gt; loops through the 3 testimonials, returning each as a map&lt;/li&gt;
&lt;li&gt;The inner &lt;code&gt;{{- range . -}}&lt;/code&gt; accesses the map itself&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;create-shortcode-layoutsshortcodestestimonials&#34;&gt;Create shortcode: &lt;code&gt;/layouts/shortcodes/testimonials&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;We&amp;rsquo;ve made the partial, but those only work in &lt;code&gt;.html&lt;/code&gt; files. For &lt;code&gt;.md&lt;/code&gt; files we need a shortcode, so we make a simple wrapper (all it does is call the partial, passing the same context via the dot).&lt;/p&gt;
&lt;p&gt;This may seem redundant, it&amp;rsquo;s just a pattern I like to follow as I often end up wanting to use the same functionality in both &lt;code&gt;.html&lt;/code&gt; and &lt;code&gt;.md&lt;/code&gt; files. If I make the shortcode first (as I used to) then when I decide I need a partial I have to convert the shortcode to a partial then make a wrapper like this&amp;hellip; it&amp;rsquo;s happened enough that I just do it this way by habit now.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{{&lt;/span&gt;&lt;span class=&#34;cm&#34;&gt;/* /layouts/shortcodes/testimonials */&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{{&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;partial&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;testimonials&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;usage&#34;&gt;Usage&lt;/h3&gt;
&lt;p&gt;To add the testimonials to a page we just add &lt;code&gt;{{&amp;lt; testimonials &amp;gt;}}&lt;/code&gt; to the corresponding &lt;code&gt;.md&lt;/code&gt; file in our Hugo site.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>End Results and Bonus</title>
      <link>https://mm-dev.rocks/posts/decapitating-macbook/end-results-and-bonus/</link>
      <pubDate>Thu, 06 Mar 2025 12:26:15 +0000</pubDate>
      <guid>https://mm-dev.rocks/posts/decapitating-macbook/end-results-and-bonus/</guid>
      <description>&lt;h4 id=&#34;good&#34;&gt;Good&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;I love that it&amp;rsquo;s silent&lt;/li&gt;
&lt;li&gt;Apple Silicon is as good as I&amp;rsquo;d hoped &amp;mdash; the bang-per-watt is great for my circumstances with solar power etc&lt;/li&gt;
&lt;li&gt;The annoyances with not knowing when it&amp;rsquo;s turned on are largely alleviated with my scripts, as long as I&amp;rsquo;m using it on my LAN (which, realistically I always am)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;bad&#34;&gt;Bad&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;MacOS, when I do have to use it (sorry Mac fans&amp;hellip; I do at least prefer it to Windows)&lt;/li&gt;
&lt;li&gt;The battery does not hold power at all well when turned off (if I turn it off &amp;mdash; yes &lt;em&gt;off&lt;/em&gt;, not asleep &amp;mdash; with a full battery, a few days later it&amp;rsquo;s completely dead and needs to be charged before I can turn it on)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&amp;rsquo;m never going to use this as my main machine, and that was never my plan. I&amp;rsquo;m into minimal Linux with invisible tiling window managers like DWM, and dragging windows around, being forced to watch animations etc won&amp;rsquo;t cut it for my preferences.&lt;/p&gt;
&lt;p&gt;I do know about Asahi Linux but that makes power usage much less special and I&amp;rsquo;m not sure how it plays with the development stack I need for Flutter. More crucially, at time of writing DP Alt Mode is not working, which means monitors plugged in to the USB-C port won&amp;rsquo;t work. Having cut the display off, this scuppers me.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;For me this was an investment into being able to responsibly build software for the Apple ecosystem and its users who have different preferences than mine. It&amp;rsquo;s a tool for a specific sub-set of tasks, and under those conditions it does what I need.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h3 id=&#34;bonus-e-ink-macbook&#34;&gt;Bonus: E-ink Macbook&lt;/h3&gt;
&lt;p&gt;Remember I mentioned that I had a Boox Max Lumi which could function as a monitor for the Macbook? Here it is in full effect.&lt;/p&gt;
&lt;ul class=&#39;gallery single-item&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_aqEI_1&#39; /&gt;
      &lt;label for=&#39;item_aqEI_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/decapitating-macbook/macbook-eink-max-lumi.jpg&#39; alt=&#39;Macbook using Boox Max Lumi as an E-ink display&#39; /&gt;
        &lt;span&gt;Macbook using Boox Max Lumi as an E-ink display&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After tweaking some settings in &lt;em&gt;Accessibility&lt;/em&gt;/&lt;em&gt;Display&lt;/em&gt;, MacOS can be made to look half-decent on e-ink.&lt;/p&gt;
&lt;p&gt;All the usual caveats about e-ink apply re refresh rates and ghosting of course, but I could easily see myself working quite happily in Vim on it &lt;em&gt;in some imaginary future dystopia where I&amp;rsquo;m trapped in Apple&amp;rsquo;s walled garden, all that exists, protected by a multi-trillion dollar Reality Distortion Field that they managed to power up just before the bombs dropped&amp;hellip;&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Running Headless: What I&#39;ve Learned</title>
      <link>https://mm-dev.rocks/posts/decapitating-macbook/running-headless-what-ive-learned/</link>
      <pubDate>Thu, 06 Mar 2025 12:26:14 +0000</pubDate>
      <guid>https://mm-dev.rocks/posts/decapitating-macbook/running-headless-what-ive-learned/</guid>
      <description>&lt;h4 id=&#34;the-lid&#34;&gt;The lid&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Removing the lid turns it on (hall sensors in the base are activated by magnets in the lid)&lt;/li&gt;
&lt;li&gt;Replacing the lid leaves the device visible on the network, but it can no longer be &lt;code&gt;SSH&lt;/code&gt;ed into (some form of &amp;lsquo;sleeping&amp;rsquo;)&lt;/li&gt;
&lt;li&gt;During a user-initiated shutdown process (which takes some time, up to 20-30 seconds], replacing the lid re-wakes the machine &amp;mdash; so &lt;em&gt;wait 30 seconds or more before replacing the lid&lt;/em&gt;
&lt;small class=&#34;special&#34;&gt;
As the lid is no longer attached, &amp;lsquo;opening/closing&amp;rsquo; makes no sense so I&amp;rsquo;ll instead use &amp;lsquo;removing/replacing&amp;rsquo;&lt;/small&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;power-button&#34;&gt;Power button&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Holding the power button for &lt;em&gt;5&lt;/em&gt; seconds will begin the &lt;em&gt;shutdown&lt;/em&gt; process, but the machine will still be visible on the network for a few seconds, &lt;em&gt;allow at least 15-20 seconds for shutdown to complete&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Holding the power button for &lt;em&gt;2&lt;/em&gt; seconds will turn the machine &lt;em&gt;on&lt;/em&gt; but it &lt;em&gt;takes maybe 20 seconds&lt;/em&gt; to show up on the network
&lt;small class=&#34;special&#34;&gt;
As mentioned in previous parts of this series, &amp;lsquo;showing on the network&amp;rsquo; is taken as a proxy for &amp;lsquo;machine is turned on&amp;rsquo;&lt;/small&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;from-dead-nolow-power&#34;&gt;From dead (no/low power)&lt;/h4&gt;
&lt;p&gt;A couple of times I&amp;rsquo;ve not used the machine for a week or more and the battery has got very flat &amp;mdash; honestly it&amp;rsquo;s disappointing that it isn&amp;rsquo;t engineered to handle this situation better.&lt;/p&gt;
&lt;p&gt;For a while I struggled to charge it, trying all of the following with no success:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Charging for over 10 mins in first USB-C port&lt;/li&gt;
&lt;li&gt;Charging for over 10 mins in second USB-C port&lt;/li&gt;
&lt;li&gt;Tried powering up several times during the charge periods above&lt;/li&gt;
&lt;li&gt;After each attempt at powering up, waited 2 minutes for sign of life via the Macbook being seen on the LAN
&lt;small class=&#34;special&#34;&gt;
Charging units are dedicated USB PD/QC chargers wired directly to DC 12V/20V feeds, very stable/reliable and with ample amperage&lt;/small&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What eventually got past this problem:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Unplugged all cables&lt;/li&gt;
&lt;li&gt;Plugged one end of a charging cable into the Macbook first (top/corner port)&lt;/li&gt;
&lt;li&gt;THEN plugged the other end of the cable into the USB charger&lt;/li&gt;
&lt;li&gt;30 seconds later I was in (Macbook showed on router as a client and could be &lt;code&gt;ssh&lt;/code&gt;ed into etc)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This could be a red herring eg the order of plugging in cables was not important but instead the battery had accumulated a charge and the final successful attempts just pushed it above some threshold that would allow it to boot. The fact that the response was so quick (30 secs after plugging in the cables in this order the machine was on) makes me think this is unlikely.&lt;/p&gt;
&lt;p&gt;The only other explanation I can think of is that &lt;em&gt;which end of the charging cable is plugged in first makes a difference&lt;/em&gt;, possibly due to something related to USB-C handshake/power negotiations that I&amp;rsquo;m ignorant of. This is a hunch and might make no sense in reality. I leave the info/observation here with very little weight attached to it, it is what it is.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Remote Access</title>
      <link>https://mm-dev.rocks/posts/decapitating-macbook/remote-access/</link>
      <pubDate>Thu, 06 Mar 2025 12:26:13 +0000</pubDate>
      <guid>https://mm-dev.rocks/posts/decapitating-macbook/remote-access/</guid>
      <description>&lt;p&gt;Now I&amp;rsquo;m past the novelty of plugging in monitors this is how I actually use the machine.&lt;/p&gt;
&lt;h3 id=&#34;remote-builds-over-ssh&#34;&gt;Remote builds over SSH&lt;/h3&gt;
&lt;p&gt;&lt;a href = &#34;https://mm-dev.rocks/posts/android-as-a-dev-environment/intro/&#34; title = &#34;Android as a Dev Environment&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;I&amp;rsquo;m working on an Android tablet most of the time&lt;/a&gt;, using Termux, Proot-distro and Termux/X11 to run Debian Bookworm arm64. This setup has shortcomings but I find it very comfortable and manage to get 90% of my work done on it.&lt;/p&gt;
&lt;p&gt;One current problem is that the arm64 version of the Android SDK can&amp;rsquo;t build arm64 APKs for Android. During main development I just build/run the Linux version of whichever app I&amp;rsquo;m working on &amp;mdash; Flutter is multi-platform and suprisingly consistent between platforms. But of course I need to build APKs for Android devices at some point.&lt;/p&gt;
&lt;p&gt;When I just need to quickly build some APKs for a given project, I use something like the following script (this one is for &lt;em&gt;auDAV&lt;/em&gt;, my WebDAV audiobook player app):&lt;/p&gt;

    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;#!/bin/sh
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Script to be run from Termux/proot-distro&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# - Connects to build machine AIR (host defined in `~/.ssh/config`)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# - Logs in to working directory&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# - Builds APKs&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# - Syncs APKs to local machine&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;remote_commands&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;$(&lt;/span&gt;cat &lt;span class=&#34;s&#34;&gt;&amp;lt;&amp;lt; &amp;#39;EOF&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;echo &amp;#34;~~~~~~~ Remote: Source local environment ~~~~~~~&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;source ~/.zprofile
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;source ~/.zshrc
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;echo &amp;#34;~~~~~~~ Remote: Change working directory ~~~~~~~&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;cd ~/development/audav
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;echo &amp;#34;~~~~~~~~~ Remote: Pull latest from git ~~~~~~~~~&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;git pull
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;echo -e &amp;#34;\n~~~~~~~~~~~~~~ Remote: Build APKs ~~~~~~~~~~~~~~&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;flutter build apk --split-per-abi
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;EOF&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;ssh AIR &lt;span class=&#34;s2&#34;&gt;&amp;#34;bash --login -c &amp;#39;&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;remote_commands&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#39;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;\n~~~~~~~~~~~ Local: Sync APKs to local ~~~~~~~~~~&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;rsync -r --mkpath --progress AIR:development/audav/build/app/outputs/flutter-apk &lt;span class=&#34;nv&#34;&gt;$HOME&lt;/span&gt;/000-WORK/audav/build/app/outputs/
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
    &lt;p class=&#34;live-code-embed chroma&#34;&gt;&lt;span&gt;From&lt;/span&gt;&lt;a href=&#34;https://codeberg.org/mm-dev/shell-scripts/raw/branch/termux-proot-ubuntu/audav-build-on-mac&#34;&gt;https://codeberg.org/mm-dev/shell-scripts/raw/branch/termux-proot-ubuntu/audav-build-on-mac&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&#34;full-remote-graphical-access-with-nomachine&#34;&gt;Full remote graphical access with NoMachine&lt;/h3&gt;
&lt;ul class=&#39;gallery single-item&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_t4gj_1&#39; /&gt;
      &lt;label for=&#39;item_t4gj_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/decapitating-macbook/s8-nomachine-macos.jpg&#39; alt=&#39;Remoting into MacOS with S8 tablet, trackball and split keyboard&#39; /&gt;
        &lt;span&gt;Remoting into MacOS with S8 tablet, trackball and split keyboard&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To build apps for MacOS, iOS and iPadOS I must endure the torturous window management, tedious animations, and terrible file manager of MacOS (and that&amp;rsquo;s before we even get to XCode).&lt;/p&gt;
&lt;p&gt;To ease my suffering I can at least use MacOS via the OLED screen of a Galaxy Tab S8 Ultra, a DEFT PRO trackball and a Ferris Sweep split mechanical keyboard.&lt;/p&gt;
&lt;p&gt;&lt;a href = &#34;https://www.nomachine.com/&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;NoMachine&lt;/a&gt; works well for remoting into MacOS and over the LAN it performs well with very little lag. There&amp;rsquo;s client software for Android and arm64 Linux meaning it&amp;rsquo;s quick and easy for me to just pop in to MacOS from my favoured Linux/DWM environment.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Magnets</title>
      <link>https://mm-dev.rocks/posts/decapitating-macbook/magnets/</link>
      <pubDate>Thu, 06 Mar 2025 12:26:12 +0000</pubDate>
      <guid>https://mm-dev.rocks/posts/decapitating-macbook/magnets/</guid>
      <description>&lt;ul class=&#39;gallery single-item&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_GIAH_1&#39; /&gt;
      &lt;label for=&#39;item_GIAH_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/decapitating-macbook/macbook-lid-magnets-dont-do.jpg&#39; alt=&#39;Macbook sitting on top of its lid. Don&amp;#39;t do it.&#39; /&gt;
        &lt;span&gt;Macbook sitting on top of its lid. Don&amp;#39;t do it.&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I experienced a &lt;em&gt;ridiculous&lt;/em&gt; amount of confusion and wasted time due to a silly oversight I made (repeatedly!).&lt;/p&gt;
&lt;p&gt;As mentioned earlier, I&amp;rsquo;d kept the lid for the Macbook, detached from its hinges and with the broken screen removed, to use as a kind of&amp;hellip; lid.&lt;/p&gt;
&lt;p&gt;When in use, it seemed like the obvious place to put the lid was under the Macbook, where it fitted perfectly.&lt;/p&gt;
&lt;p&gt;For the first few hours, days, even weeks, I had intermittent problems with the machine randomly turning off/on (actually sleeping/waking but I didn&amp;rsquo;t know that at the time).&lt;/p&gt;
&lt;p&gt;Remember I was having enough trouble knowing whether the machine was turned on or not in those early days.&lt;/p&gt;
&lt;p&gt;It was the magnets in the lid, activating the hall sensors in the base and making it think I was opening/closing the lid. Due to the thickness of the base the magnets weren&amp;rsquo;t as close as they&amp;rsquo;d normally be, and the alignment wasn&amp;rsquo;t perfect. So this wasn&amp;rsquo;t a simple matter of &amp;lsquo;when the lid is under the laptop it goes to sleep&amp;rsquo;. This was a sporadic problem that could come and go as things wobbled in the environment. It took me a while to make the mental connection and work out what was happening.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# TODO Remove magnets from lid!&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description>
    </item>
    
    <item>
      <title>Factory Reset</title>
      <link>https://mm-dev.rocks/posts/decapitating-macbook/factory-reset/</link>
      <pubDate>Thu, 06 Mar 2025 12:26:11 +0000</pubDate>
      <guid>https://mm-dev.rocks/posts/decapitating-macbook/factory-reset/</guid>
      <description>&lt;p&gt;The display was removed, things were looking clean.&lt;/p&gt;
&lt;p&gt;This is a used device. It seemed like the OS was a fresh install, but I like to be sure, so I did a factory reset. The process seemed smooth, until the reboot.&lt;/p&gt;
&lt;p&gt;I wasn&amp;rsquo;t sure how long to wait for boot after a factory reset. I&amp;rsquo;ve had some devices (eg Android phones after installing custom ROMS) take many minutes on first boot after a reset, but online info suggested this should be pretty quick on an M1 Mac.&lt;/p&gt;
&lt;p&gt;Minutes passed and the display wasn&amp;rsquo;t showing anything. I wasn&amp;rsquo;t sure what was going on, and I started playing guessing games with the power button. I wished again that there was a power LED. With the broken screen removed I couldn&amp;rsquo;t even rely on those few lines of pixels lighting up to give me clues.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m still not sure if I broke/interrupted something, or if this was a normal part of the reset process. I tried various patterns of tapping, holding, holding for longer, each time waiting for something to happen for 10, 30 or 60 seconds. How long should I wait before I expect to see something on the screen?&lt;/p&gt;
&lt;p&gt;Eventually I got something to happen. I was back to the black (but lit/powered on) screen. I guessed I was somewhere mid-boot. I tried the &lt;code&gt;CMD + F1&lt;/code&gt; combo which had made the external monitor primary when I was on the login screen. It didn&amp;rsquo;t work &amp;ndash; the key combo/switching must be a feature of MacOS, which I was not yet booted into. A little reading around online hinted that I might be in recovery mode.&lt;/p&gt;
&lt;p&gt;I found a video explaining how recovery mode worked on a machine with an external monitor and no internal display, and how to handle it.&lt;/p&gt;
&lt;h3 id=&#34;recovery-mode-when-you-only-have-an-external-monitor&#34;&gt;Recovery mode when you only have an external monitor&lt;/h3&gt;
&lt;p&gt;The situation is this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Recovery mode recognises your external monitor, but treats it as an extension to the internal display&lt;/li&gt;
&lt;li&gt;The window with all of the useful user interface is there, but it&amp;rsquo;s &amp;lsquo;on the internal display&amp;rsquo; (whether that exists or not)&lt;/li&gt;
&lt;li&gt;The phantom internal display is drawn off to the right of the external monitor&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;In my case, the resolution of my external monitor was smaller in height than the internal monitor. This may have made the situation more confusing. I &lt;em&gt;think&lt;/em&gt; that the top menu bar extends across both displays, and if my monitor was tall enough I&amp;rsquo;d have seen the bar, giving a massively more helpful clue than the empty black screen.&lt;/p&gt;&lt;/blockquote&gt;
&lt;ul class=&#39;gallery single-item&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_jQVI_1&#39; /&gt;
      &lt;label for=&#39;item_jQVI_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/decapitating-macbook/macos-recovery-phantom-screen.jpg&#39; alt=&#39;MacOS Recovery Phantom Screen&#39; /&gt;
        &lt;span&gt;MacOS Recovery Phantom Screen&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To get stuff done in recovery mode, you have to drag the window from phantom screen, onto the external display. The window is only draggable by its title bar. So you have to 
&lt;a data-image-ref=&#34;MacOS Recovery Phantom Screen&#34; href=&#34;#&#34;&gt;shoot your touchpad pointer off the right-hand side of your screen, aim for where you think the title bar might be, and try to tap/drag it back over to your real visible screen on the left&lt;/a&gt;
.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;This took me ages&lt;/em&gt;. I think the window on the invisible screen was centred, but because it was a different resolution to my monitor it was hard to visualise and aim for it. But once done, I could access everything in recovery mode.&lt;/p&gt;
&lt;p&gt;After finishing the reset I booted. The monitor doesn&amp;rsquo;t start showing stuff until later in the boot process but that&amp;rsquo;s ok.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;I&amp;rsquo;m in, on the external monitor, with the Macbook&amp;rsquo;s internal screen removed&lt;/em&gt;.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Is This Thing On?</title>
      <link>https://mm-dev.rocks/posts/decapitating-macbook/is-this-thing-on/</link>
      <pubDate>Thu, 06 Mar 2025 12:26:11 +0000</pubDate>
      <guid>https://mm-dev.rocks/posts/decapitating-macbook/is-this-thing-on/</guid>
      <description>&lt;p&gt;One of the most difficult problems I&amp;rsquo;ve had with using this machine headless, is &lt;em&gt;knowing whether it&amp;rsquo;s turned on or not&lt;/em&gt;. Seriously.&lt;/p&gt;
&lt;p&gt;If I connect the external monitor (ok, not headless in that case), no image on the screen might mean:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The Macbook is not turned on&lt;/li&gt;
&lt;li&gt;The Macbook is asleep&lt;/li&gt;
&lt;li&gt;There is a problem with the monitor or connection (eg bad cable)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If I try to connect over NoMachine or VNC and fail, that might mean:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The Macbook is not turned on&lt;/li&gt;
&lt;li&gt;The Macbook is asleep&lt;/li&gt;
&lt;li&gt;The Macbook is not connected to the network&lt;/li&gt;
&lt;li&gt;NoMachine/VNC is not started on the Macbook&lt;/li&gt;
&lt;li&gt;I&amp;rsquo;ve configured something wrongly with NoMachine/VNC&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I really dislike the millions of blinking LEDs designers love to stick on electronic devices. They constantly nag or distract, I think they are bad. But I&amp;rsquo;d kill for one one this Macbook!&lt;/p&gt;
&lt;h3 id=&#34;things-ive-tried&#34;&gt;Things I&amp;rsquo;ve tried&lt;/h3&gt;
&lt;ul class=&#39;gallery single-item&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_Otin_1&#39; /&gt;
      &lt;label for=&#39;item_Otin_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/decapitating-macbook/macbook-sd-reader-led.jpg&#39; alt=&#39;USB-C SD card reader with LED indicator&#39; /&gt;
        &lt;span&gt;USB-C SD card reader with LED indicator&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I have a small USB-C SD card reader. It has a red LED which lights up when it is receiving power. For a while I used this, but it&amp;rsquo;s impractical. Although it&amp;rsquo;s small, it still sticks out to be breakage risk. A nice bit of leverage on the USB port, just waiting to wrench it off if it catches on something (a bit like the 1st-gen Apple Pencil when it was charging).&lt;/p&gt;
&lt;ul class=&#39;gallery single-item&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_J3Xs_1&#39; /&gt;
      &lt;label for=&#39;item_J3Xs_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/decapitating-macbook/macbook-capslock-led.jpg&#39; alt=&#39;Capslock button with LED indicator&#39; /&gt;
        &lt;span&gt;Capslock button with LED indicator&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At some point I noticed that the &lt;code&gt;caps lock&lt;/code&gt; button on the keyboard had an LED which lit when caps was locked. &lt;em&gt;And&lt;/em&gt; (remembering I&amp;rsquo;d usually be using a Bluetooth keyboard and pointing device), the caps lock could be applied to the built-in keyboard without affecting the Bluetooth keyboard. I could just leave caps lock on and the LED would stay lit.&lt;/p&gt;
&lt;p&gt;The showstopper problem with both of the above ideas was that they both only worked when the Macbook was fully booted and ready to rock. I guess the ports and caps lock LED are powered off at other times. Most of my periods of confusion as to whether it was powered on or not were happening when it was half-on, during boot, sleep, shutdown etc.&lt;/p&gt;
&lt;h3 id=&#34;my-current-best-solution&#34;&gt;My current-best solution&lt;/h3&gt;
&lt;p&gt;I noticed that my router, in its list of &amp;lsquo;attached clients&amp;rsquo; was providing relatively quick/up-to-date information about whether the Macbook was connected or not. It seemed to connect to Wi-Fi quite early in the boot process, and disconnect soon after I initiated a shutdown.&lt;/p&gt;
&lt;p&gt;Following on from that, I started &lt;code&gt;ping&lt;/code&gt;ing the Macbook in a terminal window, which I could keep nearby while I worked. This was getting close enough for my purposes.&lt;/p&gt;
&lt;p&gt;The obvious next step was to script it properly. My aim was to get a brief status line telling me if the Macbook was connected or not, and its battery charge level. The following script did the job:&lt;/p&gt;

    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Ping a Mac on the LAN&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# If it&amp;#39;s alive, check its battery level using a seperate script `battery-check` (present on the&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Mac)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Return the result with coloured text to indicate off/on status, and rough battery level&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Loop repeatedly, overwriting the results text each time onto the same single line&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;ip_to_check&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;192.168.8.123
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;hostname&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;AIR
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;wht&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;\033[1;15m&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;red&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;\033[1;31m&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;ylw&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;\033[1;33m&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;ong&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;\033[38;5;208m&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;grn&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;\033[1;32m&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;nc&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;\033[0m&amp;#39;&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;# No Colour&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;update_battery &lt;span class=&#34;o&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;c1&#34;&gt;# Log in with SSH and call script to get battery charge info.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nv&#34;&gt;battery_check_output&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;$(&lt;/span&gt;ssh AIR &lt;span class=&#34;s2&#34;&gt;&amp;#34;bash --login -c battery-check&amp;#34;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;c1&#34;&gt;# `battery-check` returns a string like `battery: 18%`, we want to extract the number.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nv&#34;&gt;percentage&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;$(&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;battery_check_output&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; tr -dc &lt;span class=&#34;s1&#34;&gt;&amp;#39;0-9&amp;#39;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;[[&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$percentage&lt;/span&gt; -lt &lt;span class=&#34;m&#34;&gt;15&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;]]&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nv&#34;&gt;battcolor&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$red&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;elif&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;[[&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$percentage&lt;/span&gt; -lt &lt;span class=&#34;m&#34;&gt;30&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;]]&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nv&#34;&gt;battcolor&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$ong&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;elif&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;[[&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$percentage&lt;/span&gt; -lt &lt;span class=&#34;m&#34;&gt;80&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;]]&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nv&#34;&gt;battcolor&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$ylw&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nv&#34;&gt;battcolor&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$grn&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;fi&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;o&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Hide cursor&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;tput civis
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Sometimes (too soon after boot?) the first call fails&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Machete through it&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;update_battery
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sleep &lt;span class=&#34;m&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;update_battery
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sleep &lt;span class=&#34;m&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;while&lt;/span&gt; :
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;c1&#34;&gt;# -c 1 ... count 1 (only ping once)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; ping -c &lt;span class=&#34;m&#34;&gt;1&lt;/span&gt; &lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;ip_to_check&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;&amp;amp;&lt;/span&gt;&amp;gt; /dev/null&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;# Temporarily go white to indicate battery has just been checked/updated (2 * 5 = 10secs)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;[[&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$i&lt;/span&gt; -lt &lt;span class=&#34;m&#34;&gt;2&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;]]&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;nv&#34;&gt;color&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$wht&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;nv&#34;&gt;color&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$battcolor&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; -ne &lt;span class=&#34;s2&#34;&gt;&amp;#34;  &lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;hostname&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt; &lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;grn&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;    ON     &lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;nc&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;color&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;percentage&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;%&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;nc&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;\r&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;# i ends up incrementing every 5secs or so&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;o&#34;&gt;((&lt;/span&gt;i++&lt;span class=&#34;o&#34;&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;# Increment and every n counts update battery&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;# 5sec * 36 = 180secs = 3mins&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;[[&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$i&lt;/span&gt; -eq &lt;span class=&#34;m&#34;&gt;36&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;]]&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;nv&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      update_battery
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; -ne &lt;span class=&#34;s2&#34;&gt;&amp;#34;  &lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;hostname&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt; &lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;red&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;   OFF       &lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;nc&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;\r&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  sleep &lt;span class=&#34;m&#34;&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
    &lt;p class=&#34;live-code-embed chroma&#34;&gt;&lt;span&gt;From&lt;/span&gt;&lt;a href=&#34;https://codeberg.org/mm-dev/shell-scripts/raw/branch/termux-proot-ubuntu/mac-get-status&#34;&gt;https://codeberg.org/mm-dev/shell-scripts/raw/branch/termux-proot-ubuntu/mac-get-status&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I already had a little script on the Mac to report the battery level (a very simple wrapper around a Mac built-in &lt;code&gt;system_profiler&lt;/code&gt;, just to shorten the output).&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;#!/bin/sh
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;percent&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;$(&lt;/span&gt;system_profiler SPPowerDataType &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; grep &lt;span class=&#34;s2&#34;&gt;&amp;#34;State of Charge (%)&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; awk &lt;span class=&#34;s1&#34;&gt;&amp;#39;{print $5}&amp;#39;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;battery: &lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;percent&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;%&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul class=&#39;gallery single-item&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_M3V4_1&#39; /&gt;
      &lt;label for=&#39;item_M3V4_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/decapitating-macbook/mac-monitor-script.jpg&#39; alt=&#39;Monitoring script in a tiny floating window with close-up&#39; /&gt;
        &lt;span&gt;Monitoring script in a tiny floating window with close-up&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then I wanted to be able to float the &lt;code&gt;mac-get-status&lt;/code&gt; script in a tiny window.&lt;/p&gt;
&lt;p&gt;This next script opens up a terminal in a new window, gives it a specific title &lt;code&gt;macmonitor&lt;/code&gt;, and runs &lt;code&gt;mac-get-status&lt;/code&gt;:&lt;/p&gt;

    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$TERMINAL&lt;/span&gt; --title macmonitor -e &lt;span class=&#34;s2&#34;&gt;&amp;#34;mac-get-status&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;&amp;amp;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
    &lt;p class=&#34;live-code-embed chroma&#34;&gt;&lt;span&gt;From&lt;/span&gt;&lt;a href=&#34;https://codeberg.org/mm-dev/shell-scripts/raw/branch/termux-proot-ubuntu/mac-monitor&#34;&gt;https://codeberg.org/mm-dev/shell-scripts/raw/branch/termux-proot-ubuntu/mac-monitor&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Then I tell my window manager (DWM) to apply a special rule to windows with that specific title. This is how it&amp;rsquo;s done in DWM but other window managers may have ways of achieving the same thing.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;static&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;const&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Rule&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;rules&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;// If an Xfce4-terminal window has title &amp;#39;macmonitor&amp;#39;:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;    &lt;span class=&#34;c1&#34;&gt;// - Make it float
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;    &lt;span class=&#34;c1&#34;&gt;// - Make its dimensions 150px x 22px
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;    &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Xfce4-terminal&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;NULL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;macmonitor&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;150&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;22&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;// other rules...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description>
    </item>
    
    <item>
      <title>Removing the Display</title>
      <link>https://mm-dev.rocks/posts/decapitating-macbook/removing-the-display/</link>
      <pubDate>Thu, 06 Mar 2025 12:26:10 +0000</pubDate>
      <guid>https://mm-dev.rocks/posts/decapitating-macbook/removing-the-display/</guid>
      <description>&lt;p&gt;Removing the display was relatively easy, but time-consuming. There were lots of screws and they had pentalobe and torx heads, but these are standard in any electronics screwdriver set &amp;mdash; I have one of those.&lt;/p&gt;
&lt;p&gt;I checked a couple of disassembly videos on YouTube to make sure I wasn&amp;rsquo;t going to encounter any plastic tabs which might break off, short tearable ribbon cables etc, and to find where the display connectors were. I&amp;rsquo;ve taken apart quite a few laptops, this wasn&amp;rsquo;t one of the worst.&lt;/p&gt;
&lt;p&gt;Separating the lid from the base was tricky. Even when all fastenings were removed it was still stuck. I couldn&amp;rsquo;t tell if it was just tight tolerances on the hinges (hey, get the tolerances tight enough and &lt;a href = &#34;https://www.youtube.com/watch?v=0INUtcvhK3U&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;metal sticks to metal&lt;/a&gt;&amp;hellip; although I think even with Apple&amp;rsquo;s admittedly precise engineering this was probably normal stuckness rather than anything more mystical), or if there was a piece of the assembly in the way, something I&amp;rsquo;d missed.&lt;/p&gt;
&lt;p&gt;I found a video suggesting to open the laptop to 90 degrees, then hang it off the edge of a table and give it a sharp whack to separate the lid from the base. They made it look easier than I found it to be, but eventually I got the lid off.&lt;/p&gt;
&lt;h3 id=&#34;keeping-the-lid&#34;&gt;Keeping the lid&lt;/h3&gt;
&lt;p&gt;After looking into various cases and sleeves (if nothing else I wanted to be able to stop keys from getting pressed when the Macbook was put away and waking it up), I decided that I&amp;rsquo;d keep the lid. It is thin, light and tough, so why not continue using it to protect the computer? It was designed for the job.&lt;/p&gt;
&lt;p&gt;Getting the glass panel out of the lid was a chore. Like most modern displays it was very thin and shatterable (pre-shattered in this case), and gripped tightly to the inside of the metal shell of the lid via some kind of adhesive or thin tape. I had to use heat and a scraper and it made a horrible and probably dangerous mess, thousands of specks of shattered material that looked pretty like toffee-apple but were actually cutty, inhalable glass.&lt;/p&gt;
&lt;p&gt;I worked slowly, every minute or so I swept up the latest shards into a container so they wouldn&amp;rsquo;t get embedded in my hands/cornea/bronchioles. I did this gently so as not to disperse the bits all over the place with the flicking of the brush.&lt;/p&gt;
&lt;p&gt;After a while I had the idea of sticking a patch of duct tape over each area of glass before I scraped it away. This worked well, preventing the pieces from scattering and making it easier to scrape the glass away from the metal shell of the lid.&lt;/p&gt;
&lt;ul class=&#39;gallery single-item&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_G95n_1&#39; /&gt;
      &lt;label for=&#39;item_G95n_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/decapitating-macbook/macbook-empty-lid-plastic.jpg&#39; alt=&#39;Macbook Air M1 lid, screen removed&#39; /&gt;
        &lt;span&gt;Macbook Air M1 lid, screen removed&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With all the glass finally removed, there were a couple of areas which remained sticky and some unfinished sharp metal edges. To prevent it from getting hair stuck to it or from scratching the keyboard/touchpad I covered the entire inside surface of the lid with some clear adhesive plastic sheet.&lt;/p&gt;
&lt;p&gt;The final effect is reminiscent of an overprotected sofa, but it&amp;rsquo;s all I had to-hand and it&amp;rsquo;ll do for now.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>It&#39;s from eBay. Does it Even Work?</title>
      <link>https://mm-dev.rocks/posts/decapitating-macbook/its-from-ebay-does-it-work/</link>
      <pubDate>Thu, 06 Mar 2025 12:26:09 +0000</pubDate>
      <guid>https://mm-dev.rocks/posts/decapitating-macbook/its-from-ebay-does-it-work/</guid>
      <description>&lt;p&gt;Being an eBay purchase (albeit from a seller with good feedback) the first thing I needed to do was make sure that this thing was actually functional.&lt;/p&gt;
&lt;p&gt;The screen was damaged (as per the listing) and displayed basically nothing, or to be specific a few single-pixel lines of colour which hinted at whether the machine was on or off, on a black smashed background.&lt;/p&gt;
&lt;p&gt;Nothing so garish as &amp;lsquo;an LED to tell you when its powered on&amp;rsquo; would be allowed on a modern Mac. &lt;em&gt;These thin, single-pixel lines of colour would be my power indicator&lt;/em&gt;.&lt;/p&gt;
&lt;h3 id=&#34;monitors-and-cables&#34;&gt;Monitors and cables&lt;/h3&gt;
&lt;p&gt;This is where I encountered my first difficulties. I have a very small, very cheap USB-C monitor. This is the only actual monitor I have. I usually run stuff headless over VNC, NoMachine etc, using tablets like my Galaxy Tab S8 Ultra as remote OLED displays. I just don&amp;rsquo;t have much need for monitors. I have a Boox Max Lumi E-ink tablet with a micro HDMI input, so that &lt;em&gt;can&lt;/em&gt; act as a monitor but&amp;hellip; maybe later.&lt;/p&gt;
&lt;p&gt;So I plugged the little monitor in to one of the two available ports on the &lt;del&gt;characterless&lt;/del&gt; bravely minimal chassis (the other port would do for power). &lt;em&gt;The monitor didn&amp;rsquo;t light up&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;A vast landscape of confusing possible states opened up before me. Was I in the wrong port? A USB-C port is not a USB-C port etc, especially where displays and power are concerned. Maybe only one specific port would power the Mac, or only one of them would output a display signal.&lt;/p&gt;
&lt;p&gt;What about the cable, both cables? Were they even providing power/pixels or did they have the wrong strands in them?&lt;/p&gt;
&lt;p&gt;Did the Macbook put out enough power over USB-C for the monitor? Did I need to plug an additional cable for power, or charge the battery some more, or plug extra power into the monitor itself?&lt;/p&gt;
&lt;p&gt;Was my monitor just not compatible? It&amp;rsquo;s a simple AliExpress generic thing, and Apple is not exactly known for its broad compatibility with 3rd-party products. Was it just not-yet-enabled in the OS?&lt;/p&gt;
&lt;p&gt;Was I even &amp;lsquo;in the OS&amp;rsquo; at this point, or was the Macbook still booting&amp;hellip; or stuck in some horrific &amp;lsquo;recovery zone&amp;rsquo; where blindly pressing the wrong button might result in destroying the OS, the only solutions to be &amp;ldquo;plug it into one of the other Apple computers that you don&amp;rsquo;t have&amp;rdquo; or &amp;ldquo;take it to an Apple Store where (if somebody dressed like you is allowed inside) a Genius will go asthmatic at the concept of a headless Macbook and make you buy a new one for a thousand of the pounds that you don&amp;rsquo;t have&amp;rdquo;?&lt;/p&gt;
&lt;p&gt;After an embarrassing amount of cable-swapping (80% of my USB-C cables either couldn&amp;rsquo;t deliver enough power, or couldn&amp;rsquo;t deliver the pixels, and I&amp;rsquo;ve been through this stuff so many times I should know by now to &lt;em&gt;never assume that my cables are good&lt;/em&gt;) I eventually made progress.&lt;/p&gt;
&lt;p&gt;The monitor lit up.&lt;/p&gt;
&lt;p&gt;Then it went black again. Not dead-black, but that rubbish glowingly-grey attempt at black that TFT panels manage, which told me that the monitor was powered up and trying to display something, but had nothing to display.&lt;/p&gt;
&lt;h3 id=&#34;power-button&#34;&gt;Power button&lt;/h3&gt;
&lt;ul class=&#39;gallery single-item&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_2HQl_1&#39; /&gt;
      &lt;label for=&#39;item_2HQl_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/decapitating-macbook/macbook-power-button-crop.jpg&#39; alt=&#39;Macbook Air M1 power button&#39; /&gt;
        &lt;span&gt;Macbook Air M1 power button&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Macbook does grant me a power button at least, in the form of a special key on the keyboard, up in the corner.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s a strange item, sitting slightly lower than the other keys on the keyboard, with a slightly different tactile response compared to the other keys. Maybe this is to mark it out as being &amp;lsquo;other&amp;rsquo; for usability reasons (which I like), or maybe it&amp;rsquo;s a side-effect of the button housing a fingerprint sensor.&lt;/p&gt;
&lt;p&gt;With my lack of Apple experience and the lack of working screen, I actually found this button a little confusing. I was never sure if I&amp;rsquo;d pressed it &amp;lsquo;properly&amp;rsquo;, ie did it require a tap, or a little hold, or did perhaps a long hold perform a different function?&lt;/p&gt;
&lt;p&gt;I tried searching for info about the button but there are so many different models of Macbook, many users with varying ways of communicating their ideas, no official detailed documentation for these things, and of course the reasonable assumption that Macbooks have screens. Yes, I was confused by a power button.&lt;/p&gt;
&lt;ul class=&#39;gallery single-item&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_JiKw_1&#39; /&gt;
      &lt;label for=&#39;item_JiKw_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/decapitating-macbook/macos-login-no-login.jpg&#39; alt=&#39;External monitor - No login UI&#39; /&gt;
        &lt;span&gt;External monitor - No login UI&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After several presses of the power button, and waits to see if a reboot was happening, more progress. Trees! The MacOS login screen appeared, only&amp;hellip; not quite. There were no buttons, dialogues, or any user interface elements.&lt;/p&gt;
&lt;p&gt;A bit of searching taught me that the missing login UI was indicative of a second screen being attached to a Macbook. The login screen was being shown (invisibly to me) on the broken internal display, and my little monitor was just acting as an extension. Fair enough.&lt;/p&gt;
&lt;p&gt;More &amp;lsquo;internet research&amp;rsquo; revealed that &lt;code&gt;CMD + F1&lt;/code&gt; would swap output to the external monitor. This worked. Great. &lt;em&gt;I now had a dinky 11&amp;quot; MacOS login screen&lt;/em&gt;.&lt;/p&gt;
&lt;ul class=&#39;gallery single-item&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_oc9h_1&#39; /&gt;
      &lt;label for=&#39;item_oc9h_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/decapitating-macbook/macos-login-screen.jpg&#39; alt=&#39;Login screen: Success&#39; /&gt;
        &lt;span&gt;Login screen: Success&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It Just (about) Works.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>M1 Macbook Air</title>
      <link>https://mm-dev.rocks/posts/decapitating-macbook/m1-macbook-air/</link>
      <pubDate>Thu, 06 Mar 2025 12:26:08 +0000</pubDate>
      <guid>https://mm-dev.rocks/posts/decapitating-macbook/m1-macbook-air/</guid>
      <description>&lt;p&gt;Despite its laptop-ness, I decided to think harder on the Macbook Air:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It&amp;rsquo;s thin, light and particularly low-powered&lt;/li&gt;
&lt;li&gt;It&amp;rsquo;s fanless&lt;/li&gt;
&lt;li&gt;Although I&amp;rsquo;ll never prefer it, Apple (/my?) users tend to &lt;em&gt;love the trackpad&lt;/em&gt;, so the ability to check out my apps on the M1 Air&amp;rsquo;s trackpad and make sure everything feels ok might be useful&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My issue with the redundant (for me) display being attached?.. &lt;em&gt;this could become an advantage&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;People sell these machines cheaply when the displays break (Apple is&amp;hellip; not big on right-to-repair). I could buy a Macbook with a broken screen and remove it. I checked whether this was possible (such things are a given with most laptops but with Apple I wasn&amp;rsquo;t sure what to expect). I found that this was doable, people were out there doing it. Fantastic.&lt;/p&gt;
&lt;h3 id=&#34;buy-it-now&#34;&gt;&amp;lsquo;BUY IT NOW&amp;rsquo;&lt;/h3&gt;
&lt;p&gt;I managed to find a smashed-screen, 8GB M1 Macbook Air in Rose Pink on eBay for £300. Really if I&amp;rsquo;d waited I could probably have picked one up for less.&lt;/p&gt;
&lt;p&gt;But this was a &lt;em&gt;strike while the iron is hot&lt;/em&gt; situation&amp;hellip; my resistance had been strong for many years, if I waited I might easily end up changing my mind.&lt;/p&gt;
&lt;h3 id=&#34;8gb-ram-rose-pink&#34;&gt;8GB RAM? Rose Pink?&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;d ideally want 16GB RAM minimum on a modern laptop, but the price difference between that and 8GB was significant, even on the second-hand market. Everyone seemed to be saying that 8GB on Apple Silicon was like 16GB on AMD64, which sounded distinctly like the reality-distortion-field in effect, but there were architectural realities such as their &amp;lsquo;Unified&amp;rsquo; memory architecture to back this up (or at least the idea that you get more bang per GB on Apple Silicon). I&amp;rsquo;d decided to trust.&lt;/p&gt;
&lt;p&gt;I was mainly interested in using this machine for Flutter development, and had found several people saying they were doing that on an M1 with 8GB without issues. [Note from the future: It&amp;rsquo;s great, no worries and easily snappy enough for me]&lt;/p&gt;
&lt;p&gt;This model was Rose Pink, the colour I&amp;rsquo;d usually be least likely to choose. Considering I was buying it to immediately butcher, this somehow made it all the more perfect.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Which Mac Should I Get?</title>
      <link>https://mm-dev.rocks/posts/decapitating-macbook/which-mac-should-i-get/</link>
      <pubDate>Thu, 06 Mar 2025 12:26:07 +0000</pubDate>
      <guid>https://mm-dev.rocks/posts/decapitating-macbook/which-mac-should-i-get/</guid>
      <description>&lt;p&gt;The strongest factors affecting my decision were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Cost.&lt;/em&gt; I wanted to spend as little as possible.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Power usage.&lt;/em&gt; &lt;a href = &#34;https://mm-dev.rocks/posts/my-working-environment/intro/&#34; title = &#34;My Working Environment&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;I live in a low-power environment&lt;/a&gt; and didn&amp;rsquo;t want to introduce a new device that was going to dramatically increase my electricity usage.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Clearly Apple Silicon was what I needed in terms of power. I&amp;rsquo;ve preferred ARM processors for years and &lt;a href = &#34;https://mm-dev.rocks/posts/android-as-a-dev-environment/intro/&#34; title = &#34;Android as a Dev Environment&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;use them for as much of my work as possible&lt;/a&gt;. Apple&amp;rsquo;s use of specialised ARM chips and tight control over their hardware/software architecture means they&amp;rsquo;re in a good position to maximise performance-per-Watt&amp;hellip; if I must paddle in Apple&amp;rsquo;s pool, Apple Silicon is a no-brainer.&lt;/p&gt;
&lt;p&gt;To keep it cheap, the obvious choice was to go with the first generation M1 chips. They reportedly still held up well and should get OS updates for a while (even longer with &lt;a href = &#34;https://dortania.github.io/OpenCore-Legacy-Patcher/&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;OpenCore Legacy Patcher&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t love laptops (I&amp;rsquo;d rather choose my own keyboard, pointing device and &lt;a href = &#34;https://mm-dev.rocks/posts/oled-and-eink-4-life/intro/&#34; title = &#34;OLED and E-Ink 4 Life!&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;screen&lt;/a&gt; so having redundant copies of these things stuck on my computer is a waste), so I was initially attracted to the &lt;strong&gt;M1 Mac Mini&lt;/strong&gt;. It&amp;rsquo;s just a computer, without Human Interface Devices that I don&amp;rsquo;t need.&lt;/p&gt;
&lt;p&gt;Two things put me off the M1 Mac Mini though:&lt;/p&gt;
&lt;h3 id=&#34;it-has-a-fan&#34;&gt;It has a fan&lt;/h3&gt;
&lt;p&gt;People say it&amp;rsquo;s silent. Even if this is true (there are levels to this), fans are mechanical devices which can wear out (and develop a noise), suck/blow dust around (needing to be cleaned), or fail. They also use power.&lt;/p&gt;
&lt;h3 id=&#34;power-delivery&#34;&gt;Power delivery&lt;/h3&gt;
&lt;p&gt;The battery bank from which I power my home is 12V (24V or 48V would be better and I&amp;rsquo;ll upgrade when the time is right). All my electricity comes from this battery bank.&lt;/p&gt;
&lt;p&gt;I have a power inverter, meaning I &lt;em&gt;can&lt;/em&gt; use appliances with normal/household plugs, but the inverter wastes some power in the conversion process, stepping up from 12V to 220V. To then plug in a power brick, which will again throw away some power in converting 220V back down to whatever voltage a device uses, means double wastage, and a mess of plugs and wires.&lt;/p&gt;
&lt;p&gt;I have DC 12V from the battery, and an easily-accessible 20V feed via a step-up converter. Then of course I have 5V over USB, not to mention all the exotic USB-C variants. Between all these methods I can power everything that I use regularly.&lt;/p&gt;
&lt;ul class=&#39;gallery single-item&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_2xSI_1&#39; /&gt;
      &lt;label for=&#39;item_2xSI_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/decapitating-macbook/xt60-adapters.jpg&#39; alt=&#39;Assorted XT-60 adapters&#39; /&gt;
        &lt;span&gt;Assorted XT-60 adapters&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When I buy a new device the first thing I do is chop off its power connector, attach it to 
&lt;a data-image-ref=&#34;Assorted XT-60 adapters&#34; href=&#34;#&#34;&gt;one of the XT60 connectors I use for everything&lt;/a&gt;
, and chuck (hoard, actually) the device&amp;rsquo;s power brick. I&amp;rsquo;ve done this with multiple laptops and small form factor PCs without issue. They usually seem to want 19V-21V and be tolerant of 20V &lt;em&gt;although I&amp;rsquo;ve heard stories of people damaging stuff with similar reckless behaviour to mine&lt;/em&gt; so maybe I&amp;rsquo;ve been lucky.&lt;/p&gt;
&lt;p&gt;I searched for info about doing this with the M1 Mac Mini&amp;rsquo;s power brick and found some people saying they had problems. While writing this I just searched again to try to find those discussions but this time I found stuff suggesting it&amp;rsquo;s fairly easy. So I&amp;rsquo;m not sure if I used the wrong search terms before, or maybe I was just too jumpy about modding power delivery for an Apple device. Apple tends to be so unfriendly towards going off-road and they&amp;rsquo;re known for implementing measures to prevent you doing things in &amp;lsquo;unapproved&amp;rsquo; ways. I didn&amp;rsquo;t want to waste money buying something I couldn&amp;rsquo;t use.&lt;/p&gt;
&lt;p&gt;For better or worse, I decided against the M1 Mac Mini.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Intro</title>
      <link>https://mm-dev.rocks/posts/decapitating-macbook/intro/</link>
      <pubDate>Thu, 06 Mar 2025 12:26:06 +0000</pubDate>
      <guid>https://mm-dev.rocks/posts/decapitating-macbook/intro/</guid>
      <description>&lt;ul class=&#39;gallery single-item&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_QGSe_1&#39; /&gt;
      &lt;label for=&#39;item_QGSe_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/decapitating-macbook/headless-mac.jpg&#39; alt=&#39;Macbook Air M1 Headless&#39; /&gt;
        &lt;span&gt;Macbook Air M1 Headless&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I don&amp;rsquo;t tend to enjoy using Apple products. I want to be able to make my software available on their platforms though.&lt;/p&gt;
&lt;p&gt;To publish apps for MacOS/iOS/iPadOS, you really need Apple hardware. Also, if I am to provide software for their platforms, and especially if I want to sell my work, &lt;em&gt;I need to be able to experience my apps in the same way my users will experience them&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s basic respect for the user.&lt;/p&gt;
&lt;h3 id=&#34;virtualisation&#34;&gt;Virtualisation&lt;/h3&gt;
&lt;p&gt;I tried Virtual Machines, probably had half a dozen MacOS VMs over the years in VirtualBox and QEMU/KVM. Getting MacOS working in a VM is difficult, inconsistent and (at least on my machines) performance isn&amp;rsquo;t great. Apple strongly does not want you doing this, you&amp;rsquo;ll always be swimming upstream.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s a joyless experience.&lt;/p&gt;
&lt;h3 id=&#34;codemagic&#34;&gt;Codemagic&lt;/h3&gt;
&lt;p&gt;I tried &lt;a href = &#34;https://codemagic.io&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;Codemagic&lt;/a&gt;, a continuous integration/continuous delivery service which lets you publish to various platforms including the Apple ones. It&amp;rsquo;s a great service and has a free tier, but it seemed that my usage would end up putting me in a paid tier, and that could get expensive for me quite quickly.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s also from-a-distance, and while Codemagic does allow remote login to the MacOS GUI it&amp;rsquo;s not the same as having a device in front of me, and if I want to thoroughly check the experience of using my apps on Codemagic&amp;rsquo;s remote machines (rather than just running builds) that&amp;rsquo;s going to quickly eat up any free minutes.&lt;/p&gt;
&lt;p&gt;I can certainly see this being a solution for some situations &amp;mdash; not mine.&lt;/p&gt;
&lt;h3 id=&#34;so&#34;&gt;So&amp;hellip;&lt;/h3&gt;
&lt;p&gt;For the first time in over 20 years of owning dozens of computers, I allowed myself to decide that I needed some kind of Mac.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Set up Vim for Flutter</title>
      <link>https://mm-dev.rocks/posts/flutter-in-the-terminal/set-up-vim-for-flutter/</link>
      <pubDate>Thu, 08 Aug 2024 17:47:33 +0100</pubDate>
      <guid>https://mm-dev.rocks/posts/flutter-in-the-terminal/set-up-vim-for-flutter/</guid>
      <description>&lt;p&gt;There are various ways of installing Vim plugins. I use &lt;code&gt;vim-plug&lt;/code&gt; in my examples here but I&amp;rsquo;ll add links to instructions for each plugin so you can do it your own way.&lt;/p&gt;
&lt;h2 id=&#34;dart&#34;&gt;Dart&lt;/h2&gt;
&lt;p&gt;As Flutter uses the Dart language, install &lt;a href = &#34;https://github.com/dart-lang/dart-vim-plugin&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;Dart support for Vim&lt;/a&gt;. I add the following to my &lt;code&gt;~/.vimrc&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-vim&#34; data-lang=&#34;vim&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;&amp;#34; Between the &amp;#39;plug#begin()&amp;#39; and &amp;#39;plug#end()&amp;#39; lines&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;Plug&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;dart-lang/dart-vim-plugin&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then I restart Vim and run &lt;code&gt;:PlugInstall&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This adds syntax highlighting and auto-indentation of Dart code.&lt;/p&gt;
&lt;h2 id=&#34;language-servers&#34;&gt;Language Servers&lt;/h2&gt;
&lt;p&gt;Language Servers/LSPs (LSP = Language Server Protocol but both terms are often used interchangeably) give your editor information about the structure of a language and enable it to do syntax highlighting and code completion, along with deeper functionality such as allowing you to jump to the definition of a function, do refactoring etc.&lt;/p&gt;
&lt;p&gt;LSPs are used in Visual Studio for a lot of its advanced functionality but they can also be used in Vim. There&amp;rsquo;s more than one way of doing this but I use &lt;a href = &#34;https://github.com/neoclide/coc.nvim&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;coc-nvim&lt;/a&gt; (despite the name it works in modern versions of Vim, not just Neovim).&lt;/p&gt;
&lt;h3 id=&#34;install-the-cocnvim-plugin-in-vim&#34;&gt;Install the &lt;code&gt;coc.nvim&lt;/code&gt; plugin in Vim&lt;/h3&gt;
&lt;p&gt;Using &lt;code&gt;vim-plug&lt;/code&gt; I add the following to my &lt;code&gt;~/.vimrc&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-vim&#34; data-lang=&#34;vim&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;&amp;#34; Between the &amp;#39;plug#begin()&amp;#39; and &amp;#39;plug#end()&amp;#39; lines&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;Plug&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;neoclide/coc.nvim&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; {&lt;span class=&#34;s1&#34;&gt;&amp;#39;branch&amp;#39;&lt;/span&gt;: &lt;span class=&#34;s1&#34;&gt;&amp;#39;release&amp;#39;&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Again, I restart Vim and run &lt;code&gt;:PlugInstall&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For other ways of installing just follow the &lt;a href = &#34;https://github.com/neoclide/coc.nvim/wiki/Install-coc.nvim&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;instructions &lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&#34;install-a-flutter-lsp&#34;&gt;Install a Flutter LSP&lt;/h3&gt;
&lt;p&gt;Install the Flutter language server in CoC, &lt;a href = &#34;https://github.com/iamcco/coc-flutter&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;coc-flutter&lt;/a&gt;. This provides lots of helpful functionality for working with Flutter in Vim as described in the link.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-vim&#34; data-lang=&#34;vim&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;CocInstall&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;coc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;flutter&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This LSP needs to know where the Flutter SDK is installed. In Vim, run &lt;code&gt;:CocList FlutterSDKs&lt;/code&gt; and make sure it finds at least 1 SDK. If not, add a line to CoC settings (&lt;code&gt;:CocConfig&lt;/code&gt;):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;flutter.sdk.path&amp;#34;&lt;/span&gt;: &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$HOME&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;/.local/flutter/bin&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can find the correct path with &lt;code&gt;which flutter&lt;/code&gt; in the terminal.&lt;br&gt;
&lt;em&gt;Originally, I had installed my Flutter SDK at &lt;code&gt;/usr/bin&lt;/code&gt;. That caused invisible errors with &lt;code&gt;coc-flutter&lt;/code&gt; as it couldn&amp;rsquo;t access the path properly to find the Flutter SDK. I was having trouble getting this step working. I then moved the Flutter SDK to &lt;code&gt;$HOME/.local/flutter&lt;/code&gt; and everything started to work properly&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&#34;cocnvim-settings&#34;&gt;&lt;code&gt;coc.nvim&lt;/code&gt; settings&lt;/h2&gt;
&lt;p&gt;To change CoC settings the command &lt;code&gt;:CocConfig&lt;/code&gt; can be used in Vim. On my system it opens up the file &lt;code&gt;~/.vim/coc-settings.json&lt;/code&gt;. You can open/edit this file directly but depending on your Vim configuration it may be in a different location.&lt;/p&gt;
&lt;p&gt;Here are some of the settings I prefer:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;diagnostic.checkCurrentLine&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;diagnostic.enableMessage&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;jump&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;colors.enable&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;suggest.noselect&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;inlayHint&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;diagnosticcheckcurrentline-true&#34;&gt;&lt;code&gt;&amp;quot;diagnostic.checkCurrentLine&amp;quot;: true,&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;By default, Coc will show you a diagnostic error or warning when your cursor is over the word causing the problem. Setting this to &lt;code&gt;true&lt;/code&gt; means you only have to be on the same line as the error to see the dialog, not the exact word.&lt;/p&gt;
&lt;h3 id=&#34;diagnosticenablemessage-jump&#34;&gt;&lt;code&gt;&amp;quot;diagnostic.enableMessage&amp;quot;: &amp;quot;jump&amp;quot;,&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;This diagnostic popups can be really helpful, but it&amp;rsquo;s all a bit frenetic for me, I&amp;rsquo;m a &lt;code&gt;prefers-reduced-motion&lt;/code&gt; kind of person. Setting this to &lt;code&gt;jump&lt;/code&gt; means they don&amp;rsquo;t pop up automatically, but can still be shown when I want to see them. I also add this line to &lt;code&gt;~/.vimrc&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-vim&#34; data-lang=&#34;vim&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;nmap&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;silent&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;gh&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Plug&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;coc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;float&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;hide&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;nmap&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;silent&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;gl&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Plug&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;coc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;diagnostic&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;info&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Which allows me to show/hide the diagnostics with &lt;code&gt;gh&lt;/code&gt;/&lt;code&gt;gl&lt;/code&gt; respectively.&lt;/p&gt;
&lt;h3 id=&#34;colorsenable-true&#34;&gt;&lt;code&gt;&amp;quot;colors.enable&amp;quot;: true,&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;This setting is for another CoC addition called &lt;code&gt;coc-highlight&lt;/code&gt;. I installed this in Vim with:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-vim&#34; data-lang=&#34;vim&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;CocInstall&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;coc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;highlight&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This adds various types of colour highlighting, but the one I was most interested in is how it will detect any colours (eg RGB or hex) in your code and change the colour of their text to give you a preview of them. This feature is enabled with the &lt;code&gt;colors.enable&lt;/code&gt; line as above.&lt;/p&gt;
&lt;h3 id=&#34;suggestnoselect-true&#34;&gt;&lt;code&gt;&amp;quot;suggest.noselect&amp;quot;: true,&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;When you start typing a word, CoC will default to inserting its first suggested autocompletion word. This is a bit more &amp;lsquo;help&amp;rsquo; than I like, so I add this setting to make the process more intentional.&lt;/p&gt;
&lt;h3 id=&#34;inlayhint-false&#34;&gt;&lt;code&gt;&amp;quot;inlayHint&amp;quot;: false,&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;By default CoC inserts &lt;em&gt;inlay hints&lt;/em&gt;, for example if you haven&amp;rsquo;t added a type hint to indicate what kind of widget is being referred to somewhere, it will insert the &lt;code&gt;&amp;lt;Widget&amp;gt;&lt;/code&gt; hint for you as a piece of magical ghost text.&lt;/p&gt;
&lt;p&gt;These bizarre bits of text appear in the middle of lines of code, except they aren&amp;rsquo;t really there, you can&amp;rsquo;t select them or delete them and if you&amp;rsquo;re moving your cursor around it acts as if they don&amp;rsquo;t exist. I&amp;rsquo;m sure some people enjoy them, and for those people I&amp;rsquo;m glad that they exist. For me, they are disgusting, obnoxious devil-warts.&lt;/p&gt;
&lt;p&gt;Thankfully, they can be vanquished by adding this line to the CoC settings.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Install Flutter</title>
      <link>https://mm-dev.rocks/posts/flutter-in-the-terminal/install-flutter/</link>
      <pubDate>Thu, 08 Aug 2024 17:47:32 +0100</pubDate>
      <guid>https://mm-dev.rocks/posts/flutter-in-the-terminal/install-flutter/</guid>
      <description>&lt;h3 id=&#34;follow-instructions-from&#34;&gt;Follow instructions from &lt;a href = &#34;https://docs.flutter.dev/get-started/install/linux/android&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;https://docs.flutter.dev/get-started/install/linux/android&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Download the &lt;code&gt;flutter_*.tar.xz&lt;/code&gt; archive&lt;/li&gt;
&lt;li&gt;Unpack the archive into &lt;code&gt;$HOME/.local/bin&lt;/code&gt; (creates &lt;code&gt;$HOME/.local/bin/flutter/bin&lt;/code&gt;)&lt;br&gt;
&lt;em&gt;At first I tried putting this at &lt;code&gt;/usr/bin&lt;/code&gt;, but that caused invisible errors in coc-flutter as it couldn&amp;rsquo;t access the path properly to find the Flutter SDK&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Add the new path as above to $PATH eg in &lt;code&gt;~/.profile&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# ~/.profile&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;export&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;PATH&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$HOME&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;/.local/bin/flutter/bin:&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$PATH&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;For ARM64 architectures the Flutter SDK cannot be found at the above link, nor in the archives. In this case the SDK can be obtained by cloning the main flutter repo, then running a &lt;code&gt;flutter&lt;/code&gt; command to trigger a download of dependencies&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;git clone -b main https://github.com/flutter/flutter.git
./flutter/bin/flutter --version
&lt;/code&gt;&lt;/pre&gt;&lt;/blockquote&gt;
&lt;h3 id=&#34;agree-to-more-licenses-and-get-flutter-doctor-to-pass&#34;&gt;Agree to more licenses and get Flutter doctor to pass&lt;/h3&gt;
&lt;p&gt;First, agree to several laughably long licence agreements which Google knows full well that nobody reads.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;flutter doctor --android-licenses
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then type Y and Enter for each prompt&lt;/p&gt;
&lt;p&gt;Now run &lt;code&gt;flutter doctor&lt;/code&gt;. It should pass every test except &amp;lsquo;Android Studio (not installed)&amp;rsquo;. If so, we&amp;rsquo;re ready to start building Flutter apps (with Android Studio not installed).&lt;/p&gt;
&lt;h3 id=&#34;sample-project&#34;&gt;Sample project&lt;/h3&gt;
&lt;p&gt;You can make a really simple sample project to check that everything&amp;rsquo;s installed correctly and see how it works.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;flutter create sample-project
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This will create a &lt;code&gt;sample-project&lt;/code&gt; directory with a Flutter app in it, a simple built-in demo of a button which counts up as you tap it.&lt;/p&gt;
&lt;p&gt;To run the app:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;cd&lt;/span&gt; sample_project
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;flutter run
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description>
    </item>
    
    <item>
      <title>Install the Android SDK</title>
      <link>https://mm-dev.rocks/posts/flutter-in-the-terminal/install-the-android-sdk/</link>
      <pubDate>Thu, 08 Aug 2024 17:47:31 +0100</pubDate>
      <guid>https://mm-dev.rocks/posts/flutter-in-the-terminal/install-the-android-sdk/</guid>
      <description>&lt;p&gt;Getting the Android SDK set up manually can be tricky, I think the expectation is that most people will use Android Studio so not a lot of work has been put in to making manual installation user-friendly.&lt;/p&gt;
&lt;h3 id=&#34;pick-a-directory-where-youll-keep-the-sdk&#34;&gt;Pick a directory where you&amp;rsquo;ll keep the SDK&lt;/h3&gt;
&lt;p&gt;This is an important directory that will always store all the Android-SDK specific tools, platforms, build tools etc. Various bits and pieces need to know where these things are, so we&amp;rsquo;ll set some environment variables for them later.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m using &lt;code&gt;$HOME/android_sdk/&lt;/code&gt; in these examples.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;mkdir &lt;span class=&#34;nv&#34;&gt;$HOME&lt;/span&gt;/android_sdk
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;install-the-sdk-manager&#34;&gt;Install the SDK Manager&lt;/h3&gt;
&lt;p&gt;This is a set of command line tools used to install different versions of the SDK (for when you want to target different Android devices).&lt;/p&gt;
&lt;p&gt;Based on Google&amp;rsquo;s instructions at &lt;a href = &#34;https://developer.android.com/tools/sdkmanager&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;https://developer.android.com/tools/sdkmanager&lt;/a&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;From the &lt;a href = &#34;https://developer.android.com/studio&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;downloads page&lt;/a&gt; (I had to scroll down a long way), download the &lt;code&gt;cmdline-tools&lt;/code&gt; zip and unzip it to the directory you created above eg &lt;code&gt;$HOME/android_sdk/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Inside the unzipped directory &lt;code&gt;cmdline-tools&lt;/code&gt; make a &amp;rsquo;latest&amp;rsquo; directory and move all of the other contents of &amp;lsquo;cmdline-tools&amp;rsquo; inside it (so you end up with &lt;code&gt;cmdline-tools/latest/bin&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You should end up with something like the following (I&amp;rsquo;m using &lt;code&gt;tree&lt;/code&gt; to list the directory contents but don&amp;rsquo;t worry about that, just make sure your directories are in the right places):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ tree -L &lt;span class=&#34;m&#34;&gt;2&lt;/span&gt; cmdline-tools/
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;cmdline-tools/
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;└── latest
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    ├── NOTICE.txt
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    ├── bin
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    ├── lib
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    └── source.properties
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;add-some-environment-variables-to-profile&#34;&gt;Add some environment variables to &lt;code&gt;~/.profile&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Make sure these make sense based on what we&amp;rsquo;ve done so far if you&amp;rsquo;ve used different directories to me.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# ~/.profile&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;export&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;ANDROID_SDK_ROOT&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$HOME&lt;/span&gt;/android_sdk
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;export&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;PATH&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$PATH&lt;/span&gt;:&lt;span class=&#34;nv&#34;&gt;$ANDROID_SDK_ROOT&lt;/span&gt;/cmdline-tools/latest/bin
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;export&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;PATH&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$PATH&lt;/span&gt;:&lt;span class=&#34;nv&#34;&gt;$ANDROID_SDK_ROOT&lt;/span&gt;/platform-tools
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Source the profile with &lt;code&gt;source ~/.profile&lt;/code&gt;. Then run &lt;code&gt;sdkmanager&lt;/code&gt; and see if it does something. If you get &lt;code&gt;command not found&lt;/code&gt; double-check everything and try rebooting.&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;&lt;span&gt;My sdkmanager binary name clash (this won&amp;rsquo;t affect you but click to expand if you&amp;rsquo;re interested)&lt;/span&gt;&lt;/summary&gt;

&lt;div&gt;
&lt;p&gt;In my case I already had a binary installed with the name &lt;code&gt;sdkmanager&lt;/code&gt;, as that&amp;rsquo;s the name Garmin use for their ConnectIQ SDK Manager. Apparently neither Garmin nor Google could foresee that anyone other than themselves might call their SDK manager software &amp;lsquo;sdkmanager&amp;rsquo;.&lt;/p&gt;
&lt;p&gt;So I had to do some hacky stuff with bash aliases in &lt;code&gt;~/.profile&lt;/code&gt; to let the two live alongside each other, and remind me of the situation:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;alias&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;sdkmanager&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;echo &amp;#34;Use either *sdkmanager-garmin* or *sdkmanager-android*&amp;#34;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;alias&lt;/span&gt; sdkmanager-android&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$ANDROID_SDK_ROOT&lt;/span&gt;/cmdline-tools/latest/bin/sdkmanager
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;alias&lt;/span&gt; sdkmanager-garmin&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$HOME&lt;/span&gt;/.local/bin/sdkmanager
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;/details&gt;
&lt;h3 id=&#34;accept-sdk-licences&#34;&gt;Accept SDK licences&lt;/h3&gt;
&lt;p&gt;The SDK licences must be accepted for everything to work.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sdkmanager --licenses
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then type Y and press Enter for each prompt&lt;/p&gt;
&lt;h3 id=&#34;install-the-android-toolchain&#34;&gt;Install the Android toolchain&lt;/h3&gt;
&lt;p&gt;To pass the &amp;lsquo;Android toolchain&amp;rsquo; step of &lt;code&gt;flutter doctor&lt;/code&gt; (in a bit) we need 3 things:&lt;/p&gt;
&lt;ol start=&#34;0&#34;&gt;
&lt;li&gt;Platform tools (generic)&lt;/li&gt;
&lt;li&gt;A valid platform&lt;/li&gt;
&lt;li&gt;Build tools to match the platform&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Various instructions online will tell you to do things like &lt;code&gt;sdkmanager &amp;quot;platform-tools&amp;quot; &amp;quot;platforms;android-33&amp;quot;&lt;/code&gt; (ie pass multiple arguments to &lt;code&gt;sdkmanager&lt;/code&gt;), but as observed in &lt;a href = &#34;https://stackoverflow.com/questions/76650927/failed-to-find-platform-sdk-with-path-platformsandroid-33&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;this StackOverflow answer&lt;/a&gt; &lt;em&gt;if using the command line tools, the platform tools need to be installed via separate commands ie:&lt;/em&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sdkmanager &lt;span class=&#34;s1&#34;&gt;&amp;#39;platform-tools&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sdkmanager &lt;span class=&#34;s1&#34;&gt;&amp;#39;platforms;android-33&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sdkmanager &lt;span class=&#34;s1&#34;&gt;&amp;#39;build-tools;33.0.2&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If the installation has worked correctly you should end up with something like:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ tree -L &lt;span class=&#34;m&#34;&gt;1&lt;/span&gt; android_sdk/
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;android_sdk/
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;├── build-tools
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;├── cmdline-tools
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;├── licenses
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;├── platform-tools
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;└── platforms
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;details&gt;
&lt;summary&gt;&lt;span&gt;Another weird, me-specific problem I encountered&amp;hellip;&lt;/span&gt;&lt;/summary&gt;

&lt;div&gt;
&lt;p&gt;At first, my installations weren&amp;rsquo;t appearing in my &lt;code&gt;android_sdk&lt;/code&gt; directory and Flutter tools were complaining about not being able to find the Android SDK.&lt;/p&gt;
&lt;p&gt;Forcing the SDK path like this worked:
&lt;code&gt;sdkmanager --verbose &amp;quot;platform-tools&amp;quot; --sdk_root=$HOME/android_sdk/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;But something smelt wrong, as the original &lt;code&gt;sdkmanager&lt;/code&gt; install commands seemed to be completing without error. I investigated this by running the following:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sdkmanager &lt;span class=&#34;s1&#34;&gt;&amp;#39;platform-tools&amp;#39;&lt;/span&gt; --verbose
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Which revealed that they were going into:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;/data/data/com.termux/files/usr/share/android-sdk/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is because I was on Android using Termux, in a proot container, and I had previously installed the SDK in the main (non proot) Termux environment. This was not needed any more so I deleted the other installation.&lt;/p&gt;
&lt;p&gt;This is an exotic situation and you&amp;rsquo;re probably not as stupid as I am to get into this situation, but maybe this note will help in some other situation.&lt;/p&gt;
&lt;/div&gt;
&lt;/details&gt;
</description>
    </item>
    
    <item>
      <title>Dependencies and Environment</title>
      <link>https://mm-dev.rocks/posts/flutter-in-the-terminal/dependencies-and-environment/</link>
      <pubDate>Thu, 08 Aug 2024 17:47:30 +0100</pubDate>
      <guid>https://mm-dev.rocks/posts/flutter-in-the-terminal/dependencies-and-environment/</guid>
      <description>&lt;p&gt;To get started I&amp;rsquo;m presuming you have a Linux installation of something like Debian/Ubuntu.&lt;/p&gt;
&lt;h2 id=&#34;general-dependencies&#34;&gt;General Dependencies&lt;/h2&gt;
&lt;h3 id=&#34;git&#34;&gt;Git&lt;/h3&gt;
&lt;p&gt;You probably already have this but just in case:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo apt install git
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;ensure-java-jdk-is-installed-and-recent-enough&#34;&gt;Ensure Java JDK is installed and recent enough&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo apt install openjdk-17-jdk
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;stuff-flutter-needs&#34;&gt;Stuff Flutter Needs&lt;/h3&gt;
&lt;p&gt;The latest info about this should be &lt;a href = &#34;https://docs.flutter.dev/get-started/install/linux/android&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;here&lt;/a&gt; but at time of writing:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo apt-get install -y curl git unzip xz-utils zip libglu1-mesa
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And some other tools Flutter uses:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo apt install clang cmake ninja-build
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;if-using-chromium-browser-instead-of-chrome&#34;&gt;If using Chromium browser instead of Chrome&lt;/h3&gt;
&lt;p&gt;The Android SDK likes to know where Chrome is on your system. I use Chromium and the SDK can&amp;rsquo;t find it.&lt;/p&gt;
&lt;p&gt;So I export an environment variable to let Flutter know where to find Chromium eg in &lt;code&gt;~/.profile&lt;/code&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;export&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;CHROME_EXECUTABLE&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;/usr/bin/chromium
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description>
    </item>
    
    <item>
      <title>Parts of the SDK</title>
      <link>https://mm-dev.rocks/posts/flutter-in-the-terminal/parts-of-the-sdk/</link>
      <pubDate>Thu, 08 Aug 2024 17:47:30 +0100</pubDate>
      <guid>https://mm-dev.rocks/posts/flutter-in-the-terminal/parts-of-the-sdk/</guid>
      <description>&lt;p&gt;There are several components making up the SDK. I&amp;rsquo;ll list them here along with examples of how they can be installed later on when the SDK manager is set up and working.&lt;/p&gt;
&lt;p&gt;This is just an overview to help your mental model of what&amp;rsquo;s what, we&amp;rsquo;re not installing anything just yet.&lt;/p&gt;
&lt;p&gt;We download them as a zip and place them in a specific directory, as detailed later.&lt;/p&gt;
&lt;h3 id=&#34;command-line-tools&#34;&gt;Command Line Tools&lt;/h3&gt;
&lt;p&gt;These commands can do several things like signing and optimising APKs, but the main one we are interested in is &lt;code&gt;sdkmanager&lt;/code&gt;. This is the command we use to install the other tools.&lt;/p&gt;
&lt;h3 id=&#34;platform-tools&#34;&gt;Platform Tools&lt;/h3&gt;
&lt;p&gt;These are tools for interfacing with Android which you may already be familiar with, such as &lt;code&gt;adb&lt;/code&gt; and &lt;code&gt;fastboot&lt;/code&gt;.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sdkmanager &lt;span class=&#34;s1&#34;&gt;&amp;#39;platform-tools&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;platforms&#34;&gt;Platforms&lt;/h3&gt;
&lt;p&gt;A platform represents a specific version of Android. It includes multiple system images, which the device emulator uses to emulate various devices on that platform.&lt;/p&gt;
&lt;p&gt;Information used to compile your app for this platform version, and source code to be used for debugging are also included.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Multiple platforms can be installed alongside each other on the same system.&lt;/em&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sdkmanager &lt;span class=&#34;s1&#34;&gt;&amp;#39;platforms;android-33&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;build-tools&#34;&gt;Build Tools&lt;/h3&gt;
&lt;p&gt;These are tools used for building Android apps. I&amp;rsquo;m not 100% clear on what these are and why they are different from Platform Tools, it seems that they were part of Platform Tools in the past but have been sectioned off into their own thing.&lt;/p&gt;
&lt;p&gt;Anyway, we need them. Usually use the latest version, but sometimes if you&amp;rsquo;re targeting specific platforms eg older ones, errors may indicate that you need an older version.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sdkmanager &lt;span class=&#34;s1&#34;&gt;&amp;#39;build-tools;33.0.2&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description>
    </item>
    
    <item>
      <title>Intro</title>
      <link>https://mm-dev.rocks/posts/flutter-in-the-terminal/intro/</link>
      <pubDate>Thu, 08 Aug 2024 17:47:29 +0100</pubDate>
      <guid>https://mm-dev.rocks/posts/flutter-in-the-terminal/intro/</guid>
      <description>&lt;p&gt;Given the choice, I&amp;rsquo;d rather avoid huge complex apps like Android Studio. They use a lot of system resources and are very GUI/point-and-clicky. Those things definitely have their place but I prefer keyboard control where it makes sense, and for me programming in Flutter is one of those situations.&lt;/p&gt;
&lt;p&gt;A lot of what Android Studio does behind the scenes is automatically editing text files and running shell commands such as &lt;code&gt;flutter run&lt;/code&gt;, &lt;code&gt;flutter build&lt;/code&gt; and so on. If you&amp;rsquo;re comfortable with the terminal these commands are fairly simple to run manually.&lt;/p&gt;
&lt;p&gt;As an IDE Android Studio also helps with stuff like syntax highlighting, code completion, refactoring etc. If you&amp;rsquo;re into Vim it can be set up to give you these pleasantries. I&amp;rsquo;m sure there is stuff Android Studio can do that Vim can&amp;rsquo;t, but whatever it is I&amp;rsquo;m not missing it and I feel good when I use Vim.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve set up a couple of systems with this Flutter dev environment recently and hit some stumbling blocks. They weren&amp;rsquo;t too hard to resolve but I wanted to write up the process for future reference and to help anybody else in a similar situation.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>I&#39;m Not a Perfectionist</title>
      <link>https://mm-dev.rocks/posts/i-am-not-a-perfectionist/</link>
      <pubDate>Tue, 06 Aug 2024 08:20:26 +0100</pubDate>
      <guid>https://mm-dev.rocks/posts/i-am-not-a-perfectionist/</guid>
      <description>&lt;p&gt;A nice compliment I&amp;rsquo;ve occasionally been paid is &amp;ldquo;you&amp;rsquo;re a perfectionist&amp;rdquo;. I&amp;rsquo;m calling it a nice compliment because it&amp;rsquo;s been delivered by kind people who have intended it as a compliment, and it&amp;rsquo;s gracious to accept it as it was intended rather than throw it back in their faces.&lt;/p&gt;
&lt;p&gt;I used to think of perfectionism as a virtue, but I no longer do.&lt;/p&gt;
&lt;h2 id=&#34;why-my-view-has-evolved&#34;&gt;Why My View Has Evolved&lt;/h2&gt;
&lt;p&gt;I used to obsess over tiny details. I mean I still do, but I&amp;rsquo;m more aware that I do it now, and experience has taught me about some of the problems doing so can cause.&lt;/p&gt;
&lt;p&gt;Now, I try harder to keep a birds-eye view of what I&amp;rsquo;m doing, assess where my actions can make the most difference, and balance where I spend my time.&lt;/p&gt;
&lt;h3 id=&#34;attentiontime-are-finite&#34;&gt;Attention/Time are Finite&lt;/h3&gt;
&lt;p&gt;There are always things that can be done to improve any project. I mean &lt;em&gt;project&lt;/em&gt; very broadly: an app, a website, an email, a painting, a loaf of bread. Is it better to spend 10 hours polishing some tiny detail, or to implement another couple of entire features?&lt;/p&gt;
&lt;p&gt;The answer is not always obvious, but for me the important thing is to ask the question and assess. Awareness.&lt;/p&gt;
&lt;h3 id=&#34;what-are-the-goals&#34;&gt;What are the Goals?&lt;/h3&gt;
&lt;p&gt;Are you trying to prove a concept and get it in front of people ASAP? Or to demonstrate your craftsmanship and attention to detail? Different goals requiring different approaches.&lt;/p&gt;
&lt;h3 id=&#34;my-perfection-may-not-be-your-perfection&#34;&gt;My Perfection May Not Be Your Perfection&lt;/h3&gt;
&lt;p&gt;It&amp;rsquo;s (almost) hilarious when I work on some small detail for hours, getting it just right. I then show my work to somebody else who immediately spots some glaring error in a completely different part of the work, and doesn&amp;rsquo;t seem even mildly impressed by my perfect detail.&lt;/p&gt;
&lt;p&gt;More than once I&amp;rsquo;ve experienced the following:&lt;/p&gt;
&lt;p&gt;When working on a painting, a friend has visited and seen it mid-way through the process. They love it, they say so and I can feel it. &amp;ldquo;Just wait until it&amp;rsquo;s finished then!&amp;rdquo; I think to myself, certain that they&amp;rsquo;ll love it twice as much. Then when it&amp;rsquo;s finished they see it and they don&amp;rsquo;t love it any more. In their eyes I&amp;rsquo;ve spoiled it. Only the rare few are secure enough in themselves and myself to be honest with me about that, but others give it away in their polite but unconvincing praise.&lt;/p&gt;
&lt;p&gt;Sometime they&amp;rsquo;re just wrong (JK, you can&amp;rsquo;t be wrong about art)! But sometimes they might be onto something&amp;hellip;&lt;/p&gt;
&lt;h3 id=&#34;diminishing-returns&#34;&gt;Diminishing Returns&lt;/h3&gt;
&lt;p&gt;&lt;a href = &#34;https://en.wikipedia.org/wiki/Diminishing_returns&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;The law of diminishing returns&lt;/a&gt; is an economic principle but it can be applied more broadly to lots of things in life.&lt;/p&gt;
&lt;p&gt;The closer you get to &amp;lsquo;perfection&amp;rsquo; (or at least your idea of it), the more the tiny imperfections stand out. They seem to multiply as they get smaller. Things that were not noticeable before, now stand out obnoxiously against their increasingly-perfect background. But your work isn&amp;rsquo;t really getting worse, you&amp;rsquo;re just looking more closely at it. You&amp;rsquo;ve zoomed in too far. You may be looking at a cathedral through a microscope. You can&amp;rsquo;t see it.&lt;/p&gt;
&lt;h2 id=&#34;paid-work&#34;&gt;Paid Work&lt;/h2&gt;
&lt;p&gt;If somebody is paying me to make something for them, I have to respect their preferences and give them what they want.&lt;/p&gt;
&lt;p&gt;In a good &lt;del&gt;working&lt;/del&gt; relationship there should be honest debate and &lt;em&gt;both sides should change their minds sometimes&lt;/em&gt;. I have experience in what I do and may have valid reasons for a disagreement. On the other hand, the client knows their business much better than I do and has reasons for their goals. Ultimately I have to remember that they are paying me for something and I want them to be happy with our transaction.&lt;/p&gt;
&lt;p&gt;If we really, consistently, don&amp;rsquo;t see eye-to-eye, then we probably aren&amp;rsquo;t a good match and should not collaborate on future projects. Even then though, I still want them to be as happy as possible so do my best to finish the project and give them what they want.&lt;/p&gt;
&lt;h2 id=&#34;fear&#34;&gt;Fear&lt;/h2&gt;
&lt;p&gt;I mentioned to a fried that I had several articles for this site that are 90% finished. I have been procrastinating.&lt;/p&gt;
&lt;p&gt;&amp;ldquo;Why not just publish them, what&amp;rsquo;s the big deal?&amp;rdquo;, she asked.&lt;/p&gt;
&lt;p&gt;Well&amp;hellip;&lt;/p&gt;
&lt;h3 id=&#34;i-may-be-misunderstood&#34;&gt;I May Be Misunderstood&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;What if I haven&amp;rsquo;t articulated clearly enough and somebody misunderstands my point?&lt;/li&gt;
&lt;li&gt;What if there are typos or mistakes in spelling and grammar? People might think I have bad language or communication skills.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We all make mistakes. We are all misunderstood, all the time (more often than some of us realise). The message we send is not the message that is received, interpreted and understood by the recipient, because our inner worlds are complex.&lt;/p&gt;
&lt;p&gt;We should try our best. Then we should let go.&lt;/p&gt;
&lt;p&gt;If something I said is misunderstood and important enough to somebody, they can ask me to clarify and give me the chance to put it right. Or maybe we just disagree &amp;mdash; that&amp;rsquo;s ok. Or maybe they read some small thing and want to extrapolate it into some caricature of who I am and think badly of me, with no chance of recompense. Buh-bye! Life is too short to deal with these kinds of people. Thanks, imperfections, for filtering those people out.&lt;/p&gt;
&lt;h3 id=&#34;i-may-not-show-my-skills-in-the-best-light&#34;&gt;I May Not Show My Skills in the Best Light&lt;/h3&gt;
&lt;p&gt;Some of my unfinished draft articles had (have!) placeholder pictures. Bad photos that I took so that I could get something on the page and get the layout right. Fuzzy, badly-lit, inconsistently white-balanced. Embarrassing.&lt;/p&gt;
&lt;p&gt;But it&amp;rsquo;s still an image, which can help explain something or give context, or just keep some people interested. It&amp;rsquo;s not set in stone, I can replace it with a better photo later on.&lt;/p&gt;
&lt;p&gt;I have published those unfinished articles. With the bad photos. Enjoy the spelling mistakes, ill-hewn thoughts, and crappy pics.&lt;/p&gt;
&lt;h2 id=&#34;so-just-stop-caring&#34;&gt;So Just Stop Caring?&lt;/h2&gt;
&lt;p&gt;No. None of this is an argument for not caring or being lazy. Details do matter, as do presentation and consistency. They add up to the perception of quality. I also want to feel good about things I make and put out into the world, and part of that is needing to know that I did my best.&lt;/p&gt;
&lt;p&gt;But, aesthetically, overwrought objects are less pleasing. Sometimes, the last 10 hours you spent on fiddling with tiny details aren&amp;rsquo;t going to be appreciated by most people, and that time could have been spent fixing more important problems, or starting the next project.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Zoom out&lt;/em&gt;. Knowing when to let go is also a virtue.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Building Apps and Getting Notified</title>
      <link>https://mm-dev.rocks/posts/my-fdroid-build-setup/building-apps-and-getting-notified/</link>
      <pubDate>Tue, 30 Jul 2024 17:11:49 +0100</pubDate>
      <guid>https://mm-dev.rocks/posts/my-fdroid-build-setup/building-apps-and-getting-notified/</guid>
      <description>&lt;p&gt;If you&amp;rsquo;ve followed along with all of the articles in this series you&amp;rsquo;ll now have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A complete F-Droid build environment set up in a container&lt;/li&gt;
&lt;li&gt;A Telegram bot raring to go&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now we&amp;rsquo;ll put it all together, add some bash scripts to make everything easy to work with, and see how to get notified in Telegram when the local F-Droid build process has finished.&lt;/p&gt;
&lt;p&gt;We use a couple of shell scripts to facilitate this, they&amp;rsquo;re available &lt;a href = &#34;https://codeberg.org/mm-dev/fdroid-shell-scripts&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;here in my Codeberg repo&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll refer to the Alpine container as the host as all of these instructions are based on that environment, and from that context the F-Droid docker container will be the guest.&lt;/p&gt;
&lt;h3 id=&#34;alpine-container-directory-structure&#34;&gt;Alpine container directory structure&lt;/h3&gt;
&lt;p&gt;So far in our Alpine container we should have a couple of directories which are our local clones of the F-Droid repos &lt;code&gt;fdroiddata&lt;/code&gt; and &lt;code&gt;fdroidserver&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We also need to add a directory where we&amp;rsquo;ll store our shell scripts:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;mkdir shell-scripts
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So now we should have the following structure (I named my Alpine container &lt;code&gt;fdroid&lt;/code&gt;):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;fdroid:~$ ls -1
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;fdroiddata
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;fdroidserver
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;shell-scripts
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;the-scripts&#34;&gt;The scripts&lt;/h2&gt;
&lt;p&gt;In &lt;code&gt;~/shell-scripts&lt;/code&gt; we&amp;rsquo;ll have two scripts. Don&amp;rsquo;t forget to make sure they&amp;rsquo;re executable with &lt;code&gt;chmod +x&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&#34;docker-run-fdroidserversh&#34;&gt;docker-run-fdroidserver.sh&lt;/h3&gt;
&lt;p&gt;Run this on the Alpine host to start the container.&lt;/p&gt;
&lt;p&gt;This is really just a &lt;code&gt;docker run&lt;/code&gt; command, based on the example given in the F-Droid docs, then heavily commented for people like me who aren&amp;rsquo;t experts in docker and need to be reminded of the basics.&lt;/p&gt;
&lt;p&gt;Here is the script:&lt;/p&gt;

    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;#!/bin/sh
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Based on F-Droid&amp;#39;s guide at https://f-droid.org/en/docs/Submitting_to_F-Droid_Quick_Start_Guide/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# The only amendment to this script from the above URL are:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# - Mount an additional volume, the `shell-scripts` directory (parent directory of this script and `fdroid-prepare-env.sh`)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# - Change the entry point to `fdroid-prepare-env.sh`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# - Pass a couple of environment variables, IDs to be used to interact with Telegram so we can get a message pushed when a process finishes&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Explanation of command:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# `run`		creates a container from the image (image is given as final argument to the command)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# --rm 		when the container is exited, automatically remove it (the container, not the image)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# -i		`--interactive`, meaning you will be put into a `/bin/bash` (from the later command) session in the container&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# -t		`--tty` container output gets sent to a virtual tty, meaning it will be formatted nicely and behave in specific expected ways&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# -u		`--user` so in this case we are running as user &amp;#39;vagrant&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# --entrypoint	overrides the default ENTRYPOINT of the image&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# -v		mount a volume, the arg:arg format mounts a local (1st arg) volume inside the container at (2nd arg)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# 		:z at the end means Docker labels the content with a shared content label. Shared volume labels allow all containers to read/write content&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# 		:Z at the end means Docker to label the content with a private unshared label. Only the current container can use a private volume&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# -e		environment variable to be passed&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# [final arg]	the image from which the container will be created&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Notes on setting up credentials for Telegram bot&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Based on tips from https://www.techrepublic.com/article/how-to-safely-store-passwords-linux-server/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# sudo apk add pass gnupg gnupg2 pinentry-tty&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# gpg2 --full-generate-key&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# pass init m@mm-dev.rocks&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# pass insert telegrambot/botid&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# pass insert telegrambot/chatid&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# gpg-connect-agent reloadagent /bye&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo docker run --rm -itu vagrant --entrypoint /home/vagrant/shell-scripts/fdroid-prepare-env.sh &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;  -v ~/fdroiddata:/build:z &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;  -v ~/fdroidserver:/home/vagrant/fdroidserver:Z &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;  -v ~/shell-scripts:/home/vagrant/shell-scripts:Z &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;  -e &lt;span class=&#34;nv&#34;&gt;TELEGRAM_BOTID&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;$(&lt;/span&gt;pass telegrambot/botid&lt;span class=&#34;k&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;  -e &lt;span class=&#34;nv&#34;&gt;TELEGRAM_CHATID&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;$(&lt;/span&gt;pass telegrambot/chatid&lt;span class=&#34;k&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;  registry.gitlab.com/fdroid/fdroidserver:buildserver
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
    &lt;p class=&#34;live-code-embed chroma&#34;&gt;&lt;span&gt;From&lt;/span&gt;&lt;a href=&#34;https://codeberg.org/mm-dev/fdroid-shell-scripts/raw/branch/master/docker-run-fdroidserver.sh&#34;&gt;https://codeberg.org/mm-dev/fdroid-shell-scripts/raw/branch/master/docker-run-fdroidserver.sh&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In addition to the F-Droid original functionality, this version does a few other important things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pass in, as environment variables, some credentials which are required to access a Telegram bot via the Telegram API
&lt;ul&gt;
&lt;li&gt;API token for the bot&lt;/li&gt;
&lt;li&gt;An ID for the chat channel that the message will be delivered to&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Mount our &lt;code&gt;~/shell-scripts&lt;/code&gt; directory in the container so that the &lt;code&gt;fdroid-prepare-env.sh&lt;/code&gt; script is available to it&lt;/li&gt;
&lt;li&gt;Set the &lt;code&gt;fdroid-prepare-env.sh&lt;/code&gt; script as the entry point for the container so that it automatically runs&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;As explained earlier in this series of articles, the Telegram credentials are stored on the host via GPG passwords and passed to the container as environment variables.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This means the credentials will be accessible in the clear to the container user so if this isn&amp;rsquo;t secure enough for your needs you should use another method.&lt;/strong&gt;&lt;/p&gt;&lt;/blockquote&gt;
&lt;h3 id=&#34;fdroid-prepare-envsh&#34;&gt;fdroid-prepare-env.sh&lt;/h3&gt;
&lt;p&gt;This script is run inside the container to finish setting it up.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fdroid-prepare-env.sh&lt;/code&gt; is stored on the Alpine host in the &lt;code&gt;~/shell-scripts&lt;/code&gt; directory&lt;/li&gt;
&lt;li&gt;But that &lt;code&gt;~/shell-scripts&lt;/code&gt; directory is mounted as a volume inside the container as &lt;code&gt;/home/vagrant/shell-scripts&lt;/code&gt;&lt;br&gt;
&lt;em&gt;&lt;code&gt;vagrant&lt;/code&gt; is the name of the user inside the container&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;So the script is accessible from inside the container&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As it is passed (by the &lt;code&gt;docker-run-fdroidserver.sh&lt;/code&gt; script) to the &lt;code&gt;docker run&lt;/code&gt; command as the entry point, it gets run automatically when the container starts.&lt;/p&gt;
&lt;p&gt;The full script is below:&lt;/p&gt;

    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# #################################&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# F-Droid build tools helper script&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# #################################&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Docker image housekeeping, stuff to be done whenever the F-Droid container is creaated.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Based on F-Droid&amp;#39;s guide at https://f-droid.org/en/docs/Submitting_to_F-Droid_Quick_Start_Guide/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# - Several commands need to be run inside the container every time it starts, meaning constandly having to refer to the above URL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# - Instead, now this script is passed as the entry point to `docker run`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# - Alongside the F-Droid recommendations, export a function `send_tg_alert`, which allows a Telegram message to be sent when a command has completed execution&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Bring in system-wide settings and add some environment variables&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# #################################&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;. /etc/profile
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;export&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;PATH&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$fdroidserver&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$PATH&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;PYTHONPATH&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$fdroidserver&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;export&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;JAVA_HOME&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;$(&lt;/span&gt;java -XshowSettings:properties -version 2&amp;gt;&lt;span class=&#34;p&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;1&lt;/span&gt; &amp;gt; /dev/null &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; grep &lt;span class=&#34;s1&#34;&gt;&amp;#39;java.home&amp;#39;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; awk -F&lt;span class=&#34;s1&#34;&gt;&amp;#39;=&amp;#39;&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;{print $2}&amp;#39;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; tr -d &lt;span class=&#34;s1&#34;&gt;&amp;#39; &amp;#39;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Create the message text to be sent to Telegram&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# #################################&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# - The message contains the shell command (from history) last entered (ie the command that was run and we are informing the recipient about)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# - Use MarkdownV2 syntax, see notes at https://core.telegram.org/bots/api#markdownv2-style&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# - Use heredoc style to create the message and allow sending of newlines to survive the following pipeline:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;#     bash | curl (GET, urlencode) | Telegram API&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;function&lt;/span&gt; get_msg_text &lt;span class=&#34;o&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;c1&#34;&gt;# Enable history, as it&amp;#39;s not usually available from within bash scripts&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nb&#34;&gt;set&lt;/span&gt; -o &lt;span class=&#34;nb&#34;&gt;history&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	cat &lt;span class=&#34;s&#34;&gt;&amp;lt;&amp;lt;EOF
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;\`\`\`
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;# F\\-Droid command finished at:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;# $(date)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;# Command as entered:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;\$ $(history | cut -c 8-)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;\`\`\`
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;EOF&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;o&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Send a Telegram message via a bot&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# #################################&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;function&lt;/span&gt; send_tg_alert &lt;span class=&#34;o&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;c1&#34;&gt;# Use heredoc style to create $MSG_TXT and allow sending of newlines to survive:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;c1&#34;&gt;# bash | curl (GET, urlencode) | Telegram API&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nv&#34;&gt;MSG_TXT&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;$(&lt;/span&gt;get_msg_text&lt;span class=&#34;k&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$MSG_TXT&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;c1&#34;&gt;# $TELEGRAM_CHATID and $TELEGRAM_BOTID should already exist in this container as environment variables,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;c1&#34;&gt;# having been passed in via the `docker run` command&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	curl &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;		--get &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;		--data-urlencode &lt;span class=&#34;s2&#34;&gt;&amp;#34;chat_id=&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;TELEGRAM_CHATID&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;		--data-urlencode &lt;span class=&#34;s2&#34;&gt;&amp;#34;parse_mode=MarkdownV2&amp;#34;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;		--data-urlencode &lt;span class=&#34;s2&#34;&gt;&amp;#34;text=&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;MSG_TXT&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;		https://api.telegram.org/bot&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;TELEGRAM_BOTID&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;/sendMessage
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;o&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Make functions available to the container and shell&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# #################################&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# If functions calls other functions, all functions in the chain need to be exported&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;export&lt;/span&gt; -f send_tg_alert
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;export&lt;/span&gt; -f get_msg_text
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Finish up...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# #################################&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Show some helpful reminders&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;cat &lt;span class=&#34;s&#34;&gt;&amp;lt;&amp;lt; EOF
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;Example commands:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;fdroid readmeta
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;fdroid rewritemeta com.example
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;fdroid checkupdates --allow-dirty com.example
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;fdroid lint com.example
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;fdroid build com.example
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;To send a Telegram message after the command has completed, append &amp;#39;; send_tg_alert&amp;#39;, eg:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;fdroid build com.example ; send_tg_alert
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s&#34;&gt;EOF&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Enter the directory which is most likely to be useful when the F-Droid commands are run&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;cd&lt;/span&gt; /build
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# By default, the container would exit at the end of this ENTRYPOINT script --- start a shell instead.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;/bin/bash &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$@&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
    &lt;p class=&#34;live-code-embed chroma&#34;&gt;&lt;span&gt;From&lt;/span&gt;&lt;a href=&#34;https://codeberg.org/mm-dev/fdroid-shell-scripts/raw/branch/master/fdroid-prepare-env.sh&#34;&gt;https://codeberg.org/mm-dev/fdroid-shell-scripts/raw/branch/master/fdroid-prepare-env.sh&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The effects of running the script in the container are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sets up some environment variables as per F-Droid&amp;rsquo;s instructions&lt;/li&gt;
&lt;li&gt;Creates and exports our &lt;code&gt;send_tg_alert&lt;/code&gt; function which can be called to send a message to a Telegram bot when a shell command has finished&lt;/li&gt;
&lt;li&gt;The notification message sent by the function will include the command which was run/finished&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Telegram messaging is useful because the F-Droid build command can take a long time to complete. The function is just appended as an extra command eg:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Build the &amp;#39;com.example.appname&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# then send a Telegram message&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;fdroid build com.example.appname &lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; send_tg_alert
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The F-Droid command will run, and when it ends a Telegram message will be sent to the specified channel, saying something like:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;# F-Droid command finished at:&lt;br&gt;
# Mon Jul 29 17:34:42 UTC 2024&lt;/p&gt;
&lt;p&gt;# Command as entered:&lt;br&gt;
$ fdroid build com.example.appname ; send_tg_alert&lt;/p&gt;&lt;/blockquote&gt;
&lt;h2 id=&#34;the-f-droid-build-commands&#34;&gt;The F-Droid build commands&lt;/h2&gt;
&lt;p&gt;Now from inside the docker container (the entry point script should have put us inside the &lt;code&gt;/build&lt;/code&gt; directory already) we can run the fdroid commands.&lt;/p&gt;
&lt;p&gt;You can run &lt;code&gt;fdroid --help&lt;/code&gt; to get a list of subcommands such as:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Build a package from source&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;fdroid build
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Read all the metadata files and exit&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;fdroid readmeta
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Rewrite all the metadata files&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;fdroid rewritemeta
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Warn about possible errors in the metadata&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;fdroid lint
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For each command you can append &lt;code&gt;--help&lt;/code&gt; for more details, eg &lt;code&gt;fdroid build --help&lt;/code&gt;, but check the &lt;a href = &#34;https://f-droid.org/en/docs/Building_Applications/&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;official documentation&lt;/a&gt; for full details.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Creating a Telegram Bot</title>
      <link>https://mm-dev.rocks/posts/my-fdroid-build-setup/creating-a-telegram-bot/</link>
      <pubDate>Tue, 30 Jul 2024 17:11:44 +0100</pubDate>
      <guid>https://mm-dev.rocks/posts/my-fdroid-build-setup/creating-a-telegram-bot/</guid>
      <description>&lt;h3 id=&#34;create-the-bot&#34;&gt;Create the bot&lt;/h3&gt;
&lt;p&gt;This is done via the Telegram app, so we need to open that up (if you don&amp;rsquo;t have it installed yet you&amp;rsquo;ll need to do that first).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use search (magnifying glass icon) to find the bot called &amp;lsquo;BotFather&amp;rsquo;. This is an official Telegram bot, used for setting up new bots. Tap it to start a chat with it.&lt;/li&gt;
&lt;li&gt;In the chat with &amp;lsquo;BotFather&amp;rsquo;, use the &lt;code&gt;/newbot&lt;/code&gt; command, this is just a matter of typing the command as a message just like you&amp;rsquo;d send a message to a person&lt;br&gt;
&lt;em&gt;The slash is part of the command so make sure you include it&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Enter a name for your bot, this is the name which will be displayed in chats&lt;/li&gt;
&lt;li&gt;Enter a username for the bot, this is for Telegram&amp;rsquo;s use and must end in &amp;lsquo;_bot&amp;rsquo; (the prompts will tell you this)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This creates the bot, then you will receive a message with some info about your new bot. The main thing you need here is the new bot&amp;rsquo;s token for accessing the HTTP API, this will be a long string something like &lt;code&gt;7901112608:AAD9fDpa9kgw3lZ10o6qANkXnB_D2ZvwuPc&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;This is your BOT TOKEN, keep a note of it as you need it to do stuff with your bot&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;check-that-everything-is-working-so-far&#34;&gt;Check that everything is working so far&amp;hellip;&lt;/h3&gt;
&lt;p&gt;In a browser, visit a special URL including your bot token (hitting it in a browser like this is one simple way of accessing the API).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The format is &lt;code&gt;https://api.telegram.org/bot&amp;lt;BOT_TOKEN&amp;gt;/getMe&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;So using the example ID above &lt;code&gt;https://api.telegram.org/bot7901112608:AAD9fDpa9kgw3lZ10o6qANkXnB_D2ZvwuPc/getMe&lt;/code&gt;&lt;br&gt;
&lt;em&gt;Make sure the &lt;code&gt;bot&lt;/code&gt; part is there just before the token as it&amp;rsquo;s an essential part of the URL&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;You should see a page in JSON format with some basic data about your new bot&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;create-a-channel&#34;&gt;Create a channel&lt;/h3&gt;
&lt;p&gt;To interact with the bot (eg to receive messages from it), you need to create a channel.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In Telegram, tap the pencil icon&lt;/li&gt;
&lt;li&gt;The &amp;lsquo;New Message&amp;rsquo; dialog will appear, tap &amp;lsquo;New Channel&amp;rsquo;&lt;/li&gt;
&lt;li&gt;Give the channel a name (and description if you wish)&lt;/li&gt;
&lt;li&gt;Choose whether it should be public or private&lt;/li&gt;
&lt;li&gt;Choose subscribers &amp;mdash; &lt;em&gt;here you need to add your new bot&lt;/em&gt;. If you can&amp;rsquo;t see it in the list, tap the blank area at the top to search and type part of its name, it should appear.&lt;br&gt;
&lt;em&gt;The list of subscribers can be edited at any time by tapping its icon in Telegram&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now you can use the API again to get the ID for the channel (or &amp;lsquo;chat&amp;rsquo; as it is referred to). The URL is similar to the one you used above to get the token ID, but the last part changes from &lt;code&gt;getMe&lt;/code&gt; to &lt;code&gt;getUpdates&lt;/code&gt;, so &lt;code&gt;https://api.telegram.org/bot&amp;lt;BOT_TOKEN&amp;gt;/getUpdates&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Like before, you will see some JSON data. You are looking for a &lt;code&gt;channel_post.chat.id&lt;/code&gt;, something like &lt;code&gt;-1002179830041&lt;/code&gt; &lt;em&gt;the dash is part of the ID&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;This is your CHAT ID, keep a note of this too as it will allow your bot to send messages to this chat/channel&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;test-your-bot&#34;&gt;Test your bot&lt;/h3&gt;
&lt;p&gt;Now you have what you need to send automated messages via your bot. It&amp;rsquo;s as simple as:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Send a message from a Telegram bot to a specific chat/channel&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;curl &lt;span class=&#34;s1&#34;&gt;&amp;#39;https://api.telegram.org/bot&amp;lt;BOT_TOKEN&amp;gt;/sendMessage?chat_id=&amp;lt;CHAT_ID&amp;gt;&amp;amp;text=&amp;lt;YOUR_MESSAGE&amp;gt;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description>
    </item>
    
    <item>
      <title>Storing Secret Credentials</title>
      <link>https://mm-dev.rocks/posts/my-fdroid-build-setup/storing-secret-credentials/</link>
      <pubDate>Tue, 30 Jul 2024 17:11:44 +0100</pubDate>
      <guid>https://mm-dev.rocks/posts/my-fdroid-build-setup/storing-secret-credentials/</guid>
      <description>&lt;p&gt;If you&amp;rsquo;ve already set up a Telegram bot and added it to a channel, you only need 2 things to be able to write code that uses that bot:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The BOT TOKEN - when you create a bot on Telegram you are given this&lt;/li&gt;
&lt;li&gt;A CHAT ID - this points to a specific chat/channel on Telegram&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These bits of info are used to send messages via the Telegram API. Anybody who has them can use your bot, so you want them to be secret, meaning you won&amp;rsquo;t want to hard-code them in any scripts.&lt;/p&gt;
&lt;h2 id=&#34;storing-bot-credentials-using-gpg&#34;&gt;Storing bot credentials using GPG&lt;/h2&gt;
&lt;p&gt;&lt;a href = &#34;https://gnupg.org/&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;GNU Privacy Guard&lt;/a&gt; is a set of popular command line tools for working with encrypted secrets. We&amp;rsquo;ll use this to manage our Telegram IDs/keys.&lt;/p&gt;
&lt;h3 id=&#34;first-make-sure-all-the-dependencies-are-installed&#34;&gt;First, make sure all the dependencies are installed&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Some distros might use gnupg, some have gnupg2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# some have both&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo apk add pass gnupg gnupg2 pinentry-tty
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;set-up-your-user-ready-for-key-storage&#34;&gt;Set up your user ready for key storage&lt;/h3&gt;
&lt;p&gt;Run the following commands and choose your options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The default options are usually fine, but check out &lt;code&gt;man pass&lt;/code&gt; to learn more&lt;/li&gt;
&lt;li&gt;Use any email address you want, it doesn&amp;rsquo;t have to be real, it will be used as an ID to work with your keystore&lt;br&gt;
&lt;em&gt;Keep a note of whatever address you used&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;gpg2 --full-generate-key
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;pass init &amp;lt;YOUR_EMAIL&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;store-your-secrets&#34;&gt;Store your secrets&lt;/h3&gt;
&lt;p&gt;Use &lt;code&gt;pass insert&lt;/code&gt; and give it an argument which you&amp;rsquo;ll use like a label for your secrets so you can edit/delete/retrieve them. You can organise them by using slashes so you can create something like a path or namespace for related secrets.&lt;/p&gt;
&lt;p&gt;You will be prompted to enter the secret you want to store, then again to confirm it.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# I want to store a couple of IDs related to a Telegram bot,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# so start both with the same `telegrambot/` string&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;pass insert telegrambot/bottoken
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;pass insert telegrambot/chatid
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;retrieve-your-secrets&#34;&gt;Retrieve your secrets&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;pass telegrambot/bottoken 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Output eg &amp;gt; t3l3GRAMbotIDiJustEnT3red&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;pass telegrambot/chatid 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Output eg &amp;gt; teleGR4MchatIDiJustEnT3red&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# So eg to set environment variables&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# !WARNING ANYONE WHO CAN READ YOUR VARIABLES&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# WILL BE ABLE TO READ THE SECRET!&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;export&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;TELEGRAM_BOT_ID&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;$(&lt;/span&gt;pass telegrambot/bottoken&lt;span class=&#34;k&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;to-let-go-of-your-cached-password&#34;&gt;To let go of your cached password&lt;/h3&gt;
&lt;p&gt;After you enter your password GPG hangs on to it for a while, which can make testing difficult while you are getting things set up. If you enter the command below it will be cleaned up and you&amp;rsquo;ll be prompted to enter your password again.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;gpg-connect-agent reloadagent /bye
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description>
    </item>
    
    <item>
      <title>Setting Up the LXC Container</title>
      <link>https://mm-dev.rocks/posts/my-fdroid-build-setup/setting-up-the-lxc-container/</link>
      <pubDate>Tue, 30 Jul 2024 17:11:22 +0100</pubDate>
      <guid>https://mm-dev.rocks/posts/my-fdroid-build-setup/setting-up-the-lxc-container/</guid>
      <description>&lt;p&gt;I&amp;rsquo;m using &lt;a href = &#34;https://linuxcontainers.org/&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;LXC containers&lt;/a&gt; on &lt;a href = &#34;https://www.proxmox.com/en/&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;Proxmox&lt;/a&gt;. If you have a different method of working with containers I&amp;rsquo;m pretty sure you&amp;rsquo;ll be able to use the info here and repurpose it to your preferences.&lt;/p&gt;
&lt;h3 id=&#34;create-an-alpine-container&#34;&gt;Create an Alpine container&lt;/h3&gt;
&lt;p&gt;I won&amp;rsquo;t go into the details of how to do this as it&amp;rsquo;s specific to Proxmox and no different to setting up any other LXC container in Proxmox.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m using &lt;a href = &#34;https://alpinelinux.org/&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;Alpine Linux&lt;/a&gt; as the container distro as it&amp;rsquo;s very lightweight and the F-Droid tools don&amp;rsquo;t need anything fancy.&lt;/p&gt;
&lt;p&gt;&lt;del&gt;In terms of resources, it doesn&amp;rsquo;t need much, the following has been more than enough for me so far:&lt;/del&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;del&gt;512MB RAM&lt;/del&gt;&lt;/li&gt;
&lt;li&gt;&lt;del&gt;512MB swap&lt;/del&gt;&lt;/li&gt;
&lt;li&gt;&lt;del&gt;8GB disk&lt;/del&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;How naive of me, Gradle is much more greedy than I originally thought and I was getting build errors due to low resources. So I set the container up with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;6GB RAM&lt;/li&gt;
&lt;li&gt;1GB swap&lt;/li&gt;
&lt;li&gt;16GB disk&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;set-up-the-container-for-f-droid-builds&#34;&gt;Set up the container for F-Droid builds&lt;/h3&gt;
&lt;p&gt;While logged into my Alpine container, first I set up some basics:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Ensure everything is up to date&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;apk update
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;apk upgrade
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Install and configure git&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;apk add git vim
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;git config --global user.name &lt;span class=&#34;s2&#34;&gt;&amp;#34;&amp;lt;YOUR_NAME&amp;gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;git config --global user.email &lt;span class=&#34;s2&#34;&gt;&amp;#34;&amp;lt;YOUR_EMAIL&amp;gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;ssh-keygen
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;cat .ssh/id_rsa.pub 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# ... then copy key to gitlab SSH keys&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;F-Droid build instructions depend on docker, so:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;apk add docker
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Start docker on boot&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;rc-update add docker boot
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Start docker now (or reboot so the above command can take effect)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;service docker start
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now get the F-Droid stuff installed:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;git clone --depth&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;1&lt;/span&gt; git@gitlab.com:&amp;lt;YOUR_ACCOUNT&amp;gt;/fdroiddata.git ~/fdroiddata
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;cd&lt;/span&gt; ~/fdroiddata
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;git checkout -b com.example
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;cp templates/build-gradle.yml metadata/com.example.yml
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# If (like me) you already created a fork before it&amp;#39;s a little different &lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# and you have to do this instead of the above&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# git clone --depth=1 git@gitlab.com:&amp;lt;YOUR_ACCOUNT&amp;gt;/fdroiddata.git ~/fdroiddata&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# cd fdroiddata/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Shallow cloning makes other branches inaccessible, fix that&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# git remote set-branches origin &amp;#39;*&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# git fetch -v --depth=1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# And now the branch can be checked out&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# git checkout com.example&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The container is set up now, later on on we&amp;rsquo;ll get around to how to actually use it&amp;hellip;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>The F-droid Build Process</title>
      <link>https://mm-dev.rocks/posts/my-fdroid-build-setup/the-fdroid-build-process/</link>
      <pubDate>Tue, 30 Jul 2024 17:11:22 +0100</pubDate>
      <guid>https://mm-dev.rocks/posts/my-fdroid-build-setup/the-fdroid-build-process/</guid>
      <description>&lt;p&gt;There are two main git repositories involved in getting an app listed on F-Droid, &lt;code&gt;fdroiddata&lt;/code&gt; and &lt;code&gt;fdroidserver&lt;/code&gt;. You&amp;rsquo;ll need local, shallow clones (see below) of them both.&lt;/p&gt;
&lt;h2 id=&#34;the-fdroiddata-repo&#34;&gt;The &lt;code&gt;fdroiddata&lt;/code&gt; repo&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;fdroiddata&lt;/code&gt; repo stores metadata about all of the apps on F-Droid.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;On a regular basis, F-Droid servers will run through each of the apps listed in the &lt;code&gt;fdroiddata&lt;/code&gt; repo, specifically the &lt;code&gt;/fdroiddata/metadata/&lt;/code&gt; directory.&lt;/p&gt;
&lt;p&gt;The metadata of each app will be read and used as instructions to build it on F-Droid&amp;rsquo;s side. It will then be available in the F-Droid app listings.&lt;/p&gt;
&lt;p&gt;Basically, apps exist on F-Droid by having their YAML file available in this repo&amp;rsquo;s &lt;code&gt;metadata&lt;/code&gt; directory:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Irrespective of F-Droid, Android apps have an &lt;code&gt;applicationId&lt;/code&gt; in their &lt;code&gt;build.gradle&lt;/code&gt; file, usually in the form of a reverse domain name eg &lt;code&gt;com.example.appname&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The YAML file must be named with that same app ID, eg &lt;code&gt;/fdroiddata/metadata/com.example.appname.yml&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;As well as this metadata, your app can/should have extra metadata in its own repo &lt;a href = &#34;https://f-droid.org/en/docs/Submitting_to_F-Droid_Quick_Start_Guide/&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;https://f-droid.org/en/docs/Submitting_to_F-Droid_Quick_Start_Guide/&lt;/a&gt;. That is something you do inside your app alongside your own code so I haven&amp;rsquo;t gone into detail about it here as things are complicated enough&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;getting-your-apps-metadata-into-the-fdroiddata-repo&#34;&gt;Getting your app&amp;rsquo;s metadata into the &lt;code&gt;fdroiddata&lt;/code&gt; repo&lt;/h3&gt;
&lt;p&gt;F-Droid official instructions are &lt;a href = &#34;https://f-droid.org/en/docs/Submitting_to_F-Droid_Quick_Start_Guide/&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;here&lt;/a&gt; and you&amp;rsquo;ll need to refer to them for proper details.&lt;/p&gt;
&lt;p&gt;Here I just want to give a high-level view to help wrap your head around how this all works. The general process is as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;On GitLab (&lt;em&gt;you need a GitLab account if you want to submit an app to F-Droid&lt;/em&gt;), make a fork of the main&lt;code&gt;fdroiddata&lt;/code&gt; repo&lt;/li&gt;
&lt;li&gt;On your dev machine, make a git &lt;em&gt;shallow&lt;/em&gt; clone of your fork eg:&lt;br&gt;
&lt;code&gt;git clone --depth=1 https:&amp;lt;PATH_TO_YOUR_FORK&amp;gt;&lt;/code&gt;&lt;br&gt;
&lt;em&gt;A shallow clone retrieves only a small number of recent commits&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Create a new branch for your app in your local clone eg:&lt;br&gt;
&lt;code&gt;git checkout -b com.example.appname&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Locally, copy a template of the metadata file and rename it to match your app eg:&lt;br&gt;
&lt;code&gt;cp fdroiddata/templates/build-gradle.yml fdroiddata/metadata/com.example.appname.yml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href = &#34;https://f-droid.org/en/docs/Submitting_to_F-Droid_Quick_Start_Guide/&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;Edit your metadata file&lt;/a&gt; to give instructions about how to build your app&lt;/li&gt;
&lt;li&gt;Test that the app builds on your local machine (see below)&lt;/li&gt;
&lt;li&gt;Once you&amp;rsquo;re happy that everything in your metadata file is ok, create a merge request on GitLab for the main repo to pull in your branch containing your new &lt;code&gt;com.example.appname.yml&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;When that merge request has been accepted, the official &lt;code&gt;fdroiddata&lt;/code&gt; repo will contain your new metadata file, meaning next time F-Droid run their build process your app will be included and built&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;the-fdroidserver-repo&#34;&gt;The &lt;code&gt;fdroidserver&lt;/code&gt; repo&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;fdroidserver&lt;/code&gt; repo contains the build tools that F-Droid use themselves to build all of the apps that they list.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;Before submitting your merge request you want to be sure that you have created your metadata (as above) correctly and that the build process works. So you create a local copy of the &lt;code&gt;fdroidserver&lt;/code&gt; repo in order to be able to complete the build process yourself. You only build your own app (luckily &amp;mdash; building all of the apps on F-Droid would take days).&lt;/p&gt;
&lt;p&gt;Explanation of how to use the build tools comes later in this series of articles.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Intro</title>
      <link>https://mm-dev.rocks/posts/my-fdroid-build-setup/intro/</link>
      <pubDate>Tue, 30 Jul 2024 17:11:20 +0100</pubDate>
      <guid>https://mm-dev.rocks/posts/my-fdroid-build-setup/intro/</guid>
      <description>&lt;p&gt;Building apps for F-Droid is a fairly complex process, involving:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A couple of git repos (besides any your own project may have)&lt;/li&gt;
&lt;li&gt;Using docker&lt;/li&gt;
&lt;li&gt;Forking and merge requests on GitLab&lt;/li&gt;
&lt;li&gt;F-Droid&amp;rsquo;s specialised build commands&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;F-Droid gives instructions on their process &lt;a href = &#34;https://f-droid.org/en/docs/Submitting_to_F-Droid_Quick_Start_Guide/&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I managed to get my first Android app (&lt;a href = &#34;https://mm-dev.rocks/posts/bendystraw-android-app/&#34; title = &#34;BendyStraw (Android App)&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;BendyStraw&lt;/a&gt;) &lt;a href = &#34;https://f-droid.org/en/packages/rocks.mm_dev.BendyStraw/&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;published on F-Droid&lt;/a&gt; but felt a little out of my depth at times and didn&amp;rsquo;t have a clear mental model of all the moving parts.&lt;/p&gt;
&lt;p&gt;So in this series of articles I&amp;rsquo;m going to write up the process in my own words. I&amp;rsquo;ll add a couple of scripts which help reduce some repetitive steps, and as the &lt;code&gt;fdroid build&lt;/code&gt; command can take a while I&amp;rsquo;ve set things up so that I can automatically be sent a message on Telegram when the process is finished.&lt;/p&gt;
&lt;h3 id=&#34;proxmox&#34;&gt;Proxmox&lt;/h3&gt;
&lt;p&gt;My main development machine runs &lt;a href = &#34;https://www.proxmox.com&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;Proxmox&lt;/a&gt;, a special OS which just hosts virtual machines and containers. Roughly speaking, a virtual machine virtualises the hardware, whereas a container virtualises the operating system.&lt;/p&gt;
&lt;p&gt;With Proxmox the main OS is only used for managing these virtual systems. Everyday computer usage such as development work is always done inside a virtual environment, for example if I&amp;rsquo;m writing Flutter code I&amp;rsquo;ll be working inside a Debian virtual machine.&lt;/p&gt;
&lt;p&gt;I decided to make a small separate container dedicated only to building apps for F-Droid. I&amp;rsquo;ll be releasing more apps on F-Droid in future so it will be nice to have a clean environment siloed off for the job. Proxmox uses LXC containers and I chose to use an Alpine Linux template to get me started because Alpine is very lean.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Vanilla JS</title>
      <link>https://mm-dev.rocks/posts/vanilla-js/</link>
      <pubDate>Fri, 05 Apr 2024 12:18:03 +0100</pubDate>
      <guid>https://mm-dev.rocks/posts/vanilla-js/</guid>
      <description>&lt;h2 id=&#34;toolchain&#34;&gt;Toolchain&lt;/h2&gt;
&lt;p&gt;I enjoy using plain/vanilla JS where possible. For my own projects I try to avoid the sprawl and long build times of NPM.&lt;/p&gt;
&lt;p&gt;I do like to split my code up into separate modules though, and to be able to benefit from bundling/minification etc, for which I use &lt;em&gt;esbuild&lt;/em&gt;. I also want to be able to use &lt;em&gt;SCSS&lt;/em&gt; and &lt;em&gt;JSDoc&lt;/em&gt; for type annotations. So I do allow myself some minimal tooling (the cone and Flake).&lt;/p&gt;
&lt;p&gt;My setup looks like this&amp;hellip;&lt;/p&gt;
&lt;h3 id=&#34;esbuild---download-a-build&#34;&gt;esbuild - &lt;a href = &#34;https://esbuild.github.io/getting-started/#download-a-build&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;Download a build&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;JS bundler&lt;br&gt;
&lt;em&gt;Combine separate modules into a single JS file, minifying/compressing if required&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Tree shaker&lt;br&gt;
&lt;em&gt;Avoid bloat by making sure only those modules/functions which are used get added&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I use esbuild for most JS projects, so I installed a standalone build which can be run from anywhere &amp;mdash; but it can also be installed using npm or other methods.&lt;/p&gt;
&lt;h3 id=&#34;sass---how-to-install-sass&#34;&gt;SASS - &lt;a href = &#34;https://sass-lang.com/install&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;How to Install SASS&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Compiler and bundler&lt;br&gt;
&lt;em&gt;Converts SASS/SCSS files into CSS&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;jsdoc---documentation&#34;&gt;JSDoc - &lt;a href = &#34;https://jsdoc.app/&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;Documentation&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Automated documentation creation&lt;br&gt;
&lt;em&gt;HTML docs for your methods, classes and variables will be created based on your code comments&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Type annotations&lt;br&gt;
&lt;em&gt;Comments in your code can tell JSDoc which data types your code expects&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Type annotations you provide (in specially-formatted comments) will be used to automatically create detailed documentation for your project.&lt;/p&gt;
&lt;p&gt;Docs are created as HTML files, with lots of clickable cross-references. The files can be styled based on a templating system and CSS styling.&lt;/p&gt;
&lt;p&gt;Lots of modern editors/IDEs will also use your type annotations to provide type hints and draw attention to potential problems eg if you try to pass an argument of the wrong type you&amp;rsquo;ll get a warning.&lt;/p&gt;
&lt;h3 id=&#34;example-of-project-setup-using-tmuxvim&#34;&gt;Example of Project Setup Using Tmux/Vim&lt;/h3&gt;
&lt;p&gt;I usually write a simple bash script for most projects. I&amp;rsquo;ll run this script when I want to start a session of working on a specific project.&lt;/p&gt;
&lt;p&gt;Below is an example build script (for &lt;a href = &#34;https://mm-dev.rocks/posts/pipe-dream-web-game/&#34; title = &#34;Pipe Dream&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;my &amp;lsquo;Pipe Dream&amp;rsquo; game&lt;/a&gt;) which does the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Enters the project directory&lt;/li&gt;
&lt;li&gt;Uses &lt;code&gt;tmux&lt;/code&gt; to open a few window panes/splits:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;esbuild&lt;/strong&gt; watching for changes to JS&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SASS&lt;/strong&gt; watching for changes to CSS&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JSDoc&lt;/strong&gt; watching the &lt;code&gt;src&lt;/code&gt; directory (in reality I&amp;rsquo;d often skip this and run it manually on-demand, as it can be slow)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;git&lt;/strong&gt; status, pane left open for commits etc&lt;/li&gt;
&lt;li&gt;A simple &lt;strong&gt;Python server&lt;/strong&gt; so I can test the app in a browser&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Opens a &lt;strong&gt;vim&lt;/strong&gt; session with the relevant source files ready for editing&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Open a JS project using esbuild, SASS and vim&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;work_dir&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$HOME&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;/pipe-dream/
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;tmux&amp;#39;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;    new-window -c &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$work_dir&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;esbuild src/main.js --outfile=www/js/pipe-dream.js --target=es6 --bundle --minify --sourcemap --global-name=PIPEDREAM --format=iife --watch&amp;#34;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;    split-window -c &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$work_dir&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;sass --watch src/scss/:www/css/&amp;#34;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;    split-window -c &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$work_dir&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;jsdoc src/ -c jsdoc.config&amp;#34;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;    split-window -c &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$work_dir&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;git status; bash -i&amp;#34;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;    split-window -c &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$work_dir&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;python -m http.server --directory www/&amp;#34;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;    &lt;span class=&#34;k&#34;&gt;select&lt;/span&gt;-layout even-vertical &lt;span class=&#34;se&#34;&gt;\;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;    new-window -c &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$work_dir&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;vim -S vim.Session&amp;#34;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;vim-configuration&#34;&gt;Vim Configuration&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m one of those Vim people. People say its codebase is bloated but in use it always feels crisp and fast to me. Because of the way you manipulate text objects and leap around the page it somehow feels efficient on low-powered/slow devices, and works particularly well with &lt;a href = &#34;https://mm-dev.rocks/posts/oled-and-eink-4-life/intro/&#34; title = &#34;OLED and E-Ink&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;e-ink devices&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Some people argue that they&amp;rsquo;re faster while using Vim than they are in other code editors. I&amp;rsquo;m not sure if I&amp;rsquo;m faster (I might be), but I&amp;rsquo;m from the school of thought that if your bottleneck when writing code is your typing speed, that might not be a good thing.&lt;/p&gt;
&lt;p&gt;More importantly for me, I like the way Vim makes you think about text, as beautifully described in this classic stackoverflow answer &amp;lsquo;&lt;a href = &#34;https://stackoverflow.com/questions/1218390/what-is-your-most-productive-shortcut-with-vim/1220118#1220118&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;Your problem with Vim is that you don&amp;rsquo;t grok vi&lt;/a&gt;&amp;rsquo;.&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t like my coding environment to be too noisy or intrusive, but there are some powerful IDE features available nowadays and I don&amp;rsquo;t want to cut off my nose to spite my face.&lt;/p&gt;
&lt;p&gt;Below is some info on some Vim plugins I use to help with JS development.&lt;/p&gt;
&lt;h3 id=&#34;coc-nvim---code-of-conquer-project-repo&#34;&gt;coc-nvim - &lt;a href = &#34;https://github.com/neoclide/coc.nvim&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;Code of Conquer project repo&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Although the &amp;rsquo;nvim&amp;rsquo; stands for Neovim (the popular Vim fork), COC also works very well in modern versions of normal Vim. This plugin allows VSCode-type IDE features, being based on the &lt;a href = &#34;https://en.wikipedia.org/wiki/Language_Server_Protocol&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;Language Server Protocol&lt;/a&gt; standard.&lt;/p&gt;
&lt;p&gt;Very simply, a language server is a tool which understands a specific language, and can be used by your editor/IDE to analyse and give feedback on your code as you write it. It can do things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Syntax highlighting&lt;/li&gt;
&lt;li&gt;Code completion&lt;/li&gt;
&lt;li&gt;Draw attention to errors and give warnings about code&lt;/li&gt;
&lt;li&gt;Help when refactoring code&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;COC isn&amp;rsquo;t a language server itself but it allows you to install and maintain whichever servers you need for the languages you use.&lt;/p&gt;
&lt;p&gt;For example the language server I use for JS is &lt;a href = &#34;https://github.com/neoclide/coc-tsserver&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;coc-tsserver&lt;/a&gt;. The &amp;rsquo;ts&amp;rsquo; stands for TypeScript but it works very well with standard JS too. Along with the benefits mentioned above, this language server allows you to use type-checking, like TypeScript but without the compilation step. It also provides documentation and hints on using standard built-in JS methods.&lt;/p&gt;
&lt;p&gt;You annotate your code by providing special comments in &lt;em&gt;JSDoc&lt;/em&gt; syntax. When these comments/annotations are present, COC and the language server give you lots of hints while writing code, and warn you if you try to provide arguments of the wrong type.&lt;/p&gt;
&lt;p&gt;Here is an example (from the &lt;a href = &#34;https://jsdoc.app/howto-es2015-classes&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;official docs&lt;/a&gt;) showing what the &lt;em&gt;JSDoc&lt;/em&gt; syntax looks like:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-JavaScript&#34; data-lang=&#34;JavaScript&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt;/** Class representing a point. */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kr&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;Point&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;cm&#34;&gt;/**
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt;     * Create a point.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt;     * @param {number} x - The x value.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt;     * @param {number} y - The y value.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt;     */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nx&#34;&gt;constructor&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;x&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;y&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;cm&#34;&gt;/**
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt;     * Get the x value.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt;     * @return {number} The x value.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt;     */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nx&#34;&gt;getX&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;cm&#34;&gt;/**
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt;     * Get the y value.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt;     * @return {number} The y value.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt;     */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nx&#34;&gt;getY&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;cm&#34;&gt;/**
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt;     * Convert a string containing two comma-separated numbers into a point.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt;     * @param {string} str - The string containing two comma-separated numbers.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt;     * @return {Point} A Point object.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt;     */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kr&#34;&gt;static&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;fromString&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;str&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;em&gt;Sometimes the help is a little noisy though&amp;hellip;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I love the functionality, but for me it&amp;rsquo;s a bit over-eager, eg if I&amp;rsquo;m half-way through typing a word I don&amp;rsquo;t need a popup telling me the word doesn&amp;rsquo;t exist.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;coc-nvim&lt;/em&gt; does also provide a small symbol at the start of each line where it detects an error or wants to warn about something. So I like to leave the symbol visible, but hide the info popups by default. I add a keyboard shortcut so I can show/hide the explanation of the error/warning when I want to.&lt;/p&gt;
&lt;p&gt;To stop the popups:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-JSON&#34; data-lang=&#34;JSON&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// In ~/.vim/coc-settings.json
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;err&#34;&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;diagnostic.enableMessage&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;jump&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;err&#34;&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To add the show/hide keyboard shortcuts to Vim:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-VimL&#34; data-lang=&#34;VimL&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;&amp;#34; In ~/.vimrc&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;nmap&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;silent&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;gh&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Plug&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;coc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;float&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;hide&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;nmap&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;silent&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;gl&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Plug&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;coc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;diagnostic&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;info&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;prettier&#34;&gt;Prettier&lt;/h3&gt;
&lt;p&gt;This &lt;a href = &#34;https://prettier.io/&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;opinionated code formatter&lt;/a&gt; formats your code according to some fairly strict rules and has few configurable options (ie it&amp;rsquo;s their way or the highway).&lt;/p&gt;
&lt;p&gt;I think most people find at least some of the rules disagreeable, but the point of this kind of tool is that it does a &amp;lsquo;decent, not perfect&amp;rsquo; job, but provides a standard for formatting and removes the need for discussing, disagreement and wasted time.&lt;/p&gt;
&lt;p&gt;I found it a little annoying at first but then got over myself and prefer now to use it whenever possible.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>The Bad Parts</title>
      <link>https://mm-dev.rocks/posts/android-as-a-dev-environment/the-bad-parts/</link>
      <pubDate>Fri, 05 Apr 2024 11:44:43 +0100</pubDate>
      <guid>https://mm-dev.rocks/posts/android-as-a-dev-environment/the-bad-parts/</guid>
      <description>&lt;h3 id=&#34;the-low-memory-killer-daemon---official-docs&#34;&gt;The Low Memory Killer Daemon - &lt;a href = &#34;https://source.android.com/docs/core/perf/lmkd&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;Official Docs&lt;/a&gt;.&lt;/h3&gt;
&lt;p&gt;This feature of modern Android versions kills tasks (/apps) when it thinks they are causing memory-related performance problems. It can feel pretty heavy-handed, with apps abruptly closing at seemingly random intervals.&lt;/p&gt;
&lt;p&gt;In practice this can irritate but I&amp;rsquo;ve never lost any work because of it. The most noticeable occurrence is when &lt;em&gt;Termux:X11&lt;/em&gt; closes &amp;mdash; my Linux desktop suddenly shuts down! Sounds terrible But actually my Vim buffers are constantly auto-saved, my browser tabs are stored&amp;hellip; I just have to restart the session, then &lt;em&gt;Tmux&lt;/em&gt;, then maybe a couple of other items. I&amp;rsquo;m back up and running in 1-2 minutes, and this problem might occur once or twice per week at the most.&lt;/p&gt;
&lt;p&gt;There are various ways to tweak the daemon though, search for terms like &amp;lsquo;disable android oom killer&amp;rsquo;, &amp;rsquo;tweak low memory killer daemon android&amp;rsquo; etc.&lt;/p&gt;
&lt;h3 id=&#34;missing-tools-for-arm-architecture&#34;&gt;Missing Tools for ARM Architecture&lt;/h3&gt;
&lt;p&gt;As the types of CPU in Android devices (usually ARM) are not the same as those used in laptops/desktops (usually X86), the standard desktop versions of software can&amp;rsquo;t be installed and compatible versions must be available (unless you want to compile them yourself).&lt;/p&gt;
&lt;p&gt;This isn&amp;rsquo;t as bad as it sounds as the package manager for whichever distro you use will automatically pull in the correct version for your CPU architecture. Some software might be missing though.&lt;/p&gt;
&lt;p&gt;A couple of specific gaps in the system affect me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Garmin Monkey C&lt;br&gt;
&lt;em&gt;Garmin apps are written in their own language &amp;lsquo;Monkey C&amp;rsquo;, which has to use their own Java-based compiler and device simulator. As far as I can tell this isn&amp;rsquo;t happening on ARM. It was hard enough to get it working on Linux and Vim as they presume Windows/VSCode in their tutorials, so once I achieved that I didn&amp;rsquo;t take it any further.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Flutter cross-compilation&lt;br&gt;
&lt;em&gt;The situation with Flutter is strange. Flutter is used to make multi-platform apps, so the same code can output apps for Linux, Android, Windows etc. I can build a Linux (ARM64) binary on my Android Ubuntu&amp;hellip; and I can build an Android binary on my (normal, X86 desktop) Linux. But I can&amp;rsquo;t build an Android binary from Android. Apparently people were able to do this on older versions of Flutter, so it seems like Google (who control Flutter) intentionally stopped supporting it.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;fingers-crossed&#34;&gt;Fingers crossed!&lt;/h3&gt;
&lt;p&gt;Mine is a niche use-case and I understand that I can&amp;rsquo;t expect organisations to support such a tiny minority as ARM Linux. Apple&amp;rsquo;s M1/M2/M3 ARM chips are doing very well, which is great if you&amp;rsquo;re into that but I don&amp;rsquo;t enjoy their ecosystem.&lt;/p&gt;
&lt;p&gt;So for now I need to hang on to my X86 desktop. But I hope ARM continues to gain recognition as more than a mobile phone CPU and can be used for more development tasks in future.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Software</title>
      <link>https://mm-dev.rocks/posts/android-as-a-dev-environment/software/</link>
      <pubDate>Fri, 05 Apr 2024 11:44:41 +0100</pubDate>
      <guid>https://mm-dev.rocks/posts/android-as-a-dev-environment/software/</guid>
      <description>&lt;p&gt;&lt;em&gt;Although this is all in relation to working on Android, the software I use is generally the same as if I was on a Linux desktop. That&amp;rsquo;s kind of the point, nowadays I can use almost the same environment on Android as I can on my desktop.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;termux&#34;&gt;Termux&lt;/h3&gt;
&lt;p&gt;&lt;a href = &#34;https://termux.dev/en/&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;Termux&lt;/a&gt; is a brilliant free terminal app and Linux environment for Android. It has most of the Linux commands I need and I can install &lt;em&gt;git&lt;/em&gt;, &lt;em&gt;Vim&lt;/em&gt;, &lt;em&gt;Tmux&lt;/em&gt;, &lt;em&gt;mutt&lt;/em&gt;, &lt;em&gt;ImageMagick&lt;/em&gt;  and the majority of the terminal software I like to use. For certain jobs such as scripting, SSH sessions and writing, this is all I need.&lt;/p&gt;
&lt;p&gt;Termux can run non-terminal, graphical apps too, but you won&amp;rsquo;t be able to see them. That&amp;rsquo;s what the next app is for.&lt;/p&gt;
&lt;h3 id=&#34;termuxx11&#34;&gt;Termux:X11&lt;/h3&gt;
&lt;p&gt;&lt;a href = &#34;https://github.com/termux/termux-x11&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;This amazingly useful app&lt;/a&gt; describes itself as being in early development, but I&amp;rsquo;ve been daily-driving it for months without significant issues.&lt;/p&gt;
&lt;p&gt;Termux:X11 is an X Server for Termux. This allows you to install graphical Linux apps, as long as they are available for your system architecture (ie AArch64 &amp;mdash; or ARM64 as it&amp;rsquo;s known &amp;mdash; for most modern Android devices).&lt;/p&gt;
&lt;p&gt;This means I can run, directly on my tablet:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Desktop &lt;em&gt;Firefox&lt;/em&gt; (so, full developer tools)&lt;/li&gt;
&lt;li&gt;Desktop &lt;em&gt;Chromium&lt;/em&gt; (this was quite hard to get installed and isn&amp;rsquo;t the absolute latest version but is still very useful for development)&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Krita&lt;/em&gt;/&lt;em&gt;Inkscape&lt;/em&gt;/&lt;em&gt;GIMP&lt;/em&gt; for image processing&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Thunar&lt;/em&gt; file manager&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Audacity&lt;/em&gt; (using &lt;em&gt;PulseAudio&lt;/em&gt; over the network to pipe the audio up to Android by using the devices own IP address&amp;hellip; I&amp;rsquo;m sure this creates some delay/latency but I&amp;rsquo;ve not noticed it in practice and for my typical usage of minor adjustments to audio files it works well)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;proot-distro&#34;&gt;PRoot Distro&lt;/h3&gt;
&lt;p&gt;This is a &amp;lsquo;container environment manager&amp;rsquo; and allows you to easily install, uninstall, backup and restore Linux distributions, alongside/on-top-of Android, sharing the kernel.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s really simple to use, eg installing Debian Linux is a matter of opening &lt;em&gt;Termux&lt;/em&gt; and entering:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;proot-distro install debian
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then for a shell login:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;proot-distro login debian
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;PRoot Distro &lt;a href = &#34;https://github.com/termux/proot-distro&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;provides a nice choice of distros&lt;/a&gt; including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Alpine Linux (edge)&lt;/li&gt;
&lt;li&gt;Arch Linux ARM&lt;/li&gt;
&lt;li&gt;Artix Linux (AArch64 only)&lt;/li&gt;
&lt;li&gt;Debian (stable)&lt;/li&gt;
&lt;li&gt;Fedora 38 (AArch64 only)&lt;/li&gt;
&lt;li&gt;Manjaro (AArch64 only)&lt;/li&gt;
&lt;li&gt;OpenSUSE (Tumbleweed)&lt;/li&gt;
&lt;li&gt;Pardus (yirmibir)&lt;/li&gt;
&lt;li&gt;Ubuntu (23.10)&lt;/li&gt;
&lt;li&gt;Void Linux&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can install as many distros as you want and swap between them easily. Each distro just sits in a directory on the Android device. Using the backup tools the entire system can be bundled up as a single archive file and moved over to another Android device.&lt;/p&gt;
&lt;h2 id=&#34;window-management&#34;&gt;Window Management&lt;/h2&gt;
&lt;p&gt;I believe that the best user interface (actually, interface of any kind) is one that I don&amp;rsquo;t even notice exists. It doesn&amp;rsquo;t shout at me, it doesn&amp;rsquo;t wow me with animations and eye-candy. It allows me to get to the tools I need to get the job done and stay immersed in the task. It is at peace with itself enough to take a back seat and not demand attention. It is, conceptually, transparent.&lt;/p&gt;
&lt;p&gt;A lot of people like the gestural UI featured in &amp;lsquo;Minority Report&amp;rsquo;. I like the film, and the scene with Tom Cruise manipulating the UI is well produced and fun to watch. But as a UI it&amp;rsquo;s terrible! Hugely inefficient with all those big sweeping hand gestures, it&amp;rsquo;s not quick or practical. I don&amp;rsquo;t want to have to perform interpretive dance to use my computer (nor watch my computer doing little performances when I&amp;rsquo;m trying to get stuff done).&lt;/p&gt;
&lt;p&gt;I found similar sentiment in this &lt;a href = &#34;https://daringfireball.net/2024/01/the_vision_pro&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;Daring Fireball article about the Apple Vision Pro&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;To me the Macintosh has always felt more like a place than a thing. Not a place I go physically, but a place my mind goes intellectually. When I’m working or playing and in the flow, it has always felt like MacOS is where I am. I’m in the Mac.&lt;/p&gt;
&lt;p&gt;&amp;ldquo;Interruptions — say, the doorbell or my phone ringing — are momentarily disorienting when I’m in the flow on the Mac, because I’m pulled out of that world and into the physical one.&amp;rdquo;&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;I am nearly exactly like this too, except I don&amp;rsquo;t want to feel like I&amp;rsquo;m &amp;ldquo;in the Linux&amp;rdquo; (and definitely not &amp;ldquo;in the Mac&amp;rdquo; or &amp;ldquo;in Windows&amp;rdquo;!). I don&amp;rsquo;t want to be aware of that layer of abstraction at all. I don&amp;rsquo;t mind feeling that I&amp;rsquo;m &amp;ldquo;in Vim&amp;rdquo;, but really I want to be &amp;ldquo;in the code&amp;rdquo; that I&amp;rsquo;m editing. I specifically don&amp;rsquo;t want to feel the manicured claw of a corporate behemoth like Apple or Microsoft on my shoulder&amp;hellip; but that&amp;rsquo;s just me.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;d rather forget that my OS(/window manager) exists. It&amp;rsquo;s a facilitator, not an end in itself.&lt;/p&gt;
&lt;h3 id=&#34;dwm-tiling-window-manager&#34;&gt;DWM: Tiling Window Manager&lt;/h3&gt;
&lt;p&gt;I used to prefer larger screens and multiple monitors. I knew about the concept of workspaces but they sounded annoying and fiddly. It took me a long time to give them a chance and it happened accidentally, as a side-effect of trying a tiling window manager.&lt;/p&gt;
&lt;p&gt;Very briefly, a tiling window manager organises your windows into tiles, meaning instead of having oddly-sized overlapping windows floating around making a confusing mess of your screen and using space inefficiently, your screen splits into fractions, each being filled by an app. If you have too many apps on the screen they can go onto other screens, or &amp;lsquo;workspaces&amp;rsquo;. Personally I tend to run most apps fullscreen most of the time, switching between them with keyboard shortcuts.&lt;/p&gt;
&lt;p&gt;I started with &lt;a href = &#34;https://i3wm.org/&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;i3 Window Manager&lt;/a&gt;, which is a really nice piece of software. After a few years of using it though I realised I didn&amp;rsquo;t use most of its features, so landed on the very minimal &lt;a href = &#34;https://dwm.suckless.org/&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;Suckless Tools DWM&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;With workspaces, each app can have its own screen and can be accessed instantly via a keyboard shortcut. With familiarity this becomes a chord in muscle memory, a hand-shape to mash at keyboard. I have habits for where I keep each app, so eg my terminal is in workspace 1 so &lt;code&gt;[Alt]+[1]&lt;/code&gt; puts me in the terminal. &lt;code&gt;[Alt]+[9]&lt;/code&gt; puts me in FireFox and so on.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve &lt;code&gt;[Alt]+[Tab]&lt;/code&gt;bed through apps in the past. Workspaces are similar except you know the keyboard chord will take you directly to the app you want, you don&amp;rsquo;t have to play &amp;lsquo;stop the magical app carousel at the right time&amp;rsquo;&amp;hellip; which may be a drawback for some I guess, as carousels can be fun.&lt;/p&gt;
&lt;p&gt;We can only focus on one thing at a time, that&amp;rsquo;s how attention works. I can see some uses for multiple screens eg if you have to monitor lots of things, like real-time charts or meters. In that case your peripheral vision is good at detecting change/motion so you can make use of all the space. But personally I&amp;rsquo;m completely over large/multiple displays. In my circumstances, they waste power, waste space, and are just not necessary.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Hardware</title>
      <link>https://mm-dev.rocks/posts/android-as-a-dev-environment/hardware/</link>
      <pubDate>Fri, 05 Apr 2024 11:44:40 +0100</pubDate>
      <guid>https://mm-dev.rocks/posts/android-as-a-dev-environment/hardware/</guid>
      <description>&lt;h2 id=&#34;hardware&#34;&gt;Hardware&lt;/h2&gt;
&lt;p&gt;My favourite and most commonly-used device is my Samsung Tab S8 Ultra:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;14.6&amp;quot; OLED display&lt;/li&gt;
&lt;li&gt;2960x1848 resolution&lt;/li&gt;
&lt;li&gt;16:10 aspect ratio&lt;/li&gt;
&lt;li&gt;8-core, 4nm processor&lt;/li&gt;
&lt;li&gt;8GB RAM&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The OLED (&lt;a href = &#34;https://mm-dev.rocks/posts/oled-and-eink-4-life/intro/&#34; title = &#34;OLED and E-Ink&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;my favourite display tech along side e-ink&lt;/a&gt;) screen is gorgeous. Black is really black and running everything in dark OLED (black) UI modes gets a little more out of the battery too.&lt;/p&gt;
&lt;p&gt;This tablet is roughly on par with average modern laptops in terms of power, but has some advantages for me.&lt;/p&gt;
&lt;h3 id=&#34;ergonomics&#34;&gt;Ergonomics&lt;/h3&gt;
&lt;p&gt;This is the main reason I generally prefer a tablet over a laptop. With a laptop
&lt;em&gt;the screen is attached to the keyboard&lt;/em&gt;, meaning:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The display is always too low, so&amp;hellip;&lt;/li&gt;
&lt;li&gt;you hunch over&amp;hellip;&lt;/li&gt;
&lt;li&gt;which is bad for your back, and&amp;hellip;&lt;/li&gt;
&lt;li&gt;&amp;hellip;affects your breathing, as&amp;hellip;
&lt;ul&gt;
&lt;li&gt;your shoulders turn inwards and compress your chest cavity, so&amp;hellip;&lt;/li&gt;
&lt;li&gt;your breaths will be shorter and more shallow&amp;hellip;&lt;/li&gt;
&lt;li&gt;increasing a tendency towards feeling stressed or anxious&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This may seem over-the-top, but we programmers spend a lot of time sitting at our machines. There&amp;rsquo;s no such thing as a good position to sit in all day, our bodies expect and require movement (I certainly need a lot more) &amp;mdash; but some positions are better than others.&lt;/p&gt;
&lt;p&gt;Another benefit of using portable/mobile devices for work is that you aren&amp;rsquo;t chained to a desk, so can move around to different seating positions throughout the day. As a self-contained unit a laptop is good for this too, but you&amp;rsquo;re still stuck with the screen and keyboard being attached to one another.&lt;/p&gt;
&lt;p&gt;I attach the tablet to a monitor arm so I can position it at a good height (ie with the top of the screen being just below eye level). But if I want to spend some time in a different position, I have a simple tablet stand which folds flat (so takes up very little storage space) and can easily be placed on top of books or boxes to elevate my tablet.
&lt;ul class=&#39;gallery&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_ysCd_1&#39; /&gt;
      &lt;label for=&#39;item_ysCd_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/working-env/tablet-arms.jpg&#39; alt=&#39;Tablet arms&#39; /&gt;
        &lt;span&gt;Tablet arms&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_ysCd_2&#39; /&gt;
      &lt;label for=&#39;item_ysCd_2&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/working-env/foldable-tablet-stand.jpg&#39; alt=&#39;Foldable tablet stand&#39; /&gt;
        &lt;span&gt;Foldable tablet stand&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;
&lt;/p&gt;
&lt;h3 id=&#34;choice-of-input-devices&#34;&gt;Choice of Input Devices&lt;/h3&gt;
&lt;p&gt;In the past I&amp;rsquo;ve preferred ThinkPads for their great keyboards and little red &lt;a href = &#34;https://en.wikipedia.org/wiki/Pointing_stick&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;TrackPoint&lt;/a&gt; pointing devices &amp;mdash; I have an old but lovely &lt;a href = &#34;https://www.thinkwiki.org/wiki/Category:X230&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;X230&lt;/a&gt; for when I do feel the need for a laptop.&lt;/p&gt;
&lt;p&gt;Nowadays though, my favourite combo is a mechanical keyboard and trackball (the &amp;lsquo;fingers on top&amp;rsquo; type of trackball suits me better than the &amp;rsquo;thumb at the side&amp;rsquo; type).&lt;/p&gt;
&lt;p&gt;Of course these devices can be attached to laptops too, but then the laptop&amp;rsquo;s own keyboard and pointing device are just sitting there doing nothing. Laptops are good for travelling but for me sitting at home they&amp;rsquo;re a waste.&lt;/p&gt;
&lt;p&gt;They used to make TVs with DVD players built-in, and common advice was that it wasn&amp;rsquo;t a wise buy, as if the DVD player stopped working you just had a wasted lump of electronics attached to your TV. I feel like this about laptops.&lt;/p&gt;
&lt;h4 id=&#34;ergodox-mechanical-keyboard&#34;&gt;Ergodox Mechanical Keyboard&lt;/h4&gt;
&lt;p&gt;I like to use an &lt;a href = &#34;https://www.ergodox.io/&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;Ergodox mechanical keyboard&lt;/a&gt; which I built myself from a kit and maintain (due to my terrible soldering joints regularly failing). This is a split keyboard, meaning it comes in 2 halves which can be positioned with a gap in the middle, allowing me to keep my arms a comfortable distance apart and avoid the hunching and bad posture. The keyboard is also programmable via the open source firmware &lt;a href = &#34;https://qmk.fm/&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;QMK&lt;/a&gt; (great project btw).&lt;/p&gt;
&lt;p&gt;As time goes by I am gradually removing keys from my layout and moving towards a layers/chording method of typing, meaning you have fewer keys and use more key combos. I&amp;rsquo;m ultimately aiming for a 40% layout (which vaguely means 40% of the number of keys on a normal keyboard). Fewer keys means fewer hand contortions and stretching fingers to fewer awkward positions, plus it makes the hardware smaller. Once I&amp;rsquo;m comfortable with that smaller number of keys I&amp;rsquo;ll build myself a Bluetooth LE &lt;a href = &#34;https://github.com/pierrechevalier83/ferris&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;Ferris keyboard&lt;/a&gt; or something similar.&lt;/p&gt;
&lt;p&gt;My Ergodox is a wired USB version but I use it over Bluetooth via a &lt;a href = &#34;http://handheldsci.com/kb/&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;BT500 Bluetooth adapter&lt;/a&gt; which converts any USB keyboard/mouse into Bluetooth. The BT500 is quite expensive for what it is (about $40) but I doubt they sell huge numbers of them so probably don&amp;rsquo;t have the economies of scale to make them cheaper. I had to order from the USA and wait a while to get it, but it&amp;rsquo;s a pretty cool and useful device.&lt;/p&gt;
&lt;h4 id=&#34;deft-pro-trackball&#34;&gt;Deft PRO Trackball&lt;/h4&gt;
&lt;p&gt;My trackball is a &lt;a href = &#34;https://elecomusa.com/products/b07c9t4ttw&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;DEFT Pro&lt;/a&gt;. It works well for my purposes and feels comfortable enough. I try to use the keyboard instead of the pointer as much as possible but sometimes a mouse/trackball is needed, eg when editing images.&lt;/p&gt;
&lt;h3 id=&#34;power-management&#34;&gt;Power Management&lt;/h3&gt;
&lt;p&gt;Android/mobile devices treat this as an important feature whereas laptops don&amp;rsquo;t always take enough care over it, especially with Linux (where power management isn&amp;rsquo;t always the best, partly because device manufacturers don&amp;rsquo;t work as closely with Linux as they do Microsoft). This isn&amp;rsquo;t true for all laptops though, and things seem to be improving.&lt;/p&gt;
&lt;p&gt;Most Android devices use ARM CPUs, which give strong performance for relatively low power usage.&lt;/p&gt;
&lt;p&gt;I admit that Windows and MacOS tend to be better-tuned for power efficiency, but if I had to use those OSes I&amp;rsquo;d be wishing for my battery to die, just so I could stop. So, swings and roundabouts.&lt;/p&gt;
&lt;h3 id=&#34;adaptable-and-easy-to-store&#34;&gt;Adaptable and Easy to Store&lt;/h3&gt;
&lt;p&gt;I can be writing code one minute and very quickly change to watching a film or casual web browsing handheld mode the next.&lt;/p&gt;
&lt;p&gt;As I&amp;rsquo;m often relying entirely on solar power for my electricity, being able to minimise the number of devices I need to keep charged up is a real benefit for me.&lt;/p&gt;
&lt;p&gt;Also, living in a small space, tablets are flat and thin &amp;mdash; so very efficient in terms of storage. Several can be stacked up on top of each other and take up no more space than a large book.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Intro</title>
      <link>https://mm-dev.rocks/posts/android-as-a-dev-environment/intro/</link>
      <pubDate>Fri, 05 Apr 2024 11:44:38 +0100</pubDate>
      <guid>https://mm-dev.rocks/posts/android-as-a-dev-environment/intro/</guid>
      <description>&lt;p&gt;I get a lot of my development work done on Android devices. That usually means a large tablet, but it&amp;rsquo;s not impossible to get work done on smaller tablets or even phones (although the smaller you go, the harder it is to maintain ergonomics and practicality).&lt;/p&gt;
&lt;p&gt;With &lt;em&gt;Termux&lt;/em&gt;/&lt;em&gt;PRoot Distro&lt;/em&gt;/&lt;em&gt;Termux:X11&lt;/em&gt; (explained below) I can run all of my most-used Linux apps just like I was sitting at a desktop, silently (ie no fans) and efficiently.&lt;/p&gt;
&lt;p&gt;Performance on my S8 Ultra tablet is great &amp;mdash; not as fast as the latest most powerful laptops but perfectly comfortable for my usage patterns.&lt;/p&gt;
&lt;p&gt;A shortcut to a simple Termux script means I can tap an icon on my Android home screen and boot into the beautiful emptiness of an Ubuntu DWM session in about 5 seconds.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Android Bluetooth Keyboard Mapping</title>
      <link>https://mm-dev.rocks/posts/android-bluetooth-keyboard-mapping/</link>
      <pubDate>Thu, 08 Feb 2024 12:05:30 +0000</pubDate>
      <guid>https://mm-dev.rocks/posts/android-bluetooth-keyboard-mapping/</guid>
      <description>&lt;p&gt;I use Bluetooth keyboards with Android a lot, and often want to remap keys (eg swapping &lt;code&gt;CAPS_LOCK&lt;/code&gt; to &lt;code&gt;CTRL_LEFT&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Maybe it&amp;rsquo;s a failing in my search technique or I&amp;rsquo;m doing something that nobody else does nowadays, but whenever I search for info about this I find really old articles with out-of-date info, or just links to apps. To be fair &lt;a href = &#34;https://f-droid.org/packages/io.github.sds100.keymapper/&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;Key Mapper&lt;/a&gt; is good and I do use it, but sometimes I just want to be able to do it at a system level without requiring extra apps.&lt;/p&gt;
&lt;p&gt;To remap keys in Android, you need to edit a particular keylayout &lt;code&gt;*.kl&lt;/code&gt; file, specific to the device (eg Bluetooth keyboard) you want to re-map. The files have names like &lt;code&gt;Vendor_04e8.kl&lt;/code&gt;. The name is derived from various properties of the device, such as vendor ID, product ID and other things &lt;a href = &#34;https://source.android.com/docs/core/interaction/input/key-layout-files&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;as explained here in the Android docs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The sticking point for me, for &lt;em&gt;ages&lt;/em&gt;, was that I had trouble finding the vendor and product codes and, consequently, the correct file to edit. In Linux I&amp;rsquo;d use &lt;code&gt;lsusb&lt;/code&gt; or &lt;code&gt;bluetoothctl&lt;/code&gt; or something like that, but on Android even in the wonderful &lt;a href = &#34;https://termux.dev/en/&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;Termux&lt;/a&gt;, these things don&amp;rsquo;t work properly for reasons I haven&amp;rsquo;t completely wrapped my head around, related to security and/or the way Android and its underlying Linux kernel inter-relate.&lt;/p&gt;
&lt;h2 id=&#34;problem-1-finding-which-keylayout-file-needs-to-be-edited&#34;&gt;Problem 1: Finding which keylayout file needs to be edited&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;In Termux or whichever shell/terminal you use, do &lt;code&gt;su&lt;/code&gt; to log in as superuser&lt;/li&gt;
&lt;li&gt;Do &lt;code&gt;dumpsys&lt;/code&gt; and wait for it to finish. It can take a while (couple of minutes on some of my devices)&amp;hellip;&lt;br&gt;
&lt;em&gt;If I don&amp;rsquo;t do this step, on some devices the next step doesn&amp;rsquo;t work&amp;hellip; I hate this kind of situation as I have no idea why this should work and it feels nonsensical, but all I know is it fixed the problem when I encountered it.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Do &lt;code&gt;dumpsys input&lt;/code&gt; to narrow the output&lt;br&gt;
&lt;em&gt;Even this input-specific command outputs quite a lot. I tend to just take my time and scroll through it, but you can make it easier by trying something like:&lt;/em&gt;&lt;br&gt;
&lt;code&gt;dumpsys input | grep -i -A12 bluetooth&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;-i&lt;/code&gt; means case-insensitive&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;-A12&lt;/code&gt; means &lt;em&gt;show the matching line plus the &lt;code&gt;12&lt;/code&gt; lines &lt;code&gt;A&lt;/code&gt;fter it&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;You may want to tweak the number but for me 12 was good&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;In the output, look for a description for your device (mine is &lt;code&gt;Device 45: Bluetooth 3.0 Keyboard&lt;/code&gt;), and soon after that you&amp;rsquo;ll find a line like &lt;code&gt;KeyLayoutFile: /system/usr/keylayout/Vendor_04e8.kl&lt;/code&gt; &amp;mdash; that&amp;rsquo;s the &lt;code&gt;*.kl&lt;/code&gt; file you need to edit to remap your keyboard&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;problem-2-editing-system-files-on-modern-android&#34;&gt;Problem 2: Editing system files on modern Android&lt;/h2&gt;
&lt;p&gt;On modern versions of Android, even if you have root, it&amp;rsquo;s not possible to directly edit files in the &lt;code&gt;/system/&lt;/code&gt; directory (it is mounted as read-only).&lt;/p&gt;
&lt;p&gt;The way around this is to &lt;a href = &#34;https://topjohnwu.github.io/Magisk/guides.html#magisk-modules&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;make a Magisk module&lt;/a&gt;. This is simpler than it might sound. The basic idea is that you create a folder in &lt;code&gt;/data/adb/modules&lt;/code&gt;, (eg in my case I use &lt;code&gt;mm&lt;/code&gt;, so &lt;code&gt;/data/adb/modules/mm/&lt;/code&gt;). Inside that folder, you can create files and folders which Magisk will overlay onto the system at boot time, eg:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;/data/adb/modules/mm/system/usr
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# becomes&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;/system/usr
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;/data/adb/modules/mm/system/usr/keylayout/Vendor_04e8.kl
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# becomes&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;/system/usr/keylayout/Vendor_04e8.kl
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now you can edit the keylayout file to make your changes, eg to change &lt;code&gt;CAPS_LOCK&lt;/code&gt; to &lt;code&gt;CTRL_LEFT&lt;/code&gt; on my keyboard it&amp;rsquo;s:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# key 58    CAPS_LOCK&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;key &lt;span class=&#34;m&#34;&gt;58&lt;/span&gt;    CTRL_LEFT
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;other-apps-which-might-be-useful&#34;&gt;Other apps which might be useful&lt;/h2&gt;
&lt;p&gt;Depending on your Android device and software version, it can be difficult/confusing to find the right key codes. I find that one or another of the apps below usually gets me what I need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href = &#34;https://apkpure.net/hardware-keyboard-check/com.darkoak.android.hardwarekeyboardcheck&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;HardwareKeyboardCheck by Mr. Greenheart&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href = &#34;https://apkpure.net/key-tester/com.flossga.android.whichbutton&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;WhichButton aka Key Tester by FLOSS AG&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Pipe Dream (Web Game)</title>
      <link>https://mm-dev.rocks/posts/pipe-dream-web-game/</link>
      <pubDate>Wed, 15 Nov 2023 16:04:45 +0000</pubDate>
      <guid>https://mm-dev.rocks/posts/pipe-dream-web-game/</guid>
      <description>&lt;p&gt;Pipe Dream is a game you can play in your browser &lt;a href = &#34;/pipe-dream&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You don&amp;rsquo;t need to log in or install anything and of course there are no ads/tracking.&lt;/p&gt;
&lt;p&gt;The game is about as simple as it gets. There are loads of blobs floating about &amp;mdash; avoid some of them and eat the others.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;To control the player&lt;/strong&gt; move your mouse pointer (or drag your finger) over the &lt;strong&gt;control area&lt;/strong&gt; - a rectangle on the right or bottom of the screen (depending on which way around your display is oriented)&lt;/li&gt;
&lt;li&gt;When you hit one of the &lt;strong&gt;avoid blobs&lt;/strong&gt; you lose &lt;strong&gt;health&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Each level gives you a different amount of &lt;strong&gt;time&lt;/strong&gt; to complete it&lt;/li&gt;
&lt;li&gt;To complete a level, eat all the &lt;strong&gt;edible blobs&lt;/strong&gt; before you run out of &lt;strong&gt;time&lt;/strong&gt;, or &lt;strong&gt;health&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ul class=&#39;gallery&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_Qe5h_1&#39; /&gt;
      &lt;label for=&#39;item_Qe5h_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/pipe-dream/pd-intro.png&#39; alt=&#39;Intro screen&#39; /&gt;
        &lt;span&gt;Intro screen&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_Qe5h_2&#39; /&gt;
      &lt;label for=&#39;item_Qe5h_2&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/pipe-dream/pd-basic.png&#39; alt=&#39;Level BASIC&#39; /&gt;
        &lt;span&gt;Level BASIC&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_Qe5h_3&#39; /&gt;
      &lt;label for=&#39;item_Qe5h_3&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/pipe-dream/pd-tooez.png&#39; alt=&#39;Level TOOEZ&#39; /&gt;
        &lt;span&gt;Level TOOEZ&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_Qe5h_4&#39; /&gt;
      &lt;label for=&#39;item_Qe5h_4&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/pipe-dream/pd-jungl.png&#39; alt=&#39;Level JUNGL&#39; /&gt;
        &lt;span&gt;Level JUNGL&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_Qe5h_5&#39; /&gt;
      &lt;label for=&#39;item_Qe5h_5&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/pipe-dream/pd-mauve.png&#39; alt=&#39;Level MAUVE&#39; /&gt;
        &lt;span&gt;Level MAUVE&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_Qe5h_6&#39; /&gt;
      &lt;label for=&#39;item_Qe5h_6&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/pipe-dream/pd-rrred.png&#39; alt=&#39;Level RRRED&#39; /&gt;
        &lt;span&gt;Level RRRED&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_Qe5h_7&#39; /&gt;
      &lt;label for=&#39;item_Qe5h_7&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/pipe-dream/pd-mustd.png&#39; alt=&#39;Level MUSTD&#39; /&gt;
        &lt;span&gt;Level MUSTD&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_Qe5h_8&#39; /&gt;
      &lt;label for=&#39;item_Qe5h_8&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/pipe-dream/pd-mult1.png&#39; alt=&#39;Level MULT1&#39; /&gt;
        &lt;span&gt;Level MULT1&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_Qe5h_9&#39; /&gt;
      &lt;label for=&#39;item_Qe5h_9&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/pipe-dream/pd-mult2.png&#39; alt=&#39;Level MULT2&#39; /&gt;
        &lt;span&gt;Level MULT2&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_Qe5h_10&#39; /&gt;
      &lt;label for=&#39;item_Qe5h_10&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/pipe-dream/pd-merng.png&#39; alt=&#39;Level MERNG&#39; /&gt;
        &lt;span&gt;Level MERNG&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_Qe5h_11&#39; /&gt;
      &lt;label for=&#39;item_Qe5h_11&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/pipe-dream/pd-dense.png&#39; alt=&#39;Level DENSE&#39; /&gt;
        &lt;span&gt;Level DENSE&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_Qe5h_12&#39; /&gt;
      &lt;label for=&#39;item_Qe5h_12&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/pipe-dream/pd-reddd.png&#39; alt=&#39;Level REDDD&#39; /&gt;
        &lt;span&gt;Level REDDD&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_Qe5h_13&#39; /&gt;
      &lt;label for=&#39;item_Qe5h_13&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/pipe-dream/pd-lmolv.png&#39; alt=&#39;Level LMOLV&#39; /&gt;
        &lt;span&gt;Level LMOLV&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_Qe5h_14&#39; /&gt;
      &lt;label for=&#39;item_Qe5h_14&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/pipe-dream/pd-blrds.png&#39; alt=&#39;Level BLRDS&#39; /&gt;
        &lt;span&gt;Level BLRDS&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_Qe5h_15&#39; /&gt;
      &lt;label for=&#39;item_Qe5h_15&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/pipe-dream/pd-gardn.png&#39; alt=&#39;Level GARDN&#39; /&gt;
        &lt;span&gt;Level GARDN&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_Qe5h_16&#39; /&gt;
      &lt;label for=&#39;item_Qe5h_16&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/pipe-dream/pd-gnsmk.png&#39; alt=&#39;Level GNSMK&#39; /&gt;
        &lt;span&gt;Level GNSMK&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_Qe5h_17&#39; /&gt;
      &lt;label for=&#39;item_Qe5h_17&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/pipe-dream/pd-strry.png&#39; alt=&#39;Level STRRY&#39; /&gt;
        &lt;span&gt;Level STRRY&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_Qe5h_18&#39; /&gt;
      &lt;label for=&#39;item_Qe5h_18&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/pipe-dream/pd-pekmn.png&#39; alt=&#39;Level PEKMN&#39; /&gt;
        &lt;span&gt;Level PEKMN&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_Qe5h_19&#39; /&gt;
      &lt;label for=&#39;item_Qe5h_19&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/pipe-dream/pd-swarm.png&#39; alt=&#39;Level SWARM&#39; /&gt;
        &lt;span&gt;Level SWARM&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_Qe5h_20&#39; /&gt;
      &lt;label for=&#39;item_Qe5h_20&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/pipe-dream/pd-wista.png&#39; alt=&#39;Level WISTA&#39; /&gt;
        &lt;span&gt;Level WISTA&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_Qe5h_21&#39; /&gt;
      &lt;label for=&#39;item_Qe5h_21&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/pipe-dream/pd-alien.png&#39; alt=&#39;Level ALIEN&#39; /&gt;
        &lt;span&gt;Level ALIEN&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_Qe5h_22&#39; /&gt;
      &lt;label for=&#39;item_Qe5h_22&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/pipe-dream/pd-chpln.png&#39; alt=&#39;Level CHPLN&#39; /&gt;
        &lt;span&gt;Level CHPLN&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_Qe5h_23&#39; /&gt;
      &lt;label for=&#39;item_Qe5h_23&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/pipe-dream/pd-oprtn.png&#39; alt=&#39;Level OPRTN&#39; /&gt;
        &lt;span&gt;Level OPRTN&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_Qe5h_24&#39; /&gt;
      &lt;label for=&#39;item_Qe5h_24&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/pipe-dream/pd-ntpor.png&#39; alt=&#39;Level NTPOR&#39; /&gt;
        &lt;span&gt;Level NTPOR&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    
    <item>
      <title>OLED Devices</title>
      <link>https://mm-dev.rocks/posts/oled-and-eink-4-life/oled-devices/</link>
      <pubDate>Thu, 02 Nov 2023 15:57:18 +0000</pubDate>
      <guid>https://mm-dev.rocks/posts/oled-and-eink-4-life/oled-devices/</guid>
      <description>&lt;h2 id=&#34;samsung-galaxy-tab-s8-ultra&#34;&gt;Samsung Galaxy Tab S8 Ultra&lt;/h2&gt;
&lt;p&gt;&lt;ul class=&#39;gallery single-item&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_FPS8_1&#39; /&gt;
      &lt;label for=&#39;item_FPS8_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/oled-eink/tab-s8-2.jpg&#39; alt=&#39;Tab S8 Ultra&#39; /&gt;
        &lt;span&gt;Tab S8 Ultra&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;

My main device, I use this for most of my coding work and it&amp;rsquo;s gorgeous for media consumption.&lt;/p&gt;
&lt;p&gt;I run &lt;a href = &#34;https://github.com/termux/termux-x11&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;Termux X11&lt;/a&gt; with &lt;a href = &#34;https://github.com/termux/proot-distro&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;proot-distro&lt;/a&gt; allowing me to use a relatively complete Linux (Ubuntu 20.04), including full desktop (aarch64) versions of Firefox and Chromium.&lt;/p&gt;
&lt;p&gt;If I need more power, or to access a Windows VM, I use this same device via &lt;a href = &#34;https://github.com/gujjwal00/avnc&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;AVNC&lt;/a&gt; to VNC into my small form factor PC.&lt;/p&gt;
&lt;h2 id=&#34;samsung-galaxy-tab-s4&#34;&gt;Samsung Galaxy Tab S4&lt;/h2&gt;
&lt;p&gt;&lt;ul class=&#39;gallery single-item&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_F8AQ_1&#39; /&gt;
      &lt;label for=&#39;item_F8AQ_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/oled-eink/tab-s4.jpg&#39; alt=&#39;Tab S4&#39; /&gt;
        &lt;span&gt;Tab S4&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;

I daily drove this for a couple of years as my main device. Although the screen is only 10.5&amp;quot; it&amp;rsquo;s a very capable device and can do pretty much everything the S8 Ultra can, just smaller and a bit slower (I find it to be quite usable).&lt;/p&gt;
&lt;h2 id=&#34;samsung-galaxy-note-3&#34;&gt;Samsung Galaxy Note 3&lt;/h2&gt;
&lt;p&gt;&lt;ul class=&#39;gallery single-item&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_sONQ_1&#39; /&gt;
      &lt;label for=&#39;item_sONQ_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/oled-eink/note-3.jpg&#39; alt=&#39;Note 3&#39; /&gt;
        &lt;span&gt;Note 3&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;

This is like the baby version of the above 2 devices, I use it without a SIM as a mini tablet.&lt;/p&gt;
&lt;p&gt;Again, it can pretty much do the same things that they can, only smaller and noticeably slower. It&amp;rsquo;s a 10 year old device at time of writing (was released in September 2013). It was a flagship device and actually holds its own quite well despite its age. It&amp;rsquo;s still perfectly usable.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve even used it for (small) paid jobs a couple of times when I first started to live off solar and was adjusting to the low power situation. I attached it to an arm to hold it up close to my eyes, and with a Bluetooth keyboard and mouse.&lt;/p&gt;
&lt;h2 id=&#34;blackberry-q10&#34;&gt;BlackBerry Q10&lt;/h2&gt;
&lt;p&gt;&lt;ul class=&#39;gallery single-item&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_eihl_1&#39; /&gt;
      &lt;label for=&#39;item_eihl_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/oled-eink/blackberry-q10.jpg&#39; alt=&#39;BlackBerry Q10&#39; /&gt;
        &lt;span&gt;BlackBerry Q10&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;

I stopped using my Note 3 as a phone and &amp;lsquo;downgraded&amp;rsquo; to this, to use as a dumb-ish phone. It has good call quality and reception and a hardware QWERTY keyboard which I wanted for texting. It&amp;rsquo;s small and well-built.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>E-ink Devices</title>
      <link>https://mm-dev.rocks/posts/oled-and-eink-4-life/eink-devices/</link>
      <pubDate>Thu, 02 Nov 2023 15:53:18 +0000</pubDate>
      <guid>https://mm-dev.rocks/posts/oled-and-eink-4-life/eink-devices/</guid>
      <description>&lt;h2 id=&#34;remarkable&#34;&gt;reMarkable&lt;/h2&gt;
&lt;p&gt;&lt;ul class=&#39;gallery single-item&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_ZM6G_1&#39; /&gt;
      &lt;label for=&#39;item_ZM6G_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/oled-eink/rm1.jpg&#39; alt=&#39;reMarkable 1 (my drawing of John C. Lilly)&#39; /&gt;
        &lt;span&gt;reMarkable 1 (my drawing of John C. Lilly)&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;

I use this for notes and sketching. Also sometimes as an e-reader (with KOReader) as the screen size is really nice and it&amp;rsquo;s a very light device.&lt;/p&gt;
&lt;p&gt;I find that the act of writing with a pen feels good and believe it helps me with thinking. My thoughts seem to work in a different pattern somehow when I&amp;rsquo;m using a pen vs a keyboard &amp;mdash; especially when I&amp;rsquo;m planning an app or trying to work through a problem that I&amp;rsquo;m going around in circles with in my head. I find visualising on &amp;lsquo;paper&amp;rsquo; often helps me to crack a problem. Different senses, muscles in use, different groups of neurons firing.&lt;/p&gt;
&lt;p&gt;None of this is exclusive to reMarkable, paper and pen would work just as well. The reasons I prefer the reMarkable are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It feels nice. When I use real paper I prefer it to be good paper. But then I feel precious about it, and it makes me reluctant to write or draw (I had a lovely little Moleskine notebook but rarely used it as I didn&amp;rsquo;t want to &amp;lsquo;waste it&amp;rsquo;).&lt;/li&gt;
&lt;li&gt;It has a few extra features like being able to cut/copy/paste, making it easy to rearrange notes and clean them up.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I also like that it runs Linux and you can SSH into it and get root.&lt;/p&gt;
&lt;p&gt;I have the first generation, the rM1, which I specifically prefer over the later version. The rM2 replaced the plastic screen with glass, and the outer casing with metal. It is now more &amp;lsquo;premium&amp;rsquo;, and I think more people prefer the industrial design of this model. Honestly I find it dull and characterless, yet another metal and glass slab. The newer device is more brittle and likely to shatter, and for me it&amp;rsquo;s moving towards being more like an iPad (my least-favourite device, and one I keep only for FaceTime, it otherwise sits unused on a shelf). My plastic rM1 is softer, lighter, can flex a little without shattering, and the screen has some give in it which is satisfying to write and draw on. It has less RAM, a slower processor and worse battery life, so I lose out there, but I like the way it feels so much. Ultimately I can write very comfortably on it, and make very complex drawings (as complex as I would ever want to draw at that size anyway), so although I&amp;rsquo;d prefer the upgraded internals, I wouldn&amp;rsquo;t swap what I have.&lt;/p&gt;
&lt;h2 id=&#34;onyx-boox-nova-3&#34;&gt;Onyx Boox Nova 3&lt;/h2&gt;
&lt;p&gt;&lt;ul class=&#39;gallery single-item&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_vie3_1&#39; /&gt;
      &lt;label for=&#39;item_vie3_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/oled-eink/nova3-brydge.jpg&#39; alt=&#39;Boox Nova 3 with Brydge Keyboard&#39; /&gt;
        &lt;span&gt;Boox Nova 3 with Brydge Keyboard&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;

I keep a keyboard attached to this and use it as a small, peaceful writing device.&lt;/p&gt;
&lt;p&gt;Each weekend I take 24 hours away from the internet and electronics. Some weeks I&amp;rsquo;m more strict than others so sometimes I do some writing or check emails, and this is the device I use. With the light off, writing markdown in Vim, it barely feels electronic, more akin to a typewriter.&lt;/p&gt;
&lt;p&gt;Keyboards made to fit iPad Mini (or at least old versions like the 4th gen) fit the Nova (7.8&amp;quot; screen) pretty well. They attach with the tablet sliding into a little rubberised grip, which seems a bit kludgey but works fine.&lt;/p&gt;
&lt;p&gt;I first bought a cheap Arteck keyboard which was selling for less than £10 on Amazon (those iPad models for which the keyboards are compatible are old, so there is probably not a huge market for accessories). It was OK but didn&amp;rsquo;t exactly feel high end. It creaked and felt hollow around the palm rest, and the most annoying problem for me was that the screen(/tablet) could only rotate back a little, not far past 90 degrees. This meant it was difficult to get an angle which was good for both typing and seeing the screen. It proved the concept (that the iPad keyboards could work) though.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;d heard good things about the Brydge ones. They were silly prices in the UK though, well over £100 (about 3x the US cost). But I waited and kept checking eBay, eventually snagging one for less than £20.&lt;/p&gt;
&lt;p&gt;The Brydge keyboard is a blatant step up in build quality. It feels really solid, made of lovely materials &amp;mdash; the shell is thick aluminium and the rubbers and plastics feel dense and durable. The keys have a great action, although the travel isn&amp;rsquo;t huge it feels well-defined&amp;hellip; when you press a key you know you&amp;rsquo;ve pressed it.&lt;/p&gt;
&lt;p&gt;Some truly bizarre design choices have been made though: &lt;code&gt;tab&lt;/code&gt; is hidden behind a modifier, and look at that weirdly massive &lt;code&gt;Q&lt;/code&gt; key (in the pic)!&lt;/p&gt;
&lt;p&gt;For now there&amp;rsquo;s not much I can do about the &lt;code&gt;Q&lt;/code&gt; key but I can hammer out some of the other weirdnesses with the &lt;em&gt;Key Mapper&lt;/em&gt; app on Android.&lt;/p&gt;
&lt;p&gt;Like all Boox devices the Nova 3 runs Android, and therefore Termux. I have root. So I can use it for most things I do on my other devices, but its small screen makes it less than ideal. As a writing device though, it is &amp;lsquo;andsome.&lt;/p&gt;
&lt;h2 id=&#34;onyx-boox-max-lumi&#34;&gt;Onyx Boox Max Lumi&lt;/h2&gt;
&lt;p&gt;&lt;ul class=&#39;gallery single-item&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_7XRy_1&#39; /&gt;
      &lt;label for=&#39;item_7XRy_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/oled-eink/max-lumi-white.jpg&#39; alt=&#39;Boox Max Lumi&#39; /&gt;
        &lt;span&gt;Boox Max Lumi&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;

This is my favourite device for reading those books which have a lot of important formatting, such as programming textbooks, or illustrations. A lot of documents are only available as PDFs which don&amp;rsquo;t re-flow very well on smaller screens, so this is great for those too.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s the first &lt;em&gt;Max&lt;/em&gt; (13.3&amp;quot;) device with a light (even though I prefer e-ink in its natural unlit form, an optional light is definitely handy at times). Although it&amp;rsquo;s a couple of generations old now, it still feels light, fast, and reading large-format books on it is very pleasurable. The e-ink panel is a &lt;em&gt;Mobius&lt;/em&gt;, which has been superseded by panels with higher contrast ratios now, but does have a plastic substrate meaning it should be even more forgiving to flexing and bending. Similarly to the &lt;em&gt;reMarkable 1&lt;/em&gt;, this device feels warm, soft and friendly, rather than cold and dystopian like so many modern devices.&lt;/p&gt;
&lt;p&gt;I find the tone of the bezel around an e-ink screen can really affect my perception of the screen. If you consider that the screens have quite a compressed dynamic range (spanning middle shades of grey rather than true black/white), then surrounding them with a pure black or a pure white bezel changes the overall dynamic range within your field of view &amp;mdash; now you have a more extreme tone in view, and the range of greys on the screen becomes comparitively compressed/more narrow.&lt;/p&gt;
&lt;p&gt;The &lt;em&gt;Max Lumi&lt;/em&gt; comes with a black bezel. I&amp;rsquo;ve temporarily modded mine with white tape. I definitely prefer the device with a white bezel, although my solution feels tatty (as you can see in the pic). I don&amp;rsquo;t know of a reasonable way to change the bezel to white permanently, but if I find one I&amp;rsquo;ll do it.&lt;/p&gt;
&lt;h2 id=&#34;kobo-mini&#34;&gt;Kobo Mini&lt;/h2&gt;
&lt;p&gt;&lt;ul class=&#39;gallery single-item&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_q2Gn_1&#39; /&gt;
      &lt;label for=&#39;item_q2Gn_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/oled-eink/kobo-mini.jpg&#39; alt=&#39;Kobo Mini&#39; /&gt;
        &lt;span&gt;Kobo Mini&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;

This is the device I take with me in my rucksack when I go into the city for grocery shopping, to read while I wait at bus stops. I bought it for next to nothing on eBay as it wasn&amp;rsquo;t working, but it was a software problem I was able to fix. It&amp;rsquo;s small and light, so cheap that I don&amp;rsquo;t mind if it gets damaged, and it doesn&amp;rsquo;t look shiny or expensive so doesn&amp;rsquo;t attract attention. It would be my favourite small e-reader if only it had a front light.&lt;/p&gt;
&lt;h2 id=&#34;kobo-glo&#34;&gt;Kobo Glo&lt;/h2&gt;
&lt;p&gt;&lt;ul class=&#39;gallery single-item&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_naxs_1&#39; /&gt;
      &lt;label for=&#39;item_naxs_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/oled-eink/kobo-glo-2.jpg&#39; alt=&#39;Kobo Glo&#39; /&gt;
        &lt;span&gt;Kobo Glo&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;

The Kobo Glo is basically the same device as the Kobo Mini, with a slightly larger screen and a front light.&lt;/p&gt;
&lt;p&gt;I run KOReader on all of my e-readers as it&amp;rsquo;s a great bit of software and I can sync books and reading progress without being logged in to a Kobo account.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Intro</title>
      <link>https://mm-dev.rocks/posts/oled-and-eink-4-life/intro/</link>
      <pubDate>Thu, 02 Nov 2023 15:47:18 +0000</pubDate>
      <guid>https://mm-dev.rocks/posts/oled-and-eink-4-life/intro/</guid>
      <description>&lt;p&gt;In recent years I&amp;rsquo;ve developed pretty strong preferences for a couple of display technologies. For the time being at least, I&amp;rsquo;ll probably only buy devices that use these technologies (OLED and E-Ink).&lt;/p&gt;
&lt;p&gt;My preferences are bound to environmental factors, so I should point out that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;While using electronic devices I usually work in a relatively dimly-lit environment&lt;/li&gt;
&lt;li&gt;For most of the year I live off solar power so try to limit my power usage&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I spend a lot of time looking at screens and want to be as comfortable as possible. I like the screen to be dim, without sacrificing too much clarity or colour accuracy. The darker the room, the dimmer the screen can be and still appear bright. I don&amp;rsquo;t want to live in complete darkness, so the best balance for me ends up being a relatively dim room where I can still see clearly enough to read a book (for example).&lt;/p&gt;
&lt;p&gt;Once I got used to my first OLED display, I really stopped enjoying the way LCD looks. Insipid blacks are inherent to LCDs; they use an evenly-lit screen, then block out pixels (sub-pixels, more accurately) when they are supposed to be black. The effect is like covering a torch with a piece of paper. Better quality displays might block out more of the light, but they always let some through, and the blacks end up as some form of grey. LCD viewing angles (even the better ones) mean images look weird once you move off-axis. It all just feels so&amp;hellip; &lt;em&gt;digital&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&#34;oled&#34;&gt;OLED&lt;/h2&gt;
&lt;p&gt;OLEDs are far more interesting in that each pixel is like its own little light source, which can be turned off independently of the other pixels. Black is truly the absence of light. In a dark room, black parts of an OLED display completely disappear into the darkness.&lt;/p&gt;
&lt;p&gt;Truly black pixels (ie &lt;code&gt;rgb(0, 0, 0)&lt;/code&gt;), don&amp;rsquo;t use any power, so dark mode UIs can use a little less power than alternative colours. I don&amp;rsquo;t know how significant this is, probably not very but I like dark themes anyway so it&amp;rsquo;s nice to use them by default and have a feeling that I&amp;rsquo;m at least steering battery life in the right direction.&lt;/p&gt;
&lt;p&gt;At full brightness OLEDs are apparently less efficient than some other displays, but as I&amp;rsquo;ve already said I tend to use mine at pretty low brightness (usually the bottom 25% but even when the room is bright I still rarely go over 50-60% on my devices).&lt;/p&gt;
&lt;h3 id=&#34;oled-disadvantages&#34;&gt;OLED Disadvantages&lt;/h3&gt;
&lt;p&gt;Some common criticisms of OLED are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Lack of brightness compared to some other displays&lt;/li&gt;
&lt;li&gt;Burn-in (image retention), where if the same image appears on-screen too much of the time, a kind of ghost of it can be &amp;lsquo;burnt in&amp;rsquo; to the display&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The lack of brightness is not a concern for me. I don&amp;rsquo;t use my main computing devices outdoors. I don&amp;rsquo;t go out as often as I should as it is, so when I do I don&amp;rsquo;t want to spend time looking at screens. I have a &lt;a href = &#34;https://www.garmin.com/en-GB/p/603267&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;Garmin Fenix 5 Plus&lt;/a&gt; (with a transflective memory-in-pixel display) watch for the time and GPS tracking/maps if I need it. If I expect to sit waiting for something (like a bus), I take an old &lt;em&gt;Kobo Mini&lt;/em&gt; with an e-ink screen. My phone will be in my bag or pocket and probably stay there.&lt;/p&gt;
&lt;p&gt;Burn-in has also not affected my use. I still use my &lt;em&gt;Galaxy Note 3&lt;/em&gt; (phone) daily as a mini tablet for podcasts and videos. The Note 3 is just over 10 years old at time of writing. &lt;del&gt;I think technically there is some burn-in, as when I look at a pure black screen, at its dimmest setting, in the dark, I can see some slight scratchy artefacts. But I never noticed them in real usage, ie they have never stood out when I&amp;rsquo;m watching videos or browsing the web.&lt;/del&gt; (I wrote this from memory, but I just turned the lights off and got my camera out to get a pic of this but I can&amp;rsquo;t even see it, so it can&amp;rsquo;t be that bad. I&amp;rsquo;ll update if I see it again!)&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t have always-visible taskbars and status bars, so there aren&amp;rsquo;t really any elements which always appear on my screen and would attract burn-in. I also notice I&amp;rsquo;ve unintentionally mostly used Samsung AMOLED panels, which have a pretty good reputation. So my experience may not be typical, although from what I hear this does generally seem to be less of an issue than it used to be. Modern OLED devices also use some tricks such as &amp;lsquo;moving the screen around&amp;rsquo; by a few pixels periodically, and even detecting static logos and reducing their brightness.&lt;/p&gt;
&lt;h2 id=&#34;e-ink&#34;&gt;E-Ink&lt;/h2&gt;
&lt;p&gt;E-paper/e-ink (&lt;a href = &#34;https://en.wikipedia.org/wiki/Electronic_paper&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;Wikipedia: Electronic paper&lt;/a&gt;) also really appeals to me&amp;hellip; a display made of tiny capsules containing an actual ink-like substance, in white and black, with each colour being either repelled or attracted by opposing electrical charges. It&amp;rsquo;s like electrically moving ink around. The capsules/pixels retain their colour (tone, really) even if there is no power at all (I&amp;rsquo;ve bought a few old broken e-ink readers from eBay, they often have an ancient image from the last-read book on their screen, even if the battery is removed or the screen is cracked).&lt;/p&gt;
&lt;p&gt;Although lots of e-ink devices do have a light for their display, it&amp;rsquo;s a front light, with a very different quality to backlights, and is optional. For me, e-ink looks its best when viewed under reflective light (ie the ambient light of the room you&amp;rsquo;re in) although the frontlight is definitely handy for when you&amp;rsquo;re in a darker environment.&lt;/p&gt;
&lt;p&gt;Something about this technology feels really warm and comfortable. I particularly like it on cheaper devices, they feel like some kind of future commodity tech, unlike the common contemporary fragile glass/metal slabs, which need babying, feeling like a responsibility more than a tool. So much modern tech is all about being the brightest and flashiest, shouting for attention.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>My OLED and E-ink Devices</title>
      <link>https://mm-dev.rocks/posts/oled-and-eink-4-life/my-oled-and-eink-devices/</link>
      <pubDate>Thu, 02 Nov 2023 15:47:18 +0000</pubDate>
      <guid>https://mm-dev.rocks/posts/oled-and-eink-4-life/my-oled-and-eink-devices/</guid>
      <description>&lt;p&gt;Generally, I prefer my computing devices in a tablet format. A display, with all the gubbins inside, which allows me to connect my input devices of choice without redundancy. It means I can position the display at an appropriate height. If I have a keyboard I like, I can use it with all of my devices, rather than it being glued inergonomically to a particular screen. The same applies to pointing devices (I like trackballs).&lt;/p&gt;
&lt;p&gt;Almost all of my e-ink and OLED devices have a Wacom layer, so I can use the same stylus for all of them.&lt;/p&gt;
&lt;p&gt;In compiling this article I&amp;rsquo;ve come to realise I&amp;rsquo;ve built up a little collection. I don&amp;rsquo;t &lt;em&gt;need&lt;/em&gt; all of these but find a certain comfort in using different devices for different purposes, they take up very little space and I get a lot of enjoyment from them. I bought them all second-hand and am a pretty keen shopper so got some great deals, rarely paying more than half the retail price. In some cases I bought broken ones and Frankensteined them into a single working unit. I guess there are worse ways to spend money.&lt;/p&gt;
&lt;small class=&#34;special&#34;&gt;
&lt;h4 id=&#34;onyx-linux-and-gplv2&#34;&gt;Onyx, Linux and GPLv2&lt;/h4&gt;
&lt;p&gt; &lt;/p&gt;
&lt;p&gt;&lt;em&gt;Onyx&lt;/em&gt; (the manufacturers of a couple of my e-ink devices detailed below) seem to be wilfully breaking the licensing terms of the Linux kernel, by refusing to release their modified version of this GPLv2-licensed work. It leaves a bad taste in my mouth as I love the concept of open source software. It&amp;rsquo;s a shame because although I have 2 of their devices, I kind of begrudge it, don&amp;rsquo;t really want to buy another in future, and it makes me not want to speak highly of them as a company.&lt;br&gt;
 &lt;/p&gt;
&lt;p&gt;I do think it&amp;rsquo;s a complex issue and may be just as likely to have roots in genuine cultural differences, as much as it might just be about greed. The &lt;em&gt;Wired&lt;/em&gt; documentary &lt;a href = &#34;https://www.youtube.com/watch?v=SGJ5cZnoodY&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;Shenzhen: The Silicon Valley of Hardware&lt;/a&gt; really affected my thinking on this, it&amp;rsquo;s an entertaining watch if you have any interest in modern technology, and touches on some different viewpoints on intellectual property.&lt;br&gt;
 &lt;/p&gt;
&lt;p&gt;It would be nice if &lt;em&gt;Onyx&lt;/em&gt; at least spoke openly about it, at the moment their attitude just feels like a middle finger to OSS.&lt;/p&gt;
&lt;/small&gt;

</description>
    </item>
    
    <item>
      <title>Staying Pragmatic</title>
      <link>https://mm-dev.rocks/posts/my-working-environment/staying-pragmatic/</link>
      <pubDate>Wed, 11 Oct 2023 10:37:55 +0000</pubDate>
      <guid>https://mm-dev.rocks/posts/my-working-environment/staying-pragmatic/</guid>
      <description>&lt;p&gt;I&amp;rsquo;ll never stop learning new things. A while back I built &lt;a href = &#34;https://mm-dev.rocks/posts/bendystraw-android-app/&#34; title = &#34;BendyStraw&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;my first Garmin watch app&lt;/a&gt;. This involved using:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A new-to-me language called &lt;a href = &#34;https://developer.garmin.com/connect-iq/monkey-c/&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;Monkey C&lt;/a&gt; and its special compiler&lt;/li&gt;
&lt;li&gt;A device simulator&lt;/li&gt;
&lt;li&gt;Garmin&amp;rsquo;s Connect IQ SDK&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most developers are doing this on Windows, using either &lt;em&gt;Eclipse&lt;/em&gt; or &lt;em&gt;VSCode&lt;/em&gt;. VSCode is very nice software but I try to avoid Microsoft. If I must use Windows I boot up a virtual machine, hold my nose and get the job done. But I prefer to use Linux.&lt;/p&gt;
&lt;p&gt;Arguments about Microsoft and their effect on the world tend to get long and boring, so I&amp;rsquo;ll just say I don&amp;rsquo;t like them myself and personally avoid their products insofar as it&amp;rsquo;s practical. No this doesn&amp;rsquo;t mean I like other megacorps like Google or Apple instead.&lt;/p&gt;
&lt;p&gt;Regardless, pragmatism means I have to use things from some of these companies some of the time. To exclude all megacorp-related products I&amp;rsquo;d have to avoid everything to do with computing. However, in life we have a finite number conscious moments. It&amp;rsquo;s not possible for every one of those moments to be spent perfectly, but the more I can move towards spending more of them in places I like, supporting things I believe in, and away from places I don&amp;rsquo;t like and things I don&amp;rsquo;t believe in, the better.&lt;/p&gt;
&lt;p&gt;Anyway when you&amp;rsquo;re learning new computer stuff, you&amp;rsquo;d best be using the same environment as the majority. Otherwise, you&amp;rsquo;re going to have lots of extra problems outside the normal expected beginner problems in that space. I don&amp;rsquo;t always mind this, I believe that doing things the hard way is rarely a waste of time as you have to solve problems which is, if nothing else, a useful exercise that keeps you sharp. Sometimes I can&amp;rsquo;t afford to take the hard way because of real time constraints or because I&amp;rsquo;m working on somebody else&amp;rsquo;s time. But not always.&lt;/p&gt;
&lt;p&gt;So if you want to learn how to develop for Garmin devices, or Android apps in Flutter, and you want to use Linux, the easiest way is to use the most popular/mainstream distro, Ubuntu. Instructions will usually be aimed at Ubuntu users, and the differences between Ubuntu and other distros aren&amp;rsquo;t always easy to work out.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>My &#39;Big PC&#39;</title>
      <link>https://mm-dev.rocks/posts/my-working-environment/pc-for-other-software-development/</link>
      <pubDate>Wed, 11 Oct 2023 10:35:55 +0000</pubDate>
      <guid>https://mm-dev.rocks/posts/my-working-environment/pc-for-other-software-development/</guid>
      <description>&lt;p&gt;Sometimes I need to step up to the next level and use a desktop PC:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Some development tools are not available for my Android system architecture (AArch64), namely Garmin Connect IQ tools and a small part of Flutter (which I use to develop Android apps)&lt;/li&gt;
&lt;li&gt;It&amp;rsquo;s useful to connect other devices via USB to test software on a real device (eg my Garmin watch) during development (while it may be possible to do this on Android in some cases using USB On-The-Go and wireless connections, there are likely to be parts of it that are very difficult or impossible)&lt;/li&gt;
&lt;li&gt;While learning completely new things, tutorials and guides will usually presume you&amp;rsquo;re using desktop software &amp;mdash; matching their environment can ease the process&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So I also have a small form-factor desktop PC, a Beelink SER4. It&amp;rsquo;s tiny, silent (after I modded the fan) and draws &amp;lt;10W during my average usage.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ryzen 7 4800U&lt;/li&gt;
&lt;li&gt;32GB RAM&lt;/li&gt;
&lt;li&gt;Couple of SSDs&lt;/li&gt;
&lt;li&gt;20V power input, meaning it fits in with my wiring setup without any extra adapters&lt;/li&gt;
&lt;/ul&gt;
&lt;ul class=&#39;gallery single-item&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_rXRT_1&#39; /&gt;
      &lt;label for=&#39;item_rXRT_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/working-env/beelink-ser4.jpg&#39; alt=&#39;Beelink SER4&#39; /&gt;
        &lt;span&gt;Beelink SER4&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I have it 
&lt;a data-image-ref=&#34;Beelink SER4&#34; href=&#34;#&#34;&gt;stuck underneath a storage cabinet with velcro&lt;/a&gt;
. It has no display, keyboard or mouse attached, so I continue using my Android tablet as a display, remote controlling the desktop via VNC (using AVNC). That way I carry on using my preferred trackball, keyboard and the OLED tablet display that I enjoy so much.&lt;/p&gt;
&lt;p&gt;The SER4 is running &lt;a href = &#34;https://voidlinux.org/&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;Void Linux&lt;/a&gt;. Void is great, and it can do anything any other Linux can do, but it is quite niche, and sometimes things need to be done in different ways. So I have an Ubuntu virtual machine too, plus some other VMs like Windows and MacOS for occasional testing.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Update: Now I use Proxmox, info coming soon&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Keeping My Equipment Powered</title>
      <link>https://mm-dev.rocks/posts/my-working-environment/keeping-my-equipment-powered/</link>
      <pubDate>Wed, 11 Oct 2023 10:34:55 +0000</pubDate>
      <guid>https://mm-dev.rocks/posts/my-working-environment/keeping-my-equipment-powered/</guid>
      <description>&lt;h3 id=&#34;solar-power&#34;&gt;Solar Power&lt;/h3&gt;
&lt;p&gt;My aim is to live completely off solar power. I&amp;rsquo;m not quite there yet. With my current system it works out roughly like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;During the &lt;em&gt;sunniest&lt;/em&gt; 6 months of the year &lt;em&gt;I can basically do what I want&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;During the &lt;em&gt;darker months&lt;/em&gt;, depending on how much and what kind of work I have (and therefore how much power my computing devices need), &lt;em&gt;I must supplement my power by plugging into mains electricity&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you&amp;rsquo;re used to having normal mains electricity, it can be hard to understand how much of a difference small changes to electrical usage can make to my day. People are naturally used to thinking in terms of cost: &amp;ldquo;that only costs a few pounds a year to run, it&amp;rsquo;s basically nothing&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;For me it&amp;rsquo;s not about cost &amp;mdash; at least not directly &amp;mdash; it&amp;rsquo;s about &amp;ldquo;can I watch a film this evening on the large tablet instead of the medium-sized one?&amp;rdquo; or &amp;ldquo;can I get through to December instead of November without hooking up to the grid?&amp;rdquo;. The difference between using a 10&amp;quot; or 15&amp;quot; tablet (a difference in electricity usage which might add up to pennies a month from the grid) is noticeable for me. The step up to my (low power!) desktop is significant.&lt;/p&gt;
&lt;p&gt;As a rough guide, I average about 1 unit (kWh) of electricity a day (if you&amp;rsquo;re in bricks and mortar that&amp;rsquo;s about 25p&amp;rsquo;s worth at time of writing). If you don&amp;rsquo;t know what that means, look at your electricity bill and see how many units you use a month or quarter. According to &lt;a href = &#34;https://www.ofgem.gov.uk/information-consumers/energy-advice-households/average-gas-and-electricity-use-explained&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;Ofgem&lt;/a&gt; at time of writing, a low-usage average 1-2 bedroom flat will use about 1,800kWh annually, so about 5kWh/day which is around 5x my usage.&lt;/p&gt;
&lt;p&gt;I realise a lot of people would hate living like this, but I enjoy it. I find it fun and interesting. What needs to get done, gets done. I work full-time hours (on my own projects if I don&amp;rsquo;t have a contract). I bake bread, cook most of my food from scratch from basic ingredients. I paint, I solder, I do bad carpentry. I watch films, listen to music. It&amp;rsquo;s all fine.&lt;/p&gt;
&lt;h3 id=&#34;flank-it&#34;&gt;Flank it&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;I&amp;rsquo;m pragmatic and prioritise work over my lifestyle experiments&lt;/em&gt;. If it&amp;rsquo;s the dead of winter and I need to work on something that requires me to use my desktop, I&amp;rsquo;ll get plugged in to mains. But outside that, I&amp;rsquo;m always looking to extend the proportion of the year where I can support myself with solar, with the end goal of course being 100% of the year.&lt;/p&gt;
&lt;p&gt;To do this I &lt;em&gt;squeeze the problem from two sides&lt;/em&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Increase the amount of solar power I can grab (eg by upgrading equipment)&lt;/li&gt;
&lt;li&gt;Decrease my energy usage, by making sure I&amp;rsquo;m always using the lowest-power device that I have which can adequately do the job&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Also, as a long-term bearing I steer more towards work which suits lower power, eg vanilla JS over long NPM builds, Vim over VSCode and so on.&lt;/p&gt;
&lt;h3 id=&#34;applying-progressive-enhancement-to-choosing-a-device&#34;&gt;Applying Progressive Enhancement to Choosing a Device&lt;/h3&gt;
&lt;p&gt;There&amp;rsquo;s an idea called &amp;lsquo;mobile first&amp;rsquo; or &amp;lsquo;progressive enhancement&amp;rsquo; in web development, where we aim first to make a website work properly on mobile devices, then work up through larger devices (tablets, desktops), tweaking visuals and possibly adding features as the devices get larger and more powerful.&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t personally like removing features for mobile devices &amp;mdash; the fact that I&amp;rsquo;m using a mobile device should not be taken as a signal that I want to be excluded from accessing functionality of your site.&lt;/p&gt;
&lt;p&gt;In terms of visual/interface design though, things do need to work differently depending on whether you&amp;rsquo;re at a small or large device, whether you have a touchscreen, a physical keyboard, are using a screen-reader due to problems with vision or some other reason&amp;hellip;&lt;/p&gt;
&lt;p&gt;I apply something similar to this method of progressive enhancement when choosing which computing device to use for work. My preference is to use an Android tablet (&lt;a href = &#34;https://mm-dev.rocks/posts/android-as-a-dev-environment/intro/&#34; title = &#34;Android as a Dev Environment&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;where you can get a surprising amount done nowadays&lt;/a&gt;), stepping up to a Linux PC when more power is needed or tools aren&amp;rsquo;t available on Android, then adding virtual machines if eg I need to use Windows.&lt;/p&gt;
&lt;p&gt;So I start at the top of this list and work my way down as the jobs get bigger.&lt;/p&gt;
&lt;ol start=&#34;0&#34;&gt;
&lt;li&gt;Android&lt;/li&gt;
&lt;li&gt;Linux PC&lt;/li&gt;
&lt;li&gt;Windows/MacOS VMs&lt;/li&gt;
&lt;li&gt;Online build tools&lt;/li&gt;
&lt;/ol&gt;
</description>
    </item>
    
    <item>
      <title>Intro</title>
      <link>https://mm-dev.rocks/posts/my-working-environment/intro/</link>
      <pubDate>Wed, 11 Oct 2023 10:33:55 +0000</pubDate>
      <guid>https://mm-dev.rocks/posts/my-working-environment/intro/</guid>
      <description>&lt;h2 id=&#34;small-space-small-footprint&#34;&gt;Small Space, Small Footprint&lt;/h2&gt;
&lt;h3 id=&#34;cara-van-life&#34;&gt;(Cara)-Van Life&lt;/h3&gt;
&lt;p&gt;A few years ago I decided to leave bricks and mortar behind and move into a caravan. A small, summer touring caravan, German-built in 1983. The construction is more solid than a modern &lt;a href = &#34;https://www.etymonline.com/word/van&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;&amp;lsquo;van&lt;/a&gt; would be, but it is, ultimately, built for summer.
&lt;ul class=&#39;gallery single-item&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_4CqQ_1&#39; /&gt;
      &lt;label for=&#39;item_4CqQ_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/working-env/cvan.jpg&#39; alt=&#39;Painted caravan&#39; /&gt;
        &lt;span&gt;Painted caravan&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;
&lt;/p&gt;
&lt;p&gt;The first couple of winters in here were hard, but there was a feeling of excitement and adventure which made it tolerable. Now those feelings have lessened, but I&amp;rsquo;ve got better at managing the seasonal extremes of temperature, potential for damp/mould, adapted my cooking methods and so on. Now it&amp;rsquo;s just normal life.&lt;/p&gt;
&lt;p&gt;And that incudes work.&lt;/p&gt;
&lt;h3 id=&#34;owning-things&#34;&gt;Owning Things&lt;/h3&gt;
&lt;p&gt;Although I enjoy the caravan, my decision to move into it came from a more general desire for a change in lifestyle.&lt;/p&gt;
&lt;p&gt;I used to have a lot of possessions. Having to drag them around with me was a limit on my options in life. Any new flat or house had to big enough. Larger homes cost more. Moving vans have to be bigger. Things need looking after, they get dusty or broken. As they say, &amp;ldquo;your possessions end up owning you&amp;rdquo;. I wanted to stop owning so many things and to feel more adaptable.&lt;/p&gt;
&lt;p&gt;Now that I live in a tiny space I can&amp;rsquo;t afford to hang on to things &amp;ldquo;just in case&amp;rdquo;, anything I decide to keep has to have earned its place. Storage space is precious.&lt;/p&gt;
&lt;p&gt;&lt;a href = &#34;https://www.theminimalists.com/minimalism/&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;Minimalism&lt;/a&gt; has become fashionable and there are some extremists who treat it like a cult, never the less it contains some ideas which I find appealing. I doubt anybody would call my current living space minimalist, but I have applied minimalist thinking to decisions about what to keep and what to sell/give/throw away.&lt;/p&gt;
&lt;p&gt;I am a maker though. I paint, cook, repair stuff, tinker with electronics. So I need at least a basic set of tools and equipment. And my space is &lt;em&gt;really small&lt;/em&gt; (about 6ft wide, 6ft tall, 13ft long). So my home will never be one of those white empty halls you see in the pictures of minimalist homes. My space still looks cluttered&amp;hellip; but everything in here does belong and has purpose.&lt;/p&gt;
&lt;p&gt;Most importantly for me, I&amp;rsquo;ve changed my level of attachment to things. I still appreciate well-made objects which perform their role well. But I know now that when I no longer have those specific objects, I will pick up new versions. Things I need, to do what I want to do, will come and go. My attachment to the specific items is less than it used to be.&lt;/p&gt;
&lt;h3 id=&#34;digital-transience&#34;&gt;Digital Transience&lt;/h3&gt;
&lt;p&gt;The modern world of software fits nicely with this mentality. I don&amp;rsquo;t need hard drives full of stuff. Anything important to me is in git repos (I prefer &lt;a href = &#34;https://codeberg.org/&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;Codeberg&lt;/a&gt;), or is backed up on somebody else&amp;rsquo;s computer (ie the cloud).&lt;/p&gt;
&lt;p&gt;If I lose all my possessions tomorrow, the day after tomorrow (presuming some of my brain is still functioning) I&amp;rsquo;ll be able to start picking up from where I left off, even if all I can afford is an old &lt;a href = &#34;https://mm-dev.rocks/posts/android-as-a-dev-environment/intro/&#34; title = &#34;Android as a Dev Environment&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;Android tablet and Bluetooth keyboard&lt;/a&gt;.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Work Considerations</title>
      <link>https://mm-dev.rocks/posts/my-working-environment/work-considerations/</link>
      <pubDate>Wed, 11 Oct 2023 10:33:55 +0000</pubDate>
      <guid>https://mm-dev.rocks/posts/my-working-environment/work-considerations/</guid>
      <description>&lt;p&gt;I&amp;rsquo;ve had to adapt my working methods to fit with this current lifestyle, namely to solve problems related to the following key areas:&lt;/p&gt;
&lt;ul class=&#39;gallery single-item&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_27om_1&#39; /&gt;
      &lt;label for=&#39;item_27om_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/working-env/tablet-arms.jpg&#39; alt=&#39;Tablet arms&#39; /&gt;
        &lt;span&gt;Tablet arms&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;physical-working-space&#34;&gt;Physical Working Space&lt;/h3&gt;
&lt;p&gt;Small! So I have a 
&lt;a data-image-ref=&#34;Tablet arms&#34; href=&#34;#&#34;&gt;couple of tablet arms&lt;/a&gt;
 extending from a pole near the ceiling. They provide me with a nice healthy angle for my tablets while I&amp;rsquo;m working, then when I&amp;rsquo;m not working and don&amp;rsquo;t need them they can be pushed out of the way so I don&amp;rsquo;t hit my head on them.&lt;/p&gt;
&lt;p&gt;I also have a 
&lt;a data-image-ref=&#34;Keyboard tray&#34; href=&#34;#&#34;&gt;little tray to hold my keyboard and trackball&lt;/a&gt;
. It can go on my lap or on top of a small beanbag (meditation cushion) for a change in position.
&lt;ul class=&#39;gallery single-item&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_kjL2_1&#39; /&gt;
      &lt;label for=&#39;item_kjL2_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/working-env/keyboard-tray.jpg&#39; alt=&#39;Keyboard tray&#39; /&gt;
        &lt;span&gt;Keyboard tray&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;
&lt;/p&gt;
&lt;h3 id=&#34;noise&#34;&gt;Noise&lt;/h3&gt;
&lt;p&gt;Rain on the roof is loud! And I&amp;rsquo;m often on farms where animals and machinery make noises. This is easily solved for me by earbuds, and for anybody I might be talking to remotely a mic helps. I use an 
&lt;a data-image-ref=&#34;Audio adapter&#34; href=&#34;#&#34;&gt;MPOW Bluetooth Receiver&lt;/a&gt;
 which has a built-in mic and a 3.5mm jack to plug my earbuds into.
&lt;ul class=&#39;gallery single-item&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_Aokw_1&#39; /&gt;
      &lt;label for=&#39;item_Aokw_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/working-env/bt-audio-adapter.jpg&#39; alt=&#39;Audio adapter&#39; /&gt;
        &lt;span&gt;Audio adapter&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;
&lt;/p&gt;
&lt;h3 id=&#34;thermal-comfort&#34;&gt;Thermal Comfort&lt;/h3&gt;
&lt;p&gt;It gets really hot in here in summer. As soon as the sun hits the metal walls I start to bake. For large parts of summer afternoons it&amp;rsquo;s 35C or above, it gets up to 45C at times if I use the oven in the afternoon! One pro tip is to not use the oven in the afternoon. I try to plan my summer cooking so that any oven usage happens as early in the morning or late in the evening as possible.&lt;/p&gt;
&lt;p&gt;Eventually, clothing comes off. A fan also helps, I found some large 12V fans on Ali Express. One thing about solar power: if it&amp;rsquo;s hot enough to need a fan, there&amp;rsquo;s probably enough sun to power it.&lt;/p&gt;
&lt;p&gt;And the winter she is cold. And damp. My first couple of years I had a badly-situated gas heater, and I hadn&amp;rsquo;t learned some of the basics, like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Not to have too many soft absorbent furnishings, or excess clothing hanging around holding on to water and blocking airflow&lt;/li&gt;
&lt;li&gt;Blinds, not curtains&amp;hellip; curtains soak up condensation from the windows&lt;/li&gt;
&lt;li&gt;The importance of ventilation (no matter how cold it might be, even &lt;a href = &#34;https://en.wikipedia.org/wiki/Igloo&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;igloos&lt;/a&gt; need airflow)&lt;/li&gt;
&lt;li&gt;Wool! The sheep worked out a material for cold and damp a long time ago .&lt;/li&gt;
&lt;li&gt;Heat, dur. I was too stingy with it in the early days, partly because it didn&amp;rsquo;t seem to have much effect due to all the fabric and useless clothing laying around. Regular heating of air and objects has to happen in the cold/wet months.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In those early days I often sat in &amp;lsquo;ideal refrigeration temperatures&amp;rsquo; (2-5C). Honestly it was quite miserable at times &amp;mdash; that feeling of adventure and hot drinks were all that kept me going.&lt;/p&gt;
&lt;p&gt;Nowadays I&amp;rsquo;ve got a lovely wood burning stove, wool everything and better airflow. It&amp;rsquo;s &lt;em&gt;cosy&lt;/em&gt;.&lt;/p&gt;
&lt;h3 id=&#34;power&#34;&gt;Power&lt;/h3&gt;
&lt;p&gt;I sustain myself with solar/photo-voltaic for as much of the year as possible, but have to ensure I have access to mains electricity during the darker winter months.&lt;/p&gt;
&lt;p&gt;In summer I have ample power, and there&amp;rsquo;s plenty of potential to extend the portion of the year when I can be electrically self-sufficient with some improvements to my system.&lt;/p&gt;
&lt;p&gt;Soon I need to upgrade to better panels. Mine are old so not at their top efficiency, plus there have been technological improvements since they were manufactured.&lt;/p&gt;
&lt;p&gt;When it&amp;rsquo;s time to upgrade my battery I&amp;rsquo;ll move from lead acid over to LiFePO4. Compaared to lead, these have a much better weight:capacity ratio, deeper discharge (lead acid have to be kept above about 50% charge so you only really get to use half of their rated capacity) and can hold a charge for longer.&lt;/p&gt;
&lt;h3 id=&#34;connectivity&#34;&gt;Connectivity&lt;/h3&gt;
&lt;p&gt;Mobile broadband is good and relatively cheap in the UK, I keep 3 different SIMS/networks available via:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A 3G/4G router (a &lt;a href = &#34;https://www.gl-inet.com/products/gl-x750/&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;GL.iNet GL-X750 (Spitz)&lt;/a&gt; running the open source router firmware &lt;a href = &#34;https://openwrt.org/&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;OpenWRT&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;My mobile phone as a hotspot&lt;/li&gt;
&lt;li&gt;A pay-as-you-go SIM which I keep barely alive (by sending an SMS before it gets killed for inactivity) as a backup&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each of these connections is on a different network provider, so between them I have 3 of the big 4 UK network operators covered and there aren&amp;rsquo;t many places where I can&amp;rsquo;t get a decent connection.&lt;/p&gt;
&lt;h4 id=&#34;router&#34;&gt;Router&lt;/h4&gt;
&lt;p&gt;The Spitz router is my main internet connection, the other 2 SIMs are on relatively low data bundles but can be topped up within minutes if my main connection is struggling and I need internet urgently eg for work.&lt;/p&gt;
&lt;p&gt;The &lt;a href = &#34;https://docs.gl-inet.com/router/en/3/specification/gl-x750/&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;router spec is here&lt;/a&gt;. It says that the maximum power draw is 6W. There&amp;rsquo;s an option in the settings to turn down the wifi power. Lower power means the signal doesn&amp;rsquo;t travel as far, but in this tiny space I can turn it right down to its minimum without noticing any problems. I measure it at about 2-3W.&lt;/p&gt;
&lt;p&gt;I also turn off the LEDs. This saves a minimal amount of power but I don&amp;rsquo;t like them flickering at night when I&amp;rsquo;m in bed.&lt;/p&gt;
&lt;h4 id=&#34;antenna&#34;&gt;Antenna&lt;/h4&gt;
&lt;p&gt;To maximise signal I&amp;rsquo;ve tried a few different &lt;em&gt;antennae&lt;/em&gt; and ended up with the &lt;a href = &#34;https://poynting.tech/antennas/xpol-1/&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;Poynting XPOL-1&lt;/a&gt; which can be mounted outside on a pole. It&amp;rsquo;s unidirectional, meaning it doesn&amp;rsquo;t need to be pointed towards the cell tower.&lt;/p&gt;
&lt;p&gt;With the radio bandwidths at which mobile networks operate, cables are really important too. Poynting supplies good thick cables and I&amp;rsquo;ve trimmed them as longer cables deteriorate the signal. From what I&amp;rsquo;ve read:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Different cable specs have different signal loss&lt;/li&gt;
&lt;li&gt;5ft of LMR100 cable (for example) is about a 2dB loss&lt;/li&gt;
&lt;li&gt;3dB loss means the signal is 50% weaker&lt;/li&gt;
&lt;li&gt;So a &lt;em&gt;15ft run of LMR100 cable&lt;/em&gt; means the signal goes down 6dB meaning you&amp;rsquo;re left with &lt;em&gt;only 25% of the original signal&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;time-and-motion&#34;&gt;Time and Motion&lt;/h3&gt;
&lt;p&gt;A lot of modern life is about stability&amp;hellip; we want to be at the same temperature all year round (so we can wear t-shirts in winter, for example). Buildings are about making a static environment. Nothing moves on its own. If you put something down somewhere (and live alone!) that thing will still be there in a day, a month, a year. We take this for granted but if you think about it, it&amp;rsquo;s a situation which wouldn&amp;rsquo;t often happen in nature. If you put something down in the forest, a creature or some weather will come along and move it soon enough.&lt;/p&gt;
&lt;p&gt;My home wobbles when it&amp;rsquo;s windy. On dark stormy nights, when the paraffin lantern is gently swinging and throwing its light around dramatically, there&amp;rsquo;s no atmosphere like it. For maximum feeling add a Moby Dick audiobook.&lt;/p&gt;
&lt;p&gt;Not to disparage the impressive achievements of technology but I like to feel the temperature extremes and to plan my day differently in summer vs winter. That&amp;rsquo;s one thing I love about living like this &amp;mdash; I feel less insulated from the seasons, they have more meaning for me than they used to.&lt;/p&gt;
&lt;h3 id=&#34;solstices-and-equinoxes&#34;&gt;Solstices and Equinoxes&lt;/h3&gt;
&lt;p&gt;The length of the days and angle of sun in the sky throughout the year affect how many photons I can harvest for power.&lt;/p&gt;
&lt;p&gt;I never expected to pay such close attention to equinoxes and solstices, those key points in the annual solar cycle. I feel a deeper understanding now of why they would have been so important to our ancestors, to the point that they were celebrated.&lt;/p&gt;
&lt;p&gt;Not everybody knows what these things are, so just in case:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Around the 21st of December, the &lt;em&gt;winter solstice&lt;/em&gt; is the &lt;em&gt;shortest&lt;/em&gt; day of the year &amp;mdash; after that the days start getting longer&lt;/li&gt;
&lt;li&gt;Around the 21st of June, the &lt;em&gt;summer solstice&lt;/em&gt; is the &lt;em&gt;longest&lt;/em&gt; day, after which the days get shorter&lt;/li&gt;
&lt;li&gt;The spring (20th of March) and autumn (22nd September) &lt;em&gt;equinoxes&lt;/em&gt; are when the &lt;em&gt;day and night are of equal length&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;All the above is &amp;lsquo;roughly correct&amp;rsquo;, the exact days and times vary depending on how far away you are from the equator and the year, and can drift by a calendar day or so.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I also notice the moon more than I used to. I have large windows on every wall, none of them more than a few feet away. I use thermal blackout blinds (they help in summer) but moonlight bleeds around their edges, so there&amp;rsquo;s always at least a vague awareness of how bright it is. I also have a skylight which is usually open when I&amp;rsquo;m in here, and in certain positions at certain times the moon shines right down through the opening, onto me as I sleep. I&amp;rsquo;m often woken in the middle of the night by a moonbeam.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>dontdillydally (Web App)</title>
      <link>https://mm-dev.rocks/posts/dontdillydally-web-app/</link>
      <pubDate>Tue, 10 Oct 2023 13:17:52 +0100</pubDate>
      <guid>https://mm-dev.rocks/posts/dontdillydally-web-app/</guid>
      <description>&lt;p&gt;&lt;strong&gt;dontdillydally&lt;/strong&gt; helps you track time and money.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Aimed at the self-employed&lt;/li&gt;
&lt;li&gt;Focused on simplicity and ease of use&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As this is a web app you don&amp;rsquo;t need to install it, just go to &lt;a href = &#34;https://dontdillydally.app&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;dontdillydally.app&lt;/a&gt; to start using it immediately in your browser.&lt;/p&gt;
&lt;h2 id=&#34;how-to-use&#34;&gt;How to Use&lt;/h2&gt;
&lt;p&gt;&lt;img class=&#34;inline-icon&#34; src=&#34;https://mm-dev.rocks/images/dontdillydally/icon-linnet.png&#34; /&gt;
 &lt;strong&gt;Tap the bird at any time in the app to see the built-in guide&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;br&gt;
On the left-hand side of the app you will see icons which you can tap to change which section you&amp;rsquo;re on or perform other functions.&lt;/p&gt;






&lt;div class=&#34;expand-all-toggle&#34;&gt;
    &lt;label for=&#34;expandAllCheckbox_Ag43&#34;&gt;&lt;h3 id=&#34;tap-below-to-expand-each-section-and-learn-more-about-it&#34;&gt;Tap below to expand each section and learn more about it:&lt;/h3&gt;
&lt;span&gt;Expand all&lt;/span&gt;
      &lt;input id=&#34;expandAllCheckbox_Ag43&#34; type=&#34;checkbox&#34; onchange=&#34;setChildExpandedStates_Ag43();&#34;/&gt;
    &lt;/label&gt;
  
  &lt;script&gt;
    function setChildExpandedStates_Ag43() {
      var checkbox_el = document.getElementById(&#39;expandAllCheckbox_Ag43&#39;);
      var all = checkbox_el.parentNode.parentNode.getElementsByTagName(&#39;details&#39;);
      for (i = 0; i &lt; all.length; i++) { 
        all[i].open = checkbox_el.checked;
      }
    }
    window.addEventListener(&#39;load&#39;, () =&gt; {
      document.getElementById(&#39;expandAllCheckbox_Ag43&#39;).parentNode.parentNode.classList.add(&#39;js-visible&#39;);
      setChildExpandedStates_Ag43();
    });
  &lt;/script&gt;
&lt;details&gt;
&lt;summary&gt;&lt;span&gt;&lt;img class=&#34;inline-icon&#34; src=&#34;https://mm-dev.rocks/images/dontdillydally/icon-worksheet.png&#34; /&gt;
 Worksheet&lt;/span&gt;&lt;/summary&gt;

&lt;div&gt;
&lt;p&gt;This is the page you&amp;rsquo;ll use the most, it&amp;rsquo;s where you enter your hours worked and any money coming in or going out.&lt;/p&gt;
&lt;p&gt;A worksheet is made of &lt;strong&gt;entries&lt;/strong&gt;. An &lt;strong&gt;entry&lt;/strong&gt; is one of the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;time&lt;/strong&gt; spent on a specific job&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;money&lt;/strong&gt; spent on something (expenses)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;money&lt;/strong&gt; coming in (income)&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;To add an entry, just tap on the date where you want to add it&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;Each &lt;strong&gt;entry&lt;/strong&gt; can have a &lt;strong&gt;client&lt;/strong&gt; and/or a &lt;strong&gt;job&lt;/strong&gt; selected. If you do this you&amp;rsquo;ll then be able to get a &lt;strong&gt;report&lt;/strong&gt; showing you how many hours you worked on that job/client, or how much money you made or spent.&lt;/p&gt;
&lt;p&gt;You can also add &lt;strong&gt;Notes&lt;/strong&gt; to an entry.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;All data fields in an entry are optional, add as much or as little info as you like.&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;By default when you add a new &lt;strong&gt;entry&lt;/strong&gt; it will be a &lt;strong&gt;time&lt;/strong&gt; entry, and show a &lt;strong&gt;clock icon&lt;/strong&gt; next to it.&lt;/li&gt;
&lt;li&gt;If you tap the clock, the icon changes to a &lt;strong&gt;credit card&lt;/strong&gt; and the entry becomes a &lt;strong&gt;money&lt;/strong&gt; entry.&lt;/li&gt;
&lt;li&gt;To enter an expense, set the value of a money entry to something negative eg &lt;code&gt;-100.00&lt;/code&gt; means you spent 100.&lt;/li&gt;
&lt;/ul&gt;
&lt;ul class=&#39;gallery&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_e8Hs_1&#39; /&gt;
      &lt;label for=&#39;item_e8Hs_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/dontdillydally/time-entry-1.png&#39; alt=&#39;Worksheet: Time entry&#39; /&gt;
        &lt;span&gt;Worksheet: Time entry&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_e8Hs_2&#39; /&gt;
      &lt;label for=&#39;item_e8Hs_2&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_e8Hs_3&#39; /&gt;
      &lt;label for=&#39;item_e8Hs_3&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_e8Hs_4&#39; /&gt;
      &lt;label for=&#39;item_e8Hs_4&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/details&gt;


&lt;details&gt;
&lt;summary&gt;&lt;span&gt;&lt;img class=&#34;inline-icon&#34; src=&#34;https://mm-dev.rocks/images/dontdillydally/icon-reports.png&#34; /&gt;
 Reports&lt;/span&gt;&lt;/summary&gt;

&lt;div&gt;
&lt;p&gt;Here you can get a clear view of everything entered into an entire worksheet, or narrow down on a specific job or client:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Choose a date range from within your worksheet&lt;/li&gt;
&lt;li&gt;Choose a specfic client or job&lt;/li&gt;
&lt;li&gt;Look at only money in/out (hide hours)&lt;/li&gt;
&lt;li&gt;Look at only hours worked (hide money)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then you&amp;rsquo;ll be able to see:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;How much time you&amp;rsquo;ve spent&lt;/li&gt;
&lt;li&gt;How much you&amp;rsquo;ve earned&lt;/li&gt;
&lt;li&gt;A list of all worksheet entries matching your selection showing eg time, monetary values, notes&lt;/li&gt;
&lt;/ul&gt;
&lt;ul class=&#39;gallery&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_ZVCf_1&#39; /&gt;
      &lt;label for=&#39;item_ZVCf_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/dontdillydally/report-bs-1.png&#39; alt=&#39;Reports: &amp;#39;BendyStraw&amp;#39; job selection&#39; /&gt;
        &lt;span&gt;Reports: &amp;#39;BendyStraw&amp;#39; job selection&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_ZVCf_2&#39; /&gt;
      &lt;label for=&#39;item_ZVCf_2&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/dontdillydally/report-bs-2.png&#39; alt=&#39;Reports: &amp;#39;BendyStraw&amp;#39; job results&#39; /&gt;
        &lt;span&gt;Reports: &amp;#39;BendyStraw&amp;#39; job results&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_ZVCf_3&#39; /&gt;
      &lt;label for=&#39;item_ZVCf_3&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/details&gt;


&lt;details&gt;
&lt;summary&gt;&lt;span&gt;&lt;img class=&#34;inline-icon&#34; src=&#34;https://mm-dev.rocks/images/dontdillydally/icon-jobs-clients.png&#34; /&gt;
 Jobs and Clients&lt;/span&gt;&lt;/summary&gt;

&lt;div&gt;
&lt;ul class=&#39;gallery float single-item&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_s9CR_1&#39; /&gt;
      &lt;label for=&#39;item_s9CR_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Jobs&lt;/strong&gt; and &lt;strong&gt;Clients&lt;/strong&gt; can be created and edited here, including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A name for the job or client&lt;/li&gt;
&lt;li&gt;Colours (foreground and background) used to represent the job or client in the app&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;details&gt;
&lt;summary&gt;&lt;span&gt;&lt;img class=&#34;inline-icon&#34; src=&#34;https://mm-dev.rocks/images/dontdillydally/icon-settings.png&#34; /&gt;
 Settings&lt;/span&gt;&lt;/summary&gt;

&lt;div&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;/div&gt;







&lt;div class=&#34;expand-all-toggle&#34;&gt;
    &lt;label for=&#34;expandAllCheckbox_eYra&#34;&gt;&lt;h3 id=&#34;the-other-icons-perform-actions-immediately-without-changing-the-page&#34;&gt;The other icons perform actions immediately without changing the page:&lt;/h3&gt;
&lt;span&gt;Expand all&lt;/span&gt;
      &lt;input id=&#34;expandAllCheckbox_eYra&#34; type=&#34;checkbox&#34; onchange=&#34;setChildExpandedStates_eYra();&#34;/&gt;
    &lt;/label&gt;
  
  &lt;script&gt;
    function setChildExpandedStates_eYra() {
      var checkbox_el = document.getElementById(&#39;expandAllCheckbox_eYra&#39;);
      var all = checkbox_el.parentNode.parentNode.getElementsByTagName(&#39;details&#39;);
      for (i = 0; i &lt; all.length; i++) { 
        all[i].open = checkbox_el.checked;
      }
    }
    window.addEventListener(&#39;load&#39;, () =&gt; {
      document.getElementById(&#39;expandAllCheckbox_eYra&#39;).parentNode.parentNode.classList.add(&#39;js-visible&#39;);
      setChildExpandedStates_eYra();
    });
  &lt;/script&gt;

&lt;details&gt;
&lt;summary&gt;&lt;span&gt;&lt;img class=&#34;inline-icon&#34; src=&#34;https://mm-dev.rocks/images/dontdillydally/icon-maximise.png&#34; /&gt;
 Minimise/maximise menu&lt;/span&gt;&lt;/summary&gt;

&lt;div&gt;
&lt;blockquote&gt;
&lt;p&gt;Toggles whether the menu is minimised or maximised.&lt;/p&gt;&lt;/blockquote&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th style=&#34;text-align: right&#34;&gt;&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;Minimised&lt;/strong&gt;&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Show only icons&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;Maximised&lt;/strong&gt;&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Show icons + text labels&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;details&gt;
&lt;summary&gt;&lt;span&gt;&lt;img class=&#34;inline-icon&#34; src=&#34;https://mm-dev.rocks/images/dontdillydally/icon-sync.png&#34; /&gt;
 WebDAV sync&lt;/span&gt;&lt;/summary&gt;

&lt;div&gt;
&lt;p&gt;This is an experimental feature to sync your data to a WebDAV server so you can keep several devices in sync.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s a work-in-progress and not really recommended for most users at this point.&lt;/p&gt;
&lt;/div&gt;
&lt;/details&gt;

&lt;details&gt;
&lt;summary&gt;&lt;span&gt;&lt;img class=&#34;inline-icon&#34; src=&#34;https://mm-dev.rocks/images/dontdillydally/icon-download.png&#34; /&gt;
 Download data as file&lt;/span&gt;&lt;/summary&gt;

&lt;div&gt;
Tap this to download all of your data and settings in a file. It will usually end up in your main &lt;code&gt;Downloads&lt;/code&gt; folder.&lt;/div&gt;
&lt;/details&gt;

&lt;details&gt;
&lt;summary&gt;&lt;span&gt;&lt;img class=&#34;inline-icon&#34; src=&#34;https://mm-dev.rocks/images/dontdillydally/icon-upload.png&#34; /&gt;
 Upload data file&lt;/span&gt;&lt;/summary&gt;

&lt;div&gt;
&lt;p&gt;If you have a file you previously downloaded you can bring that data back into the app with this button.&lt;/p&gt;
&lt;p&gt;When importing the data you will be given the option to &lt;code&gt;Merge&lt;/code&gt; or &lt;code&gt;Overwrite&lt;/code&gt; your current data.&lt;/p&gt;
&lt;/div&gt;
&lt;/details&gt;

&lt;details&gt;
&lt;summary&gt;&lt;span&gt;&lt;img class=&#34;inline-icon&#34; src=&#34;https://mm-dev.rocks/images/dontdillydally/icon-email.png&#34; /&gt;
 Email data to a preconfigured address&lt;/span&gt;&lt;/summary&gt;

&lt;div&gt;
You need to set an email address in &lt;code&gt;Settings&lt;/code&gt; for this option to work.
Your device also needs to know what to do with &lt;code&gt;mailto:&lt;/code&gt; links.&lt;/div&gt;
&lt;/details&gt;

&lt;details&gt;
&lt;summary&gt;&lt;span&gt;&lt;img class=&#34;inline-icon&#34; src=&#34;https://mm-dev.rocks/images/dontdillydally/icon-wipe.png&#34; /&gt;
 Wipe your data&lt;/span&gt;&lt;/summary&gt;

&lt;div&gt;
Completely wipe all of your data and start fresh.&lt;/div&gt;
&lt;/details&gt;


&lt;/div&gt;

&lt;h2 id=&#34;data-storage&#34;&gt;Data Storage&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Automatically saved continually while you work &lt;em&gt;The bird&amp;rsquo;s eye spins when changes are being saved, this is intentionally kept subtle but can be disabled in settings&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Your data is stored inside your browser and stays on your device, it never travels over the internet (unless you decide to send it via email) and is not stored on any servers&lt;/li&gt;
&lt;li&gt;Data uses a simple JSON format, it can be exported/imported as a file to move it between devices&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>BendyStraw (Android App)</title>
      <link>https://mm-dev.rocks/posts/bendystraw-android-app/</link>
      <pubDate>Tue, 10 Oct 2023 13:17:20 +0100</pubDate>
      <guid>https://mm-dev.rocks/posts/bendystraw-android-app/</guid>
      <description>&lt;p&gt;If you don&amp;rsquo;t already know, &lt;a href = &#34;https://newpipe.net/&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;NewPipe&lt;/a&gt; is a privacy-friendly, ad-free Android app for accessing YouTube. It&amp;rsquo;s a great piece of software and I use it all the time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NewPipe&lt;/strong&gt; works without an account. You can subscribe to channels, save playlists and do other useful things, but everything is saved locally on your device (and I love that it works like this).&lt;/p&gt;
&lt;p&gt;I use &lt;strong&gt;NewPipe&lt;/strong&gt; on several Android devices. Over time I&amp;rsquo;ve ended up with different playlists and subscriptions on each device, and sometimes I can&amp;rsquo;t be sure which device I need to use to find a certain song or video. To help with this I made &lt;strong&gt;BendyStraw&lt;/strong&gt;.&lt;/p&gt;
&lt;ul class=&#39;gallery&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_OIkr_1&#39; /&gt;
      &lt;label for=&#39;item_OIkr_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/bendy-straw/bendystraw-grab-3.webp&#39; alt=&#39;Multiple databases in colour-coded tabs&#39; /&gt;
        &lt;span&gt;Multiple databases in colour-coded tabs&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_OIkr_2&#39; /&gt;
      &lt;label for=&#39;item_OIkr_2&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/bendy-straw/bendystraw-grab-1.webp&#39; alt=&#39;Copy streams from one playlist to another&#39; /&gt;
        &lt;span&gt;Copy streams from one playlist to another&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_OIkr_3&#39; /&gt;
      &lt;label for=&#39;item_OIkr_3&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/bendy-straw/bendystraw-grab-2.webp&#39; alt=&#39;Selecting streams to delete, copy or move&#39; /&gt;
        &lt;span&gt;Selecting streams to delete, copy or move&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_OIkr_4&#39; /&gt;
      &lt;label for=&#39;item_OIkr_4&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/bendy-straw/bendystraw-grab-5.webp&#39; alt=&#39;Create new playlists&#39; /&gt;
        &lt;span&gt;Create new playlists&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;BendyStraw&lt;/strong&gt; imports &lt;code&gt;NewPipeData-*.zip&lt;/code&gt; files and lets you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Open multiple &lt;code&gt;zip&lt;/code&gt;s at the same time, so you can combine data from several devices&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Delete&lt;/code&gt; &lt;code&gt;Copy&lt;/code&gt; &lt;code&gt;Move&lt;/code&gt; &lt;code&gt;Rename&lt;/code&gt; your custom playlists&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Delete&lt;/code&gt; &lt;code&gt;Copy&lt;/code&gt; &lt;code&gt;Move&lt;/code&gt; streams from one playlist to another&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Delete&lt;/code&gt; &lt;code&gt;Copy&lt;/code&gt; &lt;code&gt;Move&lt;/code&gt; channel subscriptions between databases&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Delete&lt;/code&gt; &lt;code&gt;Copy&lt;/code&gt; &lt;code&gt;Move&lt;/code&gt; remote (bookmarked) playlists&lt;/li&gt;
&lt;li&gt;Re-order playlists, sorting the streams by &lt;code&gt;Title&lt;/code&gt;, &lt;code&gt;Channel&lt;/code&gt; or &lt;code&gt;Length&lt;/code&gt; (just tap the column headings in the tables)&lt;/li&gt;
&lt;li&gt;Streams (audio/video) can be opened directly from &lt;strong&gt;BendyStraw&lt;/strong&gt;, as the URLs are clickable
&lt;ul&gt;
&lt;li&gt;If you set video links (in your Android settings) to open in &lt;strong&gt;NewPipe&lt;/strong&gt; you can make a split-screen view and jump around your playlists&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Export playlist as raw text, for example to be used with &lt;a href = &#34;https://github.com/yt-dlp/yt-dlp&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;yt-dlp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Dark/light themes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After editing simply export a new &lt;code&gt;zip&lt;/code&gt; file, then import it back into &lt;strong&gt;NewPipe&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id=&#34;installation&#34;&gt;Installation&lt;/h2&gt;
&lt;h3 id=&#34;f-droid&#34;&gt;F-Droid&lt;/h3&gt;
&lt;p&gt;Search for &amp;lsquo;bendystraw&amp;rsquo; (all one word) in the F-Droid app, or find it at &lt;a href = &#34;https://f-droid.org/en/packages/rocks.mm_dev.BendyStraw/&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;https://f-droid.org/en/packages/rocks.mm_dev.BendyStraw/&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&#34;direct-download&#34;&gt;Direct download&lt;/h3&gt;
&lt;p&gt;The apks are also directly available on the Codeberg &lt;a href = &#34;https://codeberg.org/mm-dev/bendy-straw/releases&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;releases page&lt;/a&gt;.&lt;/p&gt;






&lt;div class=&#34;expand-all-toggle&#34;&gt;
    &lt;label for=&#34;expandAllCheckbox_1ZU7&#34;&gt;&lt;h2 id=&#34;how-to-use&#34;&gt;How to Use&lt;/h2&gt;
&lt;span&gt;Expand all&lt;/span&gt;
      &lt;input id=&#34;expandAllCheckbox_1ZU7&#34; type=&#34;checkbox&#34; onchange=&#34;setChildExpandedStates_1ZU7();&#34;/&gt;
    &lt;/label&gt;
  
  &lt;script&gt;
    function setChildExpandedStates_1ZU7() {
      var checkbox_el = document.getElementById(&#39;expandAllCheckbox_1ZU7&#39;);
      var all = checkbox_el.parentNode.parentNode.getElementsByTagName(&#39;details&#39;);
      for (i = 0; i &lt; all.length; i++) { 
        all[i].open = checkbox_el.checked;
      }
    }
    window.addEventListener(&#39;load&#39;, () =&gt; {
      document.getElementById(&#39;expandAllCheckbox_1ZU7&#39;).parentNode.parentNode.classList.add(&#39;js-visible&#39;);
      setChildExpandedStates_1ZU7();
    });
  &lt;/script&gt;

&lt;details&gt;
&lt;summary&gt;&lt;span&gt;&lt;h4 id=&#34;1-export-your-database-from-newpipe&#34;&gt;1. Export your database from &lt;strong&gt;NewPipe&lt;/strong&gt;&lt;/h4&gt;
&lt;/span&gt;&lt;/summary&gt;

&lt;div&gt;
&lt;ul&gt;
&lt;li&gt;In &lt;strong&gt;NewPipe&lt;/strong&gt; tap &lt;code&gt;Settings&lt;/code&gt; &lt;code&gt;Backup and restore&lt;/code&gt; &lt;code&gt;Export database&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;This will create a file on your device with a name like &lt;code&gt;NewPipeData-2023-09-01.zip&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/details&gt;

&lt;details&gt;
&lt;summary&gt;&lt;span&gt;&lt;h4 id=&#34;2-import-it-into-bendystraw&#34;&gt;2. Import it into &lt;strong&gt;BendyStraw&lt;/strong&gt;&lt;/h4&gt;
&lt;/span&gt;&lt;/summary&gt;

&lt;div&gt;
&lt;ul&gt;
&lt;li&gt;In &lt;strong&gt;BendyStraw&lt;/strong&gt;, tap the button with the add/plus icon, and select the file you just exported&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/details&gt;


&lt;details&gt;
&lt;summary&gt;&lt;span&gt;&lt;h4 id=&#34;3-edit-with-bendystraw&#34;&gt;3. Edit with &lt;strong&gt;BendyStraw&lt;/strong&gt;&lt;/h4&gt;
&lt;/span&gt;&lt;/summary&gt;

&lt;div&gt;
&lt;p&gt;Custom playlists have icons at the top which you can click to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Delete the playlist&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Copy the playlist&lt;/strong&gt; to another &lt;code&gt;zip&lt;/code&gt; (you need more than one &lt;code&gt;zip&lt;/code&gt; added to do this)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Export the playlist&lt;/strong&gt; as a plain text file, which for example can be used with &lt;a href = &#34;https://github.com/yt-dlp/yt-dlp&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;yt-dlp&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Tap on a playlist name to edit&lt;/strong&gt; it, and tap outside (or press return on the keyboard) to finish.&lt;/p&gt;
&lt;/div&gt;
&lt;/details&gt;

&lt;details&gt;
&lt;summary&gt;&lt;span&gt;&lt;h4 id=&#34;4-export-from-bendystraw&#34;&gt;4. Export from &lt;strong&gt;BendyStraw&lt;/strong&gt;&lt;/h4&gt;
&lt;/span&gt;&lt;/summary&gt;

&lt;div&gt;
&lt;ul&gt;
&lt;li&gt;In &lt;strong&gt;BendyStraw&lt;/strong&gt; tap the export button (the arrow icon on the bottom-right of the screen)&lt;/li&gt;
&lt;li&gt;This will create a new file named like the original but with &lt;code&gt;-bendy-straw&lt;/code&gt; at the end, so &lt;code&gt;NewPipeData.zip&lt;/code&gt; is saved as &lt;code&gt;NewPipeData-bendy-straw.zip&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/details&gt;

&lt;details&gt;
&lt;summary&gt;&lt;span&gt;&lt;h4 id=&#34;5-import-it-back-into-newpipe&#34;&gt;5. Import it back into &lt;strong&gt;NewPipe&lt;/strong&gt;&lt;/h4&gt;
&lt;/span&gt;&lt;/summary&gt;

&lt;div&gt;
&lt;ul&gt;
&lt;li&gt;In &lt;strong&gt;NewPipe&lt;/strong&gt; tap &lt;code&gt;Settings&lt;/code&gt; &lt;code&gt;Backup and restore&lt;/code&gt; &lt;code&gt;Import database&lt;/code&gt; and select the file you just saved&lt;/li&gt;
&lt;li&gt;After importing the database, &lt;strong&gt;NewPipe&lt;/strong&gt; will ask if you also want to import settings, if you&amp;rsquo;re not sure what this means just tap &amp;lsquo;cancel&amp;rsquo;, your current &lt;strong&gt;NewPipe&lt;/strong&gt; settings will remain unchanged&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/details&gt;


&lt;/div&gt;

&lt;h2 id=&#34;json-playlist-import&#34;&gt;JSON Playlist Import&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;BendyStraw&lt;/strong&gt; can import playlists from a JSON file. This is a new and basic feature which has some requirements and limitations.&lt;/p&gt;
&lt;h3 id=&#34;show-the-json-import-button&#34;&gt;Show the JSON Import Button&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;The button is hidden by default&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Go to the preferences screen (hamburger menu in top-right corner)&lt;/li&gt;
&lt;li&gt;Tick the &amp;lsquo;Show JSON Import Button&amp;rsquo; box&lt;/li&gt;
&lt;li&gt;Open at least 1 database&lt;br&gt;
&lt;em&gt;The button only shows when there is a database open, as it imports the JSON into the currently-opened database&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;limitations&#34;&gt;Limitations&lt;/h3&gt;
&lt;p&gt;We assume that all playlist entries are &lt;strong&gt;YouTube&lt;/strong&gt; streams.&lt;br&gt;
&lt;em&gt;Although &lt;strong&gt;NewPipe&lt;/strong&gt; is able to handle streams from other services too, for now this import feature is just for &lt;strong&gt;YouTube&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The JSON import format has been designed to prefer simplicity over richness of data. This comes with some pros and cons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Several stream attributes (eg &amp;lsquo;uploader URL&amp;rsquo;, &amp;lsquo;duration&amp;rsquo;, &amp;lsquo;upload date&amp;rsquo;, &amp;lsquo;view count&amp;rsquo;, &amp;rsquo;thumbnail&amp;rsquo;) are omitted initially&lt;/li&gt;
&lt;li&gt;After import into &lt;strong&gt;NewPipe&lt;/strong&gt;, before the playlist has been played, the above details (attributes) will be missing (so eg the thumbnail will just be a grey blank)&lt;/li&gt;
&lt;li&gt;The first time each stream is started, &lt;strong&gt;NewPipe&lt;/strong&gt; will grab the missing details for the stream and fill in the database&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;requirements&#34;&gt;Requirements&lt;/h3&gt;
&lt;p&gt;JSON playlists will be imported into whichever database is currently open in &lt;strong&gt;BendyStraw&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The general (pseudocode) schema is:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;PLAYLIST_NAME&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;ARRAY_OF_STREAMS&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;PLAYLIST_NAME&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;ARRAY_OF_STREAMS&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;err&#34;&gt;etc...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id=&#34;playlist_name&#34;&gt;&lt;code&gt;PLAYLIST_NAME&lt;/code&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Is expected to be unique (if it isn&amp;rsquo;t, identically-named lists will be combined into 1 playlist)&lt;/li&gt;
&lt;li&gt;If a playlist with exactly the same name already exists in the target database, entries from the JSON will be appended to that existing database&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;array_of_streams&#34;&gt;&lt;code&gt;ARRAY_OF_STREAMS&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;Each stream must be an object with named properties as follows:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I use comments in this explanation but remember comments are not valid JSON &amp;mdash; do not include comments in your JSON&lt;/p&gt;&lt;/blockquote&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;c1&#34;&gt;// These are required
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;stream_type&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;VIDEO_STREAM&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// or &amp;#39;AUDIO_STREAM&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;title&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Seasonal Affective Disorder&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;url&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;https://www.youtube.com/watch?v=3RJ5GftYT2c&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;uploader&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Hidden Valley Bushcraft&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// Uploader/channel name,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;c1&#34;&gt;// These below are optional. NewPipe will update them when the stream is loaded, so they only have meaning on first import before the playlist has been played.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;  &lt;span class=&#34;c1&#34;&gt;// It&amp;#39;s probably better to omit them but they are allowed if you want to use them.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;uploader_url&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;https://www.youtube.com/channel/UCB7BPYlL4f5j5R1Y7HUgeVg&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// Uploader/channel URL 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;duration&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;120&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// Length of stream in seconds 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;view_count&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;1273621&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// Number of times the stream has been viewed on YouTube --- simple number ie no commas, abbreviations etc 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id=&#34;complete-example-json&#34;&gt;Complete Example JSON&lt;/h4&gt;
&lt;p&gt;Below is some complete sample JSON. If this was imported into a database in &lt;strong&gt;BendyStraw&lt;/strong&gt; it would do one of the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create 2 new playlists (&amp;ldquo;Tech Things&amp;rdquo; and &amp;ldquo;Good Songs&amp;rdquo;)&lt;/li&gt;
&lt;li&gt;Or (if playlists with those names already existed) then the streams would be appended to those existing playlists&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;Tech Things&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nt&#34;&gt;&amp;#34;stream_type&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;VIDEO_STREAM&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nt&#34;&gt;&amp;#34;title&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Free software, free society: Richard Stallman at TEDxGeneva 2014&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nt&#34;&gt;&amp;#34;url&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;https://www.youtube.com/watch?v=Ag1AKIl_2GM&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nt&#34;&gt;&amp;#34;uploader&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;TEDx Talks&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nt&#34;&gt;&amp;#34;uploader_url&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;https://www.youtube.com/channel/UCsT0YIqwnpJCM-mx7-gSA4Q&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;nt&#34;&gt;&amp;#34;stream_type&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;VIDEO_STREAM&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;nt&#34;&gt;&amp;#34;title&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Shenzhen: The Silicon Valley of Hardware (Full Documentary) | Future Cities&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;nt&#34;&gt;&amp;#34;url&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;https://www.youtube.com/watch?v=SGJ5cZnoodY&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;nt&#34;&gt;&amp;#34;uploader&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Wired UK&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;nt&#34;&gt;&amp;#34;uploader_url&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;https://www.youtube.com/@wireduk&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;Good Songs&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nt&#34;&gt;&amp;#34;stream_type&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;VIDEO_STREAM&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nt&#34;&gt;&amp;#34;title&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Aztec Camera - Somewhere In My Heart (Official Music Video)&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nt&#34;&gt;&amp;#34;url&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;https://www.youtube.com/watch?v=2w1Q8ZkXZ1Q&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nt&#34;&gt;&amp;#34;uploader&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;RHINO&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nt&#34;&gt;&amp;#34;uploader_url&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;https://www.youtube.com/channel/UCWEtnEiVwUy7mwFeshyAWLA&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nt&#34;&gt;&amp;#34;stream_type&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;VIDEO_STREAM&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nt&#34;&gt;&amp;#34;title&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Imagine Dragons - Thunder&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nt&#34;&gt;&amp;#34;url&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;https://www.youtube.com/watch?v=fKopy74weus&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nt&#34;&gt;&amp;#34;uploader&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;ImagineDragonsVEVO&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nt&#34;&gt;&amp;#34;uploader_url&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;https://www.youtube.com/channel/UCpx_k19S2vUutWUUM9qmXEg&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nt&#34;&gt;&amp;#34;stream_type&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;VIDEO_STREAM&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nt&#34;&gt;&amp;#34;title&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;𝑰𝒕&amp;#39;𝒔 𝑨 𝑯𝒆𝒂𝒓𝒕𝒂𝒄𝒉𝒆 -Bonnie Tyler (Cover by Yhuan) #gutomversion #hitback #goodvibestambayan #yhuan&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nt&#34;&gt;&amp;#34;url&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;https://www.youtube.com/watch?v=AQCm7VWyg-U&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nt&#34;&gt;&amp;#34;uploader&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Yhuan Official&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nt&#34;&gt;&amp;#34;uploader_url&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;https://www.youtube.com/channel/UCDxVjSFhfiPGispcK9o0qzw&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;





&lt;div class=&#34;expand-all-toggle&#34;&gt;
    &lt;label for=&#34;expandAllCheckbox_fJI7&#34;&gt;&lt;h2 id=&#34;faq&#34;&gt;FAQ&lt;/h2&gt;
&lt;span&gt;Expand all&lt;/span&gt;
      &lt;input id=&#34;expandAllCheckbox_fJI7&#34; type=&#34;checkbox&#34; onchange=&#34;setChildExpandedStates_fJI7();&#34;/&gt;
    &lt;/label&gt;
  
  &lt;script&gt;
    function setChildExpandedStates_fJI7() {
      var checkbox_el = document.getElementById(&#39;expandAllCheckbox_fJI7&#39;);
      var all = checkbox_el.parentNode.parentNode.getElementsByTagName(&#39;details&#39;);
      for (i = 0; i &lt; all.length; i++) { 
        all[i].open = checkbox_el.checked;
      }
    }
    window.addEventListener(&#39;load&#39;, () =&gt; {
      document.getElementById(&#39;expandAllCheckbox_fJI7&#39;).parentNode.parentNode.classList.add(&#39;js-visible&#39;);
      setChildExpandedStates_fJI7();
    });
  &lt;/script&gt;


&lt;details&gt;
&lt;summary&gt;&lt;span&gt;&lt;h4 id=&#34;whats-in-the-zip-files&#34;&gt;What&amp;rsquo;s in the &lt;code&gt;zip&lt;/code&gt; files?&lt;/h4&gt;
&lt;/span&gt;&lt;/summary&gt;

&lt;div&gt;
&lt;p&gt;&lt;strong&gt;NewPipe&lt;/strong&gt; exports a &lt;code&gt;zip&lt;/code&gt; file, containing 2 files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;newpipe.db&lt;/code&gt;: A database which has all the info &lt;strong&gt;BendyStraw&lt;/strong&gt; uses, such as your playlists and channel subscriptions, plus other things like your watch history&lt;/li&gt;
&lt;li&gt;&lt;code&gt;newpipe.settings&lt;/code&gt;: Your &lt;strong&gt;NewPipe&lt;/strong&gt; settings and preferences&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For simplicity &lt;strong&gt;BendyStraw&lt;/strong&gt; sometimes refers to the &lt;code&gt;zip&lt;/code&gt; file as the database, because that&amp;rsquo;s the file &lt;strong&gt;NewPipe&lt;/strong&gt; works with for imports/exports.&lt;/p&gt;
&lt;/div&gt;
&lt;/details&gt;

&lt;details&gt;
&lt;summary&gt;&lt;span&gt;&lt;h4 id=&#34;is-my-data-private&#34;&gt;Is my data private?&lt;/h4&gt;
&lt;/span&gt;&lt;/summary&gt;

&lt;div&gt;
Yes! This is important to me as a developer. &lt;strong&gt;BendyStraw&lt;/strong&gt; only works with the tables in the database which it requires to let you do your editing. It doesn&amp;rsquo;t look into other things like your settings or watch history and it doesn&amp;rsquo;t send your data to any servers or share it with anybody. There&amp;rsquo;s no tracking and no adverts or anything like that.&lt;/div&gt;
&lt;/details&gt;

&lt;details&gt;
&lt;summary&gt;&lt;span&gt;&lt;h4 id=&#34;what-happens-to-my-newpipe-settings&#34;&gt;What happens to my &lt;strong&gt;NewPipe&lt;/strong&gt; settings?&lt;/h4&gt;
&lt;/span&gt;&lt;/summary&gt;

&lt;div&gt;
&lt;p&gt;&lt;strong&gt;BendyStraw&lt;/strong&gt; doesn&amp;rsquo;t look inside them or change them.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If you edit &lt;code&gt;zip&lt;/code&gt;s from multiple devices, or old/archived &lt;code&gt;zip&lt;/code&gt;s, be aware that your new &lt;code&gt;zip&lt;/code&gt; will have the settings from whichever original file you opened. &lt;em&gt;So be careful not to overwrite settings with an old version.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;When you import your new &lt;code&gt;zip&lt;/code&gt; into &lt;strong&gt;NewPipe&lt;/strong&gt; the simplest thing to do is just tap &amp;lsquo;cancel&amp;rsquo; when it asks if you want to also import settings.&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;details&gt;
&lt;summary&gt;&lt;span&gt;&lt;h4 id=&#34;ive-opened-multiple-databases-but-dont-see-enough-tabs&#34;&gt;I&amp;rsquo;ve opened multiple databases but don&amp;rsquo;t see enough tabs&lt;/h4&gt;
&lt;/span&gt;&lt;/summary&gt;

&lt;div&gt;
&lt;p&gt;The tabs can scroll off the edge of the screen, swipe left/right on the tab bar to see any tabs which have overflowed.&lt;/p&gt;
&lt;p&gt;You can also swipe left or right on any blank area to switch tabs.&lt;/p&gt;
&lt;/div&gt;
&lt;/details&gt;

&lt;details&gt;
&lt;summary&gt;&lt;span&gt;&lt;h4 id=&#34;how-do-i-use-an-exported-text-playlist&#34;&gt;How do I use an exported text playlist?&lt;/h4&gt;
&lt;/span&gt;&lt;/summary&gt;

&lt;div&gt;
&lt;p&gt;If you have &lt;a href = &#34;https://github.com/yt-dlp/yt-dlp&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;yt-dlp&lt;/a&gt; installed, you can batch download all of the files from the playlist like so:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In &lt;strong&gt;BendyStraw&lt;/strong&gt; locate the playlist and tap the &amp;rsquo;export&amp;rsquo; button&lt;/li&gt;
&lt;li&gt;Check the dialog to see where the text file will be saved&lt;/li&gt;
&lt;li&gt;In your terminal run &lt;code&gt;yt-dlp --batch-file [path_to_text_file.txt]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For more info check out &lt;a href = &#34;https://github.com/yt-dlp/yt-dlp&#34; target = &#34;_blank&#34; rel = &#34;nofollow noopener noreferrer&#34;&gt;the yt-dlp github&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;/details&gt;

&lt;details&gt;
&lt;summary&gt;&lt;span&gt;&lt;h4 id=&#34;can-i-export-an-m3u-playlist&#34;&gt;Can I export an M3U playlist?&lt;/h4&gt;
&lt;/span&gt;&lt;/summary&gt;

&lt;div&gt;
&lt;p&gt;I originally did build this functionality, only to find that links to eg YouTube streams no longer work in &lt;code&gt;m3u&lt;/code&gt; playlists. This is true for the players I tested and to the best of my knowledge.&lt;/p&gt;
&lt;p&gt;If I&amp;rsquo;m wrong about this, you have some other relevant info, or would like to see some other export format, please let me know.&lt;em class=&#34;star&#34;&gt;*&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;d be happy to revisit this if it&amp;rsquo;s useful.&lt;/p&gt;
&lt;p&gt;&lt;em class=&#34;star&#34;&gt;*&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a class=&#34;big-button&#34; href=&#34;mailto:hello@mm-dev.rocks&#34;&gt;&lt;a &gt;hello@mm-dev.rocks&lt;/a&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/details&gt;


&lt;details&gt;
&lt;summary&gt;&lt;span&gt;&lt;h4 id=&#34;why-do-i-need-to-grant-all-files-access-permission-in-android-13&#34;&gt;Why do I need to grant &amp;lsquo;all files access&amp;rsquo; permission in Android 13?&lt;/h4&gt;
&lt;/span&gt;&lt;/summary&gt;

&lt;div&gt;
&lt;p&gt;&lt;strong&gt;BendyStraw&lt;/strong&gt; needs to work with files from a different app (&lt;strong&gt;NewPipe&lt;/strong&gt;). The way permissions have changed in Android 13 makes this complicated, as document files (ie non-media files such as &lt;code&gt;.zip&lt;/code&gt;) can only be read from and written to the directory belonging to the app that created them. The only simple way to keep the app easy to use is to ask for these permissions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;BendyStraw&lt;/strong&gt; has no interest in doing anything unexpected to your filesystem. The code is open source, so any developer can check to make sure that it only does what it says it does.&lt;/p&gt;
&lt;/div&gt;
&lt;/details&gt;


&lt;/div&gt;

</description>
    </item>
    
    <item>
      <title>Holistik: Activity Overview (Garmin Watch App)</title>
      <link>https://mm-dev.rocks/posts/holistik-garmin-app/</link>
      <pubDate>Tue, 10 Oct 2023 13:17:08 +0100</pubDate>
      <guid>https://mm-dev.rocks/posts/holistik-garmin-app/</guid>
      <description>&lt;p&gt;Garmin watches (such as the Fenix 5 Plus which I have) gather a lot of info about your exercise activity, but to see it you need to log in to your Connect IQ account in their app or a browser, using a separate device such as a phone or laptop.&lt;/p&gt;
&lt;p&gt;One of the things I like most about this watch is that despite the usual ecosystem tie-ins, you can do most things with it offline, even including GPS navigation as it stores its own offline maps and has a GPS device built-in (unlike lots of other devices that depend on beign BlueTooth-linked to your phone for GPS features). The only sore point for me is the lack of a visual overview of activities, without logging in to Connect IQ.&lt;/p&gt;
&lt;p&gt;This app is my solution. Get a birds-eye view at different zoom levels (days, weeks or months at a time) - great when your device is offline and you want to check your activity levels at a glance!&lt;/p&gt;
&lt;ul class=&#39;gallery&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_HGaY_1&#39; /&gt;
      &lt;label for=&#39;item_HGaY_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/holistik/holistik-grab-2.png&#39; alt=&#39;Days&#39; /&gt;
        &lt;span&gt;Days&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_HGaY_2&#39; /&gt;
      &lt;label for=&#39;item_HGaY_2&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/holistik/holistik-grab-1.png&#39; alt=&#39;Weeks&#39; /&gt;
        &lt;span&gt;Weeks&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_HGaY_3&#39; /&gt;
      &lt;label for=&#39;item_HGaY_3&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/holistik/holistik-grab-3.png&#39; alt=&#39;Months&#39; /&gt;
        &lt;span&gt;Months&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;how-to-use&#34;&gt;How to Use&lt;/h2&gt;
&lt;h3 id=&#34;day-colours&#34;&gt;Day Colours&lt;/h3&gt;
&lt;p&gt;Days (squares) are coloured according to how many activities you recorded that day, plus time spent or distance covered:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th style=&#34;text-align: right&#34;&gt;Activity level&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Warm themes&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Cool themes&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;Some&lt;/strong&gt;&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;span class=&#34;chip&#34; style=&#34;background-color: #FFAA00&#34;&gt;&lt;span&gt;Yellow&lt;/span&gt;&lt;/span&gt;
&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;span class=&#34;chip&#34; style=&#34;background-color: #00FFCC&#34;&gt;&lt;span&gt;Light Green&lt;/span&gt;&lt;/span&gt;
&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;Moderate&lt;/strong&gt;&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;span class=&#34;chip&#34; style=&#34;background-color: #FF0000&#34;&gt;&lt;span&gt;Red          &lt;/span&gt;&lt;/span&gt;
&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;span class=&#34;chip&#34; style=&#34;background-color: #00FF00&#34;&gt;&lt;span&gt;Mid Green  &lt;/span&gt;&lt;/span&gt;
&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;High&lt;/strong&gt;&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;span class=&#34;chip&#34; style=&#34;background-color: #4B0000&#34;&gt;&lt;span&gt;Dark Red     &lt;/span&gt;&lt;/span&gt;
&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;span class=&#34;chip&#34; style=&#34;background-color: #023E02&#34;&gt;&lt;span&gt;Dark Green &lt;/span&gt;&lt;/span&gt;
&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Some other days are coloured to help you see what&amp;rsquo;s what. Where the year or month is written in text, the same colours will also be used.&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th style=&#34;text-align: right&#34;&gt;Day type&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Warm, Dark theme&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Cool, Dark theme&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Warm, Light theme&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;Cool, Light theme&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;No activities recorded&lt;/strong&gt;&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;span class=&#34;chip&#34; style=&#34;background-color: #AAAAAA&#34;&gt;&lt;span&gt;Light Grey&lt;/span&gt;&lt;/span&gt;
&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;span class=&#34;chip&#34; style=&#34;background-color: #AAAAAA&#34;&gt;&lt;span&gt;Light Grey&lt;/span&gt;&lt;/span&gt;
&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;span class=&#34;chip&#34; style=&#34;background-color: #AAAAAA&#34;&gt;&lt;span&gt;Light Grey&lt;/span&gt;&lt;/span&gt;
&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;span class=&#34;chip&#34; style=&#34;background-color: #AAAAAA&#34;&gt;&lt;span&gt;Light Grey&lt;/span&gt;&lt;/span&gt;
&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;Weekends&lt;/strong&gt;&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;span class=&#34;chip&#34; style=&#34;background-color: #555555&#34;&gt;&lt;span&gt;Dark Grey&lt;/span&gt;&lt;/span&gt;
&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;span class=&#34;chip&#34; style=&#34;background-color: #555555&#34;&gt;&lt;span&gt;Dark Grey&lt;/span&gt;&lt;/span&gt;
&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;span class=&#34;chip&#34; style=&#34;background-color: #555555&#34;&gt;&lt;span&gt;Dark Grey&lt;/span&gt;&lt;/span&gt;
&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;span class=&#34;chip&#34; style=&#34;background-color: #555555&#34;&gt;&lt;span&gt;Dark Grey&lt;/span&gt;&lt;/span&gt;
&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;First of the month&lt;/strong&gt;&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;span class=&#34;chip&#34; style=&#34;background-color: #000088&#34;&gt;&lt;span&gt;Dark Blue&lt;/span&gt;&lt;/span&gt;
&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;span class=&#34;chip&#34; style=&#34;background-color: #ff5500&#34;&gt;&lt;span&gt;Orange&lt;/span&gt;&lt;/span&gt;
&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;span class=&#34;chip&#34; style=&#34;background-color: #000044&#34;&gt;&lt;span&gt;Navy&lt;/span&gt;&lt;/span&gt;
&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;span class=&#34;chip&#34; style=&#34;background-color: #662200&#34;&gt;&lt;span&gt;Brown&lt;/span&gt;&lt;/span&gt;
&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;First day of the year&lt;/strong&gt;&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;span class=&#34;chip&#34; style=&#34;background-color: #440166&#34;&gt;&lt;span&gt;Purple&lt;/span&gt;&lt;/span&gt;
&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;span class=&#34;chip&#34; style=&#34;background-color: #FFAA00&#34;&gt;&lt;span&gt;Yellow&lt;/span&gt;&lt;/span&gt;
&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;span class=&#34;chip&#34; style=&#34;background-color: #662200&#34;&gt;&lt;span&gt;Brown&lt;/span&gt;&lt;/span&gt;
&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;span class=&#34;chip&#34; style=&#34;background-color: #4B0000&#34;&gt;&lt;span&gt;Dark Red&lt;/span&gt;&lt;/span&gt;
&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;watch-button-functions&#34;&gt;Watch Button Functions&lt;/h3&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th style=&#34;text-align: right&#34;&gt;&lt;/th&gt;
          &lt;th&gt;&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;Start&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;&lt;em&gt;Change view&lt;/em&gt; (Days/Weeks/Months)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;Up&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;&lt;em&gt;Previous&lt;/em&gt; day/week/month&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;Down&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;&lt;em&gt;Next&lt;/em&gt; day/week/month&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;Menu&lt;/strong&gt; &lt;small class=&#34;special&#34;&gt;
press + hold &lt;strong&gt;Up&lt;/strong&gt;&lt;/small&gt;
&lt;/td&gt;
          &lt;td&gt;Show &lt;em&gt;Settings&lt;/em&gt; screen&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;views&#34;&gt;Views&lt;/h3&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th style=&#34;text-align: right&#34;&gt;&lt;/th&gt;
          &lt;th&gt;&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;Days&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;Details of individual activities for the selected day eg &amp;ldquo;5:30am Train 32mins&amp;rdquo;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;&lt;/td&gt;
          &lt;td&gt;&lt;ul class=&#39;gallery&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_tdEK_1&#39; /&gt;
      &lt;label for=&#39;item_tdEK_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/holistik/days-warm-dark.png&#39; alt=&#39;View: Days, Theme: Warm&#39; /&gt;
        &lt;span&gt;View: Days, Theme: Warm&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_tdEK_2&#39; /&gt;
      &lt;label for=&#39;item_tdEK_2&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/holistik/days-cool-dark.png&#39; alt=&#39;View: Days, Theme: Cool&#39; /&gt;
        &lt;span&gt;View: Days, Theme: Cool&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;
&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th style=&#34;text-align: right&#34;&gt;&lt;/th&gt;
          &lt;th&gt;&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;Weeks&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;Shows combined activity totals for the selected week eg &amp;ldquo;Train 3hrs&amp;rdquo;, &amp;ldquo;Walk 7hrs 21 miles&amp;rdquo;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;&lt;/td&gt;
          &lt;td&gt;&lt;ul class=&#39;gallery&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_f2eA_1&#39; /&gt;
      &lt;label for=&#39;item_f2eA_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/holistik/weeks-warm-dark.png&#39; alt=&#39;View: Weeks, Theme: Warm&#39; /&gt;
        &lt;span&gt;View: Weeks, Theme: Warm&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_f2eA_2&#39; /&gt;
      &lt;label for=&#39;item_f2eA_2&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/holistik/weeks-cool-dark.png&#39; alt=&#39;View: Weeks, Theme: Cool&#39; /&gt;
        &lt;span&gt;View: Weeks, Theme: Cool&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;
&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th style=&#34;text-align: right&#34;&gt;&lt;/th&gt;
          &lt;th&gt;&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;Months&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;Shows combined totals just like the weeks view &lt;small class=&#34;special&#34;&gt;
Depending on device performance and number of recorded activities, it can take a second or so to jump from month to month. Buttons remain active during this time (eg if you&amp;rsquo;re on December and you press UP 6 times you&amp;rsquo;ll still land on June, it might just take a moment to catch up).&lt;/small&gt;
&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;&lt;/td&gt;
          &lt;td&gt;&lt;ul class=&#39;gallery&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_Ctji_1&#39; /&gt;
      &lt;label for=&#39;item_Ctji_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/holistik/months-warm-dark.png&#39; alt=&#39;View: Months, Theme: Warm&#39; /&gt;
        &lt;span&gt;View: Months, Theme: Warm&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_Ctji_2&#39; /&gt;
      &lt;label for=&#39;item_Ctji_2&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/holistik/months-cool-dark.png&#39; alt=&#39;View: Months, Theme: Cool&#39; /&gt;
        &lt;span&gt;View: Months, Theme: Cool&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;
&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;






&lt;div class=&#34;expand-all-toggle&#34;&gt;
    &lt;label for=&#34;expandAllCheckbox_NnWj&#34;&gt;&lt;h3 id=&#34;hints-and-tips&#34;&gt;Hints and Tips&lt;/h3&gt;
&lt;span&gt;Expand all&lt;/span&gt;
      &lt;input id=&#34;expandAllCheckbox_NnWj&#34; type=&#34;checkbox&#34; onchange=&#34;setChildExpandedStates_NnWj();&#34;/&gt;
    &lt;/label&gt;
  
  &lt;script&gt;
    function setChildExpandedStates_NnWj() {
      var checkbox_el = document.getElementById(&#39;expandAllCheckbox_NnWj&#39;);
      var all = checkbox_el.parentNode.parentNode.getElementsByTagName(&#39;details&#39;);
      for (i = 0; i &lt; all.length; i++) { 
        all[i].open = checkbox_el.checked;
      }
    }
    window.addEventListener(&#39;load&#39;, () =&gt; {
      document.getElementById(&#39;expandAllCheckbox_NnWj&#39;).parentNode.parentNode.classList.add(&#39;js-visible&#39;);
      setChildExpandedStates_NnWj();
    });
  &lt;/script&gt;


&lt;details&gt;
&lt;summary&gt;&lt;span&gt;&lt;h4 id=&#34;to-maximise-usage-of-space-on-round-screens-days-flow-in-a-snaking-pattern&#34;&gt;To maximise usage of space on round screens, days flow in a &amp;lsquo;snaking&amp;rsquo; pattern&lt;/h4&gt;
&lt;/span&gt;&lt;/summary&gt;

&lt;div&gt;
&lt;ul class=&#39;gallery float single-item&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_6viV_1&#39; /&gt;
      &lt;label for=&#39;item_6viV_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/holistik/past-future.png&#39; alt=&#39;Past &amp;gt; Future Days&#39; /&gt;
        &lt;span&gt;Past &amp;gt; Future Days&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;hellip;like an airport queue.&lt;/p&gt;
&lt;p&gt;Earlier days are to the left, later to the right.&lt;/p&gt;
&lt;/div&gt;
&lt;/details&gt;


&lt;details&gt;
&lt;summary&gt;&lt;span&gt;&lt;h4 id=&#34;the-day-you-select-in-days-view-affects-how-the-weeksmonths-are-centred-in-the-other-views&#34;&gt;The day you select in &lt;em&gt;Days View&lt;/em&gt; affects how the weeks/months are centred in the other views&lt;/h4&gt;
&lt;/span&gt;&lt;/summary&gt;

&lt;div&gt;
&lt;ul&gt;
&lt;li&gt;In Days View if you select a &lt;em&gt;Wednesday&lt;/em&gt;, then Weeks View will be centred around &lt;em&gt;Wednesdays&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;In Days View if you select the &lt;em&gt;17th&lt;/em&gt;, then Months View will be centred around the &lt;em&gt;17th&lt;/em&gt; of each month&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/details&gt;


&lt;details&gt;
&lt;summary&gt;&lt;span&gt;&lt;h4 id=&#34;holistik-only-uses-activities-which-have-been-recordedstored-on-your-watch&#34;&gt;Holistik only uses activities which have been recorded/stored on your watch&lt;/h4&gt;
&lt;/span&gt;&lt;/summary&gt;

&lt;div&gt;
&amp;mdash; so not heart rate, steps etc.&lt;/div&gt;
&lt;/details&gt;


&lt;details&gt;
&lt;summary&gt;&lt;span&gt;&lt;h4 id=&#34;holistik-runs-entirely-offline&#34;&gt;Holistik runs entirely offline&lt;/h4&gt;
&lt;/span&gt;&lt;/summary&gt;

&lt;div&gt;
&lt;p&gt;&amp;hellip;using activity data which is already stored on your watch.&lt;/p&gt;
&lt;p&gt;It does not store or send your information anywhere.&lt;/p&gt;
&lt;/div&gt;
&lt;/details&gt;


&lt;/div&gt;

&lt;h2 id=&#34;settings&#34;&gt;Settings&lt;/h2&gt;
&lt;p&gt;Press and hold &lt;em&gt;Menu&lt;/em&gt; while the app is running to change settings:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th style=&#34;text-align: right&#34;&gt;&lt;/th&gt;
          &lt;th&gt;&lt;/th&gt;
          &lt;th&gt;&lt;/th&gt;
          &lt;th&gt;&lt;/th&gt;
          &lt;th&gt;&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;Theme&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;Light, Warm&lt;/td&gt;
          &lt;td&gt;Dark, Warm&lt;/td&gt;
          &lt;td&gt;Light, Cool&lt;/td&gt;
          &lt;td&gt;Dark, Cool&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;Font&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;Small&lt;/td&gt;
          &lt;td&gt;Medium&lt;/td&gt;
          &lt;td&gt;Large&lt;/td&gt;
          &lt;td&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;Date Format&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;dd/mm&lt;/td&gt;
          &lt;td&gt;mm/dd&lt;/td&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;Difficulty&lt;/strong&gt; [1]&lt;/td&gt;
          &lt;td&gt;Basic&lt;/td&gt;
          &lt;td&gt;Intermediate&lt;/td&gt;
          &lt;td&gt;Advanced&lt;/td&gt;
          &lt;td&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;Duration Display&lt;/strong&gt; [2]&lt;/td&gt;
          &lt;td&gt;Basic&lt;/td&gt;
          &lt;td&gt;Intermediate&lt;/td&gt;
          &lt;td&gt;Advanced&lt;/td&gt;
          &lt;td&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;Duration Display&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;Hours and Fractions&lt;/td&gt;
          &lt;td&gt;Minutes&lt;/td&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;Initial View&lt;/strong&gt; [3]&lt;/td&gt;
          &lt;td&gt;Basic&lt;/td&gt;
          &lt;td&gt;Intermediate&lt;/td&gt;
          &lt;td&gt;Advanced&lt;/td&gt;
          &lt;td&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;Duration Display&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;Months&lt;/td&gt;
          &lt;td&gt;Weeks&lt;/td&gt;
          &lt;td&gt;Days&lt;/td&gt;
          &lt;td&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;Text Display&lt;/strong&gt; [4]&lt;/td&gt;
          &lt;td&gt;Basic&lt;/td&gt;
          &lt;td&gt;Intermediate&lt;/td&gt;
          &lt;td&gt;Advanced&lt;/td&gt;
          &lt;td&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;Duration Display&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;Show Dates&lt;/td&gt;
          &lt;td&gt;Show Activity Details&lt;/td&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;small class=&#34;special&#34;&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th style=&#34;text-align: right&#34;&gt;&lt;/th&gt;
          &lt;th&gt;&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;[1] &lt;strong&gt;Difficulty&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;Change according to your fitness level and the calculations will adjust so the coloured days make sense for you&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;[2] &lt;strong&gt;Duration Display&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;Activity durations can be shown in minutes (&amp;lsquo;73mins&amp;rsquo;) or to the nearest fraction of an hour (&amp;lsquo;1 1/4hrs&amp;rsquo;)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;[3] &lt;strong&gt;Initial View&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;View to display when first opening the app&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;[4] &lt;strong&gt;Text Display&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;If &amp;lsquo;Show Dates&amp;rsquo; is disabled dates will still be shown temporarily while you move backwards and forwards, then disappear after a second or two&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;/small&gt;

&lt;h2 id=&#34;app-permissions&#34;&gt;App Permissions&lt;/h2&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th style=&#34;text-align: right&#34;&gt;&lt;/th&gt;
          &lt;th&gt;&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;User Profile&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;Allows the app to read the activity data before displaying it&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: right&#34;&gt;&lt;strong&gt;Persisted Content&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;Allows the app to store/read your settings detailed above&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
</description>
    </item>
    
    <item>
      <title></title>
      <link>https://mm-dev.rocks/apps-showreel/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>https://mm-dev.rocks/apps-showreel/</guid>
      <description>&lt;ul class=&#39;gallery single-item&#39;&gt;&lt;li&gt;
      &lt;video controls width=&#34;100&#34; preload=&#34;none&#34; poster=&#34;https://video.gumlet.io/67c7096609a8835ee9a82911/67efb1487ee0a66d6b376d77/thumbnail-1-0.png?v=1743771382678&#34; aria-label=&#34;&#34;&gt;
        &lt;source src=&#34;https://video.gumlet.io/67c7096609a8835ee9a82911/67efb1487ee0a66d6b376d77/download.mp4&#34; type=&#34;video/mp4&#34; /&gt;
        Download &lt;a href=&#34;https://video.gumlet.io/67c7096609a8835ee9a82911/67efb1487ee0a66d6b376d77/download.mp4&#34;&gt;&lt;/a&gt;.
      &lt;/video&gt;
      
    &lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    
    <item>
      <title></title>
      <link>https://mm-dev.rocks/games-showreel/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>https://mm-dev.rocks/games-showreel/</guid>
      <description>&lt;ul class=&#39;gallery single-item&#39;&gt;&lt;li&gt;
      &lt;video controls width=&#34;100&#34; preload=&#34;none&#34; poster=&#34;https://video.gumlet.io/67c7096609a8835ee9a82911/681a3bab43ef94571796c198/thumbnail-1-0.png?v=1749813754457&#34; aria-label=&#34;&#34;&gt;
        &lt;source src=&#34;https://video.gumlet.io/67c7096609a8835ee9a82911/681a3bab43ef94571796c198/download.mp4&#34; type=&#34;video/mp4&#34; /&gt;
        Download &lt;a href=&#34;https://video.gumlet.io/67c7096609a8835ee9a82911/681a3bab43ef94571796c198/download.mp4&#34;&gt;&lt;/a&gt;.
      &lt;/video&gt;
      
    &lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    
    <item>
      <title></title>
      <link>https://mm-dev.rocks/showreel/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>https://mm-dev.rocks/showreel/</guid>
      <description>&lt;ul class=&#39;gallery single-item&#39;&gt;&lt;li&gt;
      &lt;video controls width=&#34;100&#34; preload=&#34;none&#34; poster=&#34;https://video.gumlet.io/67c7096609a8835ee9a82911/67efb1487ee0a66d6b376d77/thumbnail-1-0.png?v=1743771382678&#34; aria-label=&#34;&#34;&gt;
        &lt;source src=&#34;https://video.gumlet.io/67c7096609a8835ee9a82911/67efb1487ee0a66d6b376d77/download.mp4&#34; type=&#34;video/mp4&#34; /&gt;
        Download &lt;a href=&#34;https://video.gumlet.io/67c7096609a8835ee9a82911/67efb1487ee0a66d6b376d77/download.mp4&#34;&gt;&lt;/a&gt;.
      &lt;/video&gt;
      
    &lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    
    <item>
      <title>auDav Audiobook Player</title>
      <link>https://mm-dev.rocks/posts/audav-audiobook-player/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>https://mm-dev.rocks/posts/audav-audiobook-player/</guid>
      <description>&lt;p&gt;&lt;em&gt;auDav&lt;/em&gt; is an audiobook player app. It&amp;rsquo;s for use with libraries of audiobooks stored on NextCloud (or other WebDAV storage), and runs on Android, iOS/iPadOS, Windows, Linux and macOS.&lt;/p&gt;
&lt;p&gt;Key features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Visual listening progress&lt;/strong&gt; each chapter of a book is represented as a block, painted in different colours to indicate played/unplayed segments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Full-featured custom bookmarks&lt;/strong&gt; you can add notes to a bookmark and notes can be searched&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No server install&lt;/strong&gt; the app is installed on whichever device/devices you want to listen on, you enter details for your NextCloud server (login info and path to the folder where your books are stored) and the app does the rest&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clean footprint&lt;/strong&gt; &lt;em&gt;auDav&lt;/em&gt; only does what it needs to do, data such as listening progress is tied to your NextCloud user, to create different accounts you do that on NextCloud&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Loads of themes&lt;/strong&gt; multiple colours in light, dark and OLED black varieties&lt;/li&gt;
&lt;/ul&gt;
&lt;ul class=&#39;gallery&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_Myxs_1&#39; /&gt;
      &lt;label for=&#39;item_Myxs_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/audav/audav-1.webp&#39; alt=&#39;Entering a bookmark and note&#39; /&gt;
        &lt;span&gt;Entering a bookmark and note&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_Myxs_2&#39; /&gt;
      &lt;label for=&#39;item_Myxs_2&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/audav/audav-2.webp&#39; alt=&#39;Library page with theme chooser open&#39; /&gt;
        &lt;span&gt;Library page with theme chooser open&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_Myxs_3&#39; /&gt;
      &lt;label for=&#39;item_Myxs_3&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/audav/audav-4.webp&#39; alt=&#39;Playing book with visual chapter progress&#39; /&gt;
        &lt;span&gt;Playing book with visual chapter progress&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;this-app-is-95-finished-but-temporarily-on-hold-while-i-attend-to-some-other-priorities-coming-soon&#34;&gt;This app is 95% finished but temporarily on hold while I attend to some other priorities&amp;hellip; COMING SOON!&lt;/h3&gt;
</description>
    </item>
    
    <item>
      <title>High Peak Dog Walks Website</title>
      <link>https://mm-dev.rocks/posts/high-peak-dog-walks/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>https://mm-dev.rocks/posts/high-peak-dog-walks/</guid>
      <description>&lt;p&gt;A nice, simple brochure site for a dog-walking business. It&amp;rsquo;s built with &lt;em&gt;Hugo&lt;/em&gt; (the static site generator) and is relatively straightforward but I did develop a few useful working patterns for the future during its development.&lt;/p&gt;
&lt;h3 id=&#34;hugo-tips-coming-soon&#34;&gt;Hugo tips COMING SOON!&lt;/h3&gt;
&lt;ul class=&#39;gallery&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_y1YV_1&#39; /&gt;
      &lt;label for=&#39;item_y1YV_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/hpdw/hpdw-1.jpg&#39; alt=&#39;Home page&#39; /&gt;
        &lt;span&gt;Home page&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_y1YV_2&#39; /&gt;
      &lt;label for=&#39;item_y1YV_2&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/hpdw/hpdw-2.jpg&#39; alt=&#39;Services and pricing&#39; /&gt;
        &lt;span&gt;Services and pricing&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_y1YV_3&#39; /&gt;
      &lt;label for=&#39;item_y1YV_3&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/hpdw/hpdw-3.jpg&#39; alt=&#39;Contact&#39; /&gt;
        &lt;span&gt;Contact&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    
    <item>
      <title>Jumble Art Portfolio</title>
      <link>https://mm-dev.rocks/posts/jumble-art-portfolio/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>https://mm-dev.rocks/posts/jumble-art-portfolio/</guid>
      <description>&lt;p&gt;&lt;em&gt;Jumble&lt;/em&gt; is all of my art, all piled up on the metaphorical scrapheap where the modern world seems to have put art :)&lt;/p&gt;
&lt;p&gt;Just messing, this was actually a fun project where I got to learn a lot of 3D stuff that was new to me, and build some familiarity with Three.js (which is really nice). I also dipped my toe in to WASM modules for the first time and worked with Rapier physics engine.&lt;/p&gt;
&lt;h3 id=&#34;full-write-up-coming-soon&#34;&gt;Full write-up COMING SOON!&lt;/h3&gt;
&lt;ul class=&#39;gallery&#39;&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_jCIx_1&#39; /&gt;
      &lt;label for=&#39;item_jCIx_1&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/jumble/jumble-grab-4.webp&#39; alt=&#39;Paintings falling into pile&#39; /&gt;
        &lt;span&gt;Paintings falling into pile&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_jCIx_2&#39; /&gt;
      &lt;label for=&#39;item_jCIx_2&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/jumble/jumble-grab-2.webp&#39; alt=&#39;Zoomed-out pile&#39; /&gt;
        &lt;span&gt;Zoomed-out pile&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;input type=&#39;checkbox&#39; id=&#39;item_jCIx_3&#39; /&gt;
      &lt;label for=&#39;item_jCIx_3&#39; aria-label=&#39;Expand image&#39; tabindex=&#39;0&#39;&gt;&lt;img src=&#39;https://mm-dev.rocks/images/jumble/jumble-grab-9.webp&#39; alt=&#39;Pile close-up&#39; /&gt;
        &lt;span&gt;Pile close-up&lt;/span&gt;&lt;/label&gt;
    &lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    
    <item>
      <title>Let&#39;s build something!</title>
      <link>https://mm-dev.rocks/portfolio/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>https://mm-dev.rocks/portfolio/</guid>
      <description></description>
    </item>
    
    <item>
      <title>Showreel Videos</title>
      <link>https://mm-dev.rocks/posts/trailer-videos/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>https://mm-dev.rocks/posts/trailer-videos/</guid>
      <description>&lt;p&gt;Showreels featuring a mix of client work and my own open source projects. Some old, some new.&lt;/p&gt;
&lt;p&gt;Each video is less than 2 minutes long and they were all made with DaVinci Resolve.&lt;/p&gt;
&lt;ul class=&#39;gallery&#39;&gt;&lt;li&gt;
      &lt;video controls width=&#34;100&#34; preload=&#34;none&#34; poster=&#34;/images/trailer-videos/apps-video-thumbnail.png&#34; aria-label=&#34;Apps showreel 1m59s&#34;&gt;
        &lt;source src=&#34;https://video.gumlet.io/67c7096609a8835ee9a82911/67efb1487ee0a66d6b376d77/download.mp4&#34; type=&#34;video/mp4&#34; /&gt;
        Download &lt;a href=&#34;https://video.gumlet.io/67c7096609a8835ee9a82911/67efb1487ee0a66d6b376d77/download.mp4&#34;&gt;Apps showreel 1m59s&lt;/a&gt;.
      &lt;/video&gt;
      &lt;span&gt;Apps showreel 1m59s&amp;nbsp;&lt;em&gt;(video)&lt;/em&gt;&lt;/span&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;video controls width=&#34;100&#34; preload=&#34;none&#34; poster=&#34;/images/trailer-videos/games-video-thumbnail.png&#34; aria-label=&#34;Games showreel 1m59s&#34;&gt;
        &lt;source src=&#34;https://video.gumlet.io/67c7096609a8835ee9a82911/681a3bab43ef94571796c198/download.mp4&#34; type=&#34;video/mp4&#34; /&gt;
        Download &lt;a href=&#34;https://video.gumlet.io/67c7096609a8835ee9a82911/681a3bab43ef94571796c198/download.mp4&#34;&gt;Games showreel 1m59s&lt;/a&gt;.
      &lt;/video&gt;
      &lt;span&gt;Games showreel 1m59s&amp;nbsp;&lt;em&gt;(video)&lt;/em&gt;&lt;/span&gt;
    &lt;/li&gt;&lt;li&gt;
      &lt;video controls width=&#34;100&#34; preload=&#34;none&#34; poster=&#34;/images/trailer-videos/brain-training-video-thumbnail.gif&#34; aria-label=&#34;Brain training apps (Client: Zing) 1m06s&#34;&gt;
        &lt;source src=&#34;https://video.gumlet.io/67c7096609a8835ee9a82911/68ac0f980a8c57042d85b39f/download.mp4&#34; type=&#34;video/mp4&#34; /&gt;
        Download &lt;a href=&#34;https://video.gumlet.io/67c7096609a8835ee9a82911/68ac0f980a8c57042d85b39f/download.mp4&#34;&gt;Brain training apps (Client: Zing) 1m06s&lt;/a&gt;.
      &lt;/video&gt;
      &lt;span&gt;Brain training apps (Client: Zing) 1m06s&amp;nbsp;&lt;em&gt;(video)&lt;/em&gt;&lt;/span&gt;
    &lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    
  </channel>
</rss>
