<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
  xmlns:atom="http://www.w3.org/2005/Atom"
  xmlns:content="http://purl.org/rss/1.0/modules/content/"
  xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Agnel Nieves - Design Engineering</title>
    <link>https://agnelnieves.com/blog/tag/design-engineering</link>
    <description>Blog posts on Design Engineering by Agnel Nieves.</description>
    <language>en-US</language>
    <lastBuildDate>Fri, 15 May 2026 01:12:15 GMT</lastBuildDate>
    <atom:link href="https://agnelnieves.com/blog/tag/design-engineering/feed.xml" rel="self" type="application/rss+xml" />
    
    <item>
      <title><![CDATA[Building a Terminal Portfolio You Can SSH Into]]></title>
      <link>https://agnelnieves.com/blog/building-a-terminal-portfolio-you-can-ssh-into</link>
      <guid isPermaLink="true">https://agnelnieves.com/blog/building-a-terminal-portfolio-you-can-ssh-into</guid>
      <description><![CDATA[How I shipped ssh agnelnieves.sh, a terminal portfolio in Rust using ratatui, russh, and Fly.io. One binary, two modes: local CLI and SSH server.]]></description>
      <content:encoded><![CDATA[<h2>TL;DR</h2>
<p>You can now browse this site in your terminal:</p>
<pre><code class="language-bash">ssh agnelnieves.sh
</code></pre>
<p>No install. One Rust binary that runs as either a local CLI or an in-binary SSH server. No system sshd, no user accounts, no auth. The TUI is built with <a href="https://ratatui.rs">ratatui</a>; the SSH layer is <a href="https://docs.rs/russh">russh</a>; render code is shared between local and remote modes. It&#39;s hosted on Fly.io behind a dedicated IPv4, and the whole thing costs about $2/mo. The code lives as a <code>cli/</code> package inside the same Next.js project that powers this site, so the website and the terminal share a single deploy story. If you want the full build (architecture, code, Fly.io setup), I wrote it up as a <a href="/guides/ssh-terminal-portfolio.md">step-by-step guide</a>.</p>
<h2>The setup</h2>
<p><code>ssh agnelnieves.sh</code>. That&#39;s it. No <code>-l user@</code>, no keys to add, no first-time signup. The server accepts whatever username your local SSH client offers and drops you straight into a terminal UI.</p>
<p>Press <code>h</code>/<code>a</code>/<code>b</code>/<code>c</code> to jump between Home, About, Blog, and Connect. <code>j</code>/<code>k</code> (or arrows) to navigate lists. <code>Enter</code> to open. <code>q</code> to quit, which closes the SSH session.</p>
<h2>Why I built it</h2>
<p>Honestly? I saw <a href="https://terminal.shop">terminal.shop</a> and thought it was awesome. They sell coffee at <code>ssh terminal.shop</code> (that&#39;s the whole storefront, no website) and the first time I tried it I wanted to know if I could build something like it.</p>
<p>I do side projects like this when I need to step away from whatever I&#39;m shipping day-to-day. They&#39;re not pitches. They don&#39;t need a thesis. They&#39;re more like the way some people fix a bike on a Saturday: sit down, follow whatever&#39;s pulling on your curiosity, and at the end you have a thing that didn&#39;t exist before. That&#39;s the whole reason.</p>
<p>So this was that. A few evenings poking at <a href="https://ratatui.rs">ratatui</a> and <a href="https://docs.rs/russh">russh</a> to see what it would take to make <code>ssh agnelnieves.sh</code> actually work. Turns out: less than I expected. The hardest parts were the ones I didn&#39;t see coming, which is what makes any of this fun.</p>
<h2>The architecture</h2>
<p>One binary. Two modes. One render path.</p>
<pre><code>            ┌─────────────────────────────┐
            │     agnel  (single .exe)    │
            └──────────────┬──────────────┘
                           │
            ┌──────────────┴──────────────┐
            │                             │
       agnel (no flag)             agnel --serve
       Local TUI                   SSH server (Fly.io)
       crossterm stdin             russh + custom backend
            │                             │
            └──────────┬──────────────────┘
                       │
                src/render.rs   ← shared
                       │
                Per-session App state
                       │
                Live fetches from agnelnieves.com APIs
</code></pre>
<p>The pieces:</p>
<ul>
<li><strong>TUI:</strong> <a href="https://ratatui.rs">ratatui</a> draws everything: header, ticker, ASCII banner, projects list, blog reader. The render function lives in <code>src/render.rs</code> and is identical whether the binary is running on your laptop or whether you&#39;re seeing it over SSH.</li>
<li><strong>State machine:</strong> <code>App</code> in <code>src/app.rs</code> owns screen, scroll position, list selection, ticker animation, and async-fetch results funneled through an <code>mpsc::Receiver</code>.</li>
<li><strong>Data:</strong> zero content is bundled. The CLI fetches live from <code>/feed.json</code>, <code>/api/blog/[slug]/raw</code>, <code>/api/projects</code>, and <code>/api/site.json</code> on this site, cached for an hour at <code>~/.agnel/cache/</code>. So a new blog post here shows up in your terminal immediately without redeploying anything.</li>
<li><strong>SSH server:</strong> <a href="https://docs.rs/russh">russh 0.60</a> in <code>src/serve.rs</code>. One tokio task per session.</li>
</ul>
<h2>The decision that made it easy: don&#39;t fork a PTY</h2>
<p>Most &quot;expose a TUI over SSH&quot; guides suggest spawning a pseudo-terminal and <code>exec</code>-ing the TUI binary inside it. That&#39;s how <code>mosh</code>, <code>tmux</code>, and Go&#39;s <a href="https://charm.sh/blog/wish/"><code>wish</code></a> library (the one terminal.shop uses) handle things. It works, but it&#39;s heavier:</p>
<ul>
<li>Adds a libc dependency (<code>forkpty</code>)</li>
<li>Forks a process per connection (more memory, slower spawn)</li>
<li>Hard to share state between the SSH layer and the app</li>
</ul>
<p>Russh&#39;s <code>ratatui_app</code> example showed a cleaner path: implement a custom <code>std::io::Write</code> backend that funnels bytes through russh&#39;s <code>Handle::data(channel, bytes)</code> API. Ratatui doesn&#39;t know it&#39;s writing to an SSH channel instead of stdout. No fork, no PTY, just bytes:</p>
<pre><code class="language-rust">struct TerminalSink {
    sender: UnboundedSender&lt;Vec&lt;u8&gt;&gt;,
    sink: Vec&lt;u8&gt;,
}

impl std::io::Write for TerminalSink {
    fn write(&amp;mut self, buf: &amp;[u8]) -&gt; std::io::Result&lt;usize&gt; {
        self.sink.extend_from_slice(buf);
        Ok(buf.len())
    }
    fn flush(&amp;mut self) -&gt; std::io::Result&lt;()&gt; {
        if !self.sink.is_empty() {
            let _ = self.sender.send(std::mem::take(&amp;mut self.sink));
        }
        Ok(())
    }
}
</code></pre>
<p>A background tokio task drains the channel and pushes bytes back to the client through russh. Each session gets its own <code>App</code> and its own <code>Terminal&lt;CrosstermBackend&lt;TerminalSink&gt;&gt;</code>. The lock graph stays tiny: an <code>Arc&lt;Mutex&lt;SessionState&gt;&gt;</code> per session, plus a <code>HashMap</code> of all sessions for cleanup.</p>
<p>Inputs go the other direction. Russh hands raw bytes to my <code>data()</code> callback, and a small parser turns them into the same <code>crossterm::event::KeyEvent</code> values the local TUI uses. That meant zero changes to <code>App::handle_key</code>:</p>
<pre><code class="language-rust">fn parse_keys(buf: &amp;[u8]) -&gt; Vec&lt;KeyEvent&gt; {
    // 0x03 → Ctrl+C, 0x1b[A → Up, 0x1b alone → Esc,
    // printable ASCII → Char(b), etc.
}
</code></pre>
<p>Window resize hooks into the same place. <code>window_change_request</code> calls <code>terminal.resize(rect)</code>, and ratatui redraws on the next 50 ms tick.</p>
<p>The result: a single binary you can drop on a VPS, in a container, or even into a Lambda-style worker, and it just serves the TUI.</p>
<h2>What I learned about hosting</h2>
<p>A few things I didn&#39;t expect.</p>
<p><strong>1. Fly trial orgs block dedicated IPv4, and SSH needs IPv4.</strong> For HTTP, Fly&#39;s edge gives you a shared anycast IPv4 for free. For raw TCP on port 22, you need a dedicated v4 (~$2/mo), and trial orgs can&#39;t allocate one. Pure-IPv6 isn&#39;t a real option, either: most US residential ISPs still don&#39;t have native IPv6 SSH paths. Adding a card was the unlock.</p>
<p><strong>2. The SSH host key has to persist.</strong> If the container generates a fresh Ed25519 key on every boot, every returning visitor gets <code>REMOTE HOST IDENTIFICATION HAS CHANGED</code> warnings after the next deploy. The fix is a 1 GB Fly volume mounted at <code>/data</code>, with <code>--host-key /data/ssh_host_key</code>. First boot generates and writes the key; every boot after just reads it.</p>
<p><strong>3. Trial machines auto-stop after 5 minutes.</strong> That&#39;s how the Fly free tier nudges you toward billing. Once you&#39;re paying, set <code>auto_stop_machines = &quot;off&quot;</code> and <code>min_machines_running = 1</code> if you want a long-running service.</p>
<p><strong>4. <code>fly proxy</code> is fine for HTTP, weird for raw SSH.</strong> I burned half an hour on <code>Connection reset by peer</code> through <code>fly proxy 12222:2222</code> before realizing I should just <code>bash /dev/tcp/127.0.0.1/2222</code> from inside the machine via <code>flyctl ssh console</code>. The SSH server was healthy the whole time; the proxy path was the problem.</p>
<h2>ANSI Shadow beats slant</h2>
<p>The first banner I shipped used figlet&#39;s <code>slant</code> font. It looks fine in a docs file. At terminal width with full-block rendering, it falls apart into disconnected diagonals.</p>
<p>Swapped to <a href="https://patorjk.com/software/taag/#p=display&f=ANSI%20Shadow&t=AGNEL%20NIEVES">ANSI Shadow</a>, the same boxy block font terminal.shop and <a href="https://charm.sh">charm.sh</a> use. Six rows tall, about 93 columns wide, reads instantly. Probably the kind of thing I&#39;d have gotten right the first time if I&#39;d looked at more references before opening figlet.</p>
<h2>Visitor #N</h2>
<p>One fun thing I added. Look at the bottom-left of the footer when you connect: it says <code>visitor #N</code>. First SSH session ever was #1, second was #2, and so on. The count lives in a tiny text file on the Fly volume next to the host key, increments once per <code>channel_open_session</code>, and persists across deploys (host-key lesson applied).</p>
<p>Implementation is about thirty lines. An <code>AtomicU64</code> loaded from the file at startup, a <code>spawn_blocking</code> write after each <code>fetch_add</code>. The file is just a base-10 number with no header, so <code>cat /data/visitor_count</code> is the dashboard.</p>
<p>There&#39;s no leaderboard. No real meaning to the number. But watching it tick up after shipping is the moment the whole side project starts feeling worth it.</p>
<h2>The stack</h2>
<table>
<thead>
<tr>
<th>Layer</th>
<th>Tech</th>
</tr>
</thead>
<tbody><tr>
<td>TUI</td>
<td>ratatui 0.29, crossterm 0.28</td>
</tr>
<tr>
<td>SSH</td>
<td>russh 0.60</td>
</tr>
<tr>
<td>HTTP client</td>
<td>ureq 2 (sync, plenty for this)</td>
</tr>
<tr>
<td>Async runtime</td>
<td>tokio 1</td>
</tr>
<tr>
<td>CLI args</td>
<td>clap 4</td>
</tr>
<tr>
<td>Host</td>
<td>Fly.io (<code>iad</code>, shared-cpu-1x, 256 MB, 1 GB volume)</td>
</tr>
<tr>
<td>DNS</td>
<td>Vercel Domains (A + AAAA on the apex)</td>
</tr>
<tr>
<td>Binary size</td>
<td>~3 MB release (macOS arm64), 4.3 MB (Linux x86_64)</td>
</tr>
<tr>
<td>Image size</td>
<td>28 MB (Debian bookworm-slim runtime)</td>
</tr>
<tr>
<td>Cost</td>
<td>$2/mo for the dedicated IPv4 + free tier for the rest</td>
</tr>
</tbody></table>
<h2>What&#39;s next</h2>
<ul>
<li><strong>Pretty errors and loading states.</strong> Today they say <code>Loading…</code> which is functional but boring.</li>
<li><strong>Project deep-links.</strong> I render project metadata but not the original write-ups. Pulling MDX through the API would let me ship long-form case studies in-terminal.</li>
<li><strong>Cross-platform release binaries.</strong> The current install path is <code>cargo install --path cli/</code>. Homebrew tap + release artifacts for macOS and Linux is a Saturday morning.</li>
</ul>
<h2>Try it</h2>
<pre><code class="language-bash">ssh agnelnieves.sh
</code></pre>
<p>If you want to build your own version, the <a href="/guides/ssh-terminal-portfolio.md">guide</a> has the full setup (Rust + ratatui + russh + Fly.io), with a copy-paste prompt at the top so you can hand it to your AI agent of choice. If you ship one, <a href="/connect">let me know</a>.</p>
<hr>
<p><em>Built by <a href="/about">Agnel Nieves</a>, a design engineer with 15+ years across product, design systems, and crypto. More writing on <a href="/blog">the blog</a>.</em></p>
]]></content:encoded>
      <pubDate>Tue, 12 May 2026 00:00:00 GMT</pubDate>
      <author>agnel@agnelnieves.com (Agnel Nieves)</author>
      <dc:creator><![CDATA[Agnel Nieves]]></dc:creator>
      <category>Rust</category>
      <category>Ratatui</category>
      <category>SSH</category>
      <category>Fly.io</category>
      <category>Design Engineering</category>
      <category>Side Projects</category>
    </item>
    <item>
      <title><![CDATA[Design tokens: The key for consistent & reusable visual experiences]]></title>
      <link>https://agnelnieves.com/blog/design-tokens-the-key-for-consistent-and-reusable-visual-experiences</link>
      <guid isPermaLink="true">https://agnelnieves.com/blog/design-tokens-the-key-for-consistent-and-reusable-visual-experiences</guid>
      <description><![CDATA[Discover how design tokens serve as the bridge between design and development, enabling consistent, maintainable, and scalable digital experiences across platforms.]]></description>
      <content:encoded><![CDATA[<h2>Intro</h2>
<p>This day and age, we are used to digital experiences that enhance our daily lives. From user-friendly apps to interactive websites, we&#39;ve come to expect products that not only serve a purpose but also provide an enjoyable and familiar journey. However, it&#39;s no secret that not all digital encounters meet these expectations. Some leave us frustrated with their inconsistent or unfamiliar visual styles, while others lack any visual style at all, making them feel bland and, quite frankly, boring.</p>
<p>Let&#39;s pause for a moment, think about the last time you used an app or visited a website that made you wonder if you had accidentally stumbled into an entirely different platform. Buttons seemed to change colors, fonts appeared randomly altered, and spacing between elements seemed to have a mind of its own. Chances are, you were experiencing the consequences of a poorly managed design system.</p>
<p>Imagine your favorite recipe for a mouthwatering dish. Each ingredient plays a crucial role in enhancing the flavors and creating a delightful culinary journey. Similarly, a design system works in a comparable manner, seamlessly blending design and engineering to craft a harmonious user experience.</p>
<p>Yet, achieving this harmony isn&#39;t always easy, especially when designers and developers speak different languages. Designers care deeply about visual aesthetics, while developers are focused on the technical implementation.</p>
<p>This is where the term <strong>Design Token</strong> comes in. <strong>The secret ingredient to bridging the gap between design and engineering</strong> while creating consistent and enjoyable digital products. Design tokens serve as a common language that both designers and developers can understand and use to maintain a shared design system. They act as the building blocks of visual design decisions, such as colors, typography, spacing, and more.</p>
<p>So, how can design tokens transform a product from a chaotic mess into a user-friendly masterpiece? Let&#39;s take a closer look.</p>
<h2>Deep Dive</h2>
<h3>Consistency and Familiarity</h3>
<p>Design tokens bring consistency to the table, ensuring that apps or websites maintain unified visual styles across all its elements. From the vibrant colors that match a brand to the perfect typography that strikes the right tone, design tokens lay down the rules for the appearance of digital products. This consistency helps users feel at home, navigating effortlessly and trusting the platform.</p>
<h3>Faster design iterations</h3>
<p>Picture this: your team decides to update the primary color of your brand. With design tokens, you can make this change in a single place, and voila! The entire product seamlessly adopts the new color. No more hunting down every occurrence of the old color across the codebase or design files. Design tokens empower product teams to maintain the design system with ease.</p>
<h3>Reducing Technical Debt</h3>
<p>A chaotic product with inconsistent design elements is challenging to maintain and update. By utilizing design tokens, the product becomes more maintainable, as changes to design elements are centralized and easily controlled. This, in turn, reduces technical debt and streamlines future development efforts.</p>
<h3>Simplifying collaboration</h3>
<p>With Design Tokens, designers and developers no longer speak different languages and can finally be on the same page. Through collaborative efforts, they define and refine the design system, each bringing their expertise to the table. Designers can define the design tokens, while developers implement them in the codebase, resulting in a harmonious relationship between design and engineering teams. This newfound synergy leads to stronger, more enjoyable products.</p>
<h3>Enabling Reusability</h3>
<p>Design tokens promote the creation of reusable design components. Once a design component is defined and styled using design tokens, it can be reused throughout the product and even across different projects. This reusability not only saves time and effort but also ensures design consistency across the entire ecosystem.</p>
<h3>Enhancing User Trust</h3>
<p>A consistent and cohesive user interface builds trust with users. Design tokens help create a sense of reliability and professionalism, making users feel comfortable navigating the product. The result is improved user satisfaction and increased user loyalty.</p>
<h2>Real example</h2>
<p>Here&#39;s a visual example:</p>
<p><img src="https://cdn-images-1.medium.com/max/2000/1*4-aU5Do_6wozMeoQ1HLw3g.gif" alt="A visual tree representation of design tokens and their aliases with an interactive example on how they affect components."></p>
<p>In the above example, Designers create a set of defined color tokens.</p>
<p>The base color shown in the base hex value #00FF84. The Colors/Brand token alias which references the base color, The Button/Primary/Color token alias which references the brand color, and the Badge/Active/Border token alias which references the hex value with a 20% opacity.</p>
<h2>From design POV</h2>
<p>Any components from the design side can consume those variables by using Figma.</p>
<p><img src="https://cdn-images-1.medium.com/max/2748/0*B0VP_tK0KhDSwNtl.png" alt="A screenshot of Figma, using the Variables feature (as design tokens)"></p>
<h2>From engineering POV</h2>
<p>Engineering teams can leverage tools like <a href="https://tokens.studio/">Tokens Studio</a> with <a href="https://amzn.github.io/style-dictionary/#/">Style dictionary</a> to automatically sync variables and styles created from Figma into their codebase while being able to track versions when the change happened.</p>
<pre><code class="language-css">:root {
  --base: 151, 100%, 50%; /* HSL equivalent of #00FF84 */
  --colors-brand: hsl(var(--base));
  --colors-button-primary: var(--colors-brand);
  --badge-active-border: hsla(var(--base), .2);
}
</code></pre>
<h2>Wrapping Up</h2>
<p>Design tokens can bridge the gap between design and engineering by providing a single source of truth for visual style. This means that designers and engineers can work together more effectively, as they are both working with the same data. The ultimate goal is to enhance the time to live process of a product while ensuring that the visual style of a product is consistent across all platforms and devices.</p>
<h2>Resources</h2>
<ul>
<li><a href="https://help.figma.com/hc/en-us/articles/15339657135383-Guide-to-variables-in-Figma">Guide to variables Figma</a></li>
<li><a href="https://www.invisionapp.com/inside-design/guide-to-design-systems/">Comprehensive guide to design systems</a></li>
<li><a href="https://www.designbetter.co/design-systems-handbook">Design Systems Handbook</a></li>
<li><a href="https://www.youtube.com/watch?v=1ONxxlJnvdM">Figma intro to variables</a></li>
<li><a href="https://docs.tokens.studio/variables/creating-variables">Creating variables (Tokens Studio)</a></li>
</ul>
<h2>Connect</h2>
<p>I&#39;m always open to meet new people so feel free to contact me on <a href="https://twitter.com/agnelnieves">X</a></p>
<p>Thanks for reading! If you enjoyed this story, show your support by hitting the clap button, and sharing with your friends!</p>
]]></content:encoded>
      <pubDate>Sat, 12 Aug 2023 00:00:00 GMT</pubDate>
      <author>agnel@agnelnieves.com (Agnel Nieves)</author>
      <dc:creator><![CDATA[Agnel Nieves]]></dc:creator>
      <category>Design Systems</category>
      <category>Design Tokens</category>
      <category>Frontend Development</category>
      <category>UI/UX</category>
      <category>Design Engineering</category>
      <category>Design Collaboration</category>
    </item>
  </channel>
</rss>