<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>Sean Borg</title>
    <subtitle>Sean Borg an independent app developer and Rust consultant. Also a co-organiser of Rust Manchetser</subtitle>
    <link rel="self" type="application/atom+xml" href="https://seanborg.tech/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://seanborg.tech"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-07-05T00:00:00+00:00</updated>
    <id>https://seanborg.tech/atom.xml</id>
    <entry xml:lang="en">
        <title>TB: Wrapper manager</title>
        <published>2026-07-05T00:00:00+00:00</published>
        <updated>2026-07-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://seanborg.tech/tiny-blog/wrapper-manager/"/>
        <id>https://seanborg.tech/tiny-blog/wrapper-manager/</id>
        
        <content type="html" xml:base="https://seanborg.tech/tiny-blog/wrapper-manager/">&lt;p&gt;In my quest to remove all &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;nix-community&#x2F;home-manager&quot;&gt;home manager&lt;&#x2F;a&gt;
I have found &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;viperml.github.io&#x2F;wrapper-manager&#x2F;&quot;&gt;wrapper manager&lt;&#x2F;a&gt;
and thought I&#x27;d try document how I integrated it into my nixos config&lt;&#x2F;p&gt;
&lt;p&gt;I still feel like im learning nix if i&#x27;m doing anything crazy please &lt;a href=&quot;mailto:wrapper-manager@seanborg.tech&quot;&gt;email me&lt;&#x2F;a&gt;,
make an issue in this websites &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;seam345&#x2F;my-website&#x2F;-&#x2F;work_items&#x2F;new&quot;&gt;gitlab repo&lt;&#x2F;a&gt;
or if you understand radicle submit a pr&#x2F;issue to my &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;radicle.network&#x2F;nodes&#x2F;iris.radicle.network&#x2F;rad%3Az3TBqYd3Z4PvbwA7KvXyYaBY1prKa&quot;&gt;nixos config&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;My goal was to keep all my wrapped config in one directory, since I use &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cgit.krebsco.de&#x2F;krops&#x2F;about&#x2F;&quot;&gt;krops&lt;&#x2F;a&gt;
and do some crazy stuff my nixos directory looks a bit like this&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;txt&quot; class=&quot;language-txt &quot;&gt;&lt;code class=&quot;language-txt&quot; data-lang=&quot;txt&quot;&gt;.
├── config
│   ├── configuration.nix
│   ├── readme.md
│   ├── channels
│   ├── hm-to-copy
│   ├── machine-specific
│   ├── my-pkgs
│   ├── public-keys
│   ├── roles
│   ├── sean
│   ├── vars
│   └── wrapped
│       ├── atuin.nix
│       ├── cib.nix
│       ├── ci-runner-radicle.nix
│       ├── firefox.nix
│       ├── fish.nix
│       ├── git.nix
│       ├── helix
│       │   ├── codebook
│       │   │   └── codebook.toml
│       │   ├── default.nix
│       │   └── helix
│       │       ├── config.toml
│       │       └── languages.toml
│       └── sherlock.nix
├── my-devices
└── tests
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Then I can pull in all the wrapped stuff under the wrapped namespace by doing
the following in the let statement of any nix file&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;nix&quot; class=&quot;language-nix &quot;&gt;&lt;code class=&quot;language-nix&quot; data-lang=&quot;nix&quot;&gt;importJson = (import &amp;lt;nixpkgs&amp;gt; { }).lib.importJSON;
wrapper-manager-git = builtins.fetchGit {
  rev = (importJson ..&amp;#x2F;channels&amp;#x2F;wrapper-manager.json).rev;
  url = (importJson ..&amp;#x2F;channels&amp;#x2F;wrapper-manager.json).url;
  allRefs = true;
};
wrapper-manager = (import &amp;quot;${wrapper-manager-git}&amp;quot;);
wrapped = wrapper-manager.lib {
  inherit pkgs;
  modules = map (n: &amp;lt;path to wrapped folder&amp;gt;&amp;#x2F;${n}) (builtins.attrNames (builtins.readDir &amp;lt;path to wrapped folder&amp;gt;));
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;I&#x27;m making the assumption that repeating this in many files just gets squished
into 1 at the end. Also the import json allows me to pin the repo and update it
with 1 command like
&lt;code&gt;nix-shell -p nix-prefetch-git --command &#x27;nix-prefetch-git   --url https:&#x2F;&#x2F;github.com&#x2F;viperML&#x2F;wrapper-manager.git --rev refs&#x2F;heads&#x2F;master &amp;gt; config&#x2F;channels&#x2F;wrapper-manager.json&#x27;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Resulting in nix files like&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;nix&quot; class=&quot;language-nix &quot;&gt;&lt;code class=&quot;language-nix&quot; data-lang=&quot;nix&quot;&gt;{ config, pkgs, ... }:
let
  importJson = (import &amp;lt;nixpkgs&amp;gt; { }).lib.importJSON;
  wrapper-manager-git = builtins.fetchGit {
    rev = (importJson ..&amp;#x2F;channels&amp;#x2F;wrapper-manager.json).rev;
    url = (importJson ..&amp;#x2F;channels&amp;#x2F;wrapper-manager.json).url;
    allRefs = true;
  };
  wrapper-manager = (import &amp;quot;${wrapper-manager-git}&amp;quot;);
  wrapped = wrapper-manager.lib {
    inherit pkgs;
    modules = map (n: ..&amp;#x2F;wrapped&amp;#x2F;${n}) (builtins.attrNames (builtins.readDir ..&amp;#x2F;wrapped));
  };
in
{
  # &amp;lt;snip&amp;gt;
  users.users.sean = {
    shell = wrapped.config.wrappers.atuin-fish.wrapped + &amp;quot;&amp;#x2F;bin&amp;#x2F;fish&amp;quot;;
    packages = with pkgs; [
      wrapped.config.wrappers.touch-firefox.wrapped
      wrapped.config.wrappers.sean-helix.wrapped
      wrapped.config.wrappers.sean-git.wrapped
      wrapped.config.wrappers.atuin-fish.wrapped
      wrapped.config.wrappers.pc-atuin.wrapped
    }
  }
  # &amp;lt;snip&amp;gt;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Full file &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;radicle.network&#x2F;nodes&#x2F;iris.radicle.network&#x2F;rad:z3TBqYd3Z4PvbwA7KvXyYaBY1prKa&#x2F;tree&#x2F;config&#x2F;sean&#x2F;default.nix&quot;&gt;here&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>TB: Euler diagram of rust week talks</title>
        <published>2026-06-29T00:00:00+00:00</published>
        <updated>2026-06-29T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://seanborg.tech/tiny-blog/rust-week-ven-diagram/"/>
        <id>https://seanborg.tech/tiny-blog/rust-week-ven-diagram/</id>
        
        <content type="html" xml:base="https://seanborg.tech/tiny-blog/rust-week-ven-diagram/">&lt;p&gt;Was trying to pick categories that are not the track they was in so no, Project&#x2F;Applied Rut&#x2F;Industry&#x2F;Compiler categories.
Also attempting to not give a category that is extremely clear from the name unless others made a category :D&lt;&#x2F;p&gt;
&lt;p&gt;Some I couldn&#x27;t really group but put similarish ones close by... others just went over my head so I couldn&#x27;t categorise them :(
Happy to update submit suggestions to &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;seam345&#x2F;my-website&#x2F;-&#x2F;blob&#x2F;main&#x2F;content&#x2F;tiny-blog&#x2F;2026-06-29_rust-week-ven-diagram.md?ref_type=heads&quot;&gt;https:&#x2F;&#x2F;gitlab.com&#x2F;seam345&#x2F;my-website&#x2F;-&#x2F;blob&#x2F;main&#x2F;content&#x2F;tiny-blog&#x2F;2026-06-29_rust-week-ven-diagram.md?ref_type=heads&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;svg width=&quot;1920&quot; height=&quot;1080&quot; viewBox=&quot;0 0 1920 1080&quot; version=&quot;1.1&quot; id=&quot;svg1&quot; xmlns=&quot;http:&#x2F;&#x2F;www.w3.org&#x2F;2000&#x2F;svg&quot;&gt;
  &lt;g id=&quot;layer1&quot; font-size=&quot;20px&quot; fill-opacity=&quot;1&quot; stroke-opacity=&quot;1&quot; font-weight=&quot;bold&quot;&gt;
    &lt;g id=&quot;community&quot; transform=&quot;translate(0,0)&quot; style=&quot;fill:var(--vd1);stroke:var(--vd1);&quot;&gt;
      &lt;rect style=&quot;fill-opacity:0.5;stroke-width:5;&quot; width=&quot;990&quot; height=&quot;190&quot; x=&quot;2.5&quot; y=&quot;30&quot;&#x2F;&gt;
      &lt;rect style=&quot;stroke-width:0;&quot; width=&quot;120&quot; height=&quot;28&quot; x=&quot;0&quot; y=&quot;2&quot; &#x2F;&gt;
      &lt;text style=&quot;fill:var(--f2);stroke:var(--f2);&quot;
        y=&quot;24&quot; x=&quot;5&quot;&gt;community
      &lt;&#x2F;text&gt;
    &lt;&#x2F;g&gt;
    &lt;g id=&quot;linux kernel&quot; transform=&quot;translate(440,120)&quot; style=&quot;fill:var(--vd2);stroke:var(--vd2);&quot;&gt;
      &lt;rect style=&quot;fill-opacity:0.5;stroke-width:5;&quot; width=&quot;500&quot; height=&quot;225&quot; x=&quot;2.5&quot; y=&quot;30&quot;&#x2F;&gt;
      &lt;rect style=&quot;stroke-width:0;&quot; width=&quot;170&quot; height=&quot;28&quot; x=&quot;335&quot; y=&quot;2&quot; &#x2F;&gt;
      &lt;text style=&quot;fill:var(--f2);stroke:var(--f2);&quot;
        y=&quot;24&quot; x=&quot;340&quot;&gt;linux kernel
      &lt;&#x2F;text&gt;
    &lt;&#x2F;g&gt;
    &lt;g id=&quot;Safety&quot; transform=&quot;translate(0,192)&quot; style=&quot;fill:var(--vd6);stroke:var(--vd6);&quot;&gt;
      &lt;rect style=&quot;fill-opacity:0.5;stroke-width:5;&quot; width=&quot;430&quot; height=&quot;160&quot; x=&quot;2.5&quot; y=&quot;30&quot;&#x2F;&gt;
      &lt;rect style=&quot;stroke-width:0;&quot; width=&quot;105&quot; height=&quot;28&quot; x=&quot;0&quot; y=&quot;2&quot; &#x2F;&gt;
      &lt;text style=&quot;fill:var(--f2);stroke:var(--f2);&quot;
        y=&quot;24&quot; x=&quot;5&quot;&gt;Safety
      &lt;&#x2F;text&gt;
    &lt;&#x2F;g&gt;
    &lt;g id=&quot;rust lang&quot; transform=&quot;translate(0,352)&quot; style=&quot;fill:var(--vd5);stroke:var(--vd5);&quot;&gt;
      &lt;rect style=&quot;fill-opacity:0.5;stroke-width:5;&quot; width=&quot;430&quot; height=&quot;60&quot; x=&quot;2.5&quot; y=&quot;30&quot;&#x2F;&gt;
      &lt;rect style=&quot;stroke-width:0;&quot; width=&quot;105&quot; height=&quot;28&quot; x=&quot;0&quot; y=&quot;2&quot; &#x2F;&gt;
      &lt;text style=&quot;fill:var(--f2);stroke:var(--f2);&quot;
        y=&quot;24&quot; x=&quot;5&quot;&gt;rust lang
      &lt;&#x2F;text&gt;
    &lt;&#x2F;g&gt;
    &lt;g id=&quot;learning&#x2F;teaching rust&quot; transform=&quot;translate(5,427)&quot; style=&quot;fill:var(--vd4);stroke:var(--vd4);&quot;&gt;
      &lt;rect style=&quot;fill-opacity:0.5;stroke-width:5;&quot; width=&quot;599&quot; height=&quot;173&quot; x=&quot;2.5&quot; y=&quot;30&quot;&#x2F;&gt;
      &lt;rect style=&quot;stroke-width:0;&quot; width=&quot;230&quot; height=&quot;28&quot; x=&quot;0&quot; y=&quot;2&quot; &#x2F;&gt;
      &lt;text style=&quot;fill:var(--f2);stroke:var(--f2);&quot;
        y=&quot;24&quot; x=&quot;5&quot;&gt;learning&#x2F;teaching rust
      &lt;&#x2F;text&gt;
    &lt;&#x2F;g&gt;
    &lt;g id=&quot;async&quot; transform=&quot;translate(5,610)&quot; style=&quot;fill:var(--vd3);stroke:var(--vd3);&quot;&gt;
      &lt;rect style=&quot;fill-opacity:0.5;stroke-width:5;&quot; width=&quot;600&quot; height=&quot;70&quot; x=&quot;2.5&quot; y=&quot;30&quot;&#x2F;&gt;
      &lt;rect style=&quot;stroke-width:0;&quot; width=&quot;70&quot; height=&quot;28&quot; x=&quot;0&quot; y=&quot;2&quot; &#x2F;&gt;
      &lt;text style=&quot;fill:var(--f2);stroke:var(--f2);&quot;
        y=&quot;24&quot; x=&quot;5&quot;&gt;async
      &lt;&#x2F;text&gt;
    &lt;&#x2F;g&gt;
    &lt;g transform=&quot;translate(0,702)&quot; style=&quot;fill:var(--vd7);stroke:var(--vd7);&quot;&gt;
      &lt;rect style=&quot;fill-opacity:0.5;stroke-width:5;&quot; width=&quot;750&quot; height=&quot;180&quot; x=&quot;2.5&quot; y=&quot;30&quot;&#x2F;&gt;
      &lt;rect style=&quot;stroke-width:0;&quot; width=&quot;125&quot; height=&quot;28&quot; x=&quot;0&quot; y=&quot;2&quot; &#x2F;&gt;
      &lt;text style=&quot;fill:var(--f2);stroke:var(--f2);&quot;
        y=&quot;24&quot; x=&quot;5&quot;&gt;Ergonomics
      &lt;&#x2F;text&gt;
    &lt;&#x2F;g&gt;
    &lt;g transform=&quot;translate(0,802)&quot; style=&quot;fill:var(--vd8);stroke:var(--vd8);&quot;&gt;
      &lt;rect style=&quot;fill-opacity:0.5;stroke-width:5;&quot; width=&quot;430&quot; height=&quot;90&quot; x=&quot;2.5&quot; y=&quot;30&quot;&#x2F;&gt;
      &lt;rect style=&quot;stroke-width:0;&quot; width=&quot;85&quot; height=&quot;28&quot; x=&quot;0&quot; y=&quot;2&quot; &#x2F;&gt;
      &lt;text style=&quot;fill:var(--f2);stroke:var(--f2);&quot;
        y=&quot;24&quot; x=&quot;5&quot;&gt;Github
      &lt;&#x2F;text&gt;
    &lt;&#x2F;g&gt;
    &lt;g transform=&quot;translate(0,932)&quot; style=&quot;fill:var(--vd9);stroke:var(--vd9);&quot;&gt;
      &lt;rect style=&quot;fill-opacity:0.5;stroke-width:5;&quot; width=&quot;475&quot; height=&quot;90&quot; x=&quot;2.5&quot; y=&quot;30&quot;&#x2F;&gt;
      &lt;rect style=&quot;stroke-width:0;&quot; width=&quot;107&quot; height=&quot;28&quot; x=&quot;0&quot; y=&quot;2&quot; &#x2F;&gt;
      &lt;text style=&quot;fill:var(--f2);stroke:var(--f2);&quot;
        y=&quot;24&quot; x=&quot;5&quot;&gt;Dutch Gov
      &lt;&#x2F;text&gt;
    &lt;&#x2F;g&gt;
    &lt;g transform=&quot;translate(1100,0)&quot; style=&quot;fill:var(--vd10);stroke:var(--vd10);&quot;&gt;
      &lt;rect style=&quot;fill-opacity:0.5;stroke-width:5;&quot; width=&quot;790&quot; height=&quot;310&quot; x=&quot;2.5&quot; y=&quot;30&quot;&#x2F;&gt;
      &lt;rect style=&quot;stroke-width:0;&quot; width=&quot;220&quot; height=&quot;28&quot; x=&quot;0&quot; y=&quot;2&quot; &#x2F;&gt;
      &lt;text style=&quot;fill:var(--f2);stroke:var(--f2);&quot;
        y=&quot;24&quot; x=&quot;5&quot;&gt;Project&#x2F;Crate specific
      &lt;&#x2F;text&gt;
    &lt;&#x2F;g&gt;
    &lt;g transform=&quot;translate(980,230)&quot; style=&quot;fill:var(--vd11);stroke:var(--vd11);&quot;&gt;
      &lt;rect style=&quot;fill-opacity:0.5;stroke-width:5;&quot; width=&quot;790&quot; height=&quot;310&quot; x=&quot;2.5&quot; y=&quot;30&quot;&#x2F;&gt;
      &lt;rect style=&quot;stroke-width:0;&quot; width=&quot;220&quot; height=&quot;28&quot; x=&quot;0&quot; y=&quot;2&quot; &#x2F;&gt;
      &lt;text style=&quot;fill:var(--f2);stroke:var(--f2);&quot;
        y=&quot;24&quot; x=&quot;5&quot;&gt;Deep dive
      &lt;&#x2F;text&gt;
    &lt;&#x2F;g&gt;
    &lt;g transform=&quot;translate(980,391)&quot; style=&quot;fill:var(--vd12);stroke:var(--vd12);&quot;&gt;
      &lt;rect style=&quot;fill-opacity:0.5;stroke-width:5;&quot; width=&quot;790&quot; height=&quot;110&quot; x=&quot;2.5&quot; y=&quot;30&quot;&#x2F;&gt;
      &lt;rect style=&quot;stroke-width:0;&quot; width=&quot;220&quot; height=&quot;28&quot; x=&quot;0&quot; y=&quot;2&quot; &#x2F;&gt;
      &lt;text style=&quot;fill:var(--f2);stroke:var(--f2);&quot;
        y=&quot;24&quot; x=&quot;5&quot;&gt;Optimisations
      &lt;&#x2F;text&gt;
    &lt;&#x2F;g&gt;
  &lt;&#x2F;g&gt;
  &lt;g style=&quot;font-size:16px;fill-opacity:1;fill:var(--f2);stroke:var(--f2);&quot; text-anchor=&quot;middle&quot;&gt;
    &lt;a id=&quot;1&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=9330Qs_3Uhs&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=2&quot; transform=&quot;translate(8,33)&quot;&gt;
      &lt;rect style=&quot;stroke-width:0;fill:var(--c2);&quot; width=&quot;220&quot; height=&quot;50&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot;&#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot;&gt;
        &lt;tspan x=&quot;110&quot; y=&quot;20&quot;&gt;the language of empathy&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;110&quot; y=&quot;42.5&quot;&gt;(taylor cramer)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;2&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=3jM4cnEVrLc&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=3&quot; transform=&quot;translate(-3.9440911,382.92742)&quot;&gt;
      &lt;rect style=&quot;stroke-width:0;fill:var(--c2);&quot; width=&quot;161&quot; height=&quot;50&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot;&#x2F;&gt;
      &lt;text style=&quot;&quot; x=&quot;5&quot; y=&quot;20&quot;&gt;
        &lt;tspan x=&quot;80.5&quot; y=&quot;20&quot;&gt;when is never?&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;80.5&quot; y=&quot;42.5&quot;&gt;(waffle)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;3&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=ghgcrKKLBuc&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=4&quot; transform=&quot;translate(170.76357,654.01349)&quot;&gt;
      &lt;rect style=&quot;stroke-width:0;fill:var(--c2);&quot; width=&quot;417&quot; height=&quot;50&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot; &#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot;&gt;
       &lt;tspan x=&quot;210.5&quot; y=&quot;20&quot;&gt;knock, knock. race condition! who&#x27;s there?&lt;&#x2F;tspan&gt;
       &lt;tspan x=&quot;210.5&quot; y=&quot;42.5&quot;&gt;a refactoring story (stefan baumgartner)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;4&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=3YkTi0u_5LU&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=5&quot; transform=&quot;translate(647.79697,579.84145)&quot;&gt;
      &lt;rect style=&quot;stroke-width:0;fill:var(--c2);&quot; width=&quot;417&quot; height=&quot;70&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot;&#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot;&gt;
        &lt;tspan x=&quot;210.5&quot; y=&quot;20&quot;&gt;when uom isn&#x27;t enough:&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;210.5&quot; y=&quot;42.5&quot;&gt;handling units of measurement in production cad&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;210.5&quot; y=&quot;65&quot;&gt;(rose peck)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;5&quot; transform=&quot;translate(205.26999,462.04765)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=8iy37I0WE_s&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=6&quot;&gt;
      &lt;rect style=&quot;stroke-width:0;fill:var(--c2);&quot; width=&quot;240&quot; height=&quot;70&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot;&#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot;&gt;
        &lt;tspan x=&quot;120.5&quot; y=&quot;20&quot;&gt;lessons learnt from using rust&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;120.5&quot; y=&quot;42.5&quot;&gt;as the intro to programming&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;120.5&quot; y=&quot;65&quot;&gt;(andreea costea)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;6&quot; transform=&quot;translate(1144.0865,864.18533)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=Wv88a4vAE5g&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=7&quot;&gt;
      &lt;rect style=&quot;stroke-width:0;fill:var(--c2);&quot; width=&quot;195&quot; height=&quot;70&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot;&#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot;&gt;
        &lt;tspan x=&quot;100.5&quot; y=&quot;20&quot;&gt;common pitfalls of&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;100.5&quot; y=&quot;42.5&quot;&gt;rewriting things in rust&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;100.5&quot; y=&quot;65&quot;&gt;(cliff l. biffle)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;7&quot; transform=&quot;translate(1374.3207,99.371201)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=11m5HRMvPmU&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=8&quot;&gt;
      &lt;rect style=&quot;stroke-width:0;fill:var(--c2);&quot; width=&quot;245&quot; height=&quot;70&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot;&#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot;&gt;
        &lt;tspan x=&quot;122.5&quot; y=&quot;20&quot;&gt;one year of facet:&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;122.5&quot; y=&quot;42.5&quot;&gt;reflections on reflection&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;122.5&quot; y=&quot;65&quot;&gt;(fasterthanlime - amos wenger)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;8&quot; transform=&quot;translate(490,150)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=Gu17MeidD-Q&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=9&quot;&gt;
      &lt;rect width=&quot;415&quot; height=&quot;70&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot;&#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot;&gt;
        &lt;tspan x=&quot;207.5&quot; y=&quot;20&quot;&gt;growing with our users:&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;207.5&quot; y=&quot;42.5&quot;&gt;how the rust project is reshaping our communication&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;207.5&quot; y=&quot;65&quot;&gt;(niko)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;9&quot; transform=&quot;translate(4.3085614,653.12793)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=CmLAHUuUsAQ&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=10&quot;&gt;
      &lt;rect style=&quot;stroke-width:0;fill:var(--c2);&quot; width=&quot;161&quot; height=&quot;50&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot;&#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot;&gt;
        &lt;tspan x=&quot;82.5&quot; y=&quot;20&quot;&gt;Completion-based IO&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;82.5&quot; y=&quot;42.5&quot;&gt;(Alice Ryhl)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;10&quot; transform=&quot;translate(650,225)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=Nzmj7K0FNRY&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=11&quot;&gt;
      &lt;rect width=&quot;280&quot; height=&quot;70&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot;&#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot;&gt;
        &lt;tspan x=&quot;137.5&quot; y=&quot;20&quot;&gt;Untrusted data in Linux —&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;137.5&quot; y=&quot;42.5&quot;&gt;How Rust is going to save us&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;137.5&quot; y=&quot;65&quot;&gt;(Greg Kroah-Hartman)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;11&quot; transform=&quot;translate(1162.7494,78.168177)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=rekIIXbcmos&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=12&quot;&gt;
      &lt;rect width=&quot;181&quot; height=&quot;70&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot;&#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot;&gt;
        &lt;tspan x=&quot;92.5&quot; y=&quot;20&quot;&gt;Expanding Processing&#x27;s&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;92.5&quot; y=&quot;42.5&quot;&gt;Reach with Rust + Bevy&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;92.5&quot; y=&quot;65&quot;&gt;(Moon Davé)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;12&quot; transform=&quot;translate(1660.0447,94.032821)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=6yy38QI3oNM&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=13&quot;&gt;
      &lt;rect width=&quot;225&quot; height=&quot;70&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot;&#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot;&gt;
        &lt;tspan x=&quot;112.5&quot; y=&quot;20&quot;&gt;Making sound — Dragging&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;112.5&quot; y=&quot;42.5&quot;&gt;a ten year old crate into 2026&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;112.5&quot; y=&quot;65&quot;&gt;(Yara Kleingeld)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;13&quot; transform=&quot;translate(8.7303959,457.3849)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=eVv8lCtw5-s&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=14&quot;&gt;
      &lt;rect width=&quot;181&quot; height=&quot;70&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot;&#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot;&gt;
        &lt;tspan x=&quot;92.5&quot; y=&quot;20&quot;&gt;Learning Rust as First&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;92.5&quot; y=&quot;42.5&quot;&gt;Programming Language&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;92.5&quot; y=&quot;65&quot;&gt;(Karin Lammers)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;14&quot; transform=&quot;translate(1326.9185,421.64695)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=SWRL4JpaR2I&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=15&quot;&gt;
      &lt;rect width=&quot;181&quot; height=&quot;70&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot;&#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot;&gt;
        &lt;tspan x=&quot;92.5&quot; y=&quot;20&quot;&gt;Obsessive Optimization&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;92.5&quot; y=&quot;42.5&quot;&gt;with String Interning&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;92.5&quot; y=&quot;65&quot;&gt;(Arya Dradjica)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;15&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=GKMoOs2-uJE&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=16&quot; transform=&quot;translate(8,90)&quot;&gt;
      &lt;rect width=&quot;415&quot; height=&quot;50&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot;&#x2F;&gt;
      &lt;text style=&quot;fill-opacity:1;fill:var(--f1);stroke:var(--f1);&quot; x=&quot;5&quot; y=&quot;20&quot;&gt;&lt;tspan x=&quot;207.5&quot; y=&quot;20&quot;&gt;Programming Languages as a Community of Belonging&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;207.5&quot; y=&quot;42.5&quot;&gt;(Hazel Weakly)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;16&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=6uRyFMM8vDI&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=17&quot; transform=&quot;translate(8,145)&quot;&gt;
      &lt;rect width=&quot;415&quot; height=&quot;50&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot;&#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot;&gt;&lt;tspan x=&quot;207.5&quot; y=&quot;20&quot;&gt;Open Code Review: Trust Fosters Responsibility&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;207.5&quot; y=&quot;42.5&quot;&gt;(Alice Cecile)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;17&quot; transform=&quot;translate(1109.349,262.37725)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=9A38Dx9ggy8&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=18&quot;&gt;
      &lt;rect width=&quot;201&quot; height=&quot;70&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot;&#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot;&gt;
        &lt;tspan x=&quot;102.5&quot; y=&quot;20&quot;&gt;Tracking down undefined&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;102.5&quot; y=&quot;42.5&quot;&gt;behaviour in Servo&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;102.5&quot; y=&quot;65&quot;&gt;(Josh Bowman-Matthews)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;18&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=UfjYxF0CHQ4&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=19&quot; transform=&quot;translate(660,20)&quot;&gt;
      &lt;rect width=&quot;340&quot; height=&quot;75&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot;&#x2F;&gt;
      &lt;text style=&quot;fill-opacity:1;fill:var(--f1);stroke:var(--f1);&quot; x=&quot;5&quot; y=&quot;20&quot;&gt;&lt;tspan x=&quot;170.5&quot; y=&quot;20&quot;&gt;Between Computer Code And Legal Code:&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;170.5&quot; y=&quot;42.5&quot;&gt;Open Source&#x27;s Influence&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;170.5&quot; y=&quot;65&quot;&gt;(Æva Black)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;19&quot; transform=&quot;translate(823.13783,938.1085)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=5-kaCgtA8ug&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=20&quot;&gt;
      &lt;rect width=&quot;241&quot; height=&quot;50&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot;&#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot;&gt;
        &lt;tspan x=&quot;122.5&quot; y=&quot;20&quot;&gt;One Language, Two IDE Engines&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;122.5&quot; y=&quot;42.5&quot; &#x2F;&gt;&lt;tspan x=
         &quot;122.5&quot; y=&quot;42.5&quot;&gt;(Lukas Wirth Vlad Beskrovny)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;20&quot; transform=&quot;translate(158.05591,382.92742)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=_qtExc8iLSk&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=21&quot;&gt;
      &lt;rect width=&quot;241&quot; height=&quot;50&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot;&#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot;&gt;
        &lt;tspan x=&quot;120.5&quot; y=&quot;20&quot;&gt;Stabilizing decade-old features&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;120.5&quot; y=&quot;42.5&quot;&gt;(Folkert de Vries)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;21&quot; transform=&quot;translate(1217.0197,342.76652)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=osTwwllrgeA&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=22&quot;&gt;
      &lt;rect width=&quot;241&quot; height=&quot;70&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot;&#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot;&gt;
        &lt;tspan x=&quot;122.5&quot; y=&quot;20&quot;&gt;Compilers are just programs:&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;122.5&quot; y=&quot;42.5&quot;&gt;Fixing a bug in the Rust compiler&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;122.5&quot; y=&quot;65&quot;&gt;(Boxy)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;22&quot; transform=&quot;translate(1367.3446,858.61461)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=mLzJfuaomZw&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=23&quot;&gt;
      &lt;rect width=&quot;232&quot; height=&quot;70&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot;&#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot;&gt;
        &lt;tspan x=&quot;116.5&quot; y=&quot;20&quot;&gt;How we replace common C(++)&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;116.5&quot; y=&quot;42.5&quot;&gt;libraries with Rust at scale&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;116.5&quot; y=&quot;65&quot;&gt;(Bastian Kersting)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;23&quot; transform=&quot;translate(1650,955)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=MK_dHFsBQsc&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=24&quot;&gt;
      &lt;rect width=&quot;256&quot; height=&quot;50&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot;&#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot;&gt;&lt;tspan x=&quot;128.5&quot; y=&quot;20&quot;&gt;Writing GPU shaders in plain Rust&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;128.5&quot; y=&quot;42.5&quot;&gt;(Firestar99 - Sebastian Sydow)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;24&quot; transform=&quot;translate(109.58118,299.83082)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=P9lmSc4oLFs&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=25&quot;&gt;
      &lt;rect width=&quot;181&quot; height=&quot;70&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot;&#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot;&gt;
        &lt;tspan x=&quot;92.5&quot; y=&quot;20&quot;&gt;Precise, consistent, and&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;92.5&quot; y=&quot;42.5&quot;&gt;reliable code coverage&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;92.5&quot; y=&quot;65&quot;&gt;(Jynn Nelson)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;25&quot; transform=&quot;translate(1104.1939,712.88753)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=gri0xmaaOic&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=26&quot;&gt;
      &lt;rect width=&quot;135&quot; height=&quot;70&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot;&#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot;&gt;
        &lt;tspan x=&quot;70.5&quot; y=&quot;20&quot;&gt;When Iterators&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;70.5&quot; y=&quot;42.5&quot;&gt;Aren&#x27;t Zero Cost&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;70.5&quot; y=&quot;65&quot;&gt;(Xavier Dennis)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;26&quot; transform=&quot;translate(444.748,740.4294)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=ExxxtADP-t8&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=27&quot;&gt;
      &lt;rect width=&quot;181&quot; height=&quot;70&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot;&#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot;&gt;
        &lt;tspan x=&quot;92.5&quot; y=&quot;20&quot;&gt;Out of tree access&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;92.5&quot; y=&quot;42.5&quot;&gt;to compiler state&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;92.5&quot; y=&quot;65&quot;&gt;(Alona Enraght-Moony)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;27&quot; transform=&quot;translate(233.62492,220.83139)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=M5VPSUQF1q0&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=28&quot;&gt;
      &lt;rect width=&quot;121&quot; height=&quot;70&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot;&#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot;&gt;
        &lt;tspan x=&quot;62.5&quot; y=&quot;20&quot;&gt;The trials of&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;62.5&quot; y=&quot;42.5&quot;&gt;qualifying Rust&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;62.5&quot; y=&quot;65&quot;&gt;(Aïssata Maïga)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;28&quot; transform=&quot;translate(12.225,223.18372)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=CjA9ZM3xcsU&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=29&quot;&gt;
      &lt;rect width=&quot;201&quot; height=&quot;70&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot;&#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot;&gt;
        &lt;tspan x=&quot;102.5&quot; y=&quot;20&quot;&gt;Popping the Hood on Rust&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;102.5&quot; y=&quot;42.5&quot;&gt;in the 2026 Toyota RAV4&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;102.5&quot; y=&quot;65&quot;&gt;(Pete LeVasseur)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;29&quot; transform=&quot;translate(230,30)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=yqNT3UmM6Ak&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=30&quot;&gt;
      &lt;rect width=&quot;415&quot; height=&quot;50&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot;&#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot;&gt;&lt;tspan x=&quot;207.5&quot; y=&quot;20&quot;&gt;Rustaceans on The Move — The Global Impact of Rust&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;207.5&quot; y=&quot;42.5&quot;&gt;(Lori Lorusso)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;30&quot; transform=&quot;translate(575.33724,951.64223)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=FMGdrKEX_tQ&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=31&quot;&gt;
      &lt;rect width=&quot;205&quot; height=&quot;75&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot;&#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot;&gt;
        &lt;tspan x=&quot;102.5&quot; y=&quot;20&quot;&gt;Tales of a transformation&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;102.5&quot; y=&quot;42.5&quot;&gt;journey at Proton&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;102.5&quot; y=&quot;65&quot;&gt;(Jeremy Lempereur)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;31&quot; transform=&quot;translate(231.26723,969.82789)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=TexQVuBgT0k&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=32&quot;&gt;
      &lt;rect width=&quot;241&quot; height=&quot;70&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot;&#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot;&gt;
        &lt;tspan x=&quot;122.5&quot; y=&quot;20&quot;&gt;Scaling up Rust development&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;122.5&quot; y=&quot;42.5&quot;&gt;at the Dutch Electoral Council&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;122.5&quot; y=&quot;65&quot;&gt;(Mark Janssen)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;32&quot; transform=&quot;translate(3.8468556,963.74477)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=xs4hk6Dye2M&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=33&quot;&gt;
      &lt;rect width=&quot;221&quot; height=&quot;70&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot;&#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot;&gt;
        &lt;tspan x=&quot;112.5&quot; y=&quot;20&quot;&gt;Rust for Secure and&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;112.5&quot; y=&quot;42.5&quot;&gt;Sustainable Smart Metering&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;112.5&quot; y=&quot;65&quot;&gt;(Ron van der Pols)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;33&quot; transform=&quot;translate(720,300)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=E0spRyDu5kk&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=34&quot;&gt;
      &lt;rect style=&quot;stroke-width:0;fill:var(--c2);&quot; width=&quot;215&quot; height=&quot;70&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot;&#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot; id=&quot;text100&quot;&gt;&lt;tspan x=&quot;107.5&quot; y=&quot;20&quot;&gt;Oxidizing the Linux Kernel&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;107.5&quot; y=&quot;42.5&quot;&gt;Rust in Production &lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;107.5&quot; y=&quot;65&quot;&gt;Live podcast recording&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;34&quot; transform=&quot;translate(427.34576,95.308485)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=LrcTWBYJAyg&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=36&quot;&gt;
      &lt;rect style=&quot;stroke-width:0;fill:var(--c2);&quot; width=&quot;340&quot; height=&quot;50&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot; id=&quot;rect100&quot; &#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot; id=&quot;text102&quot;&gt;
        &lt;tspan x=&quot;170.5&quot; y=&quot;20&quot;&gt;One year of moderation&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;170.5&quot; y=&quot;42.5&quot;&gt;(Antonio Piraino Oli Scherer)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;35&quot; transform=&quot;translate(1328.1696,709.44536)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=9X-ngiKo_Y0&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=36&quot;&gt;
      &lt;rect width=&quot;161&quot; height=&quot;70&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot; id=&quot;rect102&quot; &#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot; id=&quot;text105&quot;&gt;
        &lt;tspan x=&quot;82.5&quot; y=&quot;20&quot;&gt;FFI in Miri at 8000&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;82.5&quot; y=&quot;42.5&quot;&gt;segfaults per second&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;82.5&quot; y=&quot;65&quot;&gt;(Nia Deckers)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;36&quot; transform=&quot;translate(164.65755,735.68196)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=S7nxamPK76M&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=37&quot;&gt;
      &lt;rect width=&quot;221&quot; height=&quot;70&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot; id=&quot;rect105&quot; &#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot; id=&quot;text108&quot;&gt;
        &lt;tspan x=&quot;112.5&quot; y=&quot;20&quot;&gt;Field Projections — Making&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;112.5&quot; y=&quot;42.5&quot;&gt;Custom Pointers feel Builtin&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;112.5&quot; y=&quot;65&quot;&gt;(Benno Lossin Nadri)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;37&quot; transform=&quot;translate(9.648129,731.95014)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=2GE0GL0ith4&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=38&quot;&gt;
      &lt;rect width=&quot;141&quot; height=&quot;70&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot; id=&quot;rect108&quot; &#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot; id=&quot;text111&quot;&gt;
        &lt;tspan x=&quot;72.5&quot; y=&quot;20&quot; &gt;Can I borrow that?&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;72.5&quot; y=&quot;42.5&quot;&gt;(Reborrow traits)&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;72.5&quot; y=&quot;65&quot;&gt;(Aapo Alasuutari)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;38&quot; transform=&quot;translate(14.516129,835.17596)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=NhTwLwznok8&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=39&quot;&gt;
      &lt;rect width=&quot;151&quot; height=&quot;70&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot; id=&quot;rect111&quot; &#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot; id=&quot;text114&quot;&gt;
        &lt;tspan x=&quot;75.5&quot; y=&quot;20&quot;&gt;Rewriting bors:&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;75.5&quot; y=&quot;42.5&quot;&gt;how hard can it be?&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;75.5&quot; y=&quot;65&quot;&gt;(Jakub Beránek)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;39&quot; transform=&quot;translate(1371.2091,952.48271)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=0c-MNe1Nwe8&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=40&quot;&gt;
      &lt;rect width=&quot;200&quot; height=&quot;24&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot; id=&quot;rect114&quot; &#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot;&gt;
        &lt;tspan x=&quot;99.5&quot; y=&quot;20&quot;&gt;Are we interop yet? (Teor)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;40&quot; transform=&quot;translate(1738.3578,878.34311)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=XnU9eBNTHG0&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=41&quot;&gt;
      &lt;rect width=&quot;165&quot; height=&quot;70&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot;&#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot;&gt;
        &lt;tspan x=&quot;85.5&quot; y=&quot;20&quot;&gt;Why Rust&#x27;s Semantics&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;85.5&quot; y=&quot;42.5&quot;&gt;Don&#x27;t Fit GPUs (Yet)&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;85.5&quot; y=&quot;65&quot;&gt;(Michał Kostrubiec)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;41&quot; transform=&quot;translate(176.17444,841.90586)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=7U-2Pxc3HQ8&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=42&quot;&gt;
      &lt;rect width=&quot;255&quot; height=&quot;50&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot; id=&quot;rect118&quot; &#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot; id=&quot;text120&quot;&gt;
        &lt;tspan x=&quot;128.5&quot; y=&quot;20&quot;&gt;Overcoming GitHub shortcomings&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;128.5&quot; y=&quot;42.5&quot;&gt;with Triagebot (Urgau)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;42&quot; transform=&quot;translate(449.42584,837.67686)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=aRwK5dbJy3Y&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=43&quot;&gt;
      &lt;rect width=&quot;262&quot; height=&quot;70&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot; id=&quot;rect120&quot; &#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot; id=&quot;text123&quot;&gt;
        &lt;tspan x=&quot;132.5&quot; y=&quot;20&quot;&gt;Rethinking the incremental system&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;132.5&quot; y=&quot;42.5&quot;&gt;in between compilation types&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;132.5&quot; y=&quot;65&quot;&gt;(Alejandra González)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;43&quot; transform=&quot;translate(1126.5139,939.6726)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=1NnCJTVYPA4&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=44&quot;&gt;
      &lt;rect width=&quot;231&quot; height=&quot;70&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot; id=&quot;rect123&quot; &#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot; id=&quot;text126&quot;&gt;
        &lt;tspan x=&quot;115.5&quot; y=&quot;20&quot;&gt;Interop is the New Rewrite:&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;115.5&quot; y=&quot;42.5&quot;&gt;Design Axioms for Rust&#x27;s Next&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;115.5&quot; y=&quot;65&quot;&gt;Frontier (Tyler Mandry)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;44&quot; transform=&quot;translate(1463.618,268.51602)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=Gx21yYLwn10&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=45&quot;&gt;
      &lt;rect width=&quot;171&quot; height=&quot;70&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot; id=&quot;rect126&quot; &#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot; id=&quot;text129&quot;&gt;
        &lt;tspan x=&quot;85.5&quot; y=&quot;20&quot;&gt;Writing a top-10&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;85.5&quot; y=&quot;42.5&quot;&gt;chess engine in Rust&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;85.5&quot; y=&quot;65&quot;&gt;(Kora Cosmo Bobak)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;45&quot; transform=&quot;translate(1630.0579,634.62727)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=8t2qE8kgBXc&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=46&quot;&gt;
      &lt;rect width=&quot;121&quot; height=&quot;70&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot; id=&quot;rect129&quot; &#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot; id=&quot;text132&quot;&gt;
        &lt;tspan x=&quot;62.5&quot; y=&quot;20&quot;&gt;Field report on&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;62.5&quot; y=&quot;42.5&quot;&gt;scripting Rust&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;62.5&quot; y=&quot;65&quot;&gt;(Gergely Nagy)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;46&quot; transform=&quot;translate(987.77294,424.04474)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=wDoqQkEylY8&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=47&quot;&gt;
      &lt;rect width=&quot;314&quot; height=&quot;70&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot; id=&quot;rect132&quot; &#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot; id=&quot;text134&quot;&gt;
        &lt;tspan x=&quot;158.5&quot; y=&quot;20&quot;&gt;Debunking Rust WasmPerformance Myths&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;158.5&quot; y=&quot;42.5&quot;&gt;(Andrew Jakubowicz Taj Pereira)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;47&quot; transform=&quot;translate(373.94385,557.392)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=RROFUwKZbCA&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=48&quot;&gt;
      &lt;rect width=&quot;231&quot; height=&quot;70&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot; id=&quot;rect134&quot; &#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot; id=&quot;text137&quot;&gt;
        &lt;tspan x=&quot;115.5&quot; y=&quot;20&quot;&gt;Shaping Tomorrow’s&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;115.5&quot; y=&quot;42.5&quot;&gt;Software Engineers&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;115.5&quot; y=&quot;65&quot;&gt;(Mordecai Emmanuel Etukudo)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;48&quot; transform=&quot;translate(1377.1814,178.62893)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=J1KcRkV_fvk&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=49&quot;&gt;
      &lt;rect width=&quot;183&quot; height=&quot;50&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot; id=&quot;rect137&quot; &#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot; id=&quot;text139&quot;&gt;
        &lt;tspan x=&quot;92.5&quot; y=&quot;20&quot;&gt;Blitz: building a modular&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;92.5&quot; y=&quot;42.5&quot;&gt;web engine (Nico Burns)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;49&quot; transform=&quot;translate(1350.5676,20.029046)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=Kl0aiTSDpSQ&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=50&quot;&gt;
      &lt;rect width=&quot;281&quot; height=&quot;70&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot; id=&quot;rect139&quot; &#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot; id=&quot;text142&quot;&gt;
        &lt;tspan x=&quot;142.5&quot; y=&quot;20&quot;&gt;Programming Your Own DSL in Rust -&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;142.5&quot; y=&quot;42.5&quot;&gt;Experiences from Developing µcad&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;142.5&quot; y=&quot;65&quot;&gt;(Michael Winkelmann)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;50&quot; transform=&quot;translate(1577.9853,177.72485)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=oOy_DRwKqLU&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=51&quot;&gt;
      &lt;rect width=&quot;325&quot; height=&quot;50&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot; id=&quot;rect142&quot; &#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot; id=&quot;text144&quot;&gt;
        &lt;tspan x=&quot;162.5&quot; y=&quot;20&quot;&gt;Racing with Rust: building telemetry tools&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;162.5&quot; y=&quot;42.5&quot;&gt;for racing simulators (Harry van Haaren)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;51&quot; transform=&quot;translate(1212.0902,160.99633)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=yWWVhbyOWWE&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=52&quot;&gt;
      &lt;rect width=&quot;141&quot; height=&quot;70&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot; id=&quot;rect144&quot; &#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot; id=&quot;text147&quot;&gt;
        &lt;tspan x=&quot;72.5&quot; y=&quot;20&quot;&gt;Typst: Designing&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;72.5&quot; y=&quot;42.5&quot;&gt;for Incrementality&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;72.5&quot; y=&quot;65&quot;&gt;(Laurenz Mädje)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
    &lt;a id=&quot;52&quot; transform=&quot;translate(1663.4898,18.753666)&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=_Vwc7y-cv2E&amp;list=PL8Q1w7Ff68DBpmF38rcIAf8Z9Gj2TnlgM&amp;index=53&quot;&gt;
      &lt;rect width=&quot;161&quot; height=&quot;70&quot; style=&quot;stroke-width:0;fill:var(--c2);&quot; x=&quot;0&quot; y=&quot;2&quot; rx=&quot;5&quot; id=&quot;rect147&quot; &#x2F;&gt;
      &lt;text x=&quot;5&quot; y=&quot;20&quot; id=&quot;text150&quot;&gt;
        &lt;tspan x=&quot;82.5&quot; y=&quot;20&quot;&gt;Creativity loves&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;82.5&quot; y=&quot;42.5&quot;&gt;constraint (solvers)&lt;&#x2F;tspan&gt;
        &lt;tspan x=&quot;82.5&quot; y=&quot;65&quot;&gt;(Adam Chalmers)&lt;&#x2F;tspan&gt;
      &lt;&#x2F;text&gt;
    &lt;&#x2F;a&gt;
  &lt;&#x2F;g&gt;
&lt;&#x2F;svg&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>TB: Build a nix pkgs out of nixpkgs</title>
        <published>2026-06-08T00:00:00+00:00</published>
        <updated>2026-06-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://seanborg.tech/tiny-blog/build-nix/"/>
        <id>https://seanborg.tech/tiny-blog/build-nix/</id>
        
        <content type="html" xml:base="https://seanborg.tech/tiny-blog/build-nix/">&lt;p&gt;Nixpkgs uses &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;nixos.org&#x2F;guides&#x2F;nix-pills&#x2F;13-callpackage-design-pattern&quot;&gt;CallPackage&lt;&#x2F;a&gt;
to simplify things in Nixpkgs&lt;&#x2F;p&gt;
&lt;p&gt;Often I find myself making things to eventually go into my nix config so taking
advantage of CallPackage is helpful, but it can be quite tedious to build my
whole config repeatedly to confirm I got all the nix right...&lt;&#x2F;p&gt;
&lt;p&gt;Here is where this short snippet comes in handy&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;nix-build -E &amp;#x27;with import &amp;lt;nixpkgs&amp;gt; { }; callPackage .&amp;#x2F;path&amp;#x2F;to&amp;#x2F;default.nix { }&amp;#x27;

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This builds like nixpkgs does allowing me to test everything without needing to
jump to my config and build the whole machine every time.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;links-to-learn-callpackage-itself&quot;&gt;Links to learn CallPackage itself&lt;&#x2F;h2&gt;
&lt;p&gt;Think I originally learned (and have now forgot) how callpackage works from &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;NixOS&#x2F;nixos-summer&#x2F;blob&#x2F;main&#x2F;content&#x2F;blog&#x2F;callpackage-a-tool-for-the-lazy.md&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;NixOS&#x2F;nixos-summer&#x2F;blob&#x2F;main&#x2F;content&#x2F;blog&#x2F;callpackage-a-tool-for-the-lazy.md&lt;&#x2F;a&gt;
this has now been used as a base to write &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;nix.dev&#x2F;tutorials&#x2F;callpackage&quot;&gt;https:&#x2F;&#x2F;nix.dev&#x2F;tutorials&#x2F;callpackage&lt;&#x2F;a&gt;
which I shall have to read at some point.&lt;&#x2F;p&gt;
&lt;p&gt;Attempting to find the original article I read I came across a few links,
I have barely skim read but hopefully will have some time to one day&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;toraritte.github.io&#x2F;posts&#x2F;callPackage&#x2F;2024-08-18-callPackage.html&quot;&gt;https:&#x2F;&#x2F;toraritte.github.io&#x2F;posts&#x2F;callPackage&#x2F;2024-08-18-callPackage.html&lt;&#x2F;a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;NixOS&#x2F;nix.dev&#x2F;issues&#x2F;651&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;NixOS&#x2F;nix.dev&#x2F;issues&#x2F;651&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;NixOS&#x2F;nixpkgs&#x2F;issues&#x2F;36354&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;NixOS&#x2F;nixpkgs&#x2F;issues&#x2F;36354&lt;&#x2F;a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;stackoverflow&lt;&#x2F;code&gt; &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;56121361&#x2F;where-is-callpackage-defined-in-the-nixpkgs-repo-or-how-to-find-nix-lambda-de&quot;&gt;Where is &lt;code&gt;callPackage&lt;&#x2F;code&gt; defined in the Nixpkgs repo
(or how to find Nix lambda definitions in general)?&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;nixos discourse&lt;&#x2F;code&gt; &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;discourse.nixos.org&#x2F;t&#x2F;where-is-callpackage-defined-exactly-part-2&#x2F;12524&#x2F;6&quot;&gt;Where is &lt;code&gt;callPackage&lt;&#x2F;code&gt; defined exactly? (part 2)&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Tangential:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;stackoverflow&lt;&#x2F;code&gt; &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;33233675&#x2F;nix-whats-the-concrete-difference-between-nixpkgs-and-nixpkgs-pkgs&quot;&gt;Nix: what&#x27;s the concrete difference between
nixpkgs and nixpkgs.pkgs?&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;stackoverflow&lt;&#x2F;code&gt; &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;66201847&#x2F;why-is-functionargs-implemented-twice-i-e-as-a-primop-and-in-lib&quot;&gt;nix - Why is &lt;code&gt;functionArgs&lt;&#x2F;code&gt; implemented twice
(i.e, as a primop and in &lt;code&gt;lib&lt;&#x2F;code&gt;)&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;stackoverflow&lt;&#x2F;code&gt; &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;46083266&#x2F;how-does-nixs-callpackage-call-functions-defined-without-an-ellipsis&quot;&gt;nixpkgs - How does Nix&#x27;s &quot;callPackage&quot; call
functions defined without an ellipsis?&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;NixOS&#x2F;nixpkgs&#x2F;pull&#x2F;270696&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;NixOS&#x2F;nixpkgs&#x2F;pull&#x2F;270696&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>TB: Rust Result&lt;T&gt;</title>
        <published>2026-04-23T00:00:00+00:00</published>
        <updated>2026-04-23T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://seanborg.tech/tiny-blog/til-rust-result/"/>
        <id>https://seanborg.tech/tiny-blog/til-rust-result/</id>
        
        <content type="html" xml:base="https://seanborg.tech/tiny-blog/til-rust-result/">&lt;p&gt;type alias Result &lt;code&gt;type Result&amp;lt;T&amp;gt; = std::result::Result&amp;lt;T, Error&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;allows you to specify &lt;code&gt;Result&amp;lt;T&amp;gt;&lt;&#x2F;code&gt; in your code instead of &lt;code&gt;Result&amp;lt;T,E&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>All the Rust Tutorials</title>
        <published>2025-08-19T00:00:00+00:00</published>
        <updated>2025-08-19T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://seanborg.tech/blog/huge-tutorial-list/"/>
        <id>https://seanborg.tech/blog/huge-tutorial-list/</id>
        
        <content type="html" xml:base="https://seanborg.tech/blog/huge-tutorial-list/">&lt;p&gt;TL;DR I made a website (webpage) of all the previous This Week in Rust
walkthroughs &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tutorials.rustmanchester.co.uk&#x2F;external-learning&#x2F;&quot;&gt;https:&#x2F;&#x2F;tutorials.rustmanchester.co.uk&#x2F;external-learning&#x2F;&lt;&#x2F;a&gt;
and I would love help making the page more useful, see &lt;a href=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;huge-tutorial-list&#x2F;#call-for-participation&quot;&gt;#Call for participation&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-idea&quot;&gt;The idea&lt;&#x2F;h2&gt;
&lt;p&gt;One night I had the idea to catalogue all the Rust tutorials I could find and
make one of those awesome list pages, both to increase awareness of &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmanchester.co.uk&quot;&gt;Rust Manchester&lt;&#x2F;a&gt;
and to have a place to direct people when they were asking for things to learn at
our events.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;searching&quot;&gt;Searching&lt;&#x2F;h3&gt;
&lt;p&gt;At the moment I think clicking &quot;show all tutorials&quot; and ctrl+f the website is the
best search, but I have started making a more robust search and as the webpage grows
it will improve&lt;&#x2F;p&gt;
&lt;p&gt;For searching there are 3 parts:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Search if text exists in the title&#x2F;summery. At the moment this is a case-insensitive
exact search, I hope to make it fuzzy soon&lt;&#x2F;li&gt;
&lt;li&gt;Format filter, all catalogue resources have a format type like; Blog, video,
...&lt;&#x2F;li&gt;
&lt;li&gt;Tag filter, I have started assigning various hierarchical tags to each resource&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
 &lt;figure class=&quot;fi ci b1&quot;&gt;&lt;img src=&quot;format_tags.png&quot; class=&quot;ci b1&quot; alt=&quot;Image showing what the tags and format options look like on the webpage&quot; width=&quot;600&quot; height=&quot;391&quot; loading=&quot;lazy&quot; &#x2F;&gt;&lt;figcaption&gt;Tag &amp;amp; Format options&lt;&#x2F;figcaption&gt;&lt;&#x2F;figure&gt;
&lt;p&gt;Formats originally was fairly straightforwards thinking each tutorial will only
be one format, but I started to come across videos that also had the same content
in a blog, or recorded talks that were born from blogs or had transcripts, giving
rise to something like the following :D&lt;&#x2F;p&gt;
 &lt;img src=&quot;example_tutorial.png&quot; class=&quot;ci b1&quot; alt=&quot;Image of an example learning resource on the website&quot; width=&quot;1033&quot; height=&quot;330&quot; loading=&quot;lazy&quot; &#x2F;&gt;
&lt;h2 id=&quot;technical&quot;&gt;Technical&lt;&#x2F;h2&gt;
&lt;p&gt;Source code at &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;rust-manchester&#x2F;website-external-learning&#x2F;&quot;&gt;https:&#x2F;&#x2F;gitlab.com&#x2F;rust-manchester&#x2F;website-external-learning&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;To kick-start the list I grabbed all the previous walkthroughs
in &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;this-week-in-rust.org&#x2F;&quot;&gt;This Week in Rust&lt;&#x2F;a&gt;,
then got to categorising &amp;amp; tagging them. Originally started with markdown
converted to html with &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.getzola.org&#x2F;&quot;&gt;Zola&lt;&#x2F;a&gt;
on Rust Manchester&#x27;s main website. I got through cataloguing about 100 before I
realised this was untenable! So I moved it to a subdomain&#x2F;separate repo, soon after
I also moved away from Zola and wrote a &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;rust-manchester&#x2F;website-external-learning&#x2F;-&#x2F;blob&#x2F;master&#x2F;tutorials.json&quot;&gt;huge json&lt;&#x2F;a&gt;
that is parsed with some JavaScript then saved to the browser local storage (indexedDB)
for better search performance and to keep the site static (for the free hosting).&lt;&#x2F;p&gt;
&lt;p&gt;Overall I&#x27;m happy with the current page and a happy
accident of all the data being in JSON is you are able to curl and
query it using JQ on the command line (if you don&#x27;t like the look&#x2F;feel of the website)
&lt;code&gt;curl https:&#x2F;&#x2F;gitlab.com&#x2F;rust-manchester&#x2F;website-external-learning&#x2F;-&#x2F;raw&#x2F;master&#x2F;tutorials.json | jq .&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;call-for-participation&quot;&gt;Call for participation&lt;&#x2F;h2&gt;
&lt;p&gt;To date I have got through cataloguing 162 of ~2000 and have quickly realised why
nobody has done this before&lt;sup class=&quot;footnote-reference&quot; id=&quot;fr-1-1&quot;&gt;&lt;a href=&quot;#fn-1&quot;&gt;[1]&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; :D&lt;&#x2F;p&gt;
&lt;p&gt;Which gets to the point of this article, I need help!&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;With the content
&lt;ul&gt;
&lt;li&gt;Help categorising, anything after 162
in &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;rust-manchester&#x2F;website-external-learning&#x2F;-&#x2F;blob&#x2F;master&#x2F;tutorials.json&quot;&gt;this JSON&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Help defining the categories, for easier categorising. I started in &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;rust-manchester&#x2F;website-external-learning&#x2F;-&#x2F;blob&#x2F;master&#x2F;README.md?ref_type=heads&amp;amp;plain=1#L95&quot;&gt;the README,&lt;&#x2F;a&gt;
but it just looks like a wall of text not very quick&#x2F;easy to parse (also
horribly written, sorry)&lt;&#x2F;li&gt;
&lt;li&gt;Help consolidating, there are a lot of blog series ( one subject being
taught over multiple blogs ) that are currently shown as separate items
but should be 1&lt;&#x2F;li&gt;
&lt;li&gt;Feedback, good or bad! I have no idea if what I&#x27;ve done is useful, I know
I&#x27;ve found some use out of it and a few of my friends have, but I don&#x27;t know
if it&#x27;s something the community would want, or heck if there is something
similar that exists that I could put my effort into&lt;&#x2F;li&gt;
&lt;li&gt;Probably some help documenting how to do all the above :&#x2F; I tried my best
with
the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;rust-manchester&#x2F;website-external-learning&#x2F;-&#x2F;blob&#x2F;master&#x2F;README.md&quot;&gt;README,&lt;&#x2F;a&gt;
but I suspect I missed a lot of things&lt;&#x2F;li&gt;
&lt;li&gt;Submitting your learning resources! If you&#x27;ve made a thing to help teach
rust I&#x27;d love to add it to this list&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;With the site, these I
made &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;rust-manchester&#x2F;website-external-learning&#x2F;-&#x2F;issues&quot;&gt;gitlab issues for&lt;&#x2F;a&gt;
&lt;ul&gt;
&lt;li&gt;Improve the search, this really encompass a lot of possible changes
&lt;ul&gt;
&lt;li&gt;Real time updating of the list instead of requiring the page to
reload &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;rust-manchester&#x2F;website-external-learning&#x2F;-&#x2F;issues&#x2F;5&quot;&gt;issue #5&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;have some and&#x2F;or&#x2F;not logic, so you can better narrow a
search &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;rust-manchester&#x2F;website-external-learning&#x2F;-&#x2F;issues&#x2F;7&quot;&gt;issue #7&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Clicking tags could auto update the search in some
way &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;rust-manchester&#x2F;website-external-learning&#x2F;-&#x2F;issues&#x2F;6&quot;&gt;issue #6&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;give a date range for
search &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;rust-manchester&#x2F;website-external-learning&#x2F;-&#x2F;issues&#x2F;8&quot;&gt;issue #8&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Clicking crate tags could jump you to the official crates.io page (realise
this is in conflict with one of my search
ideas) &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;rust-manchester&#x2F;website-external-learning&#x2F;-&#x2F;issues&#x2F;14&quot;&gt;issue#14&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Sorting
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;rust-manchester&#x2F;website-external-learning&#x2F;-&#x2F;issues&#x2F;9&quot;&gt;issue #9&lt;&#x2F;a&gt;
&lt;ul&gt;
&lt;li&gt;probably by date&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Error handing - I&#x27;m sorry I basically did none :&#x2F; js is very forgiving...
until it&#x27;s not
&lt;ul&gt;
&lt;li&gt;Handle failed saves to the
indexedDB &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;rust-manchester&#x2F;website-external-learning&#x2F;-&#x2F;issues&#x2F;11&quot;&gt;issue #11&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;handle failed downloads of the JSONL
files &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;rust-manchester&#x2F;website-external-learning&#x2F;-&#x2F;issues&#x2F;10&quot;&gt;issue #10&lt;&#x2F;a&gt;
&lt;ul&gt;
&lt;li&gt;have some retry mechanism for these&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Improve
editing &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;rust-manchester&#x2F;website-external-learning&#x2F;-&#x2F;issues&#x2F;12&quot;&gt;issue #12&lt;&#x2F;a&gt;
&lt;ul&gt;
&lt;li&gt;a way to click from website to the item in the huge json, for easy
editing&lt;&#x2F;li&gt;
&lt;li&gt;a forum to fill out that could generate a diff, or at the start an
email&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Again all of the above is very open to interpretation changes! Please &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;rust-manchester&#x2F;website-external-learning&#x2F;-&#x2F;issues&quot;&gt;open issues&lt;&#x2F;a&gt;
for any ideas you have or even email me if you don&#x27;t have Gitlab &lt;a href=&quot;mailto:sean@seanborg.tech&quot;&gt;sean@seanborg.tech&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;other-stuff&quot;&gt;Other stuff&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;daily-tutorials&quot;&gt;Daily tutorials&lt;&#x2F;h3&gt;
&lt;p&gt;Chatting about the webpage with Rory and &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;CardboardTurkey&#x2F;&quot;&gt;Kiran&lt;&#x2F;a&gt;
they came up with the idea of daily tutorials as a way to have a common tutorial
to discuss between other Rustaceans on the Rust Manchester discord, this also gave
an easy tutorial to go to when you&#x27;re not looking for something specific.
I thought it was a great idea, so it&#x27;s right at the top of the page :D also for
permalinks there is an &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tutorials.rustmanchester.co.uk&#x2F;daily&#x2F;archive&#x2F;&quot;&gt;archive page!&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;link-checking&quot;&gt;Link checking&lt;&#x2F;h3&gt;
&lt;p&gt;To attempt to keep the links relevant I am using &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;lychee.cli.rs&#x2F;&quot;&gt;Lychee&lt;&#x2F;a&gt;
however with over 2000 links I found there are a lot of transient problems...
To mitigate this issue the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;rust-manchester&#x2F;website-external-learning&#x2F;-&#x2F;blob&#x2F;master&#x2F;.gitlab-ci.yml?ref_type=heads#L74&quot;&gt;CI&lt;&#x2F;a&gt;
now caches the result of daily link checks and only errors if the link failed
for 7 days in a row, which still kicks out a lot of errors I need to address &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;rust-manchester&#x2F;website-external-learning&#x2F;-&#x2F;issues&#x2F;17&quot;&gt;&amp;gt;300&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;hr&gt;&lt;ol class=&quot;footnotes-list&quot;&gt;
&lt;li id=&quot;fn-1&quot;&gt;
&lt;p&gt;I have no idea if anyone has done this before, I went looking but no luck,
if someone has done this before please let me know as it would be a lot better
to consolidate this in one place than have 2 places :D &lt;a href=&quot;#fr-1-1&quot;&gt;↩&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Rust learning list</title>
        <published>2025-03-25T00:00:00+00:00</published>
        <updated>2025-03-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://seanborg.tech/projects/rust-learning-list/"/>
        <id>https://seanborg.tech/projects/rust-learning-list/</id>
        
        <content type="html" xml:base="https://seanborg.tech/projects/rust-learning-list/">&lt;h2 id=&quot;goal&quot;&gt;Goal&lt;&#x2F;h2&gt;
&lt;p&gt;Make a nice and easy way to search all of Rust&#x27;s learning resources by
cataloging every Rust learning resource out there!&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why&quot;&gt;Why&lt;&#x2F;h2&gt;
&lt;p&gt;Both to increase awareness of &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmanchester.co.uk&quot;&gt;Rust Manchester&lt;&#x2F;a&gt;
and to help direct people when they were asking for things to learn at our
events.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;where&quot;&gt;Where&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tutorials.rustmanchester.co.uk&#x2F;external-learning&#x2F;&quot;&gt;https:&#x2F;&#x2F;tutorials.rustmanchester.co.uk&#x2F;external-learning&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;source-code&quot;&gt;Source code&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;rust-manchester&#x2F;website-external-learning&#x2F;&quot;&gt;https:&#x2F;&#x2F;gitlab.com&#x2F;rust-manchester&#x2F;website-external-learning&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;current-state&quot;&gt;Current state&lt;&#x2F;h2&gt;
&lt;p&gt;Actively worked on, categorised ~160 of ~2000 so far!&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Structuring code in a multi-page Crux app</title>
        <published>2025-02-19T00:00:00+00:00</published>
        <updated>2025-02-19T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://seanborg.tech/blog/structuring-crux-for-multi-pages/"/>
        <id>https://seanborg.tech/blog/structuring-crux-for-multi-pages/</id>
        
        <content type="html" xml:base="https://seanborg.tech/blog/structuring-crux-for-multi-pages/">&lt;p&gt;Welcome to how I structure a multi-page application using the
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;guide.elm-lang.org&#x2F;architecture&#x2F;&quot;&gt;Elm architecture&lt;&#x2F;a&gt;
in the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;redbadger&#x2F;crux&quot;&gt;Crux&lt;&#x2F;a&gt; framework.
Writing this because I found a lot of single page app examples but haven&#x27;t come
across how people handle multiple pages.&lt;&#x2F;p&gt;
&lt;p&gt;All code examples come from my &lt;a href=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;projects&#x2F;flow-lines&#x2F;&quot;&gt;UpLine app&lt;&#x2F;a&gt;
(specifically from the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;seam345&#x2F;flow-lines&#x2F;-&#x2F;tree&#x2F;blog-post&quot;&gt;&lt;code&gt;blog-post&lt;&#x2F;code&gt; branch&lt;&#x2F;a&gt;)&lt;&#x2F;p&gt;
&lt;p&gt;The first 4 things is how I structure the core (Rust), 5 is then the
shell (Android) side.
(p.s. I don&#x27;t explain Crux at all, I am assuming your familiar with it. I would
recommend the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;redbadger.github.io&#x2F;crux&#x2F;#design-overview&quot;&gt;design overview&lt;&#x2F;a&gt;
as a quick way to understand this blog)&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Starting out with the data types I
structure &lt;a href=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;structuring-crux-for-multi-pages&#x2F;#the-model&quot;&gt;The Model&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Then continuing with the types I structure
the &lt;a href=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;structuring-crux-for-multi-pages&#x2F;#events&quot;&gt;Events&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Finally jumping
into &lt;a href=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;structuring-crux-for-multi-pages&#x2F;#the-code&quot;&gt;The Code&lt;&#x2F;a&gt;
and show how the above heavily influence the structure&lt;&#x2F;li&gt;
&lt;li&gt;A quick look at how I organised
the &lt;a href=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;structuring-crux-for-multi-pages&#x2F;#uimodel-viewmodel&quot;&gt;UiModel&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Lastly how the UiModel influences
the &lt;a href=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;structuring-crux-for-multi-pages&#x2F;#android&quot;&gt;Android&lt;&#x2F;a&gt; code
structure&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;the-model&quot;&gt;The model&lt;&#x2F;h2&gt;
&lt;p&gt;The internal data &lt;code&gt;struct&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;top-level-model&quot;&gt;Top level Model&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;#[derive(Default, Clone, Debug)]
pub struct Model {
    pub screen: Screens,
    pub user_messages: UserMessages,
    pub example_data: ExampleData,
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Above we have the &lt;code&gt;Screens&lt;&#x2F;code&gt; type, which hides all the magic, then anything
I want persistent across screens I need to also keep at this level so in
my case &lt;code&gt;user_messages&lt;&#x2F;code&gt; and &lt;code&gt;example_data&lt;&#x2F;code&gt; we keep out of &lt;code&gt;Screens&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;model-screens&quot;&gt;Model.screens&lt;&#x2F;h3&gt;
&lt;p&gt;The magic... A massive &lt;code&gt;enum&lt;&#x2F;code&gt; :D&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; order is alphabetical to be in line with file browser  
#[derive(Clone, Debug)]
pub enum Screens {
    InitialScreen(InitialScreenData),
    InputFieldsTagsScreen(InputFieldsAndTagsScreenData),
    PostWriteReviewScreen(PostWriteReviewScreenData),
    SelectOrgBucketMeasurementScreen(Box&amp;lt;SelectOrgBucketMeasurementScreenData&amp;gt;),
    SettingPreviousConnectionsScreen(SettingsPreviousConnectionsActionsScreenData),
    SettingsScreen,
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;With Rust allowing enum variants to hold internal data I make a variation per
screen appended with &lt;code&gt;Screen&lt;&#x2F;code&gt; and create a new data type per screen appended
with &lt;code&gt;ScreenData&lt;&#x2F;code&gt;, the naming scheme was helpful for myself to keep track of
what I have where.&lt;&#x2F;p&gt;
&lt;details&gt;
&lt;summery&gt;Why the box?&lt;&#x2F;summery&gt;
&lt;p&gt;Very simply clippy or the compiler suggested it once and I went with it, at some
point I should review and maybe convert others. The reason was about the enum
data types being wildly different sizes.&lt;&#x2F;p&gt;
&lt;&#x2F;details&gt;
&lt;h3 id=&quot;moving-screens&quot;&gt;Moving screens&lt;&#x2F;h3&gt;
&lt;p&gt;Originally thought moving screens would be problematic&#x2F;annoying but using
Rust&#x27;s strong typing we can get some nice wins from this. First is we can
take unvalidated user input held as &lt;code&gt;option&amp;lt;String&amp;gt;&lt;&#x2F;code&gt; and validate
it converting it to just a &lt;code&gt;String&lt;&#x2F;code&gt; or another strong type, simplifying
logic on later screens. We can see this between my first 2 screens
&lt;code&gt;initial&lt;&#x2F;code&gt;&amp;gt;&lt;code&gt;SelectOrgBucketMeasurement&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;structuring-crux-for-multi-pages&#x2F;img_1.png&quot; alt=&quot;Image displaying 2 structures and a try_from impl block&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Secondly we can leverage TryFrom quite nicely so we can do
~ &lt;code&gt;let screen2: Screen2data = screen1data.tryinto()&lt;&#x2F;code&gt;
allowing us to easily move to screen2 with the Err giving us a nice
message to relay back to the user&lt;&#x2F;p&gt;
&lt;h2 id=&quot;events&quot;&gt;Events&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;top-level-events&quot;&gt;Top level Events&lt;&#x2F;h3&gt;
&lt;p&gt;Similar to &lt;code&gt;Model&lt;&#x2F;code&gt; with &lt;code&gt;Event&lt;&#x2F;code&gt; we can associate data with the enums,
this time though we are just wrapping up more &lt;code&gt;enum&lt;&#x2F;code&gt;s as sub events
per screen, so...&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; order is alphabetical to be in line with file browser
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum Event {
    EvInitialScreen(ScEvInitial),
    EvInputFieldsAndTagsEventsScreen(ScEvInputFieldsAndTags),
    EvPostWriteReviewScreen(ScEvPostWriteReview),
    EvSettingsScreen(ScEvSettings),
    EvSelectOrgBucketMeasurementScreen(ScEvSelectOrgBucketMeasurement),
    EvSettingsPreviousConnectionsScreen(ScEvSettingPreviousConnections),
    EvGlobal(GlEvGlobal),
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;As the &lt;code&gt;Event&lt;&#x2F;code&gt; type crosses the FFI boundary I found it much nicer for type
completion in Kotlin to prepend the &lt;code&gt;Ev&lt;&#x2F;code&gt; for event variation and &lt;code&gt;ScEv&lt;&#x2F;code&gt; for
the internal data on each event variation.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;screen-event&quot;&gt;Screen event&lt;&#x2F;h3&gt;
&lt;p&gt;Internally the screen events&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; order is alphabetical to be in line with file browser
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum ScEvInitial {
    AppendPreviousConnection(PreviousConnection),
    #[serde(skip)]
    AppendPreviousConnectionResult(PersistentStorageOutput),
    InfluxBucketsConnectionTest,
    #[serde(skip)]
    InfluxBucketsConnectionTestResponse(crux_http::Result&amp;lt;crux_http::Response&amp;lt;BucketsResult&amp;gt;&amp;gt;),
    LoadPreviousConnections,
    #[serde(skip)]
    LoadPreviousConnectionsResult(PersistentStorageOutput),
    QuickActionsLoad,
    #[serde(skip)]
    QuickActionsLoadResult(PersistentStorageOutput),
    QuickActionsUserSelected(UiStringIndex),
    LoadSettingsScreen,
    SetApiToken(String),
    SetBaseUrl(String),
    UserSelectedPreviousConnection(UiStringIndex),
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Above is all the events I have on the &lt;code&gt;initial&lt;&#x2F;code&gt; screen, this does make
calling an event in Android quite a long as we need to specify screen then event
e.g.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;kotlin&quot; class=&quot;language-kotlin &quot;&gt;&lt;code class=&quot;language-kotlin&quot; data-lang=&quot;kotlin&quot;&gt;core.update(
  Event.EvInitialScreen(
    ScEvInitial.LoadPreviousConnections()
  )
)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;but I found I can make a simple function to reduce that&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;kotlin&quot; class=&quot;language-kotlin &quot;&gt;&lt;code class=&quot;language-kotlin&quot; data-lang=&quot;kotlin&quot;&gt;fun screenEvent(core: Core, coroutineScope: CoroutineScope, event: ScEvInitial) {
    coroutineScope.launch {
        core.update(
            Event.EvInitialScreen(
                event
            )
        )
    }
}

&amp;#x2F;&amp;#x2F; now calls look like
screenEvent(core, coroutineScope, LoadPreviousConnections())
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;global-events&quot;&gt;Global Events&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum GlEvGlobal {
    UpdateTutorial(u8),
    BackButtonClicked,
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Originally I had global events in the top level but figured it was just nicer
to wrap them up in another &lt;code&gt;enum&lt;&#x2F;code&gt; so they have a nice place to grow. 🌱&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-code&quot;&gt;The Code&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;app-rs-rust-entry-point&quot;&gt;app.rs (Rust entry point)&lt;&#x2F;h3&gt;
&lt;p&gt;First the &lt;code&gt;Event&lt;&#x2F;code&gt; enum and how that influences the code structure.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;structuring-crux-for-multi-pages&#x2F;Pasted%20image%2020250213195930.png&quot; alt=&quot;Image displaying Events enum in app.rs file and how tha alphabetical ordering maps to the filesystem&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;With the above picture we can see why I ordered things alphabetically, because
for each &lt;code&gt;ScEv&amp;lt;name&amp;gt;&lt;&#x2F;code&gt; I have made a folder &lt;code&gt;screens&#x2F;&amp;lt;name&amp;gt;&#x2F;mod.rs&lt;&#x2F;code&gt;. Keeping it
alphabetical helps with easily jumping between the enum folder and vice versa.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;top-level-update-function&quot;&gt;Top level update function&lt;&#x2F;h4&gt;
&lt;p&gt;Now all my top level Events are really a group of screen events my top level
update function, is just passing the event onto a function in the mod.rs of
the corresponding folder e.g. event of type &lt;code&gt;EvInitialScreen&lt;&#x2F;code&gt; will call a
function in &lt;code&gt;screens&#x2F;initial&#x2F;mod.rs&lt;&#x2F;code&gt;, I took the naming scheme of &lt;name&gt;_
screen_events
so we get &lt;code&gt;initial_screen_events&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn update(&amp;amp;self, event: Self::Event, model: &amp;amp;mut Self::Model, _caps: &amp;amp;Self::Capabilities, )
          -&amp;gt; Command&amp;lt;Effect, Event&amp;gt; {
    &amp;#x2F;&amp;#x2F; ...
    match event {
        InitialScreenEvents(internal_event) =&amp;gt; initial_screen_events(internal_event, model),
        &amp;#x2F;&amp;#x2F; ...
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h5 id=&quot;full-update-function-at-time-of-writing&quot;&gt;Full update function at time of writing&lt;&#x2F;h5&gt;
&lt;p&gt;The actuality of the function has a bit of global setup, so for me setting up
android logging and some messing with
the user_messages. But after all that we have the same match statement and
keeping the alphabetical order to help find
things, so as long as we don&#x27;t have a semicolon at the end we can quite quickly
verify that all the return commands are
actually coming from &lt;code&gt;screen&#x2F;&amp;lt;name&amp;gt;&#x2F;mod.rs&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn update(&amp;amp;self, event: Self::Event, model: &amp;amp;mut Self::Model, _caps: &amp;amp;Self::Capabilities,
) -&amp;gt; Command&amp;lt;Effect, Event&amp;gt; {
    START.call_once(|| {
        android_logger::init_once(
            Config::default()
                .with_max_level(log::LevelFilter::Warn)
                .with_tag(&amp;quot;FL:C&amp;quot;),
        );
    });

    trace!(&amp;quot;Clear all existing messages&amp;quot;);
    model.user_messages.remove_all_messages();

    trace!(&amp;quot;Run event&amp;quot;);
    trace!(&amp;quot;model:{:?}&amp;quot;, model.screen);
    trace!(&amp;quot;event:{:?}&amp;quot;, event);
    match event {
        InitialScreenEvents(internal_event) =&amp;gt; initial_screen_events(internal_event, model),
        SelectOrgBucketMeasurementScreenEvents(internal_event) =&amp;gt; {
            select_org_bucket_measurement_screen_events(internal_event, model)
        }
        InputFieldsAndTagsEventsScreenEvents(internal_event) =&amp;gt; {
            input_fields_and_tags_events(internal_event, model)
        }
        SettingsScreenEvents(internal_event) =&amp;gt; settings_events(internal_event, model),
        SettingsPreviousConnectionsScreenEvents(internal_event) =&amp;gt; {
            setting_previous_connections_events(internal_event, model)
        }
        PostWriteReviewScreenEvents(event) =&amp;gt; post_write_review_screen_events(event, model),
        GlobalEvent(event) =&amp;gt; global_events(event, model),
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;screen-mod-rs&quot;&gt;Screen mod.rs&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;events-update-function&quot;&gt;Events &#x2F; update function&lt;&#x2F;h4&gt;
&lt;p&gt;We shall get to why &lt;code&gt;mod.rs&lt;&#x2F;code&gt; has 400 lines before we get to the only function in
the file, but here is one of our&lt;code&gt;screen&#x2F;&amp;lt;name&amp;gt;&#x2F;mod.rs::&amp;lt;name&amp;gt;_screen_events&lt;&#x2F;code&gt;
functions.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;structuring-crux-for-multi-pages&#x2F;Pasted%20image%2020250214072900.png&quot; alt=&quot;Image showing how a screen enum maps to the fie system when matching&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;First now we are one level down we can get the screen data out of the model,
sadly this can&#x27;t be a thing we can assert at build time but in general the
folder structure makes this easy to maintain. This allows us to pass &lt;code&gt;sreenData&lt;&#x2F;code&gt;
to our functions reducing scope the functions can effect and saving dealing
with repeatedly asserting the screen model.&lt;&#x2F;p&gt;
&lt;p&gt;In general, we have repeated the same trick we had in &lt;code&gt;app.rs&lt;&#x2F;code&gt; now in
&lt;code&gt;screen&#x2F;&amp;lt;name&amp;gt;&#x2F;mod.rs&lt;&#x2F;code&gt; this time all our events correspond to a file. The enum
variant &#x2F; file name &#x2F; function
all have the same name (within naming conventions) e.g. variant
&lt;code&gt;BackButtonPressed()&lt;&#x2F;code&gt; &#x2F;
file name &lt;code&gt;back_button_pressed.rs&lt;&#x2F;code&gt; &#x2F; function &lt;code&gt;fn back_button_pressed() {}&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Again keeping alphabetical order to make things easier to find.&lt;&#x2F;p&gt;
&lt;details&gt;
&lt;summary&gt;My full `function` at time of writing&lt;&#x2F;summary&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;pub fn input_fields_and_tags_events(
    initial_screen_event: InputFieldsAndTagsScreenEventsData,
    model: &amp;amp;mut Model,
) -&amp;gt; Command&amp;lt;Effect, Event&amp;gt; {
    trace!(&amp;quot;input_fields_and_tags screen event: Validating model&amp;quot;);

    let Screens::InputFieldsTagsScreen(ref mut screen_data) = model.screen else {
        error!(&amp;quot;Screen was in an unexpected state {:?}&amp;quot;, model.screen);
        model.user_messages.error_report_to_developer(N065_USED);
        return crux_core::render::render();
    };

    trace!(&amp;quot;Running input fields and tags event&amp;quot;);

    match initial_screen_event {
        InputFieldsAndTagsScreenEventsData::BackButtonPressed =&amp;gt; {
            back_button_pressed(&amp;amp;mut model.screen, &amp;amp;mut model.user_messages)
        }

        InputFieldsAndTagsScreenEventsData::FieldExistingAdd(field) =&amp;gt; {
            field_existing_add(screen_data, field, &amp;amp;mut model.user_messages)
        }
        InputFieldsAndTagsScreenEventsData::FieldExistingRemove(field) =&amp;gt; {
            field_existing_remove(screen_data, field)
        }
        InputFieldsAndTagsScreenEventsData::FieldExistingValueSet(key, value) =&amp;gt; {
            field_existing_value_set(key, value, screen_data, &amp;amp;mut model.user_messages)
        }
        InputFieldsAndTagsScreenEventsData::FluxGetMeasurementsExistingFields =&amp;gt; {
            flux_get_measurements_existing_fields(
                screen_data,
                &amp;amp;mut model.user_messages,
                &amp;amp;model.example_data,
            )
        }
        InputFieldsAndTagsScreenEventsData::FluxGetMeasurementsExistingFieldsResponse(response) =&amp;gt; {
            flux_get_measurements_existing_fields_response(
                screen_data,
                response,
                &amp;amp;mut model.user_messages,
                &amp;amp;model.example_data,
            )
        }
        InputFieldsAndTagsScreenEventsData::FluxGetMeasurementsExistingTags =&amp;gt; {
            flux_get_measurements_existing_tags(
                screen_data,
                &amp;amp;mut model.user_messages,
                &amp;amp;model.example_data,
            )
        }
        InputFieldsAndTagsScreenEventsData::FluxGetMeasurementsExistingTagsResponse(response) =&amp;gt; {
            flux_get_measurements_existing_tags_response(
                screen_data,
                response,
                &amp;amp;mut model.user_messages,
                &amp;amp;model.example_data,
            )
        }
        InputFieldsAndTagsScreenEventsData::FluxGetTagsExistingValues =&amp;gt; Command::done(),
        InputFieldsAndTagsScreenEventsData::FluxGetTagsExistingValuesResponse(response, tag) =&amp;gt; {
            flux_get_tags_existing_values_response(
                screen_data,
                response,
                &amp;amp;mut model.user_messages,
                tag,
            )
        }
        InputFieldsAndTagsScreenEventsData::InfluxWrite =&amp;gt; influx_write(
            &amp;amp;mut model.screen,
            &amp;amp;mut model.user_messages,
            &amp;amp;model.example_data,
        ),
        InputFieldsAndTagsScreenEventsData::InfluxWriteResponse(response) =&amp;gt; {
            influx_write_response(response, model)
        }
        InputFieldsAndTagsScreenEventsData::LineProtocolSubmitLine =&amp;gt; Command::done(),
        InputFieldsAndTagsScreenEventsData::TagExistingAdd(tag) =&amp;gt; tag_existing_add(
            screen_data,
            &amp;amp;mut model.user_messages,
            tag,
            &amp;amp;model.example_data,
        ),
        InputFieldsAndTagsScreenEventsData::TagExistingRemove(tag) =&amp;gt; {
            tag_existing_remove(screen_data, tag)
        }
        InputFieldsAndTagsScreenEventsData::TagExistingValueAcceptModeChange(key) =&amp;gt; {
            tag_existing_value_accept_mode_change(key, screen_data, &amp;amp;mut model.user_messages)
        }
        InputFieldsAndTagsScreenEventsData::TagExistingValueDismissModeChange(key) =&amp;gt; {
            tag_existing_value_dismiss_mode_change(key, screen_data, &amp;amp;mut model.user_messages)
        }
        InputFieldsAndTagsScreenEventsData::TagExistingValueSetIndex(key, value) =&amp;gt; {
            tag_existing_value_set_index(key, value, screen_data, &amp;amp;mut model.user_messages)
        }
        InputFieldsAndTagsScreenEventsData::TagExistingValueSetUser(key, value) =&amp;gt; {
            tag_existing_value_set_user(key, value, screen_data, &amp;amp;mut model.user_messages)
        }
        InputFieldsAndTagsScreenEventsData::TagNewAdd() =&amp;gt; {
            tag_new_add(screen_data, &amp;amp;mut model.user_messages)
        }
        InputFieldsAndTagsScreenEventsData::TagNewKaySet(string_index, key) =&amp;gt; {
            tag_new_key_set(string_index, key, screen_data, &amp;amp;mut model.user_messages)
        }
        InputFieldsAndTagsScreenEventsData::TagNewRemove(string_index) =&amp;gt; {
            tag_new_remove(string_index, screen_data, &amp;amp;mut model.user_messages)
        }
        InputFieldsAndTagsScreenEventsData::TagNewValueSet(string_index, value) =&amp;gt; {
            tag_new_value_set(string_index, value, screen_data, &amp;amp;mut model.user_messages)
        }
        InputFieldsAndTagsScreenEventsData::TimeSet(time) =&amp;gt; time_set(time, screen_data),
        InputFieldsAndTagsScreenEventsData::FieldNewAdd =&amp;gt; {
            field_new_add(screen_data, &amp;amp;mut model.user_messages)
        }
        InputFieldsAndTagsScreenEventsData::FieldNewRemove(string_index) =&amp;gt; {
            field_new_remove(string_index, screen_data, &amp;amp;mut model.user_messages)
        }
        InputFieldsAndTagsScreenEventsData::FieldNewValueSet(string_index, new_value) =&amp;gt; {
            field_new_value_set(
                new_value,
                string_index,
                screen_data,
                &amp;amp;mut model.user_messages,
            )
        }
        InputFieldsAndTagsScreenEventsData::FieldNewKeySet(string_index, new_key) =&amp;gt; {
            field_new_key_set(new_key, string_index, screen_data, &amp;amp;mut model.user_messages)
        }
        InputFieldsAndTagsScreenEventsData::FieldNewChangeType(string_index, new_type) =&amp;gt; {
            field_new_change_type(
                new_type,
                string_index,
                screen_data,
                &amp;amp;mut model.user_messages,
            )
        }
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We can see, although it gets bigger so long as im not terminating the match with
a semicolon Rust will guarantee we are passing the output of each match arm as
our output to the function&lt;&#x2F;p&gt;
&lt;&#x2F;details&gt;
&lt;h4 id=&quot;screen-events-model&quot;&gt;Screen Events &amp;amp; Model&lt;&#x2F;h4&gt;
&lt;p&gt;After watching both the Events enum and Model struct grow in their respective
top level files I decided to pull them into the &lt;code&gt;screen&#x2F;&amp;lt;name&amp;gt;&#x2F;mod.rs&lt;&#x2F;code&gt; as well.
This made a lot of sense as &lt;code&gt;mod.rs&lt;&#x2F;code&gt; is the entry point for the file it is now
also where the Events and Data is defined, keeping the majority of my work on a
screen in 1 folder. Downside &lt;code&gt;mod.rs&lt;&#x2F;code&gt; gets very big, my order was:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Model&lt;&#x2F;li&gt;
&lt;li&gt;Events&lt;&#x2F;li&gt;
&lt;li&gt;Update function&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;which is how the update function ends up 400 lines down :D&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;structuring-crux-for-multi-pages&#x2F;Pasted%20image%2020250214073221.png&quot; alt=&quot;Image showing the screen unum and how it matches to the filesystem by being alphabetically ordered&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;structuring-crux-for-multi-pages&#x2F;Pasted%20image%2020250214072554.png&quot; alt=&quot;Image of a screen model and a hightlighting of the try from for the previous screen&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;In the images above we see the alphabetical ordering again, and the tryFrom
mentioned earlier on.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;full-file&quot;&gt;Full file&lt;&#x2F;h4&gt;
&lt;h3 id=&quot;function-files&quot;&gt;Function files&lt;&#x2F;h3&gt;
&lt;p&gt;Tiny subset of the function files to see how everything works.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;load-quick-actions&quot;&gt;Load quick actions&lt;&#x2F;h4&gt;
&lt;p&gt;In the bellow we have an example of a very small function but this is great to
show the testing of the commands&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;pub fn load_quick_actions() -&amp;gt; Command&amp;lt;Effect, Event&amp;gt; {
    trace!(&amp;quot;load_quick_actions called&amp;quot;);

    persistent_storage::load(QUICK_ACTIONS_FILENAME.to_owned()).then_send(construct_response_event)
}
fn construct_response_event(body: PersistentStorageOutput) -&amp;gt; Event {
    EvInitialScreen(ScEvInitial::QuickActionsLoadResult(body))
}
mod tests {
    use super::*;
    use crate::screens_common::test_common::test_shell;

    #[test]
    fn test_load_quick_actions() {
        let mut commands = load_quick_actions();

        &amp;#x2F;&amp;#x2F; confirm the correct events are called  
        let effects = commands.effects().collect();
        insta::assert_debug_snapshot!(  
            effects, @r#&amp;quot;[
                PersistentStorage(
                    Request(
                       Load(&amp;quot;quick_actions&amp;quot;,),                
                    ),            
                ),        
            ]&amp;quot;#
        );
        test_shell(effects);
        let events: Vec&amp;lt;Event&amp;gt; = commands.events().collect();
        insta::assert_debug_snapshot!(  
            events, @r#&amp;quot; [
                EvInitialScreen(
                    QuickActionsLoadResult(                    
                         FileData(&amp;quot;Just a fake test&amp;quot;,),
                      ),
                ),
            ]&amp;quot;#
        );
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;since there is no branching only one test is needed and really all im doing is
confirming I called the right &lt;code&gt;construct_response_event&lt;&#x2F;code&gt; function but as I know
this function is being called from both the &lt;code&gt;match&lt;&#x2F;code&gt; in &lt;code&gt;app.rs&lt;&#x2F;code&gt; then &lt;code&gt;screen&#x2F;&amp;lt;name&amp;gt;&#x2F;mod.rs&lt;&#x2F;code&gt;
I can be extremely confident that this function with very little scope for
mutability is the side effect of the &lt;code&gt;loadQuickActions&lt;&#x2F;code&gt; event and that it is now
100% tested.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;load-quick-actions-result&quot;&gt;Load quick actions result&lt;&#x2F;h4&gt;
&lt;p&gt;Obviously these functions don&#x27;t stay so small, here&#x27;s a more average one&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;pub fn load_quick_actions_result(
    screen_data: &amp;amp;mut InitialScreenData,
    user_messages: &amp;amp;mut UserMessages,
    response: PersistentStorageOutput,
) -&amp;gt; Command&amp;lt;Effect, Event&amp;gt; {
    trace!(&amp;quot;load_quick_actions_results called&amp;quot;);
    match response {
        PersistentStorageOutput::FileData(data) =&amp;gt; {
            debug!(&amp;quot;loaded data: {data}&amp;quot;);

            let fp = BufReader::new(data.as_bytes());
            let reader = JsonLinesReader::new(fp);
            let items = reader
                .read_all::&amp;lt;QuickAction&amp;gt;()
                .collect::&amp;lt;std::io::Result&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;&amp;gt;();

            match items {
                Ok(value) =&amp;gt; {
                    model.quick_actions = RemoteResource::Loaded(MultiChoice {
                        options: value,
                        selected: None,
                    });
                    crux_core::render::render()
                }
                Err(error) =&amp;gt; {
                    error!(&amp;quot;Failed to get previous connections, error: {:?}&amp;quot;, error);
                    error!(&amp;quot;data was {}&amp;quot;, data);
                    user_messages.add_message(
                        format!(&amp;quot;Failed to get previous connections, error: {:?}&amp;quot;, error),
                        Level::Error,
                    );
                    crux_core::render::render()
                }
            }
        }
        PersistentStorageOutput::SaveOk
        | PersistentStorageOutput::AppendOk
        | PersistentStorageOutput::Error =&amp;gt; todo!(),
        PersistentStorageOutput::LoadErrorFileDoesntExist =&amp;gt; {
            warn!(&amp;quot;Quick actions file doesnt exist!&amp;quot;);

            screen_data.quick_actions = RemoteResource::NonExistent;
            crux_core::render::render()
        }
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;details&gt;
&lt;summary&gt;Once we add the tests for this the file gets quite huge!&lt;&#x2F;summary&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;use crate::capabilities::persistent_storage::PersistentStorageOutput;
use crate::model::UserMessages;
use crate::screens::initial::{InitialScreenData, QuickAction};
use crate::screens::input_fields_and_tags::RemoteResource;
use crate::screens_common::model::multi_choice::MultiChoice;
use crate::{Effect, Event};
use crux_core::Command;
use log::{debug, error, trace, warn, Level};
use serde_jsonlines::JsonLinesReader;
use std::io::BufReader;

pub fn load_quick_actions_result(
    model: &amp;amp;mut InitialScreenData,
    user_messages: &amp;amp;mut UserMessages,
    response: PersistentStorageOutput,
) -&amp;gt; Command&amp;lt;Effect, Event&amp;gt; {
    trace!(&amp;quot;load_quick_actions_results called&amp;quot;);
    match response {
        PersistentStorageOutput::FileData(data) =&amp;gt; {
            debug!(&amp;quot;loaded data: {data}&amp;quot;);

            let fp = BufReader::new(data.as_bytes());
            let reader = JsonLinesReader::new(fp);
            let items = reader
                .read_all::&amp;lt;QuickAction&amp;gt;()
                .collect::&amp;lt;std::io::Result&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;&amp;gt;();

            match items {
                Ok(value) =&amp;gt; {
                    model.quick_actions = RemoteResource::Loaded(MultiChoice {
                        options: value,
                        selected: None,
                    });
                    crux_core::render::render()
                }
                Err(error) =&amp;gt; {
                    error!(&amp;quot;Failed to get previous connections, error: {:?}&amp;quot;, error);
                    error!(&amp;quot;data was {}&amp;quot;, data);
                    user_messages.add_message(
                        format!(&amp;quot;Failed to get previous connections, error: {:?}&amp;quot;, error),
                        Level::Error,
                    );
                    crux_core::render::render()
                }
            }
        }
        PersistentStorageOutput::SaveOk
        | PersistentStorageOutput::AppendOk
        | PersistentStorageOutput::Error =&amp;gt; todo!(),
        PersistentStorageOutput::LoadErrorFileDoesntExist =&amp;gt; {
            warn!(&amp;quot;Quick actions file doesnt exist!&amp;quot;);

            model.quick_actions = RemoteResource::NonExistent;
            crux_core::render::render()
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::screens::initial::InitialScreenData;
    use crate::screens_common::test_common::test_shell;

    #[test]
    fn test_load_previous_connections_result_empty() {
        let mut screen_data = InitialScreenData {
            ..Default::default()
        };
        let mut user_messages = UserMessages::default();
        let response = PersistentStorageOutput::FileData(&amp;quot;&amp;quot;.to_owned());

        let mut commands =
            load_quick_actions_result(&amp;amp;mut screen_data, &amp;amp;mut user_messages, response);

        &amp;#x2F;&amp;#x2F; =========================================================================================
        &amp;#x2F;&amp;#x2F; check that the view has been updated correctly
        &amp;#x2F;&amp;#x2F; =========================================================================================
        &amp;#x2F;&amp;#x2F; todo confirm if I want an empty list or a non exist
        insta::assert_debug_snapshot!(screen_data, @r#&amp;quot;
        InitialScreenData {
            api_token: None,
            base_url: None,
            quick_actions: Loaded(
                MultiChoice {
                    options: [],
                    selected: None,
                },
            ),
            previous_connections: NotLoaded(
                LoadingResource {
                    retry_count: 0,
                    failure_reasons: [],
                    current_state: Initialised,
                },
            ),
            failed_to_contact_influx: false,
        }
        &amp;quot;#);

        insta::assert_debug_snapshot!(user_messages, @r#&amp;quot;
        UserMessages {
            messages: [],
            current_id: 0,
        }
        &amp;quot;#);

        &amp;#x2F;&amp;#x2F; =========================================================================================
        &amp;#x2F;&amp;#x2F; confirm the correct events are called
        &amp;#x2F;&amp;#x2F; =========================================================================================
        let effects = commands.effects().collect();

        insta::assert_debug_snapshot!(
            effects, @r#&amp;quot;
        [
            Render(
                Request(
                    RenderOperation,
                ),
            ),
        ]
        &amp;quot;#);

        test_shell(effects);

        let events: Vec&amp;lt;Event&amp;gt; = commands.events().collect();

        insta::assert_debug_snapshot!(
            events, @&amp;quot;[]&amp;quot;);
    }

    #[test]
    fn test_load_previous_connections_result_file_not_exist() {
        let mut screen_data = InitialScreenData {
            ..Default::default()
        };
        let mut user_messages = UserMessages::default();
        let response = PersistentStorageOutput::LoadErrorFileDoesntExist;

        let mut commands =
            load_quick_actions_result(&amp;amp;mut screen_data, &amp;amp;mut user_messages, response);

        &amp;#x2F;&amp;#x2F; =========================================================================================
        &amp;#x2F;&amp;#x2F; check that the view has been updated correctly
        &amp;#x2F;&amp;#x2F; =========================================================================================
        insta::assert_debug_snapshot!(screen_data, @r#&amp;quot;
        InitialScreenData {
            api_token: None,
            base_url: None,
            quick_actions: NonExistent,
            previous_connections: NotLoaded(
                LoadingResource {
                    retry_count: 0,
                    failure_reasons: [],
                    current_state: Initialised,
                },
            ),
            failed_to_contact_influx: false,
        }
        &amp;quot;#);

        insta::assert_debug_snapshot!(user_messages, @r#&amp;quot;
        UserMessages {
            messages: [],
            current_id: 0,
        }
        &amp;quot;#);

        &amp;#x2F;&amp;#x2F; =========================================================================================
        &amp;#x2F;&amp;#x2F; confirm the correct events are called
        &amp;#x2F;&amp;#x2F; =========================================================================================
        let effects = commands.effects().collect();

        insta::assert_debug_snapshot!(
            effects, @r#&amp;quot;
        [
            Render(
                Request(
                    RenderOperation,
                ),
            ),
        ]
        &amp;quot;#);

        test_shell(effects);

        let events: Vec&amp;lt;Event&amp;gt; = commands.events().collect();

        insta::assert_debug_snapshot!(
            events, @&amp;quot;[]&amp;quot;);
    }

    #[test]
    fn test_load_previous_connections_result() {
        let mut screen_data = InitialScreenData {
            ..Default::default()
        };
        let mut user_messages = UserMessages::default();
        let response = PersistentStorageOutput::FileData(
            r#&amp;quot;{&amp;quot;name&amp;quot;:&amp;quot;test&amp;quot;,&amp;quot;api_token&amp;quot;:&amp;quot;vzO0_9-u3Y0Q-FyO88bIVK8g3beRkvk00hDppgMGiiNVmk1wm0Cmek_QT2hTxcqdUL607FSMtgephwprZPIl7A==&amp;quot;,&amp;quot;base_url&amp;quot;:&amp;quot;http:&amp;#x2F;&amp;#x2F;100.108.38.15:8086&amp;#x2F;&amp;quot;,&amp;quot;org&amp;quot;:{&amp;quot;name&amp;quot;:&amp;quot;test&amp;quot;,&amp;quot;id&amp;quot;:&amp;quot;3aeb5581111a8d5a&amp;quot;},&amp;quot;bucket&amp;quot;:&amp;quot;personal&amp;quot;,&amp;quot;measurement&amp;quot;:&amp;quot;test_crux&amp;quot;,&amp;quot;tags&amp;quot;:{&amp;quot;new_tag2&amp;quot;:&amp;quot;hey2&amp;quot;},&amp;quot;fields&amp;quot;:{&amp;quot;akey&amp;quot;:{&amp;quot;Float&amp;quot;:{&amp;quot;inner&amp;quot;:&amp;quot;&amp;quot;}}}}&amp;quot;#
                .to_owned(),
        );

        let mut commands =
            load_quick_actions_result(&amp;amp;mut screen_data, &amp;amp;mut user_messages, response);

        &amp;#x2F;&amp;#x2F; =========================================================================================
        &amp;#x2F;&amp;#x2F; check that the view has been updated correctly
        &amp;#x2F;&amp;#x2F; =========================================================================================
        insta::assert_debug_snapshot!(screen_data, @r#&amp;quot;
        InitialScreenData {
            api_token: None,
            base_url: None,
            quick_actions: Loaded(
                MultiChoice {
                    options: [
                        QuickAction {
                            name: &amp;quot;test&amp;quot;,
                            api_token: &amp;quot;vzO0_9-u3Y0Q-FyO88bIVK8g3beRkvk00hDppgMGiiNVmk1wm0Cmek_QT2hTxcqdUL607FSMtgephwprZPIl7A==&amp;quot;,
                            base_url: Url {
                                scheme: &amp;quot;http&amp;quot;,
                                cannot_be_a_base: false,
                                username: &amp;quot;&amp;quot;,
                                password: None,
                                host: Some(
                                    Ipv4(
                                        100.108.38.15,
                                    ),
                                ),
                                port: Some(
                                    8086,
                                ),
                                path: &amp;quot;&amp;#x2F;&amp;quot;,
                                query: None,
                                fragment: None,
                            },
                            org: Org {
                                name: &amp;quot;test&amp;quot;,
                                id: &amp;quot;3aeb5581111a8d5a&amp;quot;,
                            },
                            bucket: &amp;quot;personal&amp;quot;,
                            measurement: &amp;quot;test_crux&amp;quot;,
                            tags: {
                                &amp;quot;new_tag2&amp;quot;: Some(
                                    &amp;quot;hey2&amp;quot;,
                                ),
                            },
                            fields: {
                                &amp;quot;akey&amp;quot;: Float(
                                    ValidatedFieldF64 {
                                        inner: &amp;quot;&amp;quot;,
                                    },
                                ),
                            },
                        },
                    ],
                    selected: None,
                },
            ),
            previous_connections: NotLoaded(
                LoadingResource {
                    retry_count: 0,
                    failure_reasons: [],
                    current_state: Initialised,
                },
            ),
            failed_to_contact_influx: false,
        }
        &amp;quot;#);

        insta::assert_debug_snapshot!(user_messages, @r#&amp;quot;
        UserMessages {
            messages: [],
            current_id: 0,
        }
        &amp;quot;#);

        &amp;#x2F;&amp;#x2F; =========================================================================================
        &amp;#x2F;&amp;#x2F; confirm the correct events are called
        &amp;#x2F;&amp;#x2F; =========================================================================================
        let effects = commands.effects().collect();

        insta::assert_debug_snapshot!(
            effects, @r#&amp;quot;
        [
            Render(
                Request(
                    RenderOperation,
                ),
            ),
        ]
        &amp;quot;#);

        test_shell(effects);

        let events: Vec&amp;lt;Event&amp;gt; = commands.events().collect();

        insta::assert_debug_snapshot!(
            events, @&amp;quot;[]&amp;quot;);
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;details&gt;
&lt;h3 id=&quot;global-events-screen-common&quot;&gt;Global events &#x2F; Screen common&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;organisation&quot;&gt;Organisation&lt;&#x2F;h4&gt;
&lt;p&gt;Global events wise I found it nicer to group them like a screen at the moment I
group them in a &lt;code&gt;global&lt;&#x2F;code&gt; folder I am tempted to put them in &lt;code&gt;screens&#x2F;global&lt;&#x2F;code&gt;.
Other than that it&#x27;s the same structure.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;structuring-crux-for-multi-pages&#x2F;Pasted%20image%2020250214085808.png&quot; alt=&quot;Image of the global events and hightinging the screen_common folder&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;screens_common&lt;&#x2F;code&gt; is quite simply anything that I could generalise across screens
functions are top level and data types are under &lt;code&gt;screens_common&#x2F;model&lt;&#x2F;code&gt;. Stuff
in here will be imported in multiple screen functions.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;global-event-propagation&quot;&gt;Global event propagation&lt;&#x2F;h4&gt;
&lt;p&gt;A trick I found with some global events was that if the event changes depending
on what screen its on we can just make an event in the &lt;code&gt;screens&lt;&#x2F;code&gt; folder&lt;&#x2F;p&gt;
&lt;p&gt;so for the global back button in android
&lt;img src=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;structuring-crux-for-multi-pages&#x2F;Pasted%20image%2020250214090001.png&quot; alt=&quot;Image showing how the back button is originally called in global but defers to screen folders&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;I implement back depending which screen im on, this will help as well for when
I move to iOS because they don&#x27;t have a global back button, so I will just call
these events directly&lt;&#x2F;p&gt;
&lt;h2 id=&quot;uimodel-viewmodel&quot;&gt;UiModel (ViewModel)&lt;&#x2F;h2&gt;
&lt;p&gt;Last core organisation I did was the UiModel, renamed it UiModel instead of
ViewModel for fewer chars, and it made more sense to me.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;structuring-crux-for-multi-pages&#x2F;Pasted%20image%2020250214091314.png&quot; alt=&quot;Image showing the UiModel and how it maps to files&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Maybe this could belong in the &lt;code&gt;screens&#x2F;&amp;lt;name&amp;gt;&#x2F;mod.rs&lt;&#x2F;code&gt; but those files are big
enough and the UiModel crosses the FFi so made sense to keep it separate.
Organisation wise very similar setup to screens but now instead of folders I
have a file per screen.&lt;&#x2F;p&gt;
&lt;p&gt;90% of this is just from methods :D&lt;&#x2F;p&gt;
&lt;h2 id=&quot;android&quot;&gt;Android&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;organising-jetpack-compose&quot;&gt;Organising jetpack compose&lt;&#x2F;h3&gt;
&lt;p&gt;Lastly the Android (shell) side, we can keep a surprisingly similar layout
&lt;img src=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;structuring-crux-for-multi-pages&#x2F;Pasted%20image%2020250214091648.png&quot; alt=&quot;Image showing how android folder structure is layed out&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;so instead of a &lt;code&gt;match&lt;&#x2F;code&gt; statement per screen we have a &lt;code&gt;when&lt;&#x2F;code&gt; statement working
in a very similar way, from there was can call a compose method in a separate
file so now every screen has a file under &lt;code&gt;ui&#x2F;&amp;lt;screen name&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;utilising-preview&quot;&gt;Utilising preview&lt;&#x2F;h3&gt;
&lt;p&gt;Then with the lovely &lt;code&gt;@preview&lt;&#x2F;code&gt; annotation we can just build out our UI&#x27;s while
staying all within the one file, I suspect if these files grow much more I may
move to a folder structure similar to core.
&lt;img src=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;structuring-crux-for-multi-pages&#x2F;Pasted%20image%2020250214091922.png&quot; alt=&quot;Image showing @preview in jepack compose&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;&#x2F;h2&gt;
&lt;p&gt;In conclusion my folder structure in core (Rust) is&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;txt&quot; class=&quot;language-txt &quot;&gt;&lt;code class=&quot;language-txt&quot; data-lang=&quot;txt&quot;&gt;├── app.rs
├── capabilities
│   └── persistent_storage.rs
├── global
│   ├── mod.rs
│   ├── function1.rs
│   └── function2.rs
├── lib.rs
├── model.rs
├── model_ui
│   ├── mod.rs
│   ├── screens
│   │   ├── scren1.rs
│   │   └── screen2.rs
├── screens
│   ├── screen1
│   │   ├── mod.rs
│   │   ├── function1.rs
│   │   └── function2.rs
│   ├── screen2
│   │   ├── mod.rs
│   │   └── function1.rs
├── screens_common
│   ├── mod.rs
│   ├── common_function1.rs
│   ├── common_function2.rs
│   ├── model
│   │   ├── mod.rs
│   │   ├── common_type1.rs
│   │   └── common_type2.rs
└── shared.udl
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;Naming schemes
&lt;ul&gt;
&lt;li&gt;Events
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Ev&amp;lt;screen name&amp;gt;&lt;&#x2F;code&gt; for event variation&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ScEv&amp;lt;Scren name&amp;gt;&lt;&#x2F;code&gt; for the variation data&lt;&#x2F;li&gt;
&lt;li&gt;keeping it alphabetical for easy searching.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Model Screens
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;screen name&amp;gt;Screen&lt;&#x2F;code&gt; for variations&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;screen name&amp;gt;ScreenData&lt;&#x2F;code&gt; for variation data&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Ui Model (realising I don&#x27;t stick to this scheme but should update my code)
&lt;ul&gt;
&lt;li&gt;Everything is prepended with &lt;code&gt;Ui&lt;&#x2F;code&gt; to keep an easy distinction for what
goes over FFI and what doesn&#x27;t&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Ui&amp;lt;screen name&amp;gt;Screen&lt;&#x2F;code&gt; for variations&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Ui&amp;lt;screen name&amp;gt;ScreenData&lt;&#x2F;code&gt; for variation data&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;pros&quot;&gt;Pros&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;The majority of my time is spent in just a single folder working on 1 screen&lt;&#x2F;li&gt;
&lt;li&gt;good separation of functions for testing&lt;&#x2F;li&gt;
&lt;li&gt;simple naming scheme to follow&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;cons&quot;&gt;Cons&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;not 100% how this will work if I swap from multi screens to 1 large screen and
multi panes&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Websites</title>
        <published>2025-01-15T00:00:00+00:00</published>
        <updated>2025-01-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://seanborg.tech/projects/websites/"/>
        <id>https://seanborg.tech/projects/websites/</id>
        
        <content type="html" xml:base="https://seanborg.tech/projects/websites/">&lt;h2 id=&quot;goal&quot;&gt;Goal&lt;&#x2F;h2&gt;
&lt;p&gt;Make websites for Me &#x2F; Rust Manchester &#x2F; Track a Train&lt;&#x2F;p&gt;
&lt;p&gt;Learn &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.getzola.org&#x2F;&quot;&gt;Zola&lt;&#x2F;a&gt; for easy website generation with
Gitlab pages.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;where&quot;&gt;Where&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;source&quot;&gt;Source&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;rust-manchester&#x2F;website&quot;&gt;Rust Manchester&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;seam345&#x2F;my-website&quot;&gt;Sean Borg&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;sean-work&#x2F;project-choo-choo&#x2F;website&quot;&gt;Track a Train&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;websites&quot;&gt;Websites&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmanchester.co.uk&quot;&gt;https:&#x2F;&#x2F;rustmanchester.co.uk&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;seanborg.tech ... but your already here :p&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;trackatrain.co.uk&quot;&gt;https:&#x2F;&#x2F;trackatrain.co.uk&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>UpLine Privacy Policy</title>
        <published>2025-01-05T00:00:00+00:00</published>
        <updated>2025-01-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://seanborg.tech/upline-privacy-policy/"/>
        <id>https://seanborg.tech/upline-privacy-policy/</id>
        
        <content type="html" xml:base="https://seanborg.tech/upline-privacy-policy/">&lt;h1 id=&quot;privacy-policy&quot;&gt;Privacy Policy&lt;&#x2F;h1&gt;
&lt;p&gt;Your privacy is critically important to us. Your data is your own. We make
no claims to it and will only use it according to the policies outlined below.&lt;&#x2F;p&gt;
&lt;p&gt;If you have questions about deleting or correcting your personal data please
contact our &lt;a href=&quot;mailto:privacy@trackatrain.co.uk&quot;&gt;support team&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gathering-of-personally-identifying-information&quot;&gt;Gathering of Personally-Identifying Information&lt;&#x2F;h2&gt;
&lt;p&gt;UpLine allows you to upload data to an InfluxDB of your choosing, data used
during this process is only stored locally on your device for quick actions,
and any data sent is only sent to the InfluxDB server you have specified.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;protection-of-certain-personally-identifying-information&quot;&gt;Protection of Certain Personally-Identifying Information&lt;&#x2F;h2&gt;
&lt;p&gt;Any Personally-Identifying Information entered is only sent to the InfluxDB
server you have specified when using UpLine No Personally-Identifying
Information is transmitted to external locations when using UpLine.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;business-transfers&quot;&gt;Business Transfers&lt;&#x2F;h2&gt;
&lt;p&gt;If UpLine, or substantially all of its assets, were acquired, or in the
unlikely event that UpLine goes out of business or enters bankruptcy, user
information would be one of the assets that is transferred or acquired by a
third party. You acknowledge that such transfers may occur, and that any
acquirer of UpLine may continue to use your personal information as set
forth in this policy.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;privacy-policy-changes&quot;&gt;Privacy Policy Changes&lt;&#x2F;h2&gt;
&lt;p&gt;Although most changes are likely to be minor, UpLine may change its Privacy
Policy from time to time, and in UpLines&#x27;s sole discretion. UpLine
encourages visitors to frequently check this page for any changes to its
Privacy Policy. Your continued use of this site after any change in this
Privacy Policy will constitute your acceptance of such change.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;limited-warranties&quot;&gt;Limited Warranties&lt;&#x2F;h2&gt;
&lt;p&gt;We do not warrant the completeness or accuracy of the information published
on this website; nor do we commit to ensuring that the website remains
available or that the material on the website is kept up to date.&lt;&#x2F;p&gt;
&lt;p&gt;To the maximum extent permitted by applicable law, we exclude all
representations, warranties and conditions relating to this website and the
use of this website (including, without limitation, any warranties implied
by law in respect of satisfactory quality, fitness for purpose and&#x2F;or the
use of reasonable care and skill).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;indemnity&quot;&gt;Indemnity&lt;&#x2F;h2&gt;
&lt;p&gt;You hereby indemnify us and undertake to keep us indemnified against any
losses, damages, costs, liabilities and expenses (including, without
limitation, legal expenses and any amounts paid by us to a third party in
settlement of a claim or dispute on the advice of our legal advisers)
incurred or suffered by us arising out of any breach by you of any provision
of these terms of use, or arising out of any claim that you have breached
any provision of these terms of use.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>AI Boat</title>
        <published>2024-09-10T00:00:00+00:00</published>
        <updated>2024-09-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://seanborg.tech/projects/ai-boat/"/>
        <id>https://seanborg.tech/projects/ai-boat/</id>
        
        <content type="html" xml:base="https://seanborg.tech/projects/ai-boat/">&lt;h2 id=&quot;goal&quot;&gt;Goal&lt;&#x2F;h2&gt;
&lt;p&gt;Make a self-driving canal boat&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why&quot;&gt;Why&lt;&#x2F;h2&gt;
&lt;p&gt;Because it sounds like an awesome fun project&lt;&#x2F;p&gt;
&lt;h2 id=&quot;where&quot;&gt;Where&lt;&#x2F;h2&gt;
&lt;p&gt;My repo dealing with interfacing with the sensors is here
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;seam345&#x2F;ai-boat-signaller&quot;&gt;https:&#x2F;&#x2F;gitlab.com&#x2F;seam345&#x2F;ai-boat-signaller&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;current-state&quot;&gt;Current state&lt;&#x2F;h2&gt;
&lt;p&gt;It&#x27;s been a while since I&#x27;ve had a chance to work on it&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Woodchopper</title>
        <published>2024-06-25T00:00:00+00:00</published>
        <updated>2024-06-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://seanborg.tech/projects/woodchopper/"/>
        <id>https://seanborg.tech/projects/woodchopper/</id>
        
        <content type="html" xml:base="https://seanborg.tech/projects/woodchopper/">&lt;h2 id=&quot;goal&quot;&gt;Goal&lt;&#x2F;h2&gt;
&lt;p&gt;Split logs from logcat without a device accessible.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why&quot;&gt;Why&lt;&#x2F;h2&gt;
&lt;p&gt;A client of mine makes an Android device with pre-bundled apps, my main job
was working on these apps but whenever we captured logs from a customer they
were a bit of a nightmare to pull apart&lt;&#x2F;p&gt;
&lt;p&gt;Android logs from a simple &lt;code&gt;logcat -d&lt;&#x2F;code&gt; don’t print out the package ID they
come from, instead it outputs the PID and tag, I would love to know how
Android studio filters of package ID! The tag we can control and filter on
but it’s generally recommended to set the tag as the class name (todo
confirm its recommended and find a link ), I find due to Android studio
filtering with package ID in development I often don&#x27;t pay much attention to
the tags (I admit this is probably the better solution).&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.android.com&#x2F;reference&#x2F;android&#x2F;util&#x2F;Log&quot;&gt;https:&#x2F;&#x2F;developer.android.com&#x2F;reference&#x2F;android&#x2F;util&#x2F;Log&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Tip: A good convention is to declare a TAG constant in your class:
&lt;code&gt;private static final String TAG = &quot;MyActivity&quot;;&lt;&#x2F;code&gt;
and use that in subsequent calls to the log methods.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Aaa although Google never explicitly suggests using the class name it is
accidentally implied, and timber automatically uses the method name as the tag.&lt;&#x2F;p&gt;
&lt;p&gt;I find method name often is way too short lived for a tag ideally I need to
come up with a better convention but since I am inheriting a code base this
is not so easily changed. (started using abriviated app name with colon to a
smaller scope e.g. flow lines, rust core = &lt;code&gt;FL:C&lt;&#x2F;code&gt;)&lt;&#x2F;p&gt;
&lt;p&gt;A very similar application is “PID cat” which works great if you are
debugging a device you have on the desk, but this falls down if someone is
sending you system logs and you have no access to the device. This is where
woodchopper comes in splitting all logs by PID and then from there attempts
to match those logs to package IDs. Woodchopper can do this 2 ways
automatically by matching the PID on application start from the ( todo file
in demon that does this), or semi automatic by you providing some known logs
that your application emits and matching on them to give package id&lt;&#x2F;p&gt;
&lt;p&gt;Who is this for… I’m not so sure really I realise the reason I’m in this
situation is because I’m writing with an Android manufacturer and they make
some built in apps but in normal operation you wouldn’t be shipping with so
much logging&lt;&#x2F;p&gt;
&lt;p&gt;Differs from
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;JakeWharton&#x2F;pidcat&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;JakeWharton&#x2F;pidcat&lt;&#x2F;a&gt;
As this requires the device to be reachable via adb and calls the package
manager during running to get the PID to filter on&lt;&#x2F;p&gt;
&lt;h2 id=&quot;where&quot;&gt;Where&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;seam345&#x2F;woodchopper&quot;&gt;https:&#x2F;&#x2F;gitlab.com&#x2F;seam345&#x2F;woodchopper&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;current-state&quot;&gt;Current state&lt;&#x2F;h2&gt;
&lt;p&gt;I haven&#x27;t come back to it in a while atm it does manage to separate the logs
out, but I think i want to grab ratatui to properly display them nicely&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Yoto</title>
        <published>2024-06-01T00:00:00+00:00</published>
        <updated>2024-06-01T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://seanborg.tech/projects/yoto/"/>
        <id>https://seanborg.tech/projects/yoto/</id>
        
        <content type="html" xml:base="https://seanborg.tech/projects/yoto/">&lt;h2 id=&quot;goal&quot;&gt;Goal&lt;&#x2F;h2&gt;
&lt;p&gt;Run a Yoto with custom firmware so it doesn&#x27;t need a cloud connection, ever&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why&quot;&gt;Why&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;uk.yotoplay.com&#x2F;&quot;&gt;Yoto&lt;&#x2F;a&gt; is a neat little jukebox but im not a fan
of my data having to live in AWS&lt;&#x2F;p&gt;
&lt;p&gt;Also, I have never done any reverse engineering before and that sounded fun&lt;&#x2F;p&gt;
&lt;h2 id=&quot;where&quot;&gt;Where&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;sean-twitch-projects&#x2F;notes&#x2F;-&#x2F;blob&#x2F;main&#x2F;Yoto.md&quot;&gt;https:&#x2F;&#x2F;gitlab.com&#x2F;sean-twitch-projects&#x2F;notes&#x2F;-&#x2F;blob&#x2F;main&#x2F;Yoto.md&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;current-state&quot;&gt;Current state&lt;&#x2F;h2&gt;
&lt;p&gt;I was doing half well weekly streaming me tearing it apart and flashing it
then I hit a bit of a bump and failed to maintain the weekly recurrence, I
have since learned I should really have read what was on the chip before
flashing it so I may have to buy a second to actually move forwards with any
speed&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Rust Manchester meetup</title>
        <published>2024-04-01T00:00:00+00:00</published>
        <updated>2024-04-01T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://seanborg.tech/projects/rust-manchester-meetup/"/>
        <id>https://seanborg.tech/projects/rust-manchester-meetup/</id>
        
        <content type="html" xml:base="https://seanborg.tech/projects/rust-manchester-meetup/">&lt;h2 id=&quot;goal&quot;&gt;Goal&lt;&#x2F;h2&gt;
&lt;p&gt;Increase Rust jobs&#x2F;usage&#x2F;knowledge in Manchester&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why&quot;&gt;Why&lt;&#x2F;h2&gt;
&lt;p&gt;I use and enjoy writing programs in Rust I would love to see it expand in my
home city!&lt;&#x2F;p&gt;
&lt;h2 id=&quot;where&quot;&gt;Where&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmanchester.co.uk&quot;&gt;https:&#x2F;&#x2F;rustmanchester.co.uk&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;current-state&quot;&gt;Current state&lt;&#x2F;h2&gt;
&lt;p&gt;We run a ~monthly meetup at the moment alternating between talks and code
nights, please if you have a talk (and are happy to come to Manchester)
email us &lt;a href=&quot;mailto:talk@rustmanchester.co.uk&quot;&gt;talk@rustmanchester.co.uk&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>UpLine (internally FlowLine)</title>
        <published>2024-01-06T00:00:00+00:00</published>
        <updated>2024-01-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://seanborg.tech/projects/flow-lines/"/>
        <id>https://seanborg.tech/projects/flow-lines/</id>
        
        <content type="html" xml:base="https://seanborg.tech/projects/flow-lines/">&lt;h2 id=&quot;goal&quot;&gt;Goal&lt;&#x2F;h2&gt;
&lt;p&gt;Easy and simple way to log to influxdb&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why&quot;&gt;Why&lt;&#x2F;h2&gt;
&lt;p&gt;I wanted a way in input versions time series data, weight, PB’s,&lt;&#x2F;p&gt;
&lt;p&gt;tools for displaying graphs from influxdb are easy to setup and I can run
complex queries to show what I like really fast, the slow part was getting
the data in when it cant be automated&lt;&#x2F;p&gt;
&lt;h2 id=&quot;where&quot;&gt;Where&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;source-code&quot;&gt;Source code&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;seam345&#x2F;flow-lines&quot;&gt;https:&#x2F;&#x2F;gitlab.com&#x2F;seam345&#x2F;flow-lines&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;app&quot;&gt;App&lt;&#x2F;h3&gt;
&lt;p&gt;Currently, it is available in open beta
 &lt;a href=&quot;https:&#x2F;&#x2F;play.google.com&#x2F;store&#x2F;apps&#x2F;details?id=tech.seanborg.flowlines&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;GetItOnGooglePlay_Badge_Web_color_English.png&quot; class=&quot;ci&quot; alt=&quot;Get UpLine on google play&quot; width=&quot;270&quot; height=&quot;80&quot; loading=&quot;lazy&quot; &#x2F;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;p&gt;
&lt;h2 id=&quot;current-state&quot;&gt;Current state&lt;&#x2F;h2&gt;
&lt;p&gt;Actively worked on and in open beta, hopefully launched soonish&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Zigbee2MQTT types</title>
        <published>2022-10-17T00:00:00+00:00</published>
        <updated>2022-10-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://seanborg.tech/projects/zigbee2mqtt-types/"/>
        <id>https://seanborg.tech/projects/zigbee2mqtt-types/</id>
        
        <content type="html" xml:base="https://seanborg.tech/projects/zigbee2mqtt-types/">&lt;h2 id=&quot;goal&quot;&gt;Goal&lt;&#x2F;h2&gt;
&lt;p&gt;Generate Rust structs for all the Zigbee2MQTT devices&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why&quot;&gt;Why&lt;&#x2F;h2&gt;
&lt;p&gt;While creating &lt;a href=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;projects&#x2F;skipper&#x2F;&quot;&gt;Skipper&lt;&#x2F;a&gt; I found myself being lazy with
how I was defining my structs to consume MQTT payloads and given they are
already defined in Zigbee2MQTT, so I went looking for a way to generate them
programmatically.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;where&quot;&gt;Where&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;seam345&#x2F;zigbee2mqtt-types&quot;&gt;https:&#x2F;&#x2F;gitlab.com&#x2F;seam345&#x2F;zigbee2mqtt-types&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;current-state&quot;&gt;Current state&lt;&#x2F;h2&gt;
&lt;p&gt;Unmaintained... I found a lot of bugs in them at some point they need fixing&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Skipper</title>
        <published>2022-09-28T00:00:00+00:00</published>
        <updated>2022-09-28T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://seanborg.tech/projects/skipper/"/>
        <id>https://seanborg.tech/projects/skipper/</id>
        
        <content type="html" xml:base="https://seanborg.tech/projects/skipper/">&lt;h2 id=&quot;goal&quot;&gt;Goal&lt;&#x2F;h2&gt;
&lt;p&gt;Automate as much of the electronics in my boat, don&#x27;t ask me why I don&#x27;t use
home assistant (it would be easier)&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why&quot;&gt;Why&lt;&#x2F;h2&gt;
&lt;p&gt;Fun&lt;&#x2F;p&gt;
&lt;h2 id=&quot;where&quot;&gt;Where&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;seam345&#x2F;skipper&quot;&gt;https:&#x2F;&#x2F;gitlab.com&#x2F;seam345&#x2F;skipper&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;current-state&quot;&gt;Current state&lt;&#x2F;h2&gt;
&lt;p&gt;Not staying on the boat frequently enough to keep this maintained :&#x2F;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>ESP32 Rust</title>
        <published>2022-07-22T00:00:00+00:00</published>
        <updated>2022-07-22T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://seanborg.tech/projects/esp-32/"/>
        <id>https://seanborg.tech/projects/esp-32/</id>
        
        <content type="html" xml:base="https://seanborg.tech/projects/esp-32/">&lt;h2 id=&quot;goal&quot;&gt;Goal&lt;&#x2F;h2&gt;
&lt;p&gt;Make something useful with an ESP32-C3 and learn writing bear-metal Rust&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why&quot;&gt;Why&lt;&#x2F;h2&gt;
&lt;p&gt;Always wanted to do more low level coding, I&#x27;ve done some in the past with
Arduino&#x27;s and would like to practice with Rust&lt;&#x2F;p&gt;
&lt;h2 id=&quot;where&quot;&gt;Where&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;seam345&#x2F;esp32-rust&quot;&gt;https:&#x2F;&#x2F;gitlab.com&#x2F;seam345&#x2F;esp32-rust&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;current-state&quot;&gt;Current state&lt;&#x2F;h2&gt;
&lt;p&gt;Mostly an abandoned project at this point, but I do use it as an
accelerometer sensor in my boat :D&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>GPS fixer</title>
        <published>2022-07-13T00:00:00+00:00</published>
        <updated>2022-07-13T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://seanborg.tech/projects/gpx-fixer/"/>
        <id>https://seanborg.tech/projects/gpx-fixer/</id>
        
        <content type="html" xml:base="https://seanborg.tech/projects/gpx-fixer/">&lt;h2 id=&quot;goal&quot;&gt;Goal&lt;&#x2F;h2&gt;
&lt;p&gt;Basically replace Google timeline&lt;&#x2F;p&gt;
&lt;p&gt;Using &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;play.google.com&#x2F;store&#x2F;apps&#x2F;details?id=com.ilyabogdanovich.geotracker&amp;amp;hl=en-US&quot;&gt;Geo tracker&lt;&#x2F;a&gt;
I have years of GPX files that would be great to look back on&lt;&#x2F;p&gt;
&lt;p&gt;But I need a nice way to Correct and simplify GPX files.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;things-to-correct&quot;&gt;Things to correct&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;GPS drift when in same location without additional date like satellite count&lt;&#x2F;li&gt;
&lt;li&gt;Loss of gps when on repeated routes&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;where&quot;&gt;Where&lt;&#x2F;h2&gt;
&lt;p&gt;Think its technically living here atm &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;seam345&#x2F;rawgpx-to-influx&quot;&gt;https:&#x2F;&#x2F;gitlab.com&#x2F;seam345&#x2F;rawgpx-to-influx&lt;&#x2F;a&gt;
a previous version was &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;seam345&#x2F;gpx-fixer&quot;&gt;https:&#x2F;&#x2F;gitlab.com&#x2F;seam345&#x2F;gpx-fixer&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;This is a project I keep starting but haven&#x27;t defined my solution properly
or bite off more than I can chew in the spare hours I give it&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Photo dater</title>
        <published>2022-07-13T00:00:00+00:00</published>
        <updated>2022-07-13T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://seanborg.tech/projects/photo-dater/"/>
        <id>https://seanborg.tech/projects/photo-dater/</id>
        
        <content type="html" xml:base="https://seanborg.tech/projects/photo-dater/">&lt;h2 id=&quot;goal&quot;&gt;Goal&lt;&#x2F;h2&gt;
&lt;p&gt;I have a lot of photos that have incorrect dates either due to the photo being a
scan, somebody has shared it with me on another date and meta data was stripped,
or incorrect date on the camera.&lt;&#x2F;p&gt;
&lt;p&gt;I want a way to fix these mistakes&lt;&#x2F;p&gt;
&lt;h3 id=&quot;feature-ideas&quot;&gt;Feature ideas&lt;&#x2F;h3&gt;
&lt;p&gt;After attempting this proble once or twice I need&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;a way to bulk edit date on a group of photos&lt;&#x2F;li&gt;
&lt;li&gt;easy ways to make a group photos, some ways to suggest could be
&lt;ul&gt;
&lt;li&gt;By folder&lt;&#x2F;li&gt;
&lt;li&gt;by scene&lt;&#x2F;li&gt;
&lt;li&gt;by location&lt;&#x2F;li&gt;
&lt;li&gt;by persons face &#x2F; maybe age&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;where&quot;&gt;Where&lt;&#x2F;h2&gt;
&lt;p&gt;my past attempts are too old a new repo needs to be made&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Nixos Config</title>
        <published>2022-05-10T00:00:00+00:00</published>
        <updated>2022-05-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://seanborg.tech/projects/nixos-config/"/>
        <id>https://seanborg.tech/projects/nixos-config/</id>
        
        <content type="html" xml:base="https://seanborg.tech/projects/nixos-config/">&lt;h2 id=&quot;goal&quot;&gt;Goal&lt;&#x2F;h2&gt;
&lt;p&gt;Configure all my computers and RPI&#x27;s&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why&quot;&gt;Why&lt;&#x2F;h2&gt;
&lt;p&gt;Used to run Arch (because it worked when iI followed the instructions) but
started managing many other headless computers&#x2F;RPI&#x27;s and NixOs helped me
keep everything in order, maybe the repo is not very well ordered though :D&lt;&#x2F;p&gt;
&lt;h2 id=&quot;where&quot;&gt;Where&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;seam345&#x2F;nixos-config&quot;&gt;https:&#x2F;&#x2F;gitlab.com&#x2F;seam345&#x2F;nixos-config&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;current-state&quot;&gt;Current state&lt;&#x2F;h2&gt;
&lt;p&gt;I should really maintain the config more but &quot;it just works&quot;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Rustkey</title>
        <published>2021-11-08T00:00:00+00:00</published>
        <updated>2021-11-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://seanborg.tech/projects/rustykey/"/>
        <id>https://seanborg.tech/projects/rustykey/</id>
        
        <content type="html" xml:base="https://seanborg.tech/projects/rustykey/">&lt;h2 id=&quot;goal&quot;&gt;Goal&lt;&#x2F;h2&gt;
&lt;p&gt;Providing a simpler interface to unlock the FDE on my computers using a
YubiKey as 2FA&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why&quot;&gt;Why&lt;&#x2F;h2&gt;
&lt;p&gt;I hit a bug in the previous implementation I was using and somebody
recommend I replace it with Rust :D&lt;&#x2F;p&gt;
&lt;h2 id=&quot;where&quot;&gt;Where&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;seam345&#x2F;rustykey&quot;&gt;https:&#x2F;&#x2F;gitlab.com&#x2F;seam345&#x2F;rustykey&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;current-state&quot;&gt;Current state&lt;&#x2F;h2&gt;
&lt;p&gt;No longer maintained after nix dropped support for the bash script version&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>3D printing Greater Manchester wards - from GeoJson to STL</title>
        <published>2019-01-01T00:00:00+00:00</published>
        <updated>2025-02-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://seanborg.tech/blog/3d-printing-greater-manchester/"/>
        <id>https://seanborg.tech/blog/3d-printing-greater-manchester/</id>
        
        <content type="html" xml:base="https://seanborg.tech/blog/3d-printing-greater-manchester/">&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;3d-printing-greater-manchester&#x2F;real-and-digital.jpg&quot; alt=&quot;IMG_20190606_155137&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;converter&quot;&gt;Converter&lt;&#x2F;h2&gt;
&lt;p&gt;The end result is &lt;a href=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;projects&#x2F;geojson-to-stl&#x2F;&quot;&gt;this tool&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-goal&quot;&gt;The Goal&lt;&#x2F;h2&gt;
&lt;p&gt;To turn this wonderful map visualisation by Jamie Whyte (used to link to
twitter I&#x27;m not linking to X) into a real physical object using 3D printing.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;additional-goal&quot;&gt;Additional Goal&lt;&#x2F;h2&gt;
&lt;p&gt;To create a simple online converter that can convert from GeoJson to STL,
making it much easier for people to print their own map data. Feel free to
check it out at &lt;a href=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;projects&#x2F;geojson-to-stl&#x2F;&quot;&gt;geojson-to-stl&lt;&#x2F;a&gt; or help me
improve it at &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;seam345&#x2F;GeoJsonToSTL&quot;&gt;GitHub&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;understanding-the-data&quot;&gt;Understanding the data&lt;&#x2F;h2&gt;
&lt;p&gt;First I look at what I have, a GeoJson file.
&lt;img src=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;3d-printing-greater-manchester&#x2F;geoJsonImage.png&quot; alt=&quot;geoJsonImage&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The GeoJson may at first be overwhelming, but it is well organised in the
JSON format. Amazing diagrams that explain the structure &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.json.org&#x2F;&quot;&gt;here&lt;&#x2F;a&gt; &lt;!--  markdownlint-disable-line MD059 MD013 --&gt;
and great online tools to make the format more readable using
indentation &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;jsonformatter.curiousconcept.com&#x2F;&quot;&gt;here&lt;&#x2F;a&gt;. &lt;!--  markdownlint-disable-line MD059 MD013 --&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;3d-printing-greater-manchester&#x2F;geoJsonImage.png&quot; alt=&quot;geoJsonImage&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;With no knowledge of GeoJson but strong familiarity with JSON I can see that
we have an object with a huge list of features, 215 to be precise, I imagine
each one is a ward by looking at the first item that is expanded a little.&lt;&#x2F;p&gt;
&lt;p&gt;The geometry object looks very useful, lets expand it …&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;3d-printing-greater-manchester&#x2F;geoJsonImagecoord.jpg&quot; alt=&quot;geoJsonImagecoord&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Ok it looks like the geometry object is a long list of coordinates. Looking
a little closer we can see that the first and last coordinate is the same,
so we have a list of 2D coordinates that loop round to form a complete shape
(a &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.mathsisfun.com&#x2F;definitions&#x2F;polygon.html&quot;&gt;polygon&lt;&#x2F;a&gt; as the
type specifies in the file). Fantastic we can use these coordinates to
generate our STL files somehow just need to convert these coordinates (known
as &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Geographic_coordinate_system&quot;&gt;Geographic coordinates&lt;&#x2F;a&gt;)
to X,Y coordinates (known as &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Cartesian_coordinate_system&quot;&gt;Cartesian coordinates&lt;&#x2F;a&gt;).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;geographic-coordinates-to-cartesian-coordinates&quot;&gt;Geographic Coordinates to Cartesian Coordinates&lt;&#x2F;h2&gt;
&lt;p&gt;I had a look around for how to convert Geographic to Cartesian coordinates
and found this &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;mathworld.wolfram.com&#x2F;CylindricalProjection.html&quot;&gt;page&lt;&#x2F;a&gt;
from Wolfram MathWorld. I needed to map the sphere to a cylinder and
then roll it out, this is archived using this formula to convert the values;&lt;&#x2F;p&gt;
&lt;p&gt;where λ is longitude and θ is latitude&lt;&#x2F;p&gt;
&lt;p&gt;λ₀ is our first longitude point making x relative to our first point however
Y is currently relative to the equator which will distort our model, so we
need to subtract tan(θ₀) to also make Y relative to my first point. This
results in:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;txt&quot; class=&quot;language-txt &quot;&gt;&lt;code class=&quot;language-txt&quot; data-lang=&quot;txt&quot;&gt;X = λ - λ₀
Y = tan(θ) - tan(θ₀)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;cartesian-coordinates-to-stl&quot;&gt;Cartesian Coordinates TO STL&lt;&#x2F;h2&gt;
&lt;p&gt;I now have a list of X,Y points, I now need to understand the STL format to
generate a correct file that Cura can understand and recreate my shape.
There are &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;STL_(file_format)&quot;&gt;2 formats STL&lt;&#x2F;a&gt;
can be, in ASCII or Binary. Given Binary’s only advantage is smaller file
size, I picked ASCII as it is just plain text, making it much easier to find
errors when things are not working correctly.&lt;&#x2F;p&gt;
&lt;p&gt;To make a shape I need to start my file with&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;txt&quot; class=&quot;language-txt &quot;&gt;&lt;code class=&quot;language-txt&quot; data-lang=&quot;txt&quot;&gt;solid name  (name can be any whatever I want)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;txt&quot; class=&quot;language-txt &quot;&gt;&lt;code class=&quot;language-txt&quot; data-lang=&quot;txt&quot;&gt;endsolid name  (name has to be the same as used at the start)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Computers are very dumb, but they can do the same thing very fast this
results in almost all 3D computer graphics being stored as a huge number of
triangles, as any shape can be broken down into a number of triangles to
represent that object.&lt;&#x2F;p&gt;
&lt;p&gt;STL defines a triangle in this format :&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;txt&quot; class=&quot;language-txt &quot;&gt;&lt;code class=&quot;language-txt&quot; data-lang=&quot;txt&quot;&gt;facet normal ni nj nk
    outer loop
        vertex v1x v1y v1z
        vertex v2x v2y v2z
        vertex v3x v3y v3z
    endloop
endfacet

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;I turned out that I could break the shape down into rectangles. As I walked
around the shape I take my current point and next point to determine the
width of the rectangle, then the height will be determined by a constant
value. These rectangles can then be split into 2 triangles by cutting
through 2 diagonal points.&lt;&#x2F;p&gt;
&lt;p&gt;The STL format for the method above&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;txt&quot; class=&quot;language-txt &quot;&gt;&lt;code class=&quot;language-txt&quot; data-lang=&quot;txt&quot;&gt;facet normal 0 0 0
    outer loop
        vertex point1X point1Y 0
        vertex point1X point1Y 0
        vertex point2X pointY2 height
    endloop
endfacet
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;txt&quot; class=&quot;language-txt &quot;&gt;&lt;code class=&quot;language-txt&quot; data-lang=&quot;txt&quot;&gt;facet normal 0 0 0
    outer loop
        vertex point1X point1Y height
        vertex point2X point1Y height
        vertex point2X point1Y 0
    endloop
endfacet
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The facet normal is a bit more complicated to explain, without getting
mathsy, but thankfully after reading about it and testing it in Cura we can
safely ignore it and just put facet normal 0 0 0. **Note this is bad
practice and will not load in some STL viewers that are stricter than
Cura.&lt;&#x2F;p&gt;
&lt;p&gt;I loaded the file into Cura to see a complete shape!!
&lt;img src=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;3d-printing-greater-manchester&#x2F;ouline.png&quot; alt=&quot;ouline&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Fantastic we have the perimeter done!! turns out we don’t need a top and
bottom face for Cura to generate the printing code, so I printed the piece.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;3d-printing-greater-manchester&#x2F;firstPieceTop.jpg&quot; alt=&quot;firstPieceTop&quot; &#x2F;&gt; &lt;img src=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;3d-printing-greater-manchester&#x2F;firstPiece.jpg&quot; alt=&quot;firstPiece&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Beautiful! Almost perfect except for the curling of the edges, I tweaked
some setting in the later prints to help reduce this. However, I had another
worry… Would they fit together, some of these angles are really small so I
tested it by printing out an area… Rochdale&#x27;s wards have some interesting
interlinks (also doubles as my hometown) so I printed Rochdale.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;tolerance&quot;&gt;Tolerance&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;3d-printing-greater-manchester&#x2F;tolerance1.jpg&quot; alt=&quot;IMG_20190603_161815&quot; &#x2F;&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;3d-printing-greater-manchester&#x2F;image-asset.jpeg&quot; alt=&quot;image-asset&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;A wonderful print, with less curling this time!! However, my worry was
correct two of the pieces will not fit together! This is due to the plastic
having a thickness and the points have no gap to account for this, we need
to add some tolerance. How much tolerance was worked out later with some
trial and error later but for I needed a way to compute all the new X,Y
points with tolerance applied first.
&lt;img src=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;3d-printing-greater-manchester&#x2F;tolerance3.jpg&quot; alt=&quot;IMG_20190603_122116&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;My plan was to move each point into the shape by a set amount (tolerance).
The tricky part was to move the point in the correct direction. Originally,
I started out by using a lot of trigonometry and calculating way to many
angles (with many conditions) to work out what the correct angle was, then
calculate the line. Until I found &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;math.stackexchange.com&#x2F;questions&#x2F;2285965&#x2F;how-to-find-the-vector-formula-for-the-bisector-of-given-two-vectors&quot;&gt;this&lt;&#x2F;a&gt;
answer on a stack exchange site…&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;&lt;em&gt;By definition, the sum of two vectors is equal to the diagonal of the
parallelogram spanned by the vectors.&lt;&#x2F;em&gt;&quot;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;I did however have to draw out what the person was saying to understand what
he meant.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;3d-printing-greater-manchester&#x2F;Screenshot_20190603_181803.png&quot; alt=&quot;Screenshot_20190603_181803&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;We get 2 vectors (lines) from a point of the same length ( I can &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.khanacademy.org&#x2F;computing&#x2F;computer-programming&#x2F;programming-natural-simulations&#x2F;programming-vectors&#x2F;a&#x2F;vector-magnitude-normalization&quot;&gt;normalise&lt;&#x2F;a&gt;
any vector so this is easy enough). I then stick the opposite vector to the
end of the other vector and to get a parallelogram&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;3d-printing-greater-manchester&#x2F;Screenshot_20190603_181740.png&quot; alt=&quot;Screenshot_20190603_181740&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Draw a line from our point to the opposite point, and we have a line with a
direction exactly half the angle! Now all that is left to be done is to walk
along the line by the threshold amount to get my new X,Y coordinates for
that point.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;3d-printing-greater-manchester&#x2F;Screenshot_20190603_182343.png&quot; alt=&quot;Screenshot_20190603_182343&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;There is however 1 flaw in this method, it only ever dissect the smaller
angle of the 2 possible angles, however there are times I needed the larger
angle (orange angle) but if I followed the green line then I would have gone
in the wrong direction… Thanks to all the trigonometry I did earlier I had a
nice method to calculate if I want the convex (large) or concave (small)
side, telling me which direction to go in.&lt;&#x2F;p&gt;
&lt;p&gt;The obvious next question is what tolerance I should give the print I went
down the simple route of printing out a very thin version (to make it a
quick print) of the 2 pieces that wouldn’t fit together first with 0.1 then
0.2 and finally 0.3 millimeter tolerance.
&lt;img src=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;3d-printing-greater-manchester&#x2F;tolerance7.jpg&quot; alt=&quot;IMG_20190603_162235&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;left to right &lt;strong&gt;0.1 mm, 0.2 mm&lt;&#x2F;strong&gt; and &lt;strong&gt;0.3 mm&lt;&#x2F;strong&gt; tolerance.&lt;&#x2F;p&gt;
&lt;p&gt;The 0.1mm didn’t fit at all you can even kind of see that the bigger cut out
doesn’t even fit together. 0.2mm could fit with some squeezing but was very
eager to pop itself back out, whereas the 0.3mm fit really nicely… So 0.3mm
it is!&lt;&#x2F;p&gt;
&lt;p&gt;Everything is sorted and ready for the whole map now, stared with Rochdale
to triple check everything worked and then continued on with the rest of the
map&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;3d-printing-greater-manchester&#x2F;tolerance8.jpg&quot; alt=&quot;IMG_20190609_165321&quot; &#x2F;&gt; &lt;img src=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;3d-printing-greater-manchester&#x2F;tolerance9.jpg&quot; alt=&quot;IMG_20190609_165327&quot; &#x2F;&gt;
Images of the Rochdale area this time in the correct colours and with the
tolerance applied, we can see the pieces fit together perfectly!&lt;&#x2F;p&gt;
&lt;h2 id=&quot;reflections&quot;&gt;Reflections&lt;&#x2F;h2&gt;
&lt;p&gt;This was a great project (even if it was over a week of non-stop printing)
I’ve been overwhelmed by the reaction it has received. There are a lot of
extra improvements that I can think of making, but for a while now I’m going
to make the online converter work better and be more user-friendly.&lt;&#x2F;p&gt;
&lt;p&gt;One last note I need to thank &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;theinventhive.com&#x2F;&quot;&gt;TIH&lt;&#x2F;a&gt; for letting
me use their printer, especially for 2 weeks of nearly constant use!&lt;&#x2F;p&gt;
&lt;p&gt;I have also added a dead ends section bellow to highlight that there was a
few things that I spent a while on that was either just not used or made no
impact on the end product. The section is there to show that dead ends is
just a normal part of development and that they do usually provide something
useful, be that increased knowledge in the area or a nicer product at the end.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;dead-ends-rabbit-holes-quick-notes&quot;&gt;Dead ends - rabbit holes (quick notes)&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;using-direction-and-magnitude-to-create-the-shape&quot;&gt;Using direction and magnitude to create the shape&lt;&#x2F;h3&gt;
&lt;p&gt;Originally I was working out the parimeter from just calculating the
distance and direction from point to point, using the functions from this
amazing site &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.movable-type.co.uk&#x2F;scripts&#x2F;latlong.html&quot;&gt;https:&#x2F;&#x2F;www.movable-type.co.uk&#x2F;scripts&#x2F;latlong.html&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;3d-printing-greater-manchester&#x2F;Screenshot_20190531_142047.png&quot; alt=&quot;Screenshot_20190531_142047&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;I did this knowing that it may be off a little because you cannot map a
sphere very easily to a flat 2D plane. However, I never knew that I would be
so far by my guess I was about 1KM (on my scale 1cm) out after spending an
hour or so making sure I was definitely moving in the correct direction at
every point.&lt;&#x2F;p&gt;
&lt;p&gt;I couldn’t believe just how far out it was I expected me to be less than a
mm off and I would just snap the ends together somehow, so it was a complete
object. However, after seeing the gap it left I knew I had to do it the
proper way.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;polygon-triangulation&quot;&gt;Polygon triangulation&lt;&#x2F;h3&gt;
&lt;p&gt;(loved the name but basically I’m putting a surface on the top and bottom of my
shapes)&lt;&#x2F;p&gt;
&lt;p&gt;As mentioned earlier when I first created the perimeter of the shape, I
didn’t think Cura would add the top and base to complete the shape allowing
it to print, apparently the older versions didn’t but I couldn’t find any
word of it in the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;ultimaker.com&#x2F;en&#x2F;products&#x2F;ultimaker-cura-software&#x2F;release-notes&quot;&gt;changelog&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;So I ended up spending a whole day making this work, after some googling I
found the name for it was called “&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Polygon_triangulation&quot;&gt;Polygon triangulation&lt;&#x2F;a&gt;”
and found that the easiest algorithm to implement was probably ear clipping
admittedly this was really fun to implement, a great YouTube video to
visualise how it works &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=RMTXqTu4tKc&quot;&gt;here&lt;&#x2F;a&gt;. &lt;!--  markdownlint-disable-line MD059 MD013 --&gt;
Even though it wasn’t needed I shall still be using it with my online
conversion tool because it makes the models look significantly better when
rendering, despite it making no difference in the print.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>GeoJSON to STL</title>
        <published>2019-01-01T00:00:00+00:00</published>
        <updated>2019-01-01T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://seanborg.tech/projects/geojson-to-stl/"/>
        <id>https://seanborg.tech/projects/geojson-to-stl/</id>
        
        <content type="html" xml:base="https://seanborg.tech/projects/geojson-to-stl/">&lt;h2 id=&quot;goal&quot;&gt;Goal&lt;&#x2F;h2&gt;
&lt;p&gt;Have an easy way to covert GeoJSON to STL&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why&quot;&gt;Why&lt;&#x2F;h2&gt;
&lt;p&gt;I wanted to &lt;a href=&quot;https:&#x2F;&#x2F;seanborg.tech&#x2F;blog&#x2F;3d-printing-greater-manchester&#x2F;&quot;&gt;print out a cool map&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;where&quot;&gt;Where&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;source&quot;&gt;Source&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;seam345&#x2F;GeoJsonToSTL&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;seam345&#x2F;GeoJsonToSTL&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;working-tool&quot;&gt;Working tool&lt;&#x2F;h3&gt;
&lt;p&gt;Bellow these headings :D I do love that with a nice web JS tool I can just
embed it :D&lt;&#x2F;p&gt;
&lt;h2 id=&quot;current-state&quot;&gt;Current state&lt;&#x2F;h2&gt;
&lt;p&gt;It works, not very fancy, your millage may very I don&#x27;t ever see me updatin
this any further&lt;&#x2F;p&gt;
&lt;h1 id=&quot;the-tool&quot;&gt;The tool&lt;&#x2F;h1&gt;
&lt;p&gt;Please select a GeoJson file from your pc, then select the value you would
like to use for height and what to group the files by (group shall add the
files into folders with the group name) then click Generate STL.&lt;&#x2F;p&gt;
&lt;p&gt;Please note this is a very early version, there is no loading spinner to
signify it is working, it takes a few seconds to start downloading when I
run it, no error messages are displayed either. Also you will be downloading
a zip file I found that chrome will say it is harmful, you can just click
the arrow and tell chrome to keep the file.&lt;&#x2F;p&gt;
&lt;p&gt;Other than the initial loading of this page and 2 javascript packages &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.npmjs.com&#x2F;package&#x2F;jszip&quot;&gt;jszip&lt;&#x2F;a&gt;
and &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.npmjs.com&#x2F;package&#x2F;file-saver&quot;&gt;FileSaver&lt;&#x2F;a&gt;, no network is
required to convert the STL files and I don&#x27;t send back any data to the server.&lt;&#x2F;p&gt;
&lt;p&gt;You can let me know something went wrong on the contact me page, or on
github at &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;seam345&#x2F;GeoJsonToSTL&quot;&gt;GeoJsonToSTL&lt;&#x2F;a&gt;, feel free
to submit issues or pull requests.&lt;&#x2F;p&gt;
&lt;html&gt;
&lt;body&gt;
&lt;p&gt;&lt;input
type=&quot;file&quot;
id=&quot;files&quot;
name=&quot;files[]&quot;
&#x2F;&gt;
&lt;br&#x2F;&gt;
&lt;br&#x2F;&gt;
&lt;br&#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;input
type=&quot;range&quot;
min=&quot;0&quot;
max=&quot;20&quot;
value=&quot;0&quot;
id=&quot;toleranceRange&quot;
&#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Tolerance: &lt;span id=&quot;toleranceSpan&quot;&gt;&lt;&#x2F;span&gt;&lt;&#x2F;p&gt;
&lt;br&#x2F;&gt;
&lt;!--&lt;button onclick=&quot;test()&quot;&gt;Test&lt;&#x2F;button&gt;--&gt;
&lt;p&gt;Number of features in GeoJson: &lt;span id=&quot;featureCount&quot;&gt;N&#x2F;A&lt;&#x2F;span&gt;&lt;&#x2F;p&gt;
&lt;p id=&quot;list&quot;&gt;&lt;&#x2F;p&gt;
&lt;p id=&quot;listGroup&quot;&gt;&lt;&#x2F;p&gt;
&lt;p id=&quot;percentTitle&quot; style=&quot;white-space: pre;&quot;&gt;&lt;&#x2F;p&gt;
&lt;div style=&quot;width: 100%; height: 5px; background-color: black&quot;&gt;
  &lt;div id=&quot;percentComplete&quot; style=&quot;background-color: green; width: 0; height: 5px&quot;&gt;&lt;&#x2F;div&gt;
&lt;&#x2F;div&gt;
&lt;button onclick=&quot;myFunction()&quot;&gt;Generate STL&lt;&#x2F;button&gt;
&lt;script src=&quot;https:&#x2F;&#x2F;unpkg.com&#x2F;jszip@3.2.1&#x2F;dist&#x2F;jszip.min.js&quot;&gt;&lt;&#x2F;script&gt;
&lt;script src=&quot;https:&#x2F;&#x2F;unpkg.com&#x2F;file-saver@2.0.2&#x2F;dist&#x2F;FileSaver.min.js&quot;&gt;&lt;&#x2F;script&gt;
&lt;script&gt;
  let geoJSON   = null;
  let heightkey = null;
  let groupKey  = null;
  let groupValues = false;
  let doNotGroupKey = &quot;MyUnlikelyValueToBeAVeriableNameInSomoneElsesGeoJSON&quot;;

  let toleranceSlider = document.getElementById(&#x27;toleranceRange&#x27;);
  console.log(toleranceSlider);
  let toleranceSpanOutput       = document.getElementById(&#x27;toleranceSpan&#x27;);
  toleranceSpanOutput.innerHTML = toleranceSlider.value &#x2F; 10;

  toleranceSlider.oninput = function ()
  {
    toleranceSpanOutput.innerHTML = this.value &#x2F; 10;
  };

  function radians_to_degrees(radians)
  {
    let pi = Math.PI;
    return radians * ( 180 &#x2F; pi );
  }

  function degrees_to_radians(degrees)
  {
    return degrees * ( Math.PI &#x2F; 180 );
  }

  function handleFileSelect(evt)
  {
    let files = evt.target.files; &#x2F;&#x2F; FileList object
    console.log(files);

    &#x2F;&#x2F; files is a FileList of File objects. List some properties.
    if ( files.length === 1 )
    {
      const reader = new FileReader();

      reader.readAsText(files[0]); &#x2F;&#x2F; read file as data url

      reader.onload = (FileData) =&gt;
      {
        &#x2F;&#x2F; const tempObj: any = {};
        &#x2F;&#x2F; const target: any  = FileData.target; &#x2F;&#x2F; &lt;-- This (any) will tell compiler to shut up!
        &#x2F;&#x2F; tempObj.url        = target.result;
        &#x2F;&#x2F; tempObj.name       = event.target.files[i].name;

        &#x2F;&#x2F; todo write a verify method to let user know if its an incorrect file
        &#x2F;&#x2F; console.log(FileData.target.result);
        try
        {
          geoJSON = JSON.parse(FileData.target.result);

        } catch (error)
        {
          let message = error;
          message += &#x27;\n\nThe file that has been selected may have been incorrectly formatted or the wrong file type.&#x27;;
          window.alert(message)
        }

        &#x2F;&#x2F; this.fileList.push(tempObj);

        &#x2F;&#x2F; &lt;input type=&quot;radio&quot; name=&quot;gender&quot; value=&quot;male&quot;&gt; Male&lt;br&gt;
        &#x2F;&#x2F; &lt;input type=&quot;radio&quot; name=&quot;gender&quot; value=&quot;female&quot;&gt; Female&lt;br&gt;
        &#x2F;&#x2F; &lt;input type=&quot;radio&quot; name=&quot;gender&quot; value=&quot;other&quot;&gt; Other
        &#x2F;&#x2F; console.log(geoJSON);
        &#x2F;&#x2F; console.log(geoJSON[&#x27;features&#x27;]);

        let output = &#x27;Please select a variable to use as height below: &lt;br&#x2F;&gt;&#x27;;
        for (let key in  geoJSON[&#x27;features&#x27;][0][&#x27;properties&#x27;])
        {
          &#x2F;&#x2F; skip loop if the property is from prototype
          if ( !geoJSON[&#x27;features&#x27;][0][&#x27;properties&#x27;].hasOwnProperty(key) ) continue;

          output += &#x27;&lt;input type=&quot;radio&quot; name=&quot;height&quot; value=&quot;&#x27; + key + &#x27;&quot;&gt; &#x27; + key + &#x27;&lt;br&#x2F;&gt;&#x27;;

        }
        &#x2F;&#x2F; console.log(output);
        document.getElementById(&#x27;list&#x27;).innerHTML         = output;
        document.getElementById(&#x27;featureCount&#x27;).innerHTML = geoJSON[&#x27;features&#x27;].length;
        let heightRadio                                   = document.getElementsByName(&#x27;height&#x27;);
        for (let i = 0; i &lt; heightRadio.length; i++)
        {
          heightRadio[i].addEventListener(&#x27;change&#x27;, handleHeightSelect, false)
        }

        let output2 = &#x27;Please select a variable to group object by below, or select the no group option at the bottom: &lt;br&#x2F;&gt;&#x27;;
        for (let key in  geoJSON[&#x27;features&#x27;][0][&#x27;properties&#x27;])
        {
          &#x2F;&#x2F; skip loop if the property is from prototype
          if ( !geoJSON[&#x27;features&#x27;][0][&#x27;properties&#x27;].hasOwnProperty(key) ) continue;

          output2 += &#x27;&lt;input type=&quot;radio&quot; name=&quot;group&quot; value=&quot;&#x27; + key + &#x27;&quot;&gt; &#x27; + key + &#x27;&lt;br&#x2F;&gt;&#x27;;

        }
          output2 += &#x27;&lt;input type=&quot;radio&quot; name=&quot;group&quot; value=&quot;&#x27;+doNotGroupKey+&#x27;&quot;&gt;No Grouping&lt;br&#x2F;&gt;&#x27;;
        &#x2F;&#x2F; console.log(output);
        document.getElementById(&#x27;listGroup&#x27;).innerHTML = output2;
        let groupRadio                                 = document.getElementsByName(&#x27;group&#x27;);
        for (let i = 0; i &lt; heightRadio.length; i++)
        {
          groupRadio[i].addEventListener(&#x27;change&#x27;, handleGroupSelect, false)
        }

      };
    }
  }

  function handleGroupSelect()
  {
    groupKey = this.value;
    groupValues = this.value !== doNotGroupKey;
  }

  function handleHeightSelect()
  {
    &#x2F;&#x2F; console.log(this.value);
    heightkey = this.value;
  }

  document.getElementById(&#x27;files&#x27;).addEventListener(&#x27;change&#x27;, handleFileSelect, false);

  function myFunction()
  {
    let stlFolders = {};

    if (groupValues) {
      &#x2F;&#x2F; Create a new key for every unique folder I am going to make
      for (let i = 0; i &lt; geoJSON[&#x27;features&#x27;].length; i++) {
        if (!(geoJSON[&#x27;features&#x27;][i][&#x27;properties&#x27;][groupKey] in stlFolders))
        &#x2F;&#x2F; if ( !( geoJSON[&#x27;features&#x27;][j][&#x27;properties&#x27;][&#x27;winning_party&#x27;] in stlFolders ) )
        {
          stlFolders[geoJSON[&#x27;features&#x27;][i][&#x27;properties&#x27;][groupKey]] = [];
        }
      }
    }else
    {
      stlFolders[&#x27;STLs&#x27;] = [];
    }

    document.getElementById(&#x27;percentTitle&#x27;).innerHTML         = &quot;Converting GeoJson to STL&quot;;
    let stl = [];
    for (let j = 0; j &lt; geoJSON[&#x27;features&#x27;].length; j++)
    {
      console.log(&#x27;Start shape: %d&#x27;, j);
      let percent = (j&#x2F; geoJSON[&#x27;features&#x27;].length) * 100;
      document.getElementById(&#x27;percentComplete&#x27;).width = percent + &#x27;%&#x27;;
      let newStl        = &#x27;solid shape&#x27; + j + &#x27;\n&#x27;;
      &#x2F;&#x2F; let xShapeOrigin  = ( j % 10 ) * 50;
      let xShapeOrigin  = 0;
      &#x2F;&#x2F; let yShapeOrigin  = Math.floor(j &#x2F; 10) * 50;
      let yShapeOrigin  = 0;
      let previousPoint = [xShapeOrigin, yShapeOrigin];
      let height        = geoJSON[&#x27;features&#x27;][j][&#x27;properties&#x27;][heightkey];
      if ( height == null || height &lt; 1 )
      {
        height = 2;
      } else
      {
        height = height &#x2F; 200;
      }
      &#x2F;&#x2F; console.log(heightkey);
      let relativePoints = [];
      relativePoints.push(previousPoint);

      let coordinatesFirst1 = geoJSON[&#x27;features&#x27;][j][&#x27;geometry&#x27;][&#x27;coordinates&#x27;][0][0];
      &#x2F;&#x2F; console.log(coordinates1);
      let firstLat1         = coordinatesFirst1[1];
      let firstLon1         = coordinatesFirst1[0];

      const originOffset = getOriginOffset(firstLat1, firstLon1);

      for (let i = 1; i &lt; geoJSON[&#x27;features&#x27;][j][&#x27;geometry&#x27;][&#x27;coordinates&#x27;][0].length; i++)
      {
        let coordinates = geoJSON[&#x27;features&#x27;][j][&#x27;geometry&#x27;][&#x27;coordinates&#x27;][0][i];
        let lat         = coordinates[1];
        let lon         = coordinates[0];

        let result = getStlFromPoints(lat, lon, height, previousPoint, originOffset);

        &#x2F;&#x2F; newStl += result[0];
        previousPoint = result[1];
        relativePoints.push(previousPoint);
      }

      relativePoints.splice(relativePoints.length - 1, 1);  &#x2F;&#x2F; last and first point are the same so remove one

      relativePoints = shrinkShape(relativePoints, toleranceSpanOutput.innerText);
      newStl += pointsToSTLPerimeter(relativePoints, height);

      newStl += earClipping(relativePoints, height);
      newStl += &#x27;endsolid shape&#x27; + j + &#x27;\n&#x27;;
      &#x2F;&#x2F; console.log(newStl);
      stl.push(newStl);

      if (groupValues)
      {
        stlFolders[geoJSON[&#x27;features&#x27;][j][&#x27;properties&#x27;][groupKey]].push(newStl);
      }else
      {
        stlFolders[&#x27;STLs&#x27;].push(newStl);
      }

    }
    console.log(&quot;zipping files to save&quot;);
    const zip = new JSZip();

    for (let folderName in stlFolders)
    {
      const zippedFolder = zip.folder(folderName);
      for (let i = 0; i &lt; stlFolders[folderName].length; i++)
      {
        zippedFolder.file(i + &#x27;.stl&#x27;, stlFolders[folderName][i]);
      }
    }

    console.log(&quot;generating save blob&quot;);
    document.getElementById(&#x27;percentTitle&#x27;).innerHTML         = &quot;Converting GeoJson to STL: Done\nGenerating save file&quot;;
    zip.generateAsync({type: &quot;blob&quot;}, function updateCallback(metadata) {
      &#x2F;&#x2F; console.log(&quot;generate percent &quot; + metadata.percent + &quot;%&quot;);
      document.getElementById(&#x27;percentTitle&#x27;).innerHTML         = &quot;Converting GeoJson to STL: Done\nGenerating save file: &quot;+ metadata.percent + &#x27;%&#x27;;
      document.getElementById(&#x27;percentComplete&#x27;).style.width = metadata.percent + &#x27;%&#x27;;
    })
        .then(function (blob)
        {
          saveAs(blob, &quot;shapes.zip&quot;);
        });
  }

  function pointsToSTLPerimeter(pointArray, height)
  {

    let stl = &#x27;&#x27;;
    for (let i = 0; i &lt; pointArray.length; i++)
    {
      let y = pointArray[( pointArray.length + i + 1 ) % pointArray.length][1] - pointArray[( pointArray.length + i + 0 ) % pointArray.length][1];
      let x = pointArray[( pointArray.length + i + 1 ) % pointArray.length][0] - pointArray[( pointArray.length + i + 0 ) % pointArray.length][0];

      let normalVector = [-y, x, 0];
      let vertex1      = [pointArray[( pointArray.length + i + 0 ) % pointArray.length][0], pointArray[( pointArray.length + i + 0 ) % pointArray.length][1], 0];
      let vertex2      = [pointArray[( pointArray.length + i + 0 ) % pointArray.length][0], pointArray[( pointArray.length + i + 0 ) % pointArray.length][1], height];
      let vertex3      = [pointArray[( pointArray.length + i + 1 ) % pointArray.length][0], pointArray[( pointArray.length + i + 1 ) % pointArray.length][1], 0];
      let vertex4      = [pointArray[( pointArray.length + i + 1 ) % pointArray.length][0], pointArray[( pointArray.length + i + 1 ) % pointArray.length][1], height];

      stl += createStlTriangle(normalVector, vertex1, vertex2, vertex3);
      stl += createStlTriangle(normalVector, vertex2, vertex3, vertex4);
    }
    return stl;
  }

  function shrinkShape(pointArray, tolerance, debug = false)
  {
    if ( debug )
    {
      console.log(&#x27;called shrinkShape(%s, %s)&#x27;, JSON.stringify(pointArray), JSON.stringify(tolerance));
    }
    let newPointArray = [];
    for (let i = 0; i &lt; pointArray.length; i++)
    {

      let vertex1 = pointArray[( pointArray.length + i - 1 ) % pointArray.length];
      let vertex2 = pointArray[( pointArray.length + i + 0 ) % pointArray.length]; &#x2F;&#x2F; modulo to wrap around when I hit the end of the array I need to wrap around otherwise i miss out 2 potential triangles
      let vertex3 = pointArray[( pointArray.length + i + 1 ) % pointArray.length];

      let vector1       = [vertex1[0] - vertex2[0], vertex1[1] - vertex2[1]];
      let vector1Length = Math.sqrt(( vector1[0] * vector1[0] ) + ( vector1[1] * vector1[1] ));
      let vector1Normal = [vector1[0] &#x2F; vector1Length, vector1[1] &#x2F; vector1Length];

      let vector2       = [vertex3[0] - vertex2[0], vertex3[1] - vertex2[1]];
      let vector2Length = Math.sqrt(( vector2[0] * vector2[0] ) + ( vector2[1] * vector2[1] ));
      let vector2Normal = [vector2[0] &#x2F; vector2Length, vector2[1] &#x2F; vector2Length];

      let direction           = [vector1Normal[0] + vector2Normal[0], vector1Normal[1] + vector2Normal[1]];
      let directionLength     = Math.sqrt(( direction[0] * direction[0] ) + ( direction[1] * direction[1] ));
      let directionNormalised = [direction[0] &#x2F; directionLength, direction[1] &#x2F; directionLength];

      let newX = vertex2[0];
      let newY = vertex2[1];
      if ( isConcave(vertex1, vertex2, vertex3) )
      {

        newX += ( tolerance * direction[0] );
        newY += ( tolerance * direction[1] );
      } else
      {
        newX -= ( tolerance * direction[0] );
        newY -= ( tolerance * direction[1] );
      }

      if ( debug )
      {
        console.log(&#x27;oldXY [%f, %f] new XY [%f, %f]&#x27;, pointArray[i][0], pointArray[i][1], newX, newY);
        console.log(&#x27;direction %s vector1 [%f, %f], vector2 [%f, %f]&#x27;, JSON.stringify(direction), vector1[0], vector1[1], vector2[0], vector2[1]);
        console.log(&#x27;direction length %s directionNormalised %s&#x27;, JSON.stringify(directionLength), JSON.stringify(directionNormalised), vector1[1], vector2[0], vector2[1]);
        console.log(&#x27;isConcave %s&#x27;, JSON.stringify(isConcave(vertex1, vertex2, vertex3)));
        console.log(&#x27; &#x27;);
        console.log(&#x27; &#x27;);
      }
      newPointArray.push([newX, newY])
    }
    return newPointArray;
  }

  function getOriginOffset(lat, long)
  {
    return [long, Math.tan(lat)]
  }

  function getStlFromPoints(lat, lon, height, previousPoint, originOffset)
  {
    let stl = &#x27;&#x27;;

    let x            = lon - originOffset[0];
    let y            = Math.tan(lat) - originOffset[1];
    let Rx           = 0.001513292705; &#x2F;&#x2F; took mid x of the manchester map and moved 1 degree and divided it by the miles (i calculated them on this website https:&#x2F;&#x2F;www.movable-type.co.uk&#x2F;scripts&#x2F;latlong.html)  to get this (additionally divided by 10 to get mm instead of cm for km)
    let Ry           = 0.000899280575; &#x2F;&#x2F; y never changes so it was 1 degree by 111.2 regardless of position (additionally divided by 10 to get mm instead of cm for km)
    let currentPoint = [x &#x2F; Rx, y &#x2F; Ry];

    let normalVector = [-y, x, 0];
    let vertex1      = [previousPoint[0], previousPoint[1], 0];
    let vertex2      = [previousPoint[0], previousPoint[1], height];
    let vertex3      = [currentPoint[0], currentPoint[1], 0];
    let vertex4      = [currentPoint[0], currentPoint[1], height];

    stl += createStlTriangle(normalVector, vertex1, vertex2, vertex3);
    stl += createStlTriangle(normalVector, vertex2, vertex3, vertex4);

    return [stl, currentPoint];
  }

  function createStlTriangle(normal, vertex1, vertex2, vertex3)
  {
    let stl = &#x27;&#x27;;
    stl += &#x27;facet normal &#x27; + normal[0] + &#x27; &#x27; + normal[1] + &#x27; &#x27; + normal[2] + &#x27;\n&#x27;;
    stl += &#x27;outer loop \n&#x27;;
    stl += &#x27;vertex &#x27; + vertex1[0] + &#x27; &#x27; + vertex1[1] + &#x27; &#x27; + vertex1[2] + &#x27;\n&#x27;;
    stl += &#x27;vertex &#x27; + vertex2[0] + &#x27; &#x27; + vertex2[1] + &#x27; &#x27; + vertex2[2] + &#x27;\n&#x27;;
    stl += &#x27;vertex &#x27; + vertex3[0] + &#x27; &#x27; + vertex3[1] + &#x27; &#x27; + vertex3[2] + &#x27;\n&#x27;;
    stl += &#x27;endloop \n&#x27;;
    stl += &#x27;endfacet \n&#x27;;
    return stl;
  }

  function earClipping(pointArray, height, debugLevel = 0)
  {
    let stl            = &#x27;&#x27;;
    const normalTop    = [0, 0, 1];
    const normalBottom = [0, 0, -1];

    while (pointArray.length &gt; 2)
        &#x2F;&#x2F; for (let k = 0; k &lt; 6; k++)
    {
      let smallestTriangle = null;
      let smallAngle       = 180;

      for (let i = 0; i &lt; pointArray.length; i++)
      {
        let vertex1 = pointArray[i];
        let vertex2 = pointArray[( i + 1 ) % pointArray.length]; &#x2F;&#x2F; modulo to wrap around when I hit the end of the array I need to wrap around otherwise i miss out 2 potential triangles
        let vertex3 = pointArray[( i + 2 ) % pointArray.length];
        if ( debugLevel &gt; 2 )
        {
          console.log(&#x27;i : %d&#x27;, i);
          console.log(&#x27;i + 2 : %d&#x27;, ( i + 2 ) % pointArray.length);
        }

        if ( isConcave(vertex1, vertex2, vertex3) )
        {
          let currentAngle = angleDegrees(vertex1, vertex2, vertex3);
          if ( currentAngle &lt; smallAngle )
          {
            smallAngle       = currentAngle;
            smallestTriangle = i;
          }
        }
      }
      if ( debugLevel &gt; 0 )
      {
        console.log(smallAngle);
        console.log(smallestTriangle);
        console.log(JSON.stringify(pointArray));

      }
      if ( smallestTriangle == null )
      {
        break;
      }
      let vertex1Top = [pointArray[( smallestTriangle + 2 ) % pointArray.length][0], pointArray[( smallestTriangle + 2 ) % pointArray.length][1], height];
      let vertex2Top = [pointArray[( smallestTriangle + 1 ) % pointArray.length][0], pointArray[( smallestTriangle + 1 ) % pointArray.length][1], height];
      let vertex3Top = [pointArray[smallestTriangle][0], pointArray[smallestTriangle][1], height];

      let vertex1Bottom = [pointArray[( smallestTriangle + 2 ) % pointArray.length][0], pointArray[( smallestTriangle + 2 ) % pointArray.length][1], 0];
      let vertex2Bottom = [pointArray[( smallestTriangle + 1 ) % pointArray.length][0], pointArray[( smallestTriangle + 1 ) % pointArray.length][1], 0];
      let vertex3Bottom = [pointArray[smallestTriangle][0], pointArray[smallestTriangle][1], 0];

      stl += createStlTriangle(normalTop, vertex1Top, vertex2Top, vertex3Top);
      stl += createStlTriangle(normalBottom, vertex1Bottom, vertex2Bottom, vertex3Bottom);

      pointArray.splice(( smallestTriangle + 1 ) % pointArray.length, 1); &#x2F;&#x2F; remove item from list for next iteration
    }
    &#x2F;&#x2F; console.log(pointArray);
    return stl;
  }

  function isConcave(point2D1, point2D2, point2D3, debug = false)
  { &#x2F;&#x2F; https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;7785601&#x2F;detecting-if-angle-is-more-than-180-degrees
    if ( debug )
    {
      console.log(&#x27;called isConvex( %s, %s, %s)&#x27;, JSON.stringify(point2D1), JSON.stringify(point2D2), JSON.stringify(point2D3));
    }
    let vector1 = [point2D1[0] - point2D2[0], point2D1[1] - point2D2[1]];
    let vector2 = [point2D3[0] - point2D2[0], point2D3[1] - point2D2[1]];

    if ( debug )
    {
      console.log(&#x27;calculated zcross to be %s&#x27;, JSON.stringify(( ( vector1[0] * vector2[1] ) - ( vector2[0] * vector1[1] ) )));

    }
    &#x2F;&#x2F; x1y2-x2y1
    return ( ( vector1[0] * vector2[1] ) - ( vector2[0] * vector1[1] ) ) &gt; 0
  }

  function angleDegrees(point2D1, point2D2, point2D3, debug = false)
  {
    if ( debug )
    {
      console.log(&#x27;called isConvex( %s, %s, %s)&#x27;, JSON.stringify(point2D1), JSON.stringify(point2D2), JSON.stringify(point2D3));
    }
    let vector1       = [point2D1[0] - point2D2[0], point2D1[1] - point2D2[1]];
    let vector2       = [point2D3[0] - point2D2[0], point2D3[1] - point2D2[1]];
    let vector1Length = Math.sqrt(vector1[0] * vector1[0] + vector1[1] * vector1[1]);
    let vector2Length = Math.sqrt(vector2[0] * vector2[0] + vector2[1] * vector2[1]);

    let dotOfv1v2 = ( vector1[0] * vector2[0] ) + ( vector1[1] * vector2[1] );
    let angle     = Math.acos(dotOfv1v2 &#x2F; ( vector1Length * vector2Length ));
    if ( debug )
    {
      console.log(&#x27;returned degrees %d&#x27;, radians_to_degrees(angle));

    }
    return radians_to_degrees(angle);
  }

  function test()
  {
    let succeeded      = true;
    let relativePoints = [];
    relativePoints.push([1, 0]);
    relativePoints.push([0, 0]);
    relativePoints.push([1, 1]);

    relativePoints.push([1, 0]);
    relativePoints.push([0, 0]);
    relativePoints.push([0, 1]);

    relativePoints.push([1, 0]);
    relativePoints.push([0, 0]);
    relativePoints.push([-1, 1]);

    relativePoints.push([1, 0]);
    relativePoints.push([0, 0]);
    relativePoints.push([-1, -1]);

    if ( isConcave(relativePoints[0], relativePoints[1], relativePoints[2]) === false )
    {
      console.log(&#x27;Error with convex points returned false should be true, result: %s&#x27;, JSON.stringify(isConcave(relativePoints[0], relativePoints[1], relativePoints[2])));
      console.log(isConcave(relativePoints[0], relativePoints[1], relativePoints[2], true));
      succeeded = false;
    }
    if ( isConcave(relativePoints[3], relativePoints[4], relativePoints[5]) === false )
    {
      console.log(&#x27;Error with convex points returned false should be true&#x27;);
      console.log(isConcave(relativePoints[3], relativePoints[4], relativePoints[5], true));
      succeeded = false;
    }
    if ( isConcave(relativePoints[6], relativePoints[7], relativePoints[8]) === false )
    {
      console.log(&#x27;Error with convex points returned false should be true&#x27;);
      console.log(isConcave(relativePoints[6], relativePoints[7], relativePoints[8], true));
      succeeded = false;
    }
    if ( isConcave(relativePoints[6], relativePoints[7], relativePoints[8]) === false )
    {
      console.log(&#x27;Error with convex points returned false should be true&#x27;);
      console.log(isConcave(relativePoints[6], relativePoints[7], relativePoints[8], true));
      succeeded = false;
    }
    if ( isConcave(relativePoints[9], relativePoints[10], relativePoints[11]) === true )
    {
      console.log(&#x27;Error with convex points returned true should be false&#x27;);
      console.log(isConcave(relativePoints[9], relativePoints[10], relativePoints[11], true));
      succeeded = false;
    }

    relativePoints = [];
    relativePoints.push([0, 0]);
    relativePoints.push([0, 10]);
    relativePoints.push([10, 10]);
    relativePoints.push([10, 0]);

    let shrunkRelativePoints = shrinkShape(relativePoints, 1);
    let precision            = 1e-10;

    if (
        !effectivelyEqual(shrunkRelativePoints[0][0], 0.70710678118, precision) || !effectivelyEqual(shrunkRelativePoints[0][1], 0.70710678118, precision) &amp;&amp;
        !effectivelyEqual(shrunkRelativePoints[1][0], 0.70710678118, precision) || !effectivelyEqual(shrunkRelativePoints[1][1], 10 - 0.70710678118, precision) &amp;&amp;
        !effectivelyEqual(shrunkRelativePoints[2][0], 10 - 0.70710678118, precision) || !effectivelyEqual(shrunkRelativePoints[2][1], 10 - 0.70710678118, precision) &amp;&amp;
        !effectivelyEqual(shrunkRelativePoints[3][0], 10 - 0.70710678118, precision) || !effectivelyEqual(shrunkRelativePoints[3][1], 0.70710678118, precision) )
    {
      console.log(&#x27;Failed to shrink points by the correct angle should be \n [[0.7071067811865475, 0.7071067811865475] [ 0.7071067811865475, 9.29289321882] [ 9.29289321882, 9.29289321882] [9.29289321882, 0.7071067811865475]] but got : \n %s&#x27;, JSON.stringify(shrunkRelativePoints));
      console.log(&#x27;see debug below ####################################################################################&#x27;);
      shrinkShape(relativePoints, 1, true);
      console.log(&#x27;end of test debug ####################################################################################&#x27;);
      succeeded = false;

    }
    if ( succeeded )
    {
      alert(&#x27;All tests passed&#x27;);
    } else
    {
      alert(&#x27;Test failed check console log&#x27;);
    }

    &#x2F;&#x2F; console.log(pointsToStl(relativePoints, 50));
    &#x2F;&#x2F; console.log(pointsToStl(shrunkRelativePoints, 75));
      &#x2F;*
       &#x2F;&#x2F; This was part of test I was running but never formalised it, it created a star which was really useful for checking concave&#x2F; convex angles correctness. I may revisit this and create some test to pass&#x2F; fail it
       relativePoints = [];
       relativePoints.push([0, 6]);
       relativePoints.push([2, 2]);
       relativePoints.push([6, 0]);
       relativePoints.push([2, -2]);
       relativePoints.push([0, -6]);
       relativePoints.push([-2, -2]);
       relativePoints.push([-6, 0]);
       relativePoints.push([-2, 2]);

       shrunkRelativePoints = shrinkShape(relativePoints, 2, true);
       console.log(pointsToSTLPerimeter(relativePoints, 50));
       console.log(pointsToSTLPerimeter(shrunkRelativePoints, 75));*&#x2F;
  }

  function effectivelyEqual(num1, num2, precision)
  {
    return Math.abs(num1 - num2) &lt; precision;
  }
&lt;&#x2F;script&gt;
&lt;&#x2F;body&gt;
&lt;&#x2F;html&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Track a Train</title>
        <published>2015-10-15T00:00:00+00:00</published>
        <updated>2015-10-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://seanborg.tech/projects/track-a-train/"/>
        <id>https://seanborg.tech/projects/track-a-train/</id>
        
        <content type="html" xml:base="https://seanborg.tech/projects/track-a-train/">&lt;h2 id=&quot;goal&quot;&gt;Goal&lt;&#x2F;h2&gt;
&lt;p&gt;To have a faster, simpler way to see when trains are delayed&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why&quot;&gt;Why&lt;&#x2F;h2&gt;
&lt;p&gt;Uk trains are continuously delayed, but I live very close to a train station
so knowing they are delayed before I set off is very beneficial to me&lt;&#x2F;p&gt;
&lt;h2 id=&quot;where&quot;&gt;Where&lt;&#x2F;h2&gt;
&lt;p&gt;This is maybe the only project closed source, till I spend the time to
remove the key I put in the repo :&#x2F;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;current-state&quot;&gt;Current state&lt;&#x2F;h2&gt;
&lt;p&gt;Unmaintained... it works but unless you know how it works it&#x27;s vey confusing&lt;&#x2F;p&gt;
</content>
        
    </entry>
</feed>
