<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>The Charm Blog</title>
    <link>https://charm.land/blog</link>
    <description>We write blog posts to make the command line glamorous</description>
    <managingEditor>vt100@charm.land (Charm)</managingEditor>
    <item>
      <title>Crush + Docker MCP</title>
      <link>https://charm.land/blog/crush-and-docker-mcp/</link>
      <description>Get MCPs on-the-fly in Crush with Docker MCP.</description>
      <content:encoded><![CDATA[<h1>Crush + Docker MCP</h1><p>Get MCPs on-the-fly in Crush with Docker MCP.<p>By Andrey Nering on 19 March 2026<figure><img src=/share.c77a2d0e799674f2.jpg width=600><figcaption>Two great flavors, all rolled into one</figcaption></figure><p>Say hello to the Docker MCP Catalog in Crush! We teamed up with
<a href=https://www.docker.com/ target=_blank rel="noopener noreferrer">Docker</a> to natively integrate the <a href=https://hub.docker.com/mcp target=_blank rel="noopener noreferrer">Docker MCP Catalog</a>
into Crush and—if we do say so ourselves—it rocks.<p>During a conversation, <a href=https://github.com/charmbracelet/crush target=_blank rel="noopener noreferrer">Crush</a> and Docker work together to pull in
relevant MCPs on demand. Working with Postgres? Building a robot? Going deep on
ARM? Crush and Docker will load in in the MCP you need, when you need it (and
yes, Crush will still ask for permission).<p>Not only is this a great way to <em>not</em> think about config, it&rsquo;s a great way to
<em>not</em> think about MCP. Ironic, eh? Of course, if you do like thinking about
MCPs you can ask Crush to add specific MCPs from the Docker MCP Catalog, too.<p>To get started, make sure Docker Desktop is running, then fire up Crush, press
<kbd>ctrl+p</kbd>, choose &ldquo;Enable Docker MCP Catalog&rdquo;, and get to work. That&rsquo;s it.<figure><img src=/command-palette.dea88a5b98ee8fe1.png width=600><figcaption>Set it and forget it</figcaption></figure><p>One of our favorite use cases is using the <a href=https://huggingface.co/mcp target=_blank rel="noopener noreferrer">Hugging Face MCP</a> to find an
appropriate model for the current machine, pull it down, and run it.<figure><video playsinline autoplay loop style="width: 800px">
<source src=/fibonacci-1080p.4e08fba04aed5e5d.webm type=video/webm><source src=/fibonacci-1080.8bb3d2d5c8a84629.mp4 type=video/mp4></video><figcaption>A match made in heaven? Docker, Crush, and HuggingFace working together to run a local model.</figcaption></figure><p>Here are some of the other things we’re doing with Crush + Docker MCP:<ul><li>Query your <a href=https://hub.docker.com/mcp/server/sqlite-mcp-server/overview target=_blank rel="noopener noreferrer">SQLite</a>, <a href=https://hub.docker.com/mcp/server/mongodb/overview target=_blank rel="noopener noreferrer">MongoDB</a> or <a href=https://hub.docker.com/mcp/server/elasticsearch/overview target=_blank rel="noopener noreferrer">Elasticsearch</a> databases<li>Manage cloud stuff like <a href=https://hub.docker.com/mcp/server/aws-core-mcp-server/overview target=_blank rel="noopener noreferrer">AWS</a>, <a href=https://hub.docker.com/mcp/server/azure/overview target=_blank rel="noopener noreferrer">Azure</a> or <a href=https://hub.docker.com/mcp/server/heroku/overview target=_blank rel="noopener noreferrer">Heroku</a><li>Send emails with <a href=https://hub.docker.com/mcp/server/resend/overview target=_blank rel="noopener noreferrer">Resend</a><li>Wrangle <a href=https://hub.docker.com/mcp/server/github-official/overview target=_blank rel="noopener noreferrer">GitHub</a> issues and pull requests<li>Interact with <a href=https://hub.docker.com/mcp/server/unreal-engine-mcp-server/overview target=_blank rel="noopener noreferrer">Unreal Engine</a><li>Search the <a href=https://hub.docker.com/mcp/server/minecraft-wiki/overview target=_blank rel="noopener noreferrer">Minecraft Wiki</a><li>And many others: <a href=https://hub.docker.com/mcp/explore target=_blank rel="noopener noreferrer">see for yourself</a></ul><p>We had a ton of fun building this with Docker and we hope you enjoy this new
feature as much as we do.<h2>Pro Tip</h2><p>Some MCPs require secrets and configuration flags. If that happens, Crush
will politely ask you to set them up, and it&rsquo;s as simple as throwing a few
commands on the ol’ CLI. For example, here’s how you would set up the Resend
MCP:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>docker mcp secret <span style=color:#ff8ec7>set</span> resend.api_key<span style=color:#ef8080>={</span>my_resend_key<span style=color:#ef8080>}</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>docker mcp config <span style=color:#ff8ec7>set</span> resend.sender<span style=color:#ef8080>={</span>my_email_address<span style=color:#ef8080>}</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>docker mcp config <span style=color:#ff8ec7>set</span> resend.reply_to<span style=color:#ef8080>={</span>my_email_address<span style=color:#ef8080>}</span>
</span></span></code></pre>]]></content:encoded>
      <author>Andrey Nering</author>
      <guid>Crush + Docker MCP</guid>
      <pubDate>Thu, 19 Mar 2026 00:00:00 +0000</pubDate>
      <source>https://charm.land/blog/crush-and-docker-mcp/</source>
    </item>
    <item>
      <title>v2</title>
      <link>https://charm.land/blog/v2/</link>
      <description>The next generations of Bubble Tea, Lip Gloss, and Bubbles are available now</description>
      <content:encoded><![CDATA[<h1>v2</h1><p>The next generations of Bubble Tea, Lip Gloss, and Bubbles are available now<p>By Christian Rocha on 23 February 2026<figure><video class=desktop playsinline autoplay loop>
<source src=/v2-1920-16x9.7897c3f5182d5f2c.webm type=video/webm><source src=/v2-1920-16x9.6a4cd4d771c3b93f.mp4 type=video/mp4></video>
<video class=mobile playsinline autoplay loop>
<source src=/v2-1920-1x1.97fd36049c4b35b4.webm type=video/webm><source src=/v2-1920-1x1.a9c23b443bffa5fb.mp4 type=video/mp4></video></figure><p>The next major versions of our terminal UI libraries—<a href=https://github.com/charmbracelet/bubbletea target=_blank rel="noopener noreferrer">Bubble Tea</a>, <a href=https://github.com/charmbracelet/lipgloss target=_blank rel="noopener noreferrer">Lip
Gloss</a>, and <a href=https://github.com/charmbracelet/bubbles target=_blank rel="noopener noreferrer">Bubbles</a>—are out of beta and ready to rock.<p>These releases bring highly optimized rendering, advanced compositing,
higher-fidelity input handling, and a more declarative API for very predictable
output.<p>The v2 branches have been powering <a href=https://github.com/charmbracelet/crush target=_blank rel="noopener noreferrer">Crush</a>, our AI coding agent, in
production from the very beginning. That is to say, everything we’re releasing
today has run under real-world constraints, on our own products, for months.<p>For details as well as upgrade guides for humans and LLMs see:<ul class=three-up><li><h3>Bubble Tea</h3><a href=https://github.com/charmbracelet/bubbletea/releases/tag/v2.0.0><img src=/bubbletea-v2-1200.51eaa1a83054ce68.jpg alt="Bubble Tea v2"></a>
<a href=https://github.com/charmbracelet/bubbletea/discussions/1374>What’s New</a><br><a href=https://github.com/charmbracelet/bubbletea/blob/main/UPGRADE_GUIDE_V2.md>Upgrade Guide</a><li><h3>Lip Gloss</h3><a href=https://github.com/charmbracelet/lipgloss/releases/tag/v2.0.0><img src=/lipgloss-v2-1200.363b10dd2667d492.jpg alt="Lip Gloss v2"></a>
<a href=https://github.com/charmbracelet/lipgloss/discussions/506>What’s New</a><br><a href=https://github.com/charmbracelet/lipgloss/blob/main/UPGRADE_GUIDE_V2.md>Upgrade Guide</a><li><h3>Bubbles</h3><a href=https://github.com/charmbracelet/bubbles/releases/tag/v2.0.0><img src=/bubbles-v2-1200.84aa75bff5a3a623.jpg alt="Bubbles v2"></a>
<a href=https://github.com/charmbracelet/bubbles/blob/main/UPGRADE_GUIDE_V2.md>Upgrade Guide</a></ul><p>But first, let’s talk about how we got here.<h2>Why v2?</h2><p>We started building terminal user interface tooling on the premise that the
terminal is a better place to work (and play) than most people realize.<p>The foundation has always been there but what was missing was software for the
next era and a lower barrier to entry for rich interaction. That&rsquo;s where the
innocently-named <a href=https://github.com/charmbracelet/bubbletea target=_blank rel="noopener noreferrer">Bubble Tea</a> (the interaction layer),
<a href=https://github.com/charmbracelet/lipgloss target=_blank rel="noopener noreferrer">Lip Gloss</a> (the layout engine), and <a href=https://github.com/charmbracelet/bubbles target=_blank rel="noopener noreferrer">Bubbles</a> (user
interface primitives) began.<p><strong>Today, the Bubble Tea ecosystem powers more than 25,000 open-source
applications.</strong> Teams at NVIDIA, GitHub, Slack, Microsoft Azure and
thousands of others build on top of them. And, throughout the history of the
project, we&rsquo;ve never pushed a breaking change.<p>So why a v2?<p>Things are changing. AI agents moved into the terminal, and suddenly the rest
of the industry saw what many already knew: the terminal is the most powerful
way to interface with the operating system. Coding tools followed. The
terminal, which was previously somewhat of a niche preference, became a primary
platform, and the weight it needed to carry changed. So we improved
the parts that needed improving.<p>The heart of v2 is the Cursed Renderer. It’s modeled on the ncurses rendering
algorithm and vastly improves what’s possible in our tooling. Rendering is
faster and more efficient by orders of magnitude. For local applications this
is very meaningful. For applications running over SSH, the changes are
monetarily quantifiable.<p>v2 also reaches deeper into what emerging terminals can actually do. There’s
richer keyboard support, inline images, synchronized rendering, clipboard
transfer over SSH, and many more small, meticulous details. The terminal is
quietly becoming far more capable than most developers realize, and v2 makes
gracefully taking advantage of those capabilities very easy.<p>There’s a reason Bubble Tea supports inline mode as a first-class use case,
a reason we chose a language that compiles to native machine code, and a reason
we’re obsessed with performance in areas most frameworks don’t consider. The
terminal is a powerful medium for both humans and machines, with real
advantages—namely speed, composability, scriptability, and deep access to the
OS—and it deserves production-grade software.<p>That’s v2.]]></content:encoded>
      <author>Christian Rocha</author>
      <guid>v2</guid>
      <pubDate>Mon, 23 Feb 2026 00:00:00 +0000</pubDate>
      <source>https://charm.land/blog/v2/</source>
    </item>
    <item>
      <title>Crush, Welcome Home</title>
      <link>https://charm.land/blog/crush-comes-home/</link>
      <description>Glamorous software meets artificial intelligence.</description>
      <content:encoded><![CDATA[<h1>Crush, Welcome Home</h1><p>We made the command line glamorous. Now we're making it intelligent.<p>By Christian Rocha on 30 July 2025<p>A few short months ago, <a href=https://github.com/kujtimiihoxha target=_blank rel="noopener noreferrer">Kujtim Hoxha</a> set out to build something interesting that would get people&rsquo;s attention. He built a terminal-based AI coding agent, and…it totally got our attention.<p>Kujtim reached for Go and the core of the Charm stack—<a href=https://github.com/charmbracelet/bubbletea target=_blank rel="noopener noreferrer">Bubble Tea</a>, <a href=https://github.com/charmbracelet/bubbles target=_blank rel="noopener noreferrer">Bubbles</a>, <a href=https://github.com/charmbracelet/lipgloss target=_blank rel="noopener noreferrer">Lip Gloss</a>, and <a href=https://charm.land/glamour>Glamour</a>—tools we&rsquo;ve been relentlessly building and refining for the past five years. What he created moved with remarkable speed and precision.<p>We were immediately floored upon seeing the project. Here was a developer who not only understood our tools deeply enough to build something exceptional with them, but who had the LLM expertise to help us realize our vision for AI-powered development tools. I caught the next flight to Prishtina, Kosovo to meet Kujtim in person, and the rest is history.<p>Kujtim&rsquo;s project, now called <a href=https://github.com/charmbracelet/crush target=_blank rel="noopener noreferrer">Crush</a>, has come home to Charm, where it was always meant to be. It will continue on with its original creator as well as the full support of the Charm team.<figure><a href=http://github.com/charmbracelet/crush><video width=800 autoplay loop muted playsinline>
<source src=/crush-demo.238e393760cf226.webm type=video/webm><source src=/crush-demo.38a5222ad3c1d296.mp4 type=video/mp4></video></a><figcaption>I remember <em>my</em> first Crush. No, it wasn’t a computer.</figcaption></figure><h2>Why Now?</h2><p>LLMs have most definitely crossed the threshold from impressive demos to genuinely useful tools. They can handle complex, multi-file reasoning and help developers work at speeds that were previously impossible. Take this website&rsquo;s subtle background effect: a GLSL shader that generates layered Gaussian noise. What would have taken hours of digging through WebGL docs and trial-and-error debugging, I built with Crush in just a few minutes.<p>But powerful AI is only half the equation. To harness it effectively, you need the right tooling and the right user interface, and we believe that interface is the terminal. Developers already live there. It&rsquo;s fast, scriptable, integrates
seamlessly with existing workflows, and has all the wealth and power of the CLI at its fingertips. Crush can directly access the same tools you can (git, docker, npm, ghc, sed, nix and so on), and it can do so while running with extensive knowledge of the tools and extreme efficiency.<p>So why now? Because the timing is perfect. We&rsquo;ve spent five years building the groundwork to make terminal experiences outstanding. Our tooling has matured into a powerful foundation for creating first-class terminal applications. Now we&rsquo;re doubling down with <a href=https://github.com/charmbracelet/ultraviolet target=_blank rel="noopener noreferrer">Ultraviolet</a>, our next-generation terminal UI toolkit that brings advanced compositing, lightning-fast rendering, and many other features we could previously only dream of.<figure><a href=https://github.com/charmbracelet/ultraviolet><video width=500 autoplay loop muted playsinline>
<source src=/uv-loop.ac2b0e37a66ccb2.webm type=video/webm><source src=/uv-loo.fdf42f4b87b1c75a.mp4 type=video/mp4></video></a><figcaption>Don’t forget to wear sunscreen.</figcaption></figure><p>We’re at a moment where everything is changing. The future of software development sits at the intersection of creative, new thinking around AI, user interfaces, collaboration, and culture. With Crush, we&rsquo;re building exactly that.<h2>Ready to Crush it?</h2><p>With over 150,000 GitHub stars and upwards of 11,000 GitHub followers (more than many major tech companies) we&rsquo;ve built a community that makes Crush possible: developers who understand that <strong class=special>glamorous software is a force multiplier.</strong><p>Come try <a href=https://github.com/charmbracelet/crush target=_blank rel="noopener noreferrer">Crush</a>, let us know what you think, and help us build the future of both AI-powered development—and software itself.]]></content:encoded>
      <author>Christian Rocha</author>
      <guid>Crush, Welcome Home</guid>
      <pubDate>Wed, 30 Jul 2025 00:00:00 +0000</pubDate>
      <source>https://charm.land/blog/crush-comes-home/</source>
    </item>
    <item>
      <title>Mods MCP Server Support</title>
      <link>https://charm.land/blog/mods-mcp/</link>
      <description>Your favorite tool for LLMs on the CLI just got an upgrade.</description>
      <content:encoded><![CDATA[<h1>Mods Now Supports MCP</h1><p>By Bashbunni on 14 July 2025<p>Big news for AI enthusiasts out there: We now support MCP in <a href=https://github.com/charmbracelet/mods target=_blank rel="noopener noreferrer">Mods</a>!</p><a class="cart mods" data-repo=charmbracelet/mods href=https://github.com/charmbracelet/mods target=_blank><div class=info><picture><source srcset=/mods.cfc9c669ca030d19.webp type=image/webp><img src=/mods.5b7f085bf69aef1d.png alt="Mods mascot"></picture><h3 class=puffy><span data-text=Mods>Mods</span></h3><ul class=badges><li><span>Built Itself</span><span>Yep</span></ul><p>AI on the command line.</div><div class=art><video loop muted playsinline>
<source src=/mods.5ca3d58a9edc9b3d.webm type=video/webm><source src=/mods.5050b5bf6f3b2d1c.mp4 type=video/mp4></video><div></div></div></a><p>MCP is a standardized way to add additional context and functionality to your LLMs, allowing you to connect them to various tools and data sources. For those who may be unfamiliar, let&rsquo;s do a quick intro.<h2>A bit about MCP servers</h2><p><a href=https://modelcontextprotocol.io/ target=_blank rel="noopener noreferrer">Model Context Protocol</a> is an open source standard for connecting AI assistants to the systems where data lives. It allows developers to connect AI models to tools and data sources provided by <a href=https://github.com/modelcontextprotocol/servers target=_blank rel="noopener noreferrer">MCP servers</a> for extended functionality and context. It&rsquo;s worth noting that the ability to tie in external sources as data to your LLMs isn&rsquo;t what&rsquo;s revolutionary here—it&rsquo;s the fact that there is a standard protocol for it. That means reliability for app developers who want to support these enhancements in their AI applications.<p>Let&rsquo;s jump into some examples of different MCP servers and their toolsets.<p>Say you&rsquo;re using Supabase in your project. You connect your LLM to the <a href=https://github.com/supabase-community/supabase-mcp target=_blank rel="noopener noreferrer">Supabase</a> MCP server to leverage its tools to interact with your Supabase project through your LLM. This allows you to do things like project management, database operations, project configuration, and more.<p>In some cases, MCP servers may be cloud-hosted by the organization or a local instance on your machine that you can run as a Docker container. Here&rsquo;s an example using mods with GitHub&rsquo;s MCP server to list issues related to a specific topic, in this case border colors, in the <a href=https://github.com/charmbracelet/lipgloss target=_blank rel="noopener noreferrer">Lip Gloss</a> repo.<figure><picture><img src=/mods-mcp-large.38e5ec1c07fdac44.gif alt="Using mods with GitHub's MCP server to list issues related to border colors in the Lip Gloss project."></picture><figcaption>mod chips > potato chips.</figcaption></figure><p>If all of this still feels like a lot, think of it this way: MCP servers allow you to work with external tools and data similar to an API, but since it&rsquo;s integrated into your LLM, you can use natural language to direct these operations.<h2>What&rsquo;s changed in Mods</h2><p>Getting your favorite servers up and running with Mods is simple. Just add MCP servers to your <code>mods.yml</code> file, just like <a href=https://github.com/caarlos0 target=_blank rel="noopener noreferrer">Carlos</a> did below. His example uses the <a href=https://github.com/github/github-mcp-server target=_blank rel="noopener noreferrer">GitHub</a>, <a href=https://github.com/modelcontextprotocol/servers-archived/tree/main/src/puppeteer target=_blank rel="noopener noreferrer">Puppeteer</a>, <a href=https://github.com/modelcontextprotocol/servers/tree/main/src/sequentialthinking target=_blank rel="noopener noreferrer">Sequential Thinking</a>, and <a href=https://github.com/modelcontextprotocol/servers/tree/main/src/time target=_blank rel="noopener noreferrer">Time</a> MCP servers.<blockquote><p>Heads up, you can open your config file with <code>mods --settings</code>. Use <code>mods --help</code> to learn more.</blockquote><pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#b083ea>mcp-servers</span><span style=color:#e8e8a8>:</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#b083ea>github</span><span style=color:#e8e8a8>:</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>    <span style=color:#b083ea>command</span><span style=color:#e8e8a8>:</span> docker
</span></span><span style=display:flex;><span style=line-height:1.4em;>    <span style=color:#b083ea>env</span><span style=color:#e8e8a8>:</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>      - GITHUB_PERSONAL_ACCESS_TOKEN=xxxyyy
</span></span><span style=display:flex;><span style=line-height:1.4em;>    <span style=color:#b083ea>args</span><span style=color:#e8e8a8>:</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>      - run
</span></span><span style=display:flex;><span style=line-height:1.4em;>      - <span style=color:#c69669>&#34;-i&#34;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>      - <span style=color:#c69669>&#34;--rm&#34;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>      - <span style=color:#c69669>&#34;-e&#34;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>      - GITHUB_PERSONAL_ACCESS_TOKEN
</span></span><span style=display:flex;><span style=line-height:1.4em;>      - <span style=color:#c69669>&#34;ghcr.io/github/github-mcp-server&#34;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#b083ea>puppeteer</span><span style=color:#e8e8a8>:</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>    <span style=color:#b083ea>command</span><span style=color:#e8e8a8>:</span> docker
</span></span><span style=display:flex;><span style=line-height:1.4em;>    <span style=color:#b083ea>args</span><span style=color:#e8e8a8>:</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>      - run
</span></span><span style=display:flex;><span style=line-height:1.4em;>      - <span style=color:#c69669>&#34;-i&#34;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>      - <span style=color:#c69669>&#34;--rm&#34;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>      - <span style=color:#c69669>&#34;--init&#34;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>      - <span style=color:#c69669>&#34;-e&#34;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>      - DOCKER_CONTAINER=true
</span></span><span style=display:flex;><span style=line-height:1.4em;>      - mcp/puppeteer
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#b083ea>sequentialthinking</span><span style=color:#e8e8a8>:</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>    <span style=color:#b083ea>command</span><span style=color:#e8e8a8>:</span> docker
</span></span><span style=display:flex;><span style=line-height:1.4em;>    <span style=color:#b083ea>args</span><span style=color:#e8e8a8>:</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>      - run
</span></span><span style=display:flex;><span style=line-height:1.4em;>      - --rm
</span></span><span style=display:flex;><span style=line-height:1.4em;>      - -i
</span></span><span style=display:flex;><span style=line-height:1.4em;>      - mcp/sequentialthinking
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#b083ea>time</span><span style=color:#e8e8a8>:</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>    <span style=color:#b083ea>command</span><span style=color:#e8e8a8>:</span> docker
</span></span><span style=display:flex;><span style=line-height:1.4em;>    <span style=color:#b083ea>args</span><span style=color:#e8e8a8>:</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>      - run
</span></span><span style=display:flex;><span style=line-height:1.4em;>      - -i
</span></span><span style=display:flex;><span style=line-height:1.4em;>      - --rm
</span></span><span style=display:flex;><span style=line-height:1.4em;>      - mcp/time
</span></span><span style=display:flex;><span style=line-height:1.4em;>      - --local-timezone=America/Sao_Paulo
</span></span></code></pre><p>All of this hard work was added in the latest <a href=https://github.com/charmbracelet/mods/releases/tag/v1.8.0 target=_blank rel="noopener noreferrer">release</a>.<h2>Whatcha think?</h2><p>Have some feedback on this post? We&rsquo;d love to hear it. Let us know on
<a href=https://charm.sh/chat>Discord</a> or via email at
<a href=mailto:vt100@charm.sh>vt100@charm.sh</a>.]]></content:encoded>
      <author>Bashbunni</author>
      <guid>Mods MCP Server Support</guid>
      <pubDate>Mon, 14 Jul 2025 00:00:00 +0000</pubDate>
      <source>https://charm.land/blog/mods-mcp/</source>
    </item>
    <item>
      <title>So Hot Right Now: Lip Gloss v2 Beta 2</title>
      <link>https://charm.land/blog/lipgloss-v2-beta-2/</link>
      <description>Compositing and layers are now available!</description>
      <content:encoded><![CDATA[<h1>So Hot Right Now: Lip Gloss v2 Beta 2</h1><p>By Andrey Nering on 30 June 2025<p><a href=https://github.com/charmbracelet/lipgloss/releases/tag/v2.0.0-beta.2 target=_blank rel="noopener noreferrer">This release</a> builds on top of the last <a href=https://github.com/charmbracelet/lipgloss/releases/tag/v2.0.0-beta.1 target=_blank rel="noopener noreferrer">Beta 1</a> release. It includes a new API for compositing layers and views, table enhancements, and a bunch of bug fixes. Let&rsquo;s get into it!<h2>Compositing</h2><figure><img src=/layers.ab21b343403141e4.png width=600><figcaption>Compositing looks so nice, doesn't it?</figcaption></figure><p>The big news in this release is compositing. Here&rsquo;s what it looks like:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>box <span style=color:#ef8080>:=</span> lipgloss<span style=color:#e8e8a8>.</span><span style=color:#00d787>NewStyle</span><span style=color:#e8e8a8>().</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>    <span style=color:#00d787>Width</span><span style=color:#e8e8a8>(</span><span style=color:#6eefc0>10</span><span style=color:#e8e8a8>).</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>    <span style=color:#00d787>Height</span><span style=color:#e8e8a8>(</span><span style=color:#6eefc0>5</span><span style=color:#e8e8a8>).</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>    <span style=color:#00d787>Border</span><span style=color:#e8e8a8>(</span>lipgloss<span style=color:#e8e8a8>.</span><span style=color:#00d787>NormalBorder</span><span style=color:#e8e8a8>())</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767>// Make some layers.
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767></span>a <span style=color:#ef8080>:=</span> lipgloss<span style=color:#e8e8a8>.</span><span style=color:#00d787>NewLayer</span><span style=color:#e8e8a8>(</span>box<span style=color:#e8e8a8>.</span><span style=color:#00d787>Render</span><span style=color:#e8e8a8>(</span><span style=color:#c69669>&#34;Who wants marmalade?&#34;</span><span style=color:#e8e8a8>))</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>b <span style=color:#ef8080>:=</span> lipgloss<span style=color:#e8e8a8>.</span><span style=color:#00d787>NewLayer</span><span style=color:#e8e8a8>(</span>box<span style=color:#e8e8a8>.</span><span style=color:#00d787>Render</span><span style=color:#e8e8a8>(</span><span style=color:#c69669>&#34;I do!&#34;</span><span style=color:#e8e8a8>))</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767>// Put layers in a canvas.
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767></span>canvas <span style=color:#ef8080>:=</span> lipgloss<span style=color:#e8e8a8>.</span><span style=color:#00d787>NewCanvas</span><span style=color:#e8e8a8>(</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>    a<span style=color:#e8e8a8>.</span><span style=color:#00d787>X</span><span style=color:#e8e8a8>(</span><span style=color:#6eefc0>5</span><span style=color:#e8e8a8>).</span><span style=color:#00d787>Y</span><span style=color:#e8e8a8>(</span><span style=color:#6eefc0>10</span><span style=color:#e8e8a8>).</span><span style=color:#00d787>Z</span><span style=color:#e8e8a8>(</span><span style=color:#6eefc0>1</span><span style=color:#e8e8a8>),</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>    b<span style=color:#e8e8a8>.</span><span style=color:#00d787>X</span><span style=color:#e8e8a8>(</span><span style=color:#6eefc0>3</span><span style=color:#e8e8a8>).</span><span style=color:#00d787>Y</span><span style=color:#e8e8a8>(</span><span style=color:#6eefc0>7</span><span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767>// Render it all out.
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767></span>lipgloss<span style=color:#e8e8a8>.</span><span style=color:#00d787>Println</span><span style=color:#e8e8a8>(</span>canvas<span style=color:#e8e8a8>.</span><span style=color:#00d787>Render</span><span style=color:#e8e8a8>())</span>
</span></span></code></pre><p>Also note that layers can also be nested (see <a href=https://pkg.go.dev/github.com/charmbracelet/lipgloss/v2@v2.0.0-beta1#Layer.AddLayers target=_blank rel="noopener noreferrer"><code>Layer.AddLayers</code></a>).
Otherwise, that’s all there is to it!<p>For more info see <a href=https://pkg.go.dev/github.com/charmbracelet/lipgloss/v2@v2.0.0-beta1#Layer target=_blank rel="noopener noreferrer"><code>Layer</code></a>, <a href=https://pkg.go.dev/github.com/charmbracelet/lipgloss/v2@v2.0.0-beta1#Canvas target=_blank rel="noopener noreferrer"><code>Canvas</code></a>, and <a href=https://github.com/charmbracelet/lipgloss/blob/e9f399ea6d86f3e0b2e540211d4c1e16ccd773ba/examples/canvas/main.go target=_blank rel="noopener noreferrer">the compositing example</a>.<p>Big shout out to <a href=https://github.com/aymanbagabas target=_blank rel="noopener noreferrer">@aymanbagabas</a> for his work
on this feature!<h2>Table Enhancements</h2><p>Tables are one of the most beloved Charm components, and we&rsquo;ve been working to
make them as polished as possible. In this release I
(<a href=https://github.com/andreynering target=_blank rel="noopener noreferrer">@andreynering</a>) fixed <em>several</em> bugs
and did many other rendering enhancements. You can check most of the fixes in
<a href=https://github.com/charmbracelet/lipgloss/pull/526 target=_blank rel="noopener noreferrer">this pull request</a>.<p>We&rsquo;re also refactoring the Bubbles&rsquo; table component to use the Lip Gloss&rsquo; table package, and making their APIs similar, as before they were relatively different. This means that if you use Tables via Bubbles in your Bubble Tea app, you&rsquo;ll be able to reap the benefits soon too!<h2>Whatcha think?</h2><p>Have some feedback on this post? We’d love to hear. Let us know in
<a href=https://charm.sh/chat>Discord</a> or via email at
<a href=mailto:vt100@charm.sh>vt100@charm.sh</a>.]]></content:encoded>
      <author>Andrey Nering</author>
      <guid>So Hot Right Now: Lip Gloss v2 Beta 2</guid>
      <pubDate>Mon, 30 Jun 2025 00:00:00 +0000</pubDate>
      <source>https://charm.land/blog/lipgloss-v2-beta-2/</source>
    </item>
    <item>
      <title>Markdown Tables Have Never Looked Better</title>
      <link>https://charm.land/blog/glamour-tables/</link>
      <description>Tables in Glamour got a big makeover this week. Here’s a little BTS.</description>
      <content:encoded><![CDATA[<h1>Markdown Tables Have Never Looked Better</h1><p>By Bashbunni on 21 April 2025<p>Tables in <a href=https://charm.sh/glamour>Glamour</a>, our markdown renderer, got a big
makeover in <a href=https://github.com/charmbracelet/glamour/releases/tag/v0.10.0 target=_blank rel="noopener noreferrer">v0.10.0</a> last week with UX improvements for screen
reader compatibility and general legibility. Here’s a little sneak peek of the
process.<h2>How it Started</h2><p>Okay, so here&rsquo;s what tables with a bunch of links used to look like:<figure><img src=/b.4da36f687ce6da9b.png width=800></figure><p>While working with the <a href=https://cli.github.com target=_blank rel="noopener noreferrer">GitHub CLI</a> team, we received feedback that links
were interrupting the flow of text, which really hurt accessibility and just
made tables really hard to read. They were also spanning multiple lines when
they would wrap, making them impossible to click. We realized we had a nice
opportunity to improve table rendering in the terminal in a big way.<h2>How it’s Going</h2><p>Rethinking how we were rendering links was the perfect opportunity to fix
broken links, text legibility, AND improve accessibility in one fell
swoop. After much consideration, we removed the URL from the table and instead
opted to show it below instead.<figure><img src=/a.adf2bbeadd03cc27.png width=800></figure><p>A big thank you to Andy, William and Kynan on the <a href=https://cli.github.com target=_blank rel="noopener noreferrer">GitHub CLI</a> team for
working with us in this redesign, and a big shout out to
<a href=https://x.com/andreynering/ target=_blank rel="noopener noreferrer">Andrey Nering</a> here on the Charm team who
absolutely crushed this release, from design to implementation. 💫<h2>A GitHub Bonus</h2><p>As a little bonus we also improved rendering for GitHub URLs, inspired by our
friends at GitHub. Check it out:<figure><img src=/c.ea794cf014490cd7.png width=800></figure><h2>Hungry for more?</h2><p>Want all the details on this release? Then you&rsquo;ll want to read <a href=https://github.com/charmbracelet/glamour/releases/tag/v0.10.0 target=_blank rel="noopener noreferrer">the full release notes on Glamour v0.10.0</a>.<h2>Whatcha think?</h2><p>Have some feedback on this post? We’d love to hear. Let us know in
<a href=https://charm.sh/chat>Discord</a> or via email at
<a href=mailto:vt100@charm.sh>vt100@charm.sh</a>.]]></content:encoded>
      <author>Bashbunni</author>
      <guid>Markdown Tables Have Never Looked Better</guid>
      <pubDate>Mon, 21 Apr 2025 00:00:00 +0000</pubDate>
      <source>https://charm.land/blog/glamour-tables/</source>
    </item>
    <item>
      <title>A Brief History of Terminal Emulators</title>
      <link>https://charm.land/blog/intro-to-terminals/</link>
      <description>What is a terminal emulator? How is it different from a typewriter? And how is it the same?</description>
      <content:encoded><![CDATA[<h1>A Brief History of Terminal Emulators</h1><p>By Ayman Bagabas on 11 March 2025<p>If you&rsquo;re reading this, you&rsquo;ve probably used a terminal emulator before. But
have you ever wondered how this modern—yet archaic—tool came to be?<p>In this post, we&rsquo;ll explore the history of terminal emulators, from the
earliest typewriters and teleprinters to modern video terminals and the
terminal emulators of today. Let&rsquo;s go!<h2>Early Terminals and Typewriters</h2><figure><img width=500 src=/typewriter.84227b29dbcbcea4.jpg alt="Olivetti Lettera 22 typewriter"><figcaption>This Olivetti Lettera 22 typewriter is not a terminal, but terminals were totally based on typewriters. The new line and carriage return lever is on the left. On the right, the paper roller and the paper release lever. The cursor is the horizontal line on the paper where the next character will be typed.</figcaption></figure><p>No, people did not use typewriters to interact with computers, but the first
terminals evolved from typewriters. In case you are not familiar with
typewriters (wink wink, kids) typewriters started as mechanical devices
introduces in the 1840s to type text on paper. They feature a keyboard for
input and paper for output. A cursor indicated the current text position,
moving as the user typed. Special components such as levers, handles, and keys
enabled cursor movement, space insertion, and paper scrolling.<p>Now with typewriters, communication was simple: type a letter, and it appears
on the paper, and later on, you would mail it to someone. After the invention
of telegraphs and telephones, people wanted to send messages faster. This need
prompted the development of teleprinters, which could send and receive messages
over telegraph and phone lines.<h3>Teletype Writers (TTYs)</h3><figure><img width=500 src=/Teletype_model_33_asr.80233f95c693bbe9.jpg alt="Teletype Model 33 ASR"><figcaption>The infamous low-cost all mechanical Model 33.</figcaption></figure><p>The Teletype Corporation trademarked the term &ldquo;teletype&rdquo; for its teleprinters
back in 1928. The name then became a synonym for all teleprinters especially in
the field of computers. The basic idea behind a teleprinter is simple, you
have two machines linked together by a wire or wirelessly. When you type a
letter on one machine, the other machine prints the same letter. This way, you
can send messages back and forth between the two machines. It&rsquo;s like a
typewriter, but instead of typing on paper, you&rsquo;re typing on a machine that
sends the message to another machine that prints it out to paper <sup id=fnref:1><a href=#fn:1 class=footnote-ref role=doc-noteref>1</a></sup>.<p>The Teletype Model 33, shown above, was one of the most popular teleprinters in
the 1960s and 1970s. It was a low-cost all-mechanical teleprinter that could
send and receive messages over phone lines. What made it special was that it
could understand the ASCII standard which made it compatible with computers.<p>Later on with the advent of interactive computing, people started connecting
ASCII aware teleprinters to computers. This way, they could type commands on
the teleprinter, and the computer would execute them and send the results back
to the teleprinter. This setup allowed users to interact with computers in real
time, enabling a wide range of applications and use cases.<p>Slowly, in the 1960s and 1970s, companies such as IBM, HP, and DEC started
experimenting with computer terminals. These terminals replaced the teleprinters
which significantly reduced paper waste and improved user experience. However,
many computer operators stuck with the teleprinters because they were more
affordable and familiar.<blockquote><p><strong>Fun Fact</strong>: The term &ldquo;TTY&rdquo; (short for TeleTYpe) is still used today to refer
to text-based terminals and terminal emulators in Unix-like operating systems.</blockquote><h2>Computer Terminals</h2><figure><img width=500 src=/DEC_VT100_terminal.309584cced5167e.jpg alt="DEC VT100 video terminal"><figcaption>VT100 was one of the first terminals to support ANSI escape codes and colors.</figcaption></figure><p>The introduction of computer terminals marked a significant advancement in
terminal and computing technology. A terminal is a device that allows users to
interact with computers, execute commands, and manage files and systems. Video
terminals come with a keyboard that is connected to electromechanical CRT
displays. They replaced teleprinters connected to computers. Above is the
legendary DEC VT100 released in 1978 was one of the first video terminals that
supported ANSI escape codes and colors.<p>Unlike mechanical teleprinters and typewriters, computer terminals needed a way
to communicate with computers in a backward-compatible fashion. Think about it,
how would you tell the computer to move the paper up? Or instruct it to move
the cursor to the next line? In a non-mechanical world, they needed a way to
send these commands from and to the computer. This is where ANSI standards and
escape codes came into play.<p>Because of the popularity, features, and adoption of the DEC VT100, and because
it adhered to many ANSI standards, it influenced many other terminals to adopt
the ANSI standards as well which later became the de facto standard for
terminal emulators.<blockquote><p><strong>Did you know?</strong> These standards are still relevant today, and they are the
reason why you can change the color of your terminal text, move the cursor
around, and clear the screen. The American Nation Standards Institute (ANSI),
formerly known as the American Standards Association (ASA), standardized the
ASCII character encoding which carried over to other standards such as X3.64,
ECMA-48, and ISO/IEC 6429 that are used today in terminal emulators.</blockquote><h2>Terminal Emulators</h2><p>As the name suggests, a terminal emulator is a software application that
&ldquo;emulates&rdquo; the functionality of a traditional computer terminal. Instead of
being a physical device connected to an external computer, a terminal emulator
is a software program that runs on a computer and provides a text-based
interface for interacting with the operating system.<p>Back in the day, computers were big, heavy, and expensive. They were usually
located in separate rooms, and users would interact with them using terminals
in a time-sharing fashion. With the advancement of personal computers and
operating systems, terminal emulators became popular as a way to interact with
the computer directly. It also provides users with a backward-compatible way to
run legacy applications and systems that were designed for the old days of
computers and traditional terminals.<h3>The Amazing XTerm</h3><figure><img width=500 src=/xterm-menus.af521a074c1bae30.gif alt="XTerm terminal"><figcaption>XTerm was one of the early modern terminal emulators.</figcaption></figure><p>Created in 1984, XTerm is a terminal emulator for the X Window System. It
started based on the DEC VT102 terminal specs and later incorporated features
from other DEC terminals like the VT220, VT320, VT420, VT520, and Tektronix 4014. Over time, XTerm introduced its own proprietary escape sequences, enabling
new features and enhanced functionality while adhering to most
ANSI, ECMA, and ISO standards.<p>As XTerm evolved and became more popular with new features, it influenced other
terminal emulators to adopt similar features and standards. This made XTerm a
kind of a standard when it comes to developing new terminal emulators. The
popular <a href=https://xtermjs.org/ target=_blank rel="noopener noreferrer">Xterm.js</a>, no pun intended, gets its name from
the original XTerm but has no affiliation with it.<p>Today, XTerm is still being updated and improved, making it one of the oldest
still maintained terminal emulator out there.<h3>Modern Terminal Emulators</h3><figure><picture><source type=image/webp srcset=/rioterm.f5cddc383d29ab7a.webp></picture><img width=600 src=/rioterm.a2cd97cf345db2af.png alt="Rio terminal"><figcaption>Yes, this is <a href=https://rioterm.com>Rio Terminal</a>.</figcaption></figure><p>Nowadays, terminal emulators have evolved significantly from their early
predecessors. Modern terminal emulators offer a wide range of features and
customization options, allowing users to tailor their terminal experience to
their needs and preferences. These tools provide a powerful interface for
interacting with operating systems, executing commands, and managing files and
systems efficiently.<p>Some notable terminal emulators used today include:<ul><li><a href=https://alacritty.org/ target=_blank rel="noopener noreferrer">Alacritty</a><li><a href=https://support.apple.com/guide/terminal/welcome/mac target=_blank rel="noopener noreferrer">Apple Terminal</a><li><a href=https://ghostty.org target=_blank rel="noopener noreferrer">Ghostty</a><li><a href=https://gitlab.gnome.org/GNOME/gnome-terminal target=_blank rel="noopener noreferrer">GNOME Terminal</a> and other <a href=https://gitlab.gnome.org/GNOME/vte target=_blank rel="noopener noreferrer">VTE based terminals</a><li><a href=https://iterm2.com/ target=_blank rel="noopener noreferrer">iTerm2</a><li><a href=https://sw.kovidgoyal.net/kitty/ target=_blank rel="noopener noreferrer">KiTTY</a><li><a href=https://konsole.kde.org target=_blank rel="noopener noreferrer">Konsole</a><li><a href=https://en.wikipedia.org/wiki/Linux_console target=_blank rel="noopener noreferrer">Linux console</a><li><a href=https://www.putty.org target=_blank rel="noopener noreferrer">PuTTY</a><li><a href=https://rioterm.com target=_blank rel="noopener noreferrer">Rio Terminal</a><li><a href=https://rxvt.sourceforge.net target=_blank rel="noopener noreferrer">Rxvt</a><li><a href=https://st.suckless.org target=_blank rel="noopener noreferrer">St</a> (a.k.a. simple terminal)<li><a href=https://learn.microsoft.com/en-us/windows/terminal/ target=_blank rel="noopener noreferrer">Windows Terminal</a><li><a href=https://xtermjs.org target=_blank rel="noopener noreferrer">Xterm.js</a> (not affiliated with XTerm)<li><a href=https://invisible-island.net/xterm/ target=_blank rel="noopener noreferrer">XTerm</a></ul><h2>’Till Next Time</h2><p>Terminal emulators have come a long way since the early days of typewriters and
teleprinters. It&rsquo;s fascinating to see how these tools have evolved over time,
adapting to new technologies and user needs. Stay tuned for more posts on
terminal emulators and escape sequences, where we&rsquo;ll explore these topics in
greater detail.<h2>Whatcha think?</h2><p>Have some feedback on this post? We’d love to hear. Let us know in
<a href=https://charm.sh/chat>Discord</a> or via email at
<a href=mailto:vt100@charm.sh>vt100@charm.sh</a>.<div class=footnotes role=doc-endnotes><hr><ol><li id=fn:1><p><a href=https://www.howtogeek.com/727213/what-are-teletypes-and-why-were-they-used-with-computers/ target=_blank rel="noopener noreferrer">https://www.howtogeek.com/727213/what-are-teletypes-and-why-were-they-used-with-computers/</a>&#160;<a href=#fnref:1 class=footnote-backref role=doc-backlink>&#8617;&#xfe0e;</a></ol></div>]]></content:encoded>
      <author>Ayman Bagabas</author>
      <guid>A Brief History of Terminal Emulators</guid>
      <pubDate>Tue, 11 Mar 2025 00:00:00 +0000</pubDate>
      <source>https://charm.land/blog/intro-to-terminals/</source>
    </item>
    <item>
      <title>Leveling up Cryptography at Charm</title>
      <link>https://charm.land/blog/geomys/</link>
      <description>Two cryptographers walk into a bar. Nobody else has a clue what they&#39;re talking about.</description>
      <content:encoded><![CDATA[<h1>Leveling up Cryptography at Charm</h1><p>By Bashbunni on 11 July 2024<figure><picture><img src=https://stuff.charm.sh/blog/geomys/office-filippo.png alt="filippo held hostage in charm office" type=image width=800></picture><figcaption>Filippo trapped at Charm HQ</figcaption></figure><h2>Supporting open source</h2><p>If you&rsquo;re familiar with Charm tools, you&rsquo;ll know we <em>love</em> SSH.
<a href=https://github.com/filosottile target=_blank rel="noopener noreferrer">Filippo</a> casually maintains the cryptography packages that ship as
part of the Go standard library. This includes our beloved
<a href=https://pkg.go.dev/golang.org/x/crypto/ssh target=_blank rel="noopener noreferrer"><code>golang.org/x/crypto/ssh</code></a>, <a href=https://pkg.go.dev/crypto/ed25519 target=_blank rel="noopener noreferrer"><code>crypto/ed25519</code></a>, and <a href=http://age-encryption.org/ target=_blank rel="noopener noreferrer"><code>age</code></a> packages.
Insanely impressive!<p>We&rsquo;re thrilled to support his work on these open source packages that are so
critical for us.<h2>Becoming a full-time open source maintainer…at scale</h2><blockquote><p><em>Instant noodles not required</em></blockquote><p>Filippo is revolutionizing what it means to be an open source maintainer by
finding creative ways to make this line of work sustainable…and it&rsquo;s working!<p>So well, in fact, that he&rsquo;s expanding his operation into
a firm of full-time independent open source maintainers, known collectively as
<a href=https://words.filippo.io/dispatches/geomys/ target=_blank rel="noopener noreferrer">Geomys</a>. First on the roster are <a href=https://github.com/drakkan target=_blank rel="noopener noreferrer">Nicola Murino</a>,
who is the dedicated maintainer for <a href=https://pkg.go.dev/golang.org/x/crypto/ssh target=_blank rel="noopener noreferrer"><code>golang.org/x/crypto/ssh</code></a>, and
<a href=https://honnef.co/ target=_blank rel="noopener noreferrer">Dominik Honnef</a> who maintains <a href=https://staticcheck.io/ target=_blank rel="noopener noreferrer"><code>staticcheck</code></a> and
<a href=https://gotraceui.dev/ target=_blank rel="noopener noreferrer"><code>gotraceui</code></a>.<p>We&rsquo;re proud and honored to support him and Geomys on their journey while they
support our team with their vast knowledge of cryptography and Go expertise. In
working with Geomys, we are maximizing our potential by connecting with the
maintainers of tools we depend on and love.<p>You can hear the entire story in his own words over on his <a href=https://words.filippo.io/dispatches/geomys/ target=_blank rel="noopener noreferrer">blog</a>.<h2>Encrypted files with SSH keys?!</h2><figure><picture><img src=https://github.com/FiloSottile/age/raw/main/logo/logo_white.svg alt="age logo" type=image width=800></picture><figcaption>Pronounced with a hard G, like chicken karaage.</figcaption></figure><pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>$ age -R ~/.ssh/id_ed25519.pub example.jpg &gt; example.jpg.age
</span></span><span style=display:flex;><span style=line-height:1.4em;>$ age -d -i ~/.ssh/id_ed25519 example.jpg.age &gt; example.jpg
</span></span></code></pre><p>Behind the scenes, we&rsquo;ve been honing Charm&rsquo;s encryption tooling. Naturally,
this brought us to <a href=http://age-encryption.org/ target=_blank rel="noopener noreferrer"><code>age</code></a>, a file encryption tool, format, and Go
library built by <a href=https://github.com/filosottile target=_blank rel="noopener noreferrer">@FiloSottile</a> and friends.<h3>SSH</h3><p>We love finding creative ways to use the SSH protocol (see <a href=https://github.com/charmbracelet/wish target=_blank rel="noopener noreferrer">wish</a>,
<a href=https://github.com/charmbracelet/melt target=_blank rel="noopener noreferrer">melt</a>, <a href=https://github.com/charmbracelet/wishlist target=_blank rel="noopener noreferrer">wishlist</a>). This is why age really stands out to us.
It supports encrypting files to SSH public keys (both <code>ssh-rsa</code> and
<code>ssh-ed25519</code>) which can then be decrypted with their corresponding private
keys. Hello?! That is <em>so</em> cool. We&rsquo;re all totally geeking out over here.<p>This also means it&rsquo;s suddenly convenient to encrypt documents for non-GPG users,
for example, after yoinking SSH public keys from a GitHub profile like so.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>$ curl https://github.com/benjojo.keys <span style=color:#e8e8a8>|</span> age -R - example.jpg &gt; example.jpg.age
</span></span></code></pre><h3>File encryption beyond GPG</h3><p>When people think file encryption, GnuPG is typically what comes to mind. Given
this, let&rsquo;s compare GnuPG and age. There was a discussion about it on
<a href=https://github.com/FiloSottile/age/discussions/432 target=_blank rel="noopener noreferrer">GitHub</a>, but I&rsquo;ll give you the summary.<p>Age makes it easy to encrypt using common best practices as these are defined by
the age developers as defaults. GnuPG requires the user to be more aware of
these best practices to get the right results with the tool. If you don&rsquo;t know
much about which encryption protocols to use depending on the context, no worries,
age gives you training wheels so you can&rsquo;t fall off the cryptographic bike.<p>We&rsquo;ll leave it to you to decide for yourself if you&rsquo;re ready to be an <a href=http://age-encryption.org/ target=_blank rel="noopener noreferrer">age</a> superfan.<h2>Additional reading (for the nerds)</h2><ul><li><a href=https://words.filippo.io/ target=_blank rel="noopener noreferrer">Filippo&rsquo;s Blog</a><li><a href=https://github.com/filosottile target=_blank rel="noopener noreferrer">Filippo&rsquo;s GitHub</a></ul><h2>Whatcha think?</h2><p>Have some feedback on this post? We’d love to hear. Let us know in
<a href=https://charm.sh/chat>Discord</a> or via email at
<a href=mailto:vt100@charm.sh>vt100@charm.sh</a>.]]></content:encoded>
      <author>Bashbunni</author>
      <guid>Leveling up Cryptography at Charm</guid>
      <pubDate>Thu, 11 Jul 2024 00:00:00 +0000</pubDate>
      <source>https://charm.land/blog/geomys/</source>
    </item>
    <item>
      <title>A coffee shop for your terminal</title>
      <link>https://charm.land/blog/terminaldotshop/</link>
      <description>coffee acquired @ ssh terminal.shop</description>
      <content:encoded><![CDATA[<h1>A coffee shop for your terminal</h1><p>By Bashbunni on 6 June 2024<p>The terminal got a whole &rsquo;lotta love recently when a handful of developers, with a combined following of over 1 million, dropped a new coffee line available for purchase <em>exclusively</em> from the terminal using the Charm stack. That&rsquo;s right, they built an e-commerce app over SSH to allow developers to refill their coffee IVs right from the comfort of the command line. Talk about a modern day luxury.<figure><picture><img src=https://stuff.charm.sh/blog/terminaldotshop/streaming-the-process.jpeg alt="ThePrimeagen and TJ DeVries building with Charm on a livestream to thousands of viewers, no pressure" type=image width=800></picture><figcaption>ThePrimeagen and TJ DeVries building with Charm on a live stream to thousands of viewers, no pressure</figcaption></figure><p>The project itself is terminal.shop, which leverages Charm tooling at every level to not only sell coffee, but to absolutely demolish their inventory. The coffee company managed to sell out within days of becoming available.<figure><picture><img src=https://stuff.charm.sh/blog/terminaldotshop/sold-out.jpeg alt="A terminal coffee shop with no coffee, restocking soon!" type=image width=800></picture><figcaption>A terminal coffee shop with no coffee, restocking soon!</figcaption></figure><p>The product first launched at one of the biggest tech conferences in the world, React Miami, which brought a lot of web developers over to the command line. Their reach has been impactful, boasting about 5 million impressions on Twitter surrounding terminal.shop and Charm as a result.<figure><picture><img src=https://stuff.charm.sh/blog/terminaldotshop/5m-impressions.png alt="A tweet about the team taking over React Miami" type=image width=800></picture><figcaption>A tweet about the team taking over React Miami</figcaption></figure><p>Behind the scenes, the team worked to build a TUI for their users with Bubble Tea while Lip Gloss swooped in to provide some lovely styles that came to resemble exactly what they had planned in their Figma designs. Wish allowed them to ship this app to users through a secure connection without the hassle of managing SSL certificates.<figure><picture><img src=https://stuff.charm.sh/blog/terminaldotshop/adam-loves-charm.png alt="Adamdotdev falling in love with Charm" type=image width=800></picture><figcaption>Adamdotdev falling in love with Charm</figcaption></figure><p>It was a pleasure (and a curse) to see them build the whole thing live on stream. Thousands of developers were tuned in to watch Prime and TJ put together this command line app, no pressure or anything. The stream was spent hacking away with charm libraries guiding them towards a beautiful end result.<figure><picture><img src=https://stuff.charm.sh/blog/terminaldotshop/say1.png alt="First day of them building in public with Charm, crushing it!" type=image width=800></picture><figcaption>First day of them building in public with Charm, crushing it!</figcaption></figure><figure><picture><img src=https://stuff.charm.sh/blog/terminaldotshop/day2.png alt="Second day of them building in public with Charm, highly caffeinated." type=image width=800></picture><figcaption>Second day of them building in public with Charm, highly caffeinated.</figcaption></figure><p>We spoke to the team after and found that the toughest part of working with the Charm tools was becoming familiar with the functional design of Bubble Tea. Given the scale of this project, we were pleasantly surprised to hear that was their biggest obstacle. They seem to be happy continuing to hack away on more command line projects. We&rsquo;ve converted even the web developers to write some Go, just don&rsquo;t ask about their backend&mldr; 💀<p>Thank you to the incredible terminal.shop team. Please continue to keep inspiring us all!<ul><li><a href=https://twitter.com/@adamdotdev target=_blank rel="noopener noreferrer">@adamdotdev</a><li><a href=https://twitter.com/@thdxr target=_blank rel="noopener noreferrer">@thdxr</a><li><a href=https://twitter.com/@ThePrimeagen target=_blank rel="noopener noreferrer">@ThePrimeagen</a><li><a href=https://twitter.com/@teej_dv target=_blank rel="noopener noreferrer">@teej_dv</a><li><a href=https://twitter.com/@iamdavidhill target=_blank rel="noopener noreferrer">@iamdavidhill</a></ul><figure><picture><img src=https://stuff.charm.sh/blog/terminaldotshop/styling-clis.png alt="Adam working through the learning curve" type=image width=800></picture><figcaption>Adam working through the learning curve</figcaption></figure><h2>Try it for yourself:</h2><ul><li><a href=https://github.com/charmbracelet/wish target=_blank rel="noopener noreferrer">DIY SSH server (Wish)</a><li><a href=https://github.com/charmbracelet/lipgloss target=_blank rel="noopener noreferrer">Styling and layouts (Lip Gloss)</a><li><a href=https://github.com/charmbracelet/bubbletea target=_blank rel="noopener noreferrer">Interactive TUIs (Bubble Tea)</a><li><a href=https://github.com/charmbracelet/huh target=_blank rel="noopener noreferrer">Quick forms (Huh)</a><li><a href=https://github.com/charmbracelet/bubbles target=_blank rel="noopener noreferrer">Reusable components for Bubble Tea (Bubbles)</a></ul><h2>Whatcha think?</h2><p>Have some feedback on this post? We’d love to hear. Let us know in
<a href=https://charm.sh/chat>Discord</a> or via email at
<a href=mailto:vt100@charm.sh>vt100@charm.sh</a>.]]></content:encoded>
      <author>Bashbunni</author>
      <guid>A coffee shop for your terminal</guid>
      <pubDate>Thu, 06 Jun 2024 00:00:00 +0000</pubDate>
      <source>https://charm.land/blog/terminaldotshop/</source>
    </item>
    <item>
      <title>Identifying Trends in Your Repos’ Issues…with AI!</title>
      <link>https://charm.land/blog/gh-mods-pop/</link>
      <description>Get a summary of your project&#39;s issues sent to your inbox with Mods and Pop</description>
      <content:encoded><![CDATA[<h1>Identifying Trends in Your Repos’ Issues…with AI!</h1><p>By Bashbunni on 2 May 2024<p>It&rsquo;s a lot of work trying to keep up with what&rsquo;s happening in your code repos,
let alone deciding what to focus on, but thankfully here&rsquo;s where AI can come in
handy. I don&rsquo;t think AI can actually do your job for you, but it provides
excellent base content to work from, hence why AI is the perfect partner for
this type of problem.<p>So you can access your GitHub issues with the command line and you have an AI
chatbot that also uses a CLI. Why not write a little script that can summarize
key trends in issues, both for bug reports and enhancement requests? That can
help us identify what to focus on. Better yet, let&rsquo;s automatically get those
summaries via email to help us stay on top of things!<figure><picture><img src=https://stuff.charm.sh/blog/gh-mods-pop/example-email.webp alt="An example of the kind of email you will receive from the script. Includes a summary of bug reports and enhancements for the repo" type=image></picture><figcaption>We’re gonna generate something like this and save you from meetings, one email at a time.</figcaption></figure><h2>Required doodads</h2><p>You&rsquo;ll need to have <a href=pop-install>Pop</a> and <a href=mods-install>Mods</a> installed to
use the provided script. Both of these programs also require environment
variables to be set. You can define these variables in the script, as
you&rsquo;ll see in the example below.<p>Alternatively, you can set your environment variables in your
<code>.bashrc</code>/<code>.zshrc</code>, then source the file with a good &lsquo;ol <code>source ~/.zshrc</code> in
your existing terminal session - note that any new sessions will use the
updated <code>rc</code> file anyway, so don&rsquo;t worry about sourcing that file for new
sessions.<p>Because my dotfiles are public, I like to use <a href=https://github.com/charmbracelet/skate target=_blank rel="noopener noreferrer">Skate</a> to protect my API
keys, so I don&rsquo;t accidentally leak any secrets. Here&rsquo;s how the two options
compare:<p><code>export OPENAI_API_KEY="&lt;private key>"</code> vs <code>export OPENAI_API_KEY=$(skate get openai-key)</code><h2>Under the hood</h2><h3>Mods</h3><a class="cart mods" data-repo=charmbracelet/mods href=https://github.com/charmbracelet/mods target=_blank><div class=info><picture><source srcset=/mods.cfc9c669ca030d19.webp type=image/webp><img src=/mods.5b7f085bf69aef1d.png alt="Mods mascot"></picture><h3 class=puffy><span data-text=Mods>Mods</span></h3><ul class=badges><li><span>Built Itself</span><span>Yep</span></ul><p>AI on the command line.</div><div class=art><video loop muted playsinline>
<source src=/mods.5ca3d58a9edc9b3d.webm type=video/webm><source src=/mods.5050b5bf6f3b2d1c.mp4 type=video/mp4></video><div></div></div></a><p>Mods is a command line interface for interacting with AI chatbots, meaning,
it&rsquo;s entirely script-friendly. It works with OpenAI-compatible endpoints and
LocalAI models. Mods uses GPT-4 by default and supports LocalAI models running
on port 8080. It falls back to using GPT-3.5 Turbo if GPT-4 isn&rsquo;t available. If
you wanna learn more, you can <a href=https://github.com/charmbracelet/mods target=_blank rel="noopener noreferrer">read all about it</a>.<p>For this script to work, you&rsquo;ll need an <code>OPENAI_API_KEY</code> environment variable
set. <a href=https://platform.openai.com/account/api-keys target=_blank rel="noopener noreferrer">Get your key</a>.<h3>Pop</h3><a class="cart pop" data-repo=charmbracelet/pop href=https://github.com/charmbracelet/pop target=_blank><div class=info><picture><source srcset=/po.91ccca7d84917a24.webp type=image/webp><img src=/po.ca6ed0d5b0a01305.png alt="Pop mascot"></picture><h3 class=puffy><span data-text=Pop>Pop</span></h3><ul class=badges><li><span>Email</span><span>Sent</span></ul><p>Send emails from your terminal</div><div class=art><video loop muted playsinline>
<source src=/pop.f815d1534b9933cc.webm type=video/webm><source src=/po.8f7515407323d352.mp4 type=video/mp4></video><div></div></div></a><p>Pop is a terminal-based program, offered as both a TUI and CLI, that allows you
to send emails from your terminal. All emails send through <a href=https://resend.com target=_blank rel="noopener noreferrer">Resend</a>
APIs or you can use a <a href="https://github.com/charmbracelet/pop?tab=readme-ov-file#smtp-configuration" target=_blank rel="noopener noreferrer">custom SMTP setup</a>.<p>To use Pop with Resend you&rsquo;ll need to export a <code>RESEND_API_KEY</code>, you can also
set <code>RESEND_FROM</code> to avoid having to type in your sending email. If you run
into any difficulties using your own domain with resend, you can always have it
send from <code>onboarding@resend.dev</code>.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#ff8ec7>export</span> RESEND_API_KEY<span style=color:#ef8080>=</span><span style=color:#0af>$(</span>pass RESEND_API_KEY<span style=color:#0af>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#ff8ec7>export</span> RESEND_FROM<span style=color:#ef8080>=</span>pop@charm.sh
</span></span></code></pre><p>Alternatively, you can use a <a href="https://github.com/charmbracelet/pop?tab=readme-ov-file#smtp-configuration" target=_blank rel="noopener noreferrer">custom SMTP configuration</a> with Pop.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#ff8ec7>export</span> POP_SMTP_HOST<span style=color:#ef8080>=</span>smtp.gmail.com
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#ff8ec7>export</span> POP_SMTP_PORT<span style=color:#ef8080>=</span><span style=color:#6eefc0>587</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#ff8ec7>export</span> POP_SMTP_USERNAME<span style=color:#ef8080>=</span>pop@charm.sh
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#ff8ec7>export</span> POP_SMTP_PASSWORD<span style=color:#ef8080>=</span>hunter2
</span></span></code></pre><p>For this script to work, you&rsquo;ll need the <code>RESEND_API_KEY</code> environment variable
or custom SMTP server configurations set. <a href=https://resend.com/api-keys target=_blank rel="noopener noreferrer">Get your key</a>.<h2>Putting it all together</h2><h3>Writing the script</h3><p>Alright, now that you know how the whole thing works, let&rsquo;s do something useful.<ol><li>Set up the <code>OPENAI_API_KEY</code> environment variable.<li>Set up the <code>RESEND_API_KEY</code> environment variable (or configure your SMTP setup).<li>Run the following script and provide the path to a local git repository as an argument</ol><pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#ff875f>#!/bin/sh
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#ff875f></span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#ff8ec7>export</span> RESEND_API_KEY<span style=color:#ef8080>=</span><span style=color:#0af>$(</span>skate get resend<span style=color:#0af>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#ff8ec7>export</span> OPENAI_API_KEY<span style=color:#ef8080>=</span><span style=color:#0af>$(</span>skate get open-ai<span style=color:#0af>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># fail if no repo provided</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>if</span> <span style=color:#ef8080>[</span> $# -ne <span style=color:#6eefc0>1</span> <span style=color:#ef8080>]</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>then</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>    <span style=color:#ff8ec7>printf</span> <span style=color:#c69669>&#34;Please provide an absolute path to a local code repository with a GitHub remote upstream.&#34;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>    <span style=color:#ff8ec7>exit</span> <span style=color:#6eefc0>1</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>else</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>    <span style=color:#ff8ec7>cd</span> $1
</span></span><span style=display:flex;><span style=line-height:1.4em;>    <span style=color:#ef8080>(</span> gh issue list <span style=color:#e8e8a8>|</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>    mods <span style=color:#c69669>&#34;what are recurring themes in bug reports given this output&#34;</span> <span style=color:#ef8080>&amp;&amp;</span> gh issue list <span style=color:#e8e8a8>|</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>    mods <span style=color:#c69669>&#34;what are recurring themes in enhancements given this output&#34;</span> <span style=color:#ef8080>)</span> <span style=color:#e8e8a8>|</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>    pop <span style=color:#afffd7>\
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#afffd7></span>      --from onboarding@resend.dev <span style=color:#afffd7>\
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#afffd7></span>      --to youremail@resend.dev <span style=color:#afffd7>\
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#afffd7></span>      --subject <span style=color:#0af>$(</span>basename $PWD<span style=color:#0af>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>fi</span>
</span></span></code></pre><p>(PSSSST! <code>basename $PWD</code> provides the name of the project whose issues are
being summarized)<p>If you wanted these summaries to recur on a weekly basis (or whatever other
regular interval you&rsquo;d like) you can choose to activate this script in a cron job.<h3>Scheduling the script</h3><h4>Cron?!</h4><p>Cron jobs are available on Unix-based systems and on Windows through a
Unix-based subsystem like WSL (Windows Subsystem for Linux). If you&rsquo;re on a
Windows machine, you can also use Windows Task Scheduler to automate tasks, but
that is beyond the scope of this post.<blockquote><p>Cron is a daemon to execute scheduled commands. By default, it runs in the
user&rsquo;s home directory.</blockquote><p>Crontabs are files that are used to schedule the execution of programs. These
files have all the instructions that you might need for the cron job to run.
Another, very popular alternative, is Systemd timer that also allows you to
schedule tasks. Cron is the more popular option, but can be hard to
troubleshoot by comparison to Systemd timers. If you&rsquo;d like to learn more, you
can do so
<a href=https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html target=_blank rel="noopener noreferrer">here</a><h4>Configuring cron</h4><p>Run <code>crontab -e</code> to create a new <a href=https://crontab.guru/ target=_blank rel="noopener noreferrer">crontab</a> (use <code>man crontab</code> or <code>tldr crontab</code>).
The first iteration might look something like this:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>PATH<span style=color:#ef8080>=</span>/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/bashbunni/go/bin:/usr/local/go:/home/bashbunni/go
</span></span><span style=display:flex;><span style=line-height:1.4em;>GOPATH<span style=color:#ef8080>=</span>$HOME/go
</span></span><span style=display:flex;><span style=line-height:1.4em;>*/1 * * * * /home/bashbunni/scripts/repo-summary /home/bashbunni/charm/bubbletea &gt;&gt; /home/bashbunni/scripts/repo-summary.log 2&gt;<span style=color:#e8e8a8>&amp;</span><span style=color:#6eefc0>1</span>
</span></span></code></pre><p>Alas, there&rsquo;s still room to improve. Try to spot the differences!<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>PATH<span style=color:#ef8080>=</span>/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/bashbunni/go/bin:/usr/local/go:/home/bashbunni/go
</span></span><span style=display:flex;><span style=line-height:1.4em;>GOPATH<span style=color:#ef8080>=</span>$HOME/go
</span></span><span style=display:flex;><span style=line-height:1.4em;>*/1 * * * * repo-summary $HOME/charm/bubbletea &gt;&gt; /var/log/repo-summary/bubbletea.log 2&gt;<span style=color:#e8e8a8>&amp;</span><span style=color:#6eefc0>1</span>
</span></span></code></pre><p>To clean up the script, you have to run <code>chmod +x repo-summary</code>, and move the
executable to <code>/usr/bin</code> which allows the script to be executable from anywhere.
You&rsquo;ll also want to move logging to <code>/var/log</code> which is typically where programs
post logs on Linux. If you&rsquo;re not using Linux, you should change this to
whatever is idiomatic with your operating system. You need to change the
permissions on the <code>/var/log/repo-summary</code> directory with <code>sudo chmod ugo+w /var/log/repo-summary</code> so that the cron job has write access to the file.<p>That&rsquo;s a fair amount of steps, so here&rsquo;s that breakdown as a list:<ol><li><code>chmod +x repo-summary</code><li><code>sudo mv repo-summary /usr/bin/repo-summary</code><li><code>sudo mkdir /var/log/repo-summary</code><li><code>sudo chmod ugo+w /var/log/repo-summary</code></ol><p>You need sudo for some of these commands given that they&rsquo;re in protected
directories.<h2>Overengineering with containers?!</h2><p>After spending a bunch of time getting this working in Docker, it turns out
it&rsquo;s not worth containerizing given that it depends on a git repo to work. I
included this section anyway because I&rsquo;m sure at least one developer will look
at this and say &ldquo;I wonder if I could containerize it&rdquo;&mldr; Just like I did.<h3>Pros of a container:</h3><ul><li>User of the script gets a &ldquo;plug-and-play&rdquo; experience.<li>Export env variables from host to container.<li>Centralize dependency management.</ul><h3>How to containerize it</h3><p>The deciding factor is that you need a local git repo to use with the script,
due to depending on the GitHub CLI results for mods to work. This gets
complicated when you containerize it. If you are curious to see what the
Dockerfile might look like, I&rsquo;ve included it to indulge your curiosities.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>FROM</span><span style=color:#c69669> golang:1.21-alpine</span><span style=color:#f1f1f1;background-color:#f05b5b>
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#f1f1f1;background-color:#f05b5b>
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#f1f1f1;background-color:#f05b5b></span><span style=color:#676767># TODO Copy your own version of the script to run</span><span style=color:#f1f1f1;background-color:#f05b5b>
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#f1f1f1;background-color:#f05b5b></span><span style=color:#676767># Don&#39;t forget, the file has to be in the same directory as the Dockerfile</span><span style=color:#f1f1f1;background-color:#f05b5b>
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#f1f1f1;background-color:#f05b5b></span><span style=color:#0af>COPY</span> repo-summary /usr/bin/repo-summary<span style=color:#f1f1f1;background-color:#f05b5b>
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#f1f1f1;background-color:#f05b5b></span><span style=color:#0af>RUN</span> chmod +x /usr/bin/repo-summary<span style=color:#f1f1f1;background-color:#f05b5b>
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#f1f1f1;background-color:#f05b5b>
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#f1f1f1;background-color:#f05b5b></span><span style=color:#676767># INSTALL DEPENDENCIES</span><span style=color:#f1f1f1;background-color:#f05b5b>
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#f1f1f1;background-color:#f05b5b></span><span style=color:#0af>ENV</span> PATH<span style=color:#ef8080>=</span><span style=color:#c69669>&#34;/usr/local/go/bin:</span><span style=color:#c69669>${</span>PATH<span style=color:#c69669>}</span><span style=color:#c69669>&#34;</span><span style=color:#f1f1f1;background-color:#f05b5b>
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#f1f1f1;background-color:#f05b5b>
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#f1f1f1;background-color:#f05b5b></span><span style=color:#676767># Go apps</span><span style=color:#f1f1f1;background-color:#f05b5b>
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#f1f1f1;background-color:#f05b5b></span><span style=color:#0af>RUN</span> go install github.com/charmbracelet/pop@latest<span style=color:#f1f1f1;background-color:#f05b5b>
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#f1f1f1;background-color:#f05b5b></span><span style=color:#0af>RUN</span> go install github.com/charmbracelet/mods@latest<span style=color:#f1f1f1;background-color:#f05b5b>
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#f1f1f1;background-color:#f05b5b>
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#f1f1f1;background-color:#f05b5b></span><span style=color:#676767># gh cli</span><span style=color:#f1f1f1;background-color:#f05b5b>
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#f1f1f1;background-color:#f05b5b></span><span style=color:#676767># If mods and pop were available through apk, we could have just used an alpine</span><span style=color:#f1f1f1;background-color:#f05b5b>
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#f1f1f1;background-color:#f05b5b></span><span style=color:#676767># container instead of using the larger, go-alpine container.</span><span style=color:#f1f1f1;background-color:#f05b5b>
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#f1f1f1;background-color:#f05b5b></span><span style=color:#0af>RUN</span> apk add github-cli<span style=color:#f1f1f1;background-color:#f05b5b>
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#f1f1f1;background-color:#f05b5b></span><span style=color:#0af>RUN</span> rm /var/cache/apk/*<span style=color:#f1f1f1;background-color:#f05b5b>
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#f1f1f1;background-color:#f05b5b>
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#f1f1f1;background-color:#f05b5b></span><span style=color:#0af>WORKDIR</span><span style=color:#c69669> /home/bubbletea</span><span style=color:#f1f1f1;background-color:#f05b5b>
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#f1f1f1;background-color:#f05b5b>
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#f1f1f1;background-color:#f05b5b></span><span style=color:#0af>CMD</span> repo-summary<span style=color:#f1f1f1;background-color:#f05b5b>
</span></span></span></code></pre><p>Then you can run it with<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>docker run -it -v <span style=color:#0af>$(</span><span style=color:#ff8ec7>pwd</span><span style=color:#0af>)</span>:/home/bubbletea -e
</span></span><span style=display:flex;><span style=line-height:1.4em;>GH_TOKEN<span style=color:#ef8080>=</span>$GH_TOKEN -e RESEND_API_KEY<span style=color:#ef8080>=</span><span style=color:#0af>$(</span>skate get resend<span style=color:#0af>)</span> -e
</span></span><span style=display:flex;><span style=line-height:1.4em;>OPENAI_API_KEY<span style=color:#ef8080>=</span><span style=color:#0af>$(</span>skate get open-ai<span style=color:#0af>)</span> &lt;image-id&gt; repo-summary ./
</span></span></code></pre><p>Okay, okay, fine I&rsquo;ll break down what that command does. You have to run
<code>docker build --tag 'bubbletea-summary' .</code> in the same directory as your
Dockerfile to create your custom image. Run the image as a container and
start an interactive terminal session in that container with <code>-it</code>. From there,
mount the desired repo as a volume (matches the <code>WORKDIR</code> specified in the
Dockerfile) with <code>-v</code>. Then pass along your environment variables from the host
machine to the container with <code>-e</code>. It&rsquo;s important to provide the image ID,
which you can see with <code>docker images</code>. It will run the <code>repo-summary</code> script
in the <code>WORKDIR</code> of the container.<p>Notice that you need to define the environment variables needed by the
container as arguments to the <code>docker run</code> command. If you were to specify
these environment variables in the Dockerfile, then they would only be
accessible at build time, not during the lifespan of the container. (Thank you,
Ayman!)<p>If you want to challenge yourself, you can try to set up a cron job in the
docker container given the information provided. Let us know how you do!<h2>Whatcha think?</h2><p>Have some feedback on this post? We’d love to hear. Let us know in
<a href=https://charm.sh/chat>Discord</a> or via email at
<a href=mailto:vt100@charm.sh>vt100@charm.sh</a>.]]></content:encoded>
      <author>Bashbunni</author>
      <guid>Identifying Trends in Your Repos’ Issues…with AI!</guid>
      <pubDate>Thu, 02 May 2024 00:00:00 +0000</pubDate>
      <source>https://charm.land/blog/gh-mods-pop/</source>
    </item>
    <item>
      <title>This is How We Do It</title>
      <link>https://charm.land/blog/100k/</link>
      <description>How we build popular open source software</description>
      <content:encoded><![CDATA[<h1>This is How We Do It</h1><p>The Charm guide to building popular open source software<p>By Christian Rocha on 6 March 2024<figure><video controls playsinline poster=/100k-00514.9a42a09076fa6b29.jpg>
<source src=https://stuff.charm.sh/site/charm-100k.mp4></video><figcaption>If purple smoke and scratched up gold don't say success, I don’t know what does.</figcaption></figure><p>GitHub stars are a great indication of sentiment and adoption in open
source. On that note, we’re thrilled to announce that we&rsquo;ve received our
100,000th star. We could not be more proud and we have you, the open source
community, to thank.<p>Here&rsquo;s are some stats on how our ⭐️100k breaks down:<ul><li>We&rsquo;ve had 16 major releases with an average of ⭐️6.3k per release<li>Every major release has over ⭐️1k<li>Our open source projects are in use at companies like GitHub, Nvidia, AWS, Microsoft, Shopify, any many others<li>2022 yielded our highest star growth at ⭐️37k (137% yoy) thanks to the ardent response to <a href=https://github.com/charmbracelet/gum target=_blank rel="noopener noreferrer">Gum</a> and <a href=https://github.com/charmbracelet/vhs target=_blank rel="noopener noreferrer">VHS</a><li>4 of our projects (<a href=https://github.com/charmbracelet/glow target=_blank rel="noopener noreferrer">Glow</a>, <a href=https://github.com/charmbracelet/gum target=_blank rel="noopener noreferrer">Gum</a>, <a href=https://github.com/charmbracelet/vhs target=_blank rel="noopener noreferrer">VHS</a>, and <a href=https://github.com/charmbracelet/bubbletea target=_blank rel="noopener noreferrer">Bubble Tea</a>) have over ⭐️10k<li><a href=https://github.com/charmbracelet/bubbletea target=_blank rel="noopener noreferrer">Bubble Tea</a>, our most-starred repo, clocks in at ⭐️23k<li><a href=https://github.com/charmbracelet/bubbletea target=_blank rel="noopener noreferrer">Bubble Tea</a> is also our fastest growing repo, though it regained the lead only after <a href=https://github.com/charmbracelet/gum target=_blank rel="noopener noreferrer">Gum</a>’s electrical-storm-like growth cooled down</ul><p>How did we do it? Here’s the Charm open source playbook based on our collective
learnings over the past four years.<hr><aside><p>Wondering how we keep an eye on our repos’ ⭐️s? We have a
<a href=https://charm.sh/stars/ target=_blank>secret web page</a> as well
as a super secret CLI command: <code>curl https://stuff.charm.sh/stars</code>.</aside><hr><h2>Starting with a Problem</h2><p>We always start with a problem of our own. This typically means a few key
factors are true:<ul><li>We have firsthand knowledge of the problem and are thus well suited to solve it.<li>We are passionate about the problem. It’s a pain point we know personally and something we’re excited to solve for.<li>We have researched the problem and have found no worthy solutions, so our offering will be at least somewhat unique.</ul><p><a href=https://github.com/charmbracelet/bubbletea target=_blank rel="noopener noreferrer">Bubble Tea</a>, our text-based user interface framework, is a great example
of this. We wanted to be able to build ephemeral, user interfaces inline in the
terminal in go without having to concern ourselves with rendering, similar to
the experience we had building UIs in the browser with <a href=https://elm-lang.org/ target=_blank rel="noopener noreferrer">Elm</a>. After a lot
of research we concluded that there was nothing in the go ecosystem that met
our needs so, out of pure necessity, we began work on what would become bubble
tea.<p>Bubble Tea is now our most popular project with ⭐️23k.<p><a href=https://github.com/charmbracelet/vhs target=_blank rel="noopener noreferrer">VHS</a>, our terminal session recorder, is another great example. After
manually recording an enormous number of GIFs of terminal sessions we knew
there had to be a better way. While other good terminal recording
solutions existed, we found nothing scriptable, and nothing that produced
GIFs. Before we knew it we had a full blown scripting language (complete with
<a href=https://github.com/charmbracelet/tree-sitter-vhs target=_blank rel="noopener noreferrer">a tree sitter grammar</a>) for
producing GIFs of terminal sessions.<p>VHS weighs in over ⭐️13k.<ul class=carts><li><a class="cart bubbletea" data-repo=charmbracelet/bubbletea href=https://github.com/charmbracelet/bubbletea target=_blank><div class=info><picture><source srcset=/bubbletea-light.41979931daa0fa73.webp type=image/webp><img src=/bubbletea-light.35d9098a40514325.png alt="Bubble Tea mascot"></picture><h3 class=puffy><span data-text="Bubble Tea">Bubble Tea</span></h3><ul class=badges><li><span>Flavor</span><span>Taro</span></ul><p>Build terminal user interfaces from the future, today.</div><div class=art><video loop muted playsinline>
<source src=/bubbletea.ad274062ea3afaff.webm type=video/webm><source src=/bubbletea.6f3a317d9ddacaed.mp4 type=video/mp4></video><div></div></div></a><li><a class="cart vhs" data-repo=charmbracelet/vhs href=https://github.com/charmbracelet/vhs target=_blank><div class=info><picture><source srcset=/vhs.c73d6b9f57ffe0d7.webp type=image/webp><img src=/vhs.2c469be9d3c15275.png alt="VHS mascot"></picture><h3 class=puffy><span data-text=VHS>VHS</span></h3><ul class=badges><li><span>GIFs</span><span>Generated</span></ul><p>Create terminal GIFs with code!</div><div class=art><video loop muted playsinline>
<source src=/vhs.e3ab9558d2f3b571.webm type=video/webm><source src=/vhs.c840694cf1e75049.mp4 type=video/mp4></video><div></div></div></a></ul><hr><h2>Developer Experience</h2><p>We build three types of things at charm: <dfn title="text-based user
interfaces">TUIs</dfn>, <dfn title="command line interfaces">CLIs</dfn> and
libraries (called <em>packages</em> in <a href=https://golang.org target=_blank rel="noopener noreferrer">Go</a>). In all cases we believe an
exceptional user experience is paramount, and for that reason they’re not all
that different. After all, &ldquo;developer experience&rdquo; is really just user
experience. APIs and CLIs are user interfaces at the end of the day and they
require the same amount of care and thoughtfulness any good visual UI.<p>Regardless of the medium, we are striving for the one goal: an intuitive and
enjoyable experience for the person using the product.<p>The process is a bit like <a href=https://paulgraham.com/hackpaint.html target=_blank rel="noopener noreferrer">making an oil painting</a>.
We start with a sketch, build a little bit, then test, evaluate, and adjust.
We’re constantly revisiting our design as the project develops, asking
ourselves if it’s still appropriate given how and where the project is
progressing.<h3>Designing Libraries and APIs</h3><p>Enjoying and getting value from an API is a visceral experience,
so we spent a lot of time and energy on getting the nuances right as the
design of the API is so key to adoption. Our goal is to produce an API that’s
intuitive, fun, and allows the developer to get a lot of value with as little
effort as possible.<p>We ask questions like:<ul><li>How can we make this API easier to figure out?<li>How can we help developers avoid mistakes?<li>How can we design our API in a way that helps language servers provide effective completions?<li>Will this API lend it’s way to future innovations? Have we made room to grow?</ul><h3>Designing TUIs</h3><figure><a href=https://githbub.com/charmbracelet/soft-serve><img width=600 src=/soft-serve.3c11e60d0f6fdf63.gif alt="a GIF of a terminal session"></a><figcaption>I still can't believe this TUI runs over ssh.</figcaption></figure><p>TUIs bear many similarities to other visual types of user interfaces, but have
the wonderful, minimal quality of being just text—at one single size—and
color. We believe a good TUI can both exist in a long-running
fashion, like <a href=https://www.vim.org target=_blank rel="noopener noreferrer">vim</a>, as well as in an ephemeral manner that complements
the CLI experience, like <a href=https://github.com/junegunn/fzf target=_blank rel="noopener noreferrer">fzf</a>.<p>When designing TUIs we ask questions like the following:<ul><li>Should this be inline, use the <dfn title="alternate screen buffer: a separate buffer that consumes the window, restoring that window's contents upon exiting">altscreen</dfn>, or operate in both contexts?<li>How can we keep the user from ever wondering what key to press?<li>How should the application behave in very small terminal windows? What about very large terminal windows?<li>Does this really need to be a TUI, or would a CLI be more appropriate?</ul><h3>Designing CLIs</h3><figure><video autoplay loop playsinline muted style="max-width: 600px">
<source src=/skat.e5bc8383909cbc80.webm type=video/webm><source src=/skate.22df4980e1f806.mp4 type=video/mp4></video><figcaption>An old Betamax recording of one of our CLIs.</figcaption></figure><p>CLIs have the wonderful benefit of minimal user interface and the powerful
ability to tap into the essence of the command line with pipelines. The lack of
a GUI also means CLIs see faster development cycles, with the costs being
opaqueness and learning curve for the user.<p>When building CLIs we ask questions like the following:<ul><li>Are these arguments and flags intuitive?<li>What do helpful error messages look like? How can error messages help users figure out how to use the product?<li>How can <code>--help</code> teach users how to get the most value out of this tool?<li>Can this CLI become more powerful by leaning into pipelines?</ul><p>We also strive to provide <dfn title="manual page">manpage</dfn> entries and completions for popular shells.<hr><h2>The README</h2><figure><video autoplay loop playsinline muted style="max-width: 700px">
<source src=/huh-read.6aa8bddfce17c5e0.webm type=video/webm><source src=/huh-readme.fd04407df34bbe0b.mp4 type=video/mp4></video><figcaption>The <a href=https://github.com/charmbracelet/huh>huh</a> README is chock-full of examples and GIFs.</figcaption></figure><p>The README is critically important to the success of an open source
product. It&rsquo;s often a developer&rsquo;s first point of contact with a project and the
place where a developer will, in a matter of seconds, judge whether the project worthy of further
consideration. With this in mind, put a lot
of effort into README design, optimizing for strong first impressions.<p>Our strategy is to simply follow the age-old rule of advertising: showing the
product. Good products, when presented correctly, will sell themselves, which
is why we spend spend so much time on user experience and attention to detail.<p>With libraries, APIs, and packages we show the product with example code,
typically placing some code right at the top of the README. We want to show the
reader how intuitive, fun and powerful the API is and help them get started as
quickly as possible.<figure><a href=https://github.com/charmbracelet/huh><img width=500 src=/huh.b4ac241ba6c0c7d3.gif alt="a gif of a terminal session"></a><figcaption>Do you want fries with that?</figcaption></figure><p>When it makes sense, we also insert GIFs of the product right at the top of the
README. While GIFs remain a technical nightmare in terms of a file format,
they&rsquo;re the most effective medium we&rsquo;ve encountered for illustrating how
software works in a concise manner. They&rsquo;re short, silent videos that
automatically autoplay and automatically loop with no user interaction,
allowing us to paint a meaningful picture of the application in just a few
seconds.<p>As mentioned earlier, we believe so much in the effectiveness of GIFs that we
built <a href=https://github.com/charmbracelet/vhs target=_blank rel="noopener noreferrer">vhs</a>, a tool for scripting small, high quality GIFs of terminal
sessions.<h3>Quick Reference</h3><p>Beyond first impressions, we also tend to include a quick reference in our
READMEs to support the docs. The quick reference gives developers more insight
into the package, helps them get started, and highlights some of the common
parts of an API. It’s an excellent bridge to the docs that can help the user
make connections and gain insight into the API in ways the full, raw
documentation cannot.<p>In go projects, the README is also browsable in the generated documentation
(called GoDocs) so having the quick reference in the README is a win-win.<hr><h2>Examples, Examples, Examples</h2><figure><video autoplay loop playsinline muted style="max-width: 700px">
<source src=/harmonica-exampl.7affb47484fd8704.webm type=video/webm><source src=/harmonica-example.f51833af0b10cc08.mp4 type=video/mp4></video><figcaption>An example in the <a href=https://github.com/charmbracelet/harmonica>Harmonica</a> README</figcaption></figure><p>We can’t emphasize the effectiveness of examples enough.<p>We’re very strong believers in learning by example and we believe code examples
are one of the best ways to learn about an API. They show developers how to use
the API in a concrete way and help them hit the ground running so they can be
more productive more quickly. Examples also serve as a cookbook, presenting the
developers with solutions to common use cases, whetting their appetites for
creative thinking with the product.<p>We commonly put a set of fully functional examples in a repository for users to
reference both online and locally in their clones. In many cases those examples
are what we ourselves used to help think about the product while we were
building it.<hr><h2>Branding</h2><p>Good branding has the power to differentiate a product in the market with
a mere glance. Our strategy is to appeal to developers on a personal level and
create something that feels human, approachable, and memorable. We want the
branding to stand apart from the common efforts we see from the vast majority
of corporations and startups in the technology space. For that reason, we spend
a lot of time looking beyond tech and instead drawing inspiration from things
like <a href=https://animalcrossing.nintendo.com target=_blank rel="noopener noreferrer">video games</a>, <a href=https://www.are.na/christian-rocha/glitter-odyssey target=_blank rel="noopener noreferrer">art</a>, <a href=https://fentybeauty.com target=_blank rel="noopener noreferrer">beauty</a>,
<a href=https://www.family.co.jp target=_blank rel="noopener noreferrer">Family Mart</a>, <a href=https://www.sanrio.com target=_blank rel="noopener noreferrer">Sanrio</a> and so on.<p>The essence of our products’ branding is the name. Our goal is to disarm our
readers, suggest how they should feel about our product, and make them
smile. For these reasons an ideal Charm name is subversive, casual, and tongue
in cheek. Sometimes a good name comes quickly. Other times it takes
awhile. Because of this we start branding efforts early on in the product
development cycle.<figure><a href=https://github.com/charmbracelet/gum><video autoplay loop playsinline muted style="max-width: 500px">
<source src=/gum-loop-1920w.3610705912d07e6.mp4 type=video/mp4><source src=/gum-loop-1920.d01d81cea9318127.webm type=video/webm></video></a><figcaption>Nothing says “handy CLI tool” like a pack of gum</figcaption></figure><p>After we’ve settled on a name we produce artwork. Good art goes beyond making
the product attractive: it equips both us <em>and</em> third parties with marketing
material for use in videos, articles, and so on. In order for third parties to
use the art, however, the art needs to be good. It needs to be something people
want to showcase. This is why we spend a lot of effort conceptualizing and
producing high quality art.<figure><div class=three-up><a href=https://youtu.be/U8zCHA-9VLA><img class=seamless src=/gum-yt-a.fa46f8bd4cc2b27d.jpg></a>
<a href=https://youtu.be/tnikefEuArQ><img class=seamless src=/gum-yt-b.a1743cd6a62a6ba1.jpg></a>
<a href=https://youtu.be/u2UZp--vFog><img class=seamless src=/gum-yt-c.cb7e7333303fdde7.jpg></a></div><figcaption>YouTubers using <a href=https://github.com/charmbracelet/gum>Gum</a> art</figcaption></figure><hr><h2>Thinking Bigger</h2><p>While we&rsquo;re branding individual projects, we&rsquo;re also playing the larger game of
increasing awareness of the Charm brand. Our goal is less about giving
individual projects a following, and more about promoting Charm as a consistent
producer of wonderful, open source software. A Charm product’s branding should
be consistent enough with other parts of the Charm narrative so it feels like
like a facet of the brand, yet unique enough so that it takes the brand to new
places.<hr><h2>Getting the Word Out</h2><p>The bulk of of our launch efforts happen in the form of the prep described
above. If we’ve done the work correctly, we’ve built a good product and
prepared the necessary marketing material. There’s no magic in the launch
itself: we simply make the repository public and simply let people know. We
star it on GitHub, tell our friends, and post around the internet to the places
you’d expect: <a href=https://twitter.com/charmcli target=_blank rel="noopener noreferrer">Twitter</a>, Reddit,
<a href=http://mastodon.social/@charmcli target=_blank rel="noopener noreferrer">Mastodon</a>, and so on.<p>With any luck, the project starts to find its way around the internet. Maybe it
will get posted to Hacker News. Maybe it bubbles up onto to <a href=https://github.com/trending target=_blank rel="noopener noreferrer">GitHub
Trending</a>. Perhaps it’ll be picked up by <a href=https://golangweekly.com target=_blank rel="noopener noreferrer">Golang Weekly</a>.
Maybe <a href=https://www.youtube.com/c/theprimeagen target=_blank rel="noopener noreferrer">The Primeagen</a> will mention it. Or maybe developers will find it
via more subtle means.<hr><h2>Embracing Open Source</h2><p>In a lot of ways, a launch is when a project&rsquo;s story really begins. Good open
source software has the unique ability to attract users, feedback, and
contributions <em>en masse</em>, so we put a significant effort into spending time
with the open source community post-launch.<p>Part of this is just paying attention. We look for patterns in feedback. We
think critically about what developers do with our projects, as well as what
they&rsquo;re want to do, but can&rsquo;t. We ask questions.<p>Another part of this is gracefully saying no. Sometimes the community will
excitedly request features or submit pull requests for things that don&rsquo;t align
with our vision for the project. When saying no to a feature or pull request,
we&rsquo;ve found the community generally understands that they don&rsquo;t always have the
full context that we do. With this in mind, we always explain our reasons for
saying no, but we also keep listening because often <em>we</em> don&rsquo;t have the context
that the community does.<p>For that reason, it&rsquo;s equally important to say yes. Our biggest projects have
taken significant turns for the better exclusively via feedback and
contributions from the community. In some cases a single comment has inspired
critical changes to our work (&ldquo;You know, if Bubble Tea worked this way then we
could…&rdquo;) and in other cases we&rsquo;ve received major, game-changing features and
optimizations from developers with incredible talent and skills far beyond our
expertise.<hr><h2>Sticking With It</h2><p>The most important thing we can do for the project after putting it out into
the world is to continue working on it. Consistent work on an open source
project post-launch is, in fact, the most important factor the project&rsquo;s
success. A big part of this, as mentioned earlier, is working with the
community. The rest is using the product, thinking about it, and continuing to
improve it. A launch is only the first step in a software project’s path to
maturity. It takes time, effort, and introspection for a project to reach its
full potential.<p>Actively maintaining open source projects also presents a sense of security to
developers. It suggests that the maintainer sees value in the project, that bug
reports will be honored, and feature requests will be entertained. It signals
to a developer that they can use the project and expect it to be relevant for
contemporary needs, and that they can expect a certain degree of support.<figure><a href=https://charm.sh/yt><img width=650 src=/bashvid.3605540895d0a89d.jpg><figcaption><a href=https://www.youtube.com/bashbunni>Bashbunni</a> talks about <a href=https://github.com/charmbracelet/pop>Pop</a> our CLI-based email tool.</figcaption></figure><p>In the same vein, we also keep promoting our work after launch. We’ll produce
short and long form video. We’ll talk about it in blog posts. We’ll feature
individuals and companies using the product. And every release, no matter how
small, is an opportunity to toot a project’s horn.<p>Sometimes our projects don’t take off immediately. Sometimes they’re not
met with the amount of acclaim we’d like. We know, however, that if we stick
with it, our software will find its way. Success doesn&rsquo;t always come quickly
but it can always be found with enough persistence.]]></content:encoded>
      <author>Christian Rocha</author>
      <guid>This is How We Do It</guid>
      <pubDate>Wed, 06 Mar 2024 00:00:00 +0000</pubDate>
      <source>https://charm.land/blog/100k/</source>
    </item>
    <item>
      <title>2023: That’s a Wrap!</title>
      <link>https://charm.land/blog/2023-roundup/</link>
      <description>Charm’s 2023 highlights</description>
      <content:encoded><![CDATA[<h1>2023: That’s a Wrap!</h1><p>Sharing Charm’s 2023 highlights<p>By Charm on 19 December 2023<p>In the spirit of open source, we’re proud to share some 2023 milestones! This
year marked a year of open source growth and partnerships. To name a few
highlights, <a href=https://github.com/charmbracelet/bubbletea target=_blank rel="noopener noreferrer">Bubble Tea</a>, our TUI framework, <a href=https://twitter.com/charmcli/status/1712841343825858579 target=_blank rel="noopener noreferrer">crossed 20,000 GitHub
stars</a>, we spoke at GitHub Universe, and we announced a
<a href=https://charm.sh/blog/the-next-generation/>$6 million round of funding</a>.<p>Here’s what that growth looks like in numbers:</p><picture class=seamless><source srcset=/charm-stats-2023-mobile.da563749e9b1c5db.svg media="(max-width: 550px)"><img class=seamless srcset=/charm-stats-2023.8f58bce588ea80a8.svg></picture><h2>Hot partnerships</h2><p>We love working with other companies who share a similar passion for creative,
powerful, developer tooling. In the summer, we launched <a href=https://github.com/charmbracelet/pop target=_blank rel="noopener noreferrer">Pop</a>,
<a href=https://www.linkedin.com/posts/zenorocha_one-of-my-favorite-developer-tool-companies-activity-7086714694413025280-jdho/ target=_blank rel="noopener noreferrer">in collaboration with Resend</a>, to enable sending emails from the
terminal. Shortly thereafter <a href=https://tailscale.com/blog/charm-wishlist target=_blank rel="noopener noreferrer">we worked with Tailscale</a> to expose
SSH endpoints on users’ Tailnets via <a href=https://github.com/charmbracelet/wish target=_blank rel="noopener noreferrer">Wish</a>, our SSH framework, and
<a href=https://github.com/charmbracelet/wishlist target=_blank rel="noopener noreferrer">Wishlist</a>, our SSH host directory. We also <a href=https://www.cockroachlabs.com/blog/cockroachdb-cli-improvements/ target=_blank rel="noopener noreferrer">worked with CockRoachDB</a>
to help make their interactive SQL Shell more powerful via <a href=https://github.com/charmbracelet/bubbletea target=_blank rel="noopener noreferrer">Bubble Tea</a>.<h2>New launches and open source adoption in industry</h2><p>We launched four new products this year:<ul><li><a href=https://github.com/charmbracelet/huh target=_blank rel="noopener noreferrer">Huh</a>: a tool for building on the command line, with first-class support for screen readers<li><a href=https://github.com/charmbracelet/mods target=_blank rel="noopener noreferrer">Mods</a>: a tool that brings LLMs to the command line in a powerful, native way (pipes!)<li><a href=https://github.com/charmbracelet/pop target=_blank rel="noopener noreferrer">Pop</a>: a tool for sending emails from the terminal, built in collaboration with <a href=https://resend.com target=_blank rel="noopener noreferrer">Resend</a><li><a href=https://github.com/charmbracelet/log target=_blank rel="noopener noreferrer">Log</a>: the most bullet-proof, glamourous logger you’ve ever seen</ul><p>Launch expanded our ability to reach new organizations and increase open source
adoption. Some of our favorite new products built with Charm technology are
<a href=https://github.com/pulumi/esc target=_blank rel="noopener noreferrer">Pulumi ESC</a>, <a href=https://github.com/DataDog/datadog-agent target=_blank rel="noopener noreferrer">Datadog Agent</a>, a daemon for collecting events
and metrics and sending them back to the Datadog mothership, and Truffle
Security, who announced <a href=https://trufflesecurity.com/blog/trufflehog-tui/ target=_blank rel="noopener noreferrer">an upgraded Trufflehog CLI</a> at the Black Hat
Conference.<p>Bubble Tea continues to be our most popular library. There are 4,487 <a href=https://github.com/charmbracelet/bubbletea target=_blank rel="noopener noreferrer">Bubble
Tea</a> apps built to date, representing 119% growth compared to January this
year. And <a href=https://github.com/charmbracelet/glow target=_blank rel="noopener noreferrer">Glow</a>, our glamorous markdown reading tool, continues to be
shared with over 53,000 users, a 55% increase since the beginning of 2023.<h2>Connecting with the community</h2><p>Our community continues to be our pride and joy. We ended 2022 at 65,000
GitHub stars. To date, we just crossed 95,000 GitHub stars! Most importantly,
we’ve continued to invest in engaging with our community online, which saw
a record 20% monthly increase across Twitter, YouTube, Discord, Mastodon,
Instagram. Our <a href=https://charm.sh/yt>YouTube</a> audience has more than doubled from that of last
year, with 5,800+ subscribers, racking over 243,000 views.<p>This month, we organized our <a href=https://twitter.com/charmcli/status/1733166187130077631 target=_blank rel="noopener noreferrer">first large community meetup</a> in
collaboration with GitHub. Taking over GitHub’s HQ, we hosted 100+ developers,
founders, open source enthusiasts with Mario Kart, Ping Pong and Piñata-busting.<figure><picture><source srcset=/charm-x-githu.1e726e3e9a69b06.webp><img width=640 srcset=/charm-x-github.b7131176fbbef576.jpg></picture><figcaption>Charm × GitHub, San Francisco</figcaption></figure><p>2023 has been so, so fun. Here’s to 2024!]]></content:encoded>
      <author>Charm</author>
      <guid>2023: That’s a Wrap!</guid>
      <pubDate>Tue, 19 Dec 2023 00:00:00 +0000</pubDate>
      <source>https://charm.land/blog/2023-roundup/</source>
    </item>
    <item>
      <title>The Next Generation of the Command Line</title>
      <link>https://charm.land/blog/the-next-generation/</link>
      <description>Where Charm’s been, and where we’re going.</description>
      <content:encoded><![CDATA[<h1>The Next Generation of the Command Line</h1><p>By Christian Rocha on 2 November 2023<p>We started Charm four years ago with the goal of making the command line
glamourous, powerful, fun and modern. Today we&rsquo;re excited to announce that
we&rsquo;ve raised $6MM led by <a href=https://www.gradient.com target=_blank rel="noopener noreferrer">Gradient</a>, Google&rsquo;s AI-focused venture fund, with
participation from new and existing investors <a href=https://cavalry.vc target=_blank rel="noopener noreferrer">Cavalry Ventures</a>, <a href=https://www.fuelcapital.com target=_blank rel="noopener noreferrer">Fuel Capital</a>,
<a href=https://www.firestreak.com target=_blank rel="noopener noreferrer">Firestreak Partners</a>, as well as the founders of <a href=https://supabase.com target=_blank rel="noopener noreferrer">Supabase</a>, <a href=https://supabase.com target=_blank rel="noopener noreferrer">Foursquare</a>,
Fleetsmith (acquired by Apple), <a href=https://www.honeycomb.io target=_blank rel="noopener noreferrer">Honeycomb</a> and others.<p>Charm started as a group of friends exchanging <code>.vimrc</code> tips and building open
source libraries. We have a background in consumer tech, with a focus on iOS
apps, but have always shared a passion for open source command line tools. We&rsquo;d
spent a lot of time at companies like Apple, Last.fm, TweetDeck, Zenly, and The
Huffington Post building fun, UX forward products that users loved, and wanted
to bring that modern product thinking to the command line.<h2>Why the command line? Why now?</h2><p>The command line has been a ubiquitous platform for computing for the past 30+
years thanks, in part, to its focus on simple tools that do one thing well, the
ability to easily compose those tools into unique solutions, and a massive
library of existing command line programs from which to draw from. Many of
these attributes stand in stark contrast to the web and its siloed data, lack
of composability and large, opaque solutions that often include a healthy dose
of tracking, ads and other dark patterns.<p>The command line seemed to us like a healthy alternative to the web and closed
mobile platforms. It was also ripe for an update with a focus on user-centric
design and encrypted, self-hostable networked services. We wanted to build the
command line platform for the next 30+ years.<h2>Why open source?</h2><p>The command line and open source have a long history together and most of the
tools available for the command line are open source. It was important to us,
from the start, to stay open source and provide open source, self-hostable
solutions to our apps. We also love working with the open source community and
have built the team around amazing open source contributors.<h2>Glow and Glamour</h2><figure><picture><img src=/glow-demo.9f1fad945f365ee0.gif alt="An example of a Little Bubble Tea TUI"></picture><figcaption>Glow uses Glamour to render markdown, obviously.</figcaption></figure><p>Our first two projects were <a href=https://github.com/charmbracelet/glow target=_blank rel="noopener noreferrer">Glow</a> and <a href=https://github.com/charmbracelet/glamour target=_blank rel="noopener noreferrer">Glamour</a>. When we thought about modern
product development one thing we felt was lacking when building command line
apps was the separation of concerns between structure and style. On the web you
have HTML and CSS, specialized languages that allow for parallel
development. We wanted that on the command line so we built Glow and Glamour
around that concept.<figure><picture><source srcset=/glow-growth.ef6356a03bc5657d.webp type=image/webp><img src=/glow-growth.cf98ff96cfe2e42d.png alt="Chart showing Glow star growth over time"></picture><figcaption>Markdown is the true medium of developer expression.</figcaption></figure><h2>Bubble Tea and Lip Gloss</h2><ul class=carts><li><a class="cart bubbletea" data-repo=charmbracelet/bubbletea href=https://github.com/charmbracelet/bubbletea target=_blank><div class=info><picture><source srcset=/bubbletea-light.41979931daa0fa73.webp type=image/webp><img src=/bubbletea-light.35d9098a40514325.png alt="Bubble Tea mascot"></picture><h3 class=puffy><span data-text="Bubble Tea">Bubble Tea</span></h3><ul class=badges><li><span>Flavor</span><span>Taro</span></ul><p>Build terminal user interfaces from the future, today.</div><div class=art><video loop muted playsinline>
<source src=/bubbletea.ad274062ea3afaff.webm type=video/webm><source src=/bubbletea.6f3a317d9ddacaed.mp4 type=video/mp4></video><div></div></div></a><li><a class="cart lipgloss" data-repo=charmbracelet/lipgloss href=https://github.com/charmbracelet/lipgloss target=_blank><div class=info><picture><source srcset=/lipgloss.2881a474c4c2e06e.webp type=image/webp><img src=/lipgloss.b7894eee773ca7ca.png alt="Lip Gloss mascot"></picture><h3 class=puffy><span data-text="Lip Gloss">Lip Gloss</span></h3><ul class=badges><li><span>Glossiness</span><span>Very</span></ul><p>Your terminal style and layout toolkit.</div><div class=art><video loop muted playsinline>
<source src=/lipgloss.df9634b0cd90c0a0.webm type=video/webm><source src=/lipgloss.f9b909d1c28daed6.mp4 type=video/mp4></video><div></div></div></a></ul><p>While it was a great step forward to be able to style command line output based
on markdown structure, we needed a better framework for
interactivity. Christian, who has a background in Haskell and Elm, wanted a way
to build textual user interfaces in a stateful manner without having to think
about rendering. With that foundation, we created <a href=https://github.com/charmbracelet/bubbletea target=_blank rel="noopener noreferrer">Bubble Tea</a>, a TUI framework
for Go based on the Elm architecture. It ended up being a great pairing of
functional programming concepts and Go&rsquo;s less functional nature.<p>This allowed us to easily build animated, reactive, TUI-based applications. We
saw amazing adoption of Bubble Tea and it now powers over 4,000 apps, boasts 20k
stars on GitHub, and is in use at major companies like AWS, NVIDIA, Microsoft
Azure, and many others.<figure><picture><img width=800 src=/bubbletea-example.fbd37bef138c0471.gif alt="An example of a Little Bubble Tea TUI"></picture><figcaption>“Would it be weird to make this progress bar bounce?” we wondered.</figcaption></figure><p>After building a couple formidable Bubble Tea applications we also realized we
also needed first class styling and layout tools. <a href=https://github.com/charmbracelet/lipgloss target=_blank rel="noopener noreferrer">Lip Gloss</a> was born and is the
layout engine for not only nearly every Bubble Tea TUI, but also many general
purpose CLI-based tools. Over 4,800 open source tools use Lip Gloss to date.<figure><picture><source srcset=/bubble-tea-growth.252b56712957efe4.webp type=image/webp><img width=1100 src=/bubble-tea-growth.31a86e01a143711a.png alt="Chart showing Bubble Tea ecosystem growth, in terms of GitHub stars over time"></picture><figcaption>Our studies show devs who build terminal stuff like weird mascots.</figcaption></figure><h2>SSH, Wish, and Soft Serve</h2><ul class=carts><li><a class="cart wish" data-repo=charmbracelet/wish href=https://github.com/charmbracelet/wish target=_blank><div class=info><picture><source srcset=/little-star.aed3872a76e89dec.webp type=image/webp><img src=/little-star.10c78ffccf7719fa.png alt="Wish mascot"></picture><h3 class=puffy><span data-text=Wish>Wish</span></h3><ul class=badges><li><span>Magical</span><span>Yes</span></ul><p>Make SSH apps, just like that!</div><div class=art><video loop muted playsinline>
<source src=/wish.36f838bff2cf831.webm type=video/webm><source src=/wish.c8d2266571bacce0.mp4 type=video/mp4></video><div></div></div></a><li><a class="cart soft-serve" data-repo=charmbracelet/soft-serve href=https://github.com/charmbracelet/soft-serve target=_blank><div class=info><picture><source srcset=/soft-serv.c8b0535c0a453f24.webp type=image/webp><img src=/soft-serve.56aa13a6587820c2.png alt="Soft Serve mascot"></picture><h3 class=puffy><span data-text="Soft Serve">Soft Serve</span></h3><ul class=badges><li><span>Consistency</span><span>Smooth</span></ul><p>The mighty, self-hostable Git server for the command line</div><div class=art><video loop muted playsinline>
<source src=/soft-serv.aa09071a71fd36aa.webm type=video/webm><source src=/soft-serve.a3e2c905b871a77d.mp4 type=video/mp4></video><div></div></div></a></ul><p>With our TUI tooling solidly in-place, we started to think about designing
command line first APIs for services. We settled on using SSH as our
protocol. Using SSH keys, we could offer seamless API access without passwords,
with anonymity and end-to-end encryption. We could even bridge identity to HTTP
based APIs with JWTs. This led to the development of <a href=https://github.com/charmbracelet/wish target=_blank rel="noopener noreferrer">Wish</a> and the <a href=https://github.com/charmbracelet/charm target=_blank rel="noopener noreferrer">Charm Cloud</a>,
both of which leverage SSH based APIs and power the backend to tools like Glow
and Skate.<p>Another advantage of the SSH protocol is that you can use it interactively with
a TUI streamed from the server. With Wish, we made it easy to plug a Bubble Tea
application into an SSH server and stream the TUI remotely. That along with
Git&rsquo;s support for SSH allowed us to build <a href=https://github.com/charmbracelet/soft-serve target=_blank rel="noopener noreferrer">Soft Serve</a>, self-hostable Git server
with a first-class TUI.<h2>Gum and VHS</h2><figure><a href=https://github.com/charmbracelet/gum><video autoplay loop playsinline muted style="max-width: 500px">
<source src=/gum-loop-1920w.3610705912d07e6.mp4 type=video/mp4><source src=/gum-loop-1920.d01d81cea9318127.webm type=video/webm></video></a><figcaption>Non-toxic?</figcaption></figure><p>Two of our latest releases have focused on bringing our platform not just to Go
developers but to people writing scripts and using shells. <a href=https://github.com/charmbracelet/gum target=_blank rel="noopener noreferrer">Gum</a> takes many of
the Bubbles from our Bubble Tea component library as well as styling and layout
feature from Lip Gloss, and creates a single binary that makes it super easy to
add interactive UI, input elements, and styling and layout to your scripts and
shell pipelines and quickly apply advanced. It&rsquo;s our fastest growing project to
date.</p><a class="cart vhs" data-repo=charmbracelet/vhs href=https://github.com/charmbracelet/vhs target=_blank><div class=info><picture><source srcset=/vhs.c73d6b9f57ffe0d7.webp type=image/webp><img src=/vhs.2c469be9d3c15275.png alt="VHS mascot"></picture><h3 class=puffy><span data-text=VHS>VHS</span></h3><ul class=badges><li><span>GIFs</span><span>Generated</span></ul><p>Create terminal GIFs with code!</div><div class=art><video loop muted playsinline>
<source src=/vhs.e3ab9558d2f3b571.webm type=video/webm><source src=/vhs.c840694cf1e75049.mp4 type=video/mp4></video><div></div></div></a><p><a href=https://github.com/charmbracelet/vhs target=_blank rel="noopener noreferrer">VHS</a> is a tool that lets you elegantly script and record screencasts of command
line apps and render them as GIFs. It&rsquo;s something we built for ourselves so we
could make recordings in our READMEs and social media posts but it&rsquo;s found wide
adoption in the community.<figure><picture><source srcset=/gum-and-vhs-growth.3c0e4e4a4ce92f10.webp type=image/webp><img width=1100 src=/gum-and-vhs-growth.c49fda3c4f8b01af.png alt="Chart showing Gum and VHS star growth over time" title="Gum and VHS ultra spike chart"></picture><figcaption>A pair of hockey sticks.</figcaption></figure><h2>Why Gradient</h2><figure><a href=https://gradient.com/><picture><img width=500 src=/gradient.f405347fb5bdc13.svg alt="The Gradient Ventures Logo"></picture></a><figcaption>Red, white and awesome.</figcaption></figure><p>From our very first conversation with <a href=https://x.com/darian314 target=_blank rel="noopener noreferrer">Darian</a>, we knew that we had found
a partner that shared our long term vision for the command line, respect for
the developer community and passion for open source. <a href=https://www.gradient.com target=_blank rel="noopener noreferrer">Gradient</a> and Darian felt
like the best possible partners to help us take Charm to the next level and
we&rsquo;re excited to be part of the Gradient family.<h2>What’s next</h2><p>We&rsquo;ve been working on the next generation of our platform on both the frontend
and backend. Over the next couple of months we&rsquo;ll begin rolling out alpha tests
of major updates to our projects and platform. We&rsquo;ll also be working on
sustainable open source software development and ethical monetization.<p>We want to work closely with the community on the future of Charm and the
command line, so please join us on <a href=/chat>Discord</a> or sign up via email below
for our next alpha launch.<form class=signup action=https://ee03647a.sibforms.com/serve/MUIEAKVVZACapZeQGzsLSkyDkloXrER0EQfYeB5kPQk2zMxS6-0YK9yapURootwOuO6MeBBiDHHCtYO5XdFormKlkv6OdXeLJbEux2XUT19pkb9NyRo5y9KUduIotu9UKzIOtA2XdrYs-gSe_WKIT_nLV1ArEmHx1HhFdPCU8jDcxCDtdyqae9tT3fLClpYQWbh0soapt7jbhVXE method=post><input name=locale type=hidden value=en>
<input name=html_type type=hidden value=simple><fieldset><input name=EMAIL type=text placeholder=your@email.pizza autocomplete=off>
<button disabled>Submit</button></fieldset></form>]]></content:encoded>
      <author>Christian Rocha</author>
      <guid>The Next Generation of the Command Line</guid>
      <pubDate>Thu, 02 Nov 2023 00:00:00 +0000</pubDate>
      <source>https://charm.land/blog/the-next-generation/</source>
    </item>
    <item>
      <title>Soft Serve now supports Git LFS and More!</title>
      <link>https://charm.land/blog/soft-serve-git-lfs-support/</link>
      <description>Your favorite self-hosted Git server got even better. Soft Serve now supports Git LFS, HTTP auth, and collaborator access levels!</description>
      <content:encoded><![CDATA[<h1>Soft Serve now supports Git LFS and More!</h1><p>By Ayman Bagabas on 21 August 2023</p><a class="cart soft-serve" data-repo=charmbracelet/soft-serve href=https://github.com/charmbracelet/soft-serve target=_blank><div class=info><picture><source srcset=/soft-serv.c8b0535c0a453f24.webp type=image/webp><img src=/soft-serve.56aa13a6587820c2.png alt="Soft Serve mascot"></picture><h3 class=puffy><span data-text="Soft Serve">Soft Serve</span></h3><ul class=badges><li><span>Consistency</span><span>Smooth</span></ul><p>The mighty, self-hostable Git server for the command line</div><div class=art><video loop muted playsinline>
<source src=/soft-serv.aa09071a71fd36aa.webm type=video/webm><source src=/soft-serve.a3e2c905b871a77d.mp4 type=video/mp4></video><div></div></div></a><p>At Charm, we use <a href=https://git-lfs.com/ target=_blank rel="noopener noreferrer">Git LFS</a> all the time to bring large files into version control. This very website uses <a href=https://git-lfs.com/ target=_blank rel="noopener noreferrer">Git LFS</a> to manage video and image assets in the repository and download them during the deployment pipeline. <a href=https://git-lfs.com/ target=_blank rel="noopener noreferrer">Git LFS</a> stands for &ldquo;Git Large File Storage&rdquo; and it is <a href=https://github.com/git-lfs/git-lfs/wiki/Implementations target=_blank rel="noopener noreferrer">widely supported</a> by many open source and commercial servers.<p><a href=https://git-lfs.com/ target=_blank rel="noopener noreferrer">Git LFS</a> works by using text pointers inside Git repositories while storing the actual file contents on a different server (for example, S3).<p>In this release, we are pleased to introduce not only Git LFS support, but also collaborator access levels, HTTP authentication, and Postgres database support. Read on for details!<h2>Git LFS Support Over HTTP and SSH</h2><p>You can now leverage <a href=https://git-lfs.com/ target=_blank rel="noopener noreferrer">Git LFS</a> in Soft Serve to manage large files over both HTTP and SSH transports, allowing you to handle large files within your repositories without unnecessarily bogging down your clones. <a href=https://github.com/git-lfs/git-lfs/blob/main/docs/api/README.md target=_blank rel="noopener noreferrer">Git LFS API</a> relies on a separate HTTPS server to handle transferring the files. The <a href=https://github.com/git-lfs/git-lfs/blob/main/docs/proposals/ssh_adapter.md target=_blank rel="noopener noreferrer">pure-SSH transfer protocol</a> was introduced in <a href=https://github.com/git-lfs/git-lfs/blob/main/CHANGELOG.md#300-24-sep-2021 target=_blank rel="noopener noreferrer">Git LFS v3.0</a>. <a href=https://github.com/charmbracelet/soft-serve target=_blank rel="noopener noreferrer">Soft Serve</a> supports both protocols.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>git lfs track *.png
</span></span><span style=display:flex;><span style=line-height:1.4em;>git add -A
</span></span><span style=display:flex;><span style=line-height:1.4em;>git push origin main
</span></span></code></pre><p>Soft Serve also supports Git LFS locks.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># As user &#34;tomato&#34; we run</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>git lfs lock path/to/file
</span></span><span style=display:flex;><span style=line-height:1.4em;>git lfs locks
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># path/to/file	tomato	ID:1</span>
</span></span></code></pre><h2>Collaborator Access Levels</h2><p>Adding collaborators is more flexible than ever. Now, you can assign a specific access level to repository collaborators. All you need to do is specify the access level in your <code>repo collab</code> command.<p>Let&rsquo;s say you want to make a user read a private repository without giving them the ability to push files and make changes. You could do so by adding a new collaborator with <code>read-only</code> access. To add a collaborator <code>frankie</code> to the repo <code>foobar</code>:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>ssh git.example.com repo collab add foobar frankie read-only
</span></span></code></pre><p>Imagine you want to make <code>frankie</code> read all the repositories under <code>docs</code>, you could do something like this:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>repos<span style=color:#ef8080>=</span><span style=color:#0af>$(</span>ssh git.example.com repo ls <span style=color:#e8e8a8>|</span> grep <span style=color:#c69669>&#34;^docs/&#34;</span><span style=color:#0af>)</span> <span style=color:#676767># Get all the repos under docs/</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>while</span> IFS<span style=color:#ef8080>=</span> <span style=color:#ff8ec7>read</span> -r repo<span style=color:#e8e8a8>;</span> <span style=color:#0af>do</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>    ssh git.example.com repo collab add $repo frankie read-only
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>done</span> <span style=color:#ef8080>&lt;&lt;&lt;</span> $repos
</span></span></code></pre><p>Here, the access level parameter is optional and defaults to <code>read-write</code> access when not specified.<h2>HTTP Authentication</h2><p>Whether you use Git over HTTP(s), SSH, or both, Soft Serve has you covered. You can use user access tokens to authenticate over HTTP(s). Use the SSH <code>token</code> command to manage your access tokens. Then use the generated tokens as either your HTTP username or password to authenticate and access your repositories.<p>To generate a new token:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>ssh git.example.com token create <span style=color:#c69669>&#39;my new token&#39;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># ss_1234abc56789012345678901234de246d798fghi</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># Or with an expiry date</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>ssh git.example.com token create --expires-in 1y <span style=color:#c69669>&#39;my other token&#39;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># ss_98fghi1234abc56789012345678901234de246d7</span>
</span></span></code></pre><p>For example, to clone a private repository <code>fajitas</code> with an access token:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>git clone https://ss_4db594cxxxxxx@git.example.com/fajitas.git
</span></span></code></pre><h2>Postgres</h2><p>You can now use Postgres instead of SQLite to store persistent data. SQLite is great for a simple and lightweight applications. If you&rsquo;re using Soft Serve as a single user, perhaps SQLite is all you need. However, if you want to use a more powerful and scalable database engine, Postgres has your back.<p>Soft Serve uses SQLite as its default database engine. To use Postgres instead, first create a new database for Soft Serve. Then configure Soft Serve to use Postgres as its database engine.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># Create a Soft Serve database</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>psql -hlocalhost -p5432 -Upostgres -c <span style=color:#c69669>&#39;CREATE DATABASE soft_serve&#39;</span>
</span></span></code></pre><p>Now once you have a Soft Serve database, configure your server to use Postgres. In your <code>config.yaml</code>, head to the <code>db</code> section to configure your database settings. Add the section if it doesn&rsquo;t exist.<pre><code class=language-conf>db:
  driver: &quot;postgres&quot;
  data_source: &quot;postgres://postgres@localhost:5432/soft_serve?sslmode=disable&quot;
</code></pre><p>Or if you&rsquo;re using environment variables to configure your server:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>SOFT_SERVE_DB_DRIVER<span style=color:#ef8080>=</span>postgres <span style=color:#afffd7>\
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#afffd7></span>SOFT_SERVE_DB_DATA_SOURCE<span style=color:#ef8080>=</span><span style=color:#c69669>&#34;postgres://postgres@localhost:5432/soft_serve?sslmode=disable&#34;</span> <span style=color:#afffd7>\
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#afffd7></span>soft serve
</span></span></code></pre><p>For more information, see <a href=https://github.com/charmbracelet/soft-serve#database-configuration target=_blank rel="noopener noreferrer">Soft Serve Database Configuration</a>.<h2>Other Enhancements</h2><p>Along with all these great features, <a href=https://github.com/charmbracelet/soft-serve/releases/tag/v0.6.0 target=_blank rel="noopener noreferrer">Soft Serve v0.6</a> brings lots of bug fixes, better error handling, and improved performance. Check out the <a href=https://github.com/charmbracelet/soft-serve/releases/tag/v0.6.0 target=_blank rel="noopener noreferrer">release page</a> for a full list of changes.<hr><p>What are you waiting for! Head to the <a href=https://github.com/charmbracelet/soft-serve/releases target=_blank rel="noopener noreferrer">Soft Serve releases</a> page to grab the latest release.</p><a class="cart soft-serve" data-repo=charmbracelet/soft-serve href=https://github.com/charmbracelet/soft-serve target=_blank><div class=info><picture><source srcset=/soft-serv.c8b0535c0a453f24.webp type=image/webp><img src=/soft-serve.56aa13a6587820c2.png alt="Soft Serve mascot"></picture><h3 class=puffy><span data-text="Soft Serve">Soft Serve</span></h3><ul class=badges><li><span>Consistency</span><span>Smooth</span></ul><p>The mighty, self-hostable Git server for the command line</div><div class=art><video loop muted playsinline>
<source src=/soft-serv.aa09071a71fd36aa.webm type=video/webm><source src=/soft-serve.a3e2c905b871a77d.mp4 type=video/mp4></video><div></div></div></a>]]></content:encoded>
      <author>Ayman Bagabas</author>
      <guid>Soft Serve now supports Git LFS and More!</guid>
      <pubDate>Mon, 21 Aug 2023 00:00:00 +0000</pubDate>
      <source>https://charm.land/blog/soft-serve-git-lfs-support/</source>
    </item>
    <item>
      <title>Wishlist Endpoint Discovery</title>
      <link>https://charm.land/blog/wishlist-sd/</link>
      <description>Learn how to use the recently-added Tailscale, DNS, and Zeroconf endpoint discovery in Wishlist, our SSH host directory.</description>
      <content:encoded><![CDATA[<h1>Wishlist Endpoint Discovery</h1><p>By Carlos Becker on 19 Jul 2023<p>We just added endpoint discovery to <a href=https://github.com/charmbracelet/wishlist target=_blank rel="noopener noreferrer">Wishlist</a>, our SSH host directory.</p><a class="cart wishlist" data-repo=charmbracelet/wishlist href=https://github.com/charmbracelet/wishlist target=_blank><div class=info><h3 class=puffy><span data-text=Wishlist>Wishlist</span></h3><ul class=badges><li><span>Convenient</span><span>Very</span></ul><p>Your SSH directory.</div><div class=art><video loop muted playsinline>
<source src=/wishlist.6ca8be742730cd8c.webm type=video/webm><source src=/wishlist.9986107d2f738a.mp4 type=video/mp4></video><div></div></div></a><p>Wishlist can act as a <a href=https://en.wikipedia.org/wiki/Bastion_host target=_blank rel="noopener noreferrer"><em>bastion</em></a>,
presenting the user with a list of hosts they can SSH into from that host.<p>You can also run it locally, in which case it becomes a <em>TUI</em> (text-based
user interface) for your <code>~/.ssh/config</code> (or to a predefined list of hosts in
a YAML configuration file).<p>But there&rsquo;s more to it: did you know you can discover hosts from several
sources?<h2>Endpoint Discovery</h2><p>Currently, we support the following sources:<ul><li>DNS SRV records<li>Zeroconf with mDNS<li>Tailscale</ul><p>Let&rsquo;s see how it works, shall we?<h3>Tailscale</h3><p>We partnered with our friends at <a href=https://tailscale.com/ target=_blank rel="noopener noreferrer">Tailscale</a> to add
endpoint discovery to <a href=https://github.com/charmbracelet/wishlist target=_blank rel="noopener noreferrer">Wishlist</a>. Check out what they have to say about it
<a href=https://tailscale.com/blog/charm-wishlist/ target=_blank rel="noopener noreferrer">here</a>.<p><img width=1200 src=https://stuff.charm.sh/wishlist/tailscale-x-charm.png alt="Tailscale and Charm logos"><p>Tailscale is a VPN service that makes devices and
applications you own securely accessible anywhere in the world.
It uses the <a href=https://www.wireguard.com target=_blank rel="noopener noreferrer">WireGuard protocol</a>, and has apps for
pretty much all platforms.<p><img src=https://vhs.charm.sh/vhs-2UNOdvOqUosKvZDbTYEMjk.gif alt="Made with VHS"><p>To discover your Tailscale-connected machines, you&rsquo;ll need an
<a href=https://login.tailscale.com/admin/settings/keys target=_blank rel="noopener noreferrer">API Access Token</a>,
and the name of your <em>tailnet</em>.<p>With that information in hand, run <code>wishlist</code> with <code>--tailscale.key</code>
(or set <code>$TAILSCALE_KEY</code>) and <code>--tailscale.net</code>, for example:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>wishlist --tailscale.net<span style=color:#ef8080>=</span>charm --tailscale.key<span style=color:#ef8080>=</span>ts-key-aaabbb...
</span></span></code></pre><p>And that&rsquo;s it! Wishlist will discover all your <em>tailnet</em>&rsquo;s machines on startup.<blockquote><p>Note: We can&rsquo;t get the open ports through the API, so all endpoints
discovered will use the default SSH port (<code>22</code>).
You can change that with <a href=#Hints><em>hints</em></a>.</blockquote><p>It&rsquo;s also worth mentioning that Tailscale&rsquo;s API keys expire after 90 days (max).
To avoid the trouble of having to change the key every couple of months,
you can also use their beta
<a href=https://tailscale.com/kb/1215/oauth-clients/ target=_blank rel="noopener noreferrer">OAuth Clients</a>.<p>Create an app with <code>devices:read</code> scope
<a href=https://login.tailscale.com/admin/settings/oauth target=_blank rel="noopener noreferrer">here</a>, and run with:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>wishlist --tailscale.net<span style=color:#ef8080>=</span>charm <span style=color:#afffd7>\
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#afffd7></span>  --tailscale.client.id<span style=color:#ef8080>=</span>aaabbb... <span style=color:#afffd7>\
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#afffd7></span>  --tailscale.client.secret<span style=color:#ef8080>=</span>tskey-client-aaaabbb...
</span></span></code></pre><p>This also gives Wishlist a more restricted access: only reading the device list,
nothing else.<h3>Zeroconf with mDNS</h3><p><a href=http://www.zeroconf.org target=_blank rel="noopener noreferrer">Zeroconf</a> enables service discovery in a network
without any operator intervention.
It was originally adapted from <em>AppleTalk</em>, circa 1997, and is tracked by
the <a href=https://datatracker.ietf.org/doc/html/rfc6762 target=_blank rel="noopener noreferrer">RFC 6762</a>.<p><img src=https://vhs.charm.sh/vhs-3HHXnaJAJmo4ELzkuEFYHh.gif alt="Made with VHS"><p>If you run anything Apple in your network, chances are they are all already
available in <code>.local</code> domains.
This happens because Apple devices employ
<a href=https://developer.apple.com/bonjour target=_blank rel="noopener noreferrer"><em>Bonjour</em></a> (a Zeroconf implementation)
which is installed and configured by default.<p>On Linux, you can use either <a href=https://avahi.org target=_blank rel="noopener noreferrer">Avahi</a> or
<a href=https://systemd.io target=_blank rel="noopener noreferrer">SystemD</a> for the same purpose.<p>For Avahi, installing and enabling the daemon (the package is usually named
<code>avahi-daemon</code>) will make your host available in the network as <code>hostname.local</code>.<p>To make the host available in Wishlist, though, you&rsquo;ll need to expose a
<code>_ssh._tcp</code> service.
You can do that by creating the file <code>/etc/avahi/services/ssh.service</code> with the
following contents:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#ff875f>&lt;?xml version=&#34;1.0&#34; standalone=&#34;no&#34;?&gt;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#ff875f>&lt;!DOCTYPE service-group SYSTEM &#34;avahi-service.dtd&#34;&gt;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#b083ea>&lt;service-group&gt;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#b083ea>&lt;name</span> <span style=color:#7a7ae6>replace-wildcards=</span><span style=color:#c69669>&#34;yes&#34;</span><span style=color:#b083ea>&gt;</span>%h<span style=color:#b083ea>&lt;/name&gt;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#b083ea>&lt;service&gt;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>    <span style=color:#b083ea>&lt;type&gt;</span>_ssh._tcp<span style=color:#b083ea>&lt;/type&gt;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>    <span style=color:#b083ea>&lt;port&gt;</span>22<span style=color:#b083ea>&lt;/port&gt;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#b083ea>&lt;/service&gt;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#b083ea>&lt;/service-group&gt;</span>
</span></span></code></pre><p>You can check if you have any available endpoints by running:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># on Linux:</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>avahi-browse --domain <span style=color:#ff8ec7>local</span> _ssh._tcp
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># on macOS:</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>dns-sd -B _ssh._tcp
</span></span></code></pre><p>All that being said, you can run <code>wishlist</code> with <code>--zeroconf.enabled</code>, and it&rsquo;ll operate using sensible defaults:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>wishlist --zeroconf.enabled
</span></span></code></pre><p>Run <code>wishlist --help | grep zeroconf</code> to see all options.<h3>DNS SRV</h3><p><a href=https://en.wikipedia.org/wiki/SRV_record target=_blank rel="noopener noreferrer">Service Records (SRV)</a>
are specified in DNS for defining the host name and port number of servers
for specific services.<p><img src=https://vhs.charm.sh/vhs-3YDAKLasKh7IgWNTkHKrHB.gif alt="Made with VHS"><p>It is defined as:<pre><code>_service._proto.name. ttl IN SRV priority weight port target
</code></pre><p>Wishlist will look for records with <code>_ssh._tcp</code> as <code>service</code> and <code>proto</code>.<p>You can mimic what it&rsquo;ll do with <code>dig</code>, e.g. for the <code>caarlos0.dev</code> domain:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>dig SRV _ssh._tcp.caarlos0.dev +short
</span></span></code></pre><p>So, for instance, if I want to expose a <code>SRV</code> record on port <code>2244</code>,
I would add an entry like this to my DNS:<pre><code>_ssh._tcp.caarlos0.dev. 300 IN SRV 10 2 2244 192.168.1.123.
</code></pre><p>Once you&rsquo;ve done that, you can run <code>wishlist</code> setting <code>--srv.domain</code> to
make it query the your name server and list the SRV records as endpoints.
For example:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>wishlist --srv.domain<span style=color:#ef8080>=</span>caarlos0.dev
</span></span></code></pre><h2>Hints</h2><p>You might be asking yourself: &ldquo;What if I want to set some more advanced
options in these endpoints?&rdquo; That&rsquo;s what hints are for.<p>Hints have a similar structure to the <code>endpoints</code> setting, but they work
differently: if a hint doesn&rsquo;t match a discovered endpoint, it won&rsquo;t get
added to the final endpoint list, whereas regular <code>endpoints</code> would.<p>It works by using a <code>match</code> field (which can be a glob), and then tries
to match all discovered endpoints <em>hostnames</em> with it.
If it matches, the options in that hint will be set onto the final endpoint.<p>You can set <code>port</code> (especially useful with Tailscale), <code>link</code>, <code>user</code>,
<code>description</code>, and more.
Here&rsquo;s an example showing all available fields:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#b083ea>hints</span><span style=color:#e8e8a8>:</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  - <span style=color:#b083ea>match</span><span style=color:#e8e8a8>:</span> <span style=color:#c69669>&#34;*.local&#34;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>    <span style=color:#b083ea>port</span><span style=color:#e8e8a8>:</span> <span style=color:#6eefc0>23234</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>    <span style=color:#b083ea>description</span><span style=color:#e8e8a8>:</span> <span style=color:#c69669>&#34;A description of this endpoint.\nCan have multiple lines.&#34;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>    <span style=color:#b083ea>user</span><span style=color:#e8e8a8>:</span> notme
</span></span><span style=display:flex;><span style=line-height:1.4em;>    <span style=color:#b083ea>remote_command</span><span style=color:#e8e8a8>:</span> uptime -a
</span></span><span style=display:flex;><span style=line-height:1.4em;>    <span style=color:#b083ea>forward_agent</span><span style=color:#e8e8a8>:</span> <span style=color:#0af>true</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>    <span style=color:#b083ea>request_tty</span><span style=color:#e8e8a8>:</span> <span style=color:#0af>true</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>    <span style=color:#b083ea>connect_timeout</span><span style=color:#e8e8a8>:</span> 10s
</span></span><span style=display:flex;><span style=line-height:1.4em;>    <span style=color:#b083ea>proxy_jump</span><span style=color:#e8e8a8>:</span> user@host:22
</span></span><span style=display:flex;><span style=line-height:1.4em;>    <span style=color:#b083ea>link</span><span style=color:#e8e8a8>:</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>      <span style=color:#b083ea>name</span><span style=color:#e8e8a8>:</span> Optional link name
</span></span><span style=display:flex;><span style=line-height:1.4em;>      <span style=color:#b083ea>url</span><span style=color:#e8e8a8>:</span> https://github.com/charmbracelet/wishlist
</span></span><span style=display:flex;><span style=line-height:1.4em;>    <span style=color:#b083ea>identity_files</span><span style=color:#e8e8a8>:</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>      - ~/.ssh/id_ed25519
</span></span><span style=display:flex;><span style=line-height:1.4em;>      - ~/.ssh/charm_id_ed25519
</span></span><span style=display:flex;><span style=line-height:1.4em;>    <span style=color:#b083ea>set_env</span><span style=color:#e8e8a8>:</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>      - FOO=bar
</span></span><span style=display:flex;><span style=line-height:1.4em;>      - BAR=baz
</span></span><span style=display:flex;><span style=line-height:1.4em;>    <span style=color:#b083ea>send_env</span><span style=color:#e8e8a8>:</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>      - LC_*
</span></span><span style=display:flex;><span style=line-height:1.4em;>      - LANG
</span></span><span style=display:flex;><span style=line-height:1.4em;>      - SOME_ENV
</span></span></code></pre><p>You can see the full, commented out, configuration file
<a href=https://github.com/charmbracelet/wishlist/blob/main/_example/config.yaml target=_blank rel="noopener noreferrer">here</a>.<p>Once you have your configuration file, run <code>wishlist</code> passing its path
to <code>--config</code>.
Wishlist will then discover all the endpoints (through all options available),
and then, if there are any <code>hints</code>, iterate through them and apply
them to the discovered nodes.<p>Finally, on server mode, you can specify a <code>--endpoints.refresh.interval</code>,
so Wishlist will re-discover the nodes and also reload the configuration file,
re-applying the hints too.<hr><p>We believe that those features will make your Wishlist-powered SSH directories
easier to maintain and to use, and can&rsquo;t wait to see what you&rsquo;ll do with it.]]></content:encoded>
      <author>Carlos Becker</author>
      <guid>Wishlist Endpoint Discovery</guid>
      <source>https://charm.land/blog/wishlist-sd/</source>
    </item>
    <item>
      <title>Email in your terminal</title>
      <link>https://charm.land/blog/pop/</link>
      <description>Learn how to use Pop, Charm&#39;s latest app, to send email from the terminal.</description>
      <content:encoded><![CDATA[<h1>Email in your terminal</h1><p>By Maas Lalani on 17 July 2023<h2>Introducing Pop</h2><a class="cart pop" data-repo=charmbracelet/pop href=https://github.com/charmbracelet/pop target=_blank><div class=info><picture><source srcset=/po.91ccca7d84917a24.webp type=image/webp><img src=/po.ca6ed0d5b0a01305.png alt="Pop mascot"></picture><h3 class=puffy><span data-text=Pop>Pop</span></h3><ul class=badges><li><span>Email</span><span>Sent</span></ul><p>Send emails from your terminal</div><div class=art><video loop muted playsinline>
<source src=/pop.f815d1534b9933cc.webm type=video/webm><source src=/po.8f7515407323d352.mp4 type=video/mp4></video><div></div></div></a><p>At Charm, we love the terminal. We also send lots of emails. So, we thought it
would be awesome to have a way to send emails from the terminal. Of course, this
hypothetical tool would need to support markdown formatting and file
attachments.<p>So, we built <a href=https://github.com/charmbracelet/pop target=_blank rel="noopener noreferrer">Pop</a>, a tool to send email
from the terminal. Pop supports markdown-based formatting, file attachments, and
has a command-line interface for when you need to write scripts.<p>Watch how quickly you can send an email, without ever leaving your terminal:</p><img width=600 src=https://vhs.charm.sh/vhs-25OHmk90ODL9BefXZ1P99I.gif alt="pop mail text-based client"><p>Even quicker with the command-line interface:</p><img width=600 src=https://vhs.charm.sh/vhs-28eAQHRSZilAiXKqTccLCr.gif alt="pop mail command line client"><p>If you pass flags to <code>pop</code> but it&rsquo;s not enough to send a valid email, <code>pop</code> will
launch the TUI with the flags passed as pre-populated values.<h2>Powered by Resend</h2><p>We partnered with our friends at <a href=https://resend.com target=_blank rel="noopener noreferrer">Resend</a> to launch Pop.</p><img width=1200 src=https://stuff.charm.sh/pop/resend-x-charm.png alt="Resend and Charm logos"><p>All emails send through <a href=https://resend.com target=_blank rel="noopener noreferrer">Resend</a> APIs under-the-hood. This
lets you send emails from custom domains and manage your sent emails from their
web interface if necessary.<p>To use <a href=https://github.com/charmbracelet/pop target=_blank rel="noopener noreferrer">Pop</a>, you&rsquo;ll need to export a
<code>RESEND_API_KEY</code>, you can also set <code>RESEND_FROM</code> to avoid having to type in your
sending email.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#ff8ec7>export</span> RESEND_API_KEY<span style=color:#ef8080>=</span><span style=color:#0af>$(</span>pass RESEND_API_KEY<span style=color:#0af>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#ff8ec7>export</span> RESEND_FROM<span style=color:#ef8080>=</span>pop@charm.sh
</span></span></code></pre><h3>Feedback</h3><p>Send us your feedback (with <a href=https://github.com/charmbracelet/pop target=_blank rel="noopener noreferrer">Pop</a>) and tell us what you think:<p><a href=mailto:vt100@charm.sh>vt100@charm.sh</a>]]></content:encoded>
      <author>Maas Lalani</author>
      <guid>Email in your terminal</guid>
      <pubDate>Mon, 17 Jul 2023 00:00:00 +0000</pubDate>
      <source>https://charm.land/blog/pop/</source>
    </item>
    <item>
      <title>Recipe: Let’s Build a Kaomoji Generator</title>
      <link>https://charm.land/blog/kamoji-generator/</link>
      <description>Harnessing the power of AI for the greater good.</description>
      <content:encoded><![CDATA[<h1>Recipe: Let’s Build a Kaomoji Generator</h1><p>By Christian Rocha on 22 May 2023<figure><video autoplay loop playsinline muted>
<source width=800 src=https://stuff.charm.sh/blog/kamoji/kamoji-cli.webm type=video/webm><source width=800 src=https://stuff.charm.sh/blog/kamoji/kamoji-cli.mp4 type=video/mp4></video><figcaption>This is what we’re gonna make.</figcaption></figure><p>Before the emoji there was the Kaomoji. A bunch of rando unicode letters smashed
together to create cute, little images.<ul><li><code>ʕっ•ᴥ•ʔっ</code> Like a bear reaching out to hug you.<li><code>｡:ﾟ(｡ﹷ ‸ ﹷ ✿)</code> Or a pouty child with a flower in her hair.<li><code>((งง •̀•̀__•́•́))งง</code> Or a dude who is so ready to get into a fight that he&rsquo;s shaking with fury.</ul><p>Like internet snowflakes, Kaomojis have seemingly infinite number of
permutations. You could have an angry bear doing a table flip, or a melancholy
man tossing stars to the wind.<p>But honestly, who has the time to come up with these? Or to research the best
ones? Computers do, that&rsquo;s who. So we&rsquo;re gonna build a little Kaomoji generator
with <a href=https://github.com/charmbracelet/mods target=_blank rel="noopener noreferrer">Mods</a>, <a href=https://github.com/charmbracelet/gum target=_blank rel="noopener noreferrer">Gum</a>, and <code>bash</code>.<h2>Mods</h2><figure><a href=https://github.com/charmbracelet/mods><picture><source srcset=https://stuff.charm.sh/blog/kamoji/mods.webp type=image/webp><img width=670 src=https://stuff.charm.sh/blog/kamoji/mods.png alt="The Mods Logo"></picture></a><figcaption>The modchip of AI.</figcaption></figure><p>The keystone to all this is <a href=https://github.com/charmbracelet/mods target=_blank rel="noopener noreferrer">Mods</a>, an open source tool that brings AI to
the command line. It&rsquo;s especially good with pipelines and generally working
with other commmand line tools in true unix fashion. You can install it
<a href=https://github.com/charmbracelet/mods#installation target=_blank rel="noopener noreferrer">here</a>.<p>You&rsquo;ll also want to <a href=https://github.com/charmbracelet/gum#installation target=_blank rel="noopener noreferrer">install Gum</a>.<h2>The script</h2><p>So let&rsquo;s get to it already. The basic approach we&rsquo;re taking is:<ol><li>Ask <code>mods</code> to generate some Kaomojis<li>Use <code>gum</code> to pick one<li>Copy the Kaomoji to the clipboard</ol><p>And here’s how we&rsquo;re doing it:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#ff875f>#!/usr/bin/env bash
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#ff875f></span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># If the user passes &#39;-h&#39;, &#39;--help&#39;, or &#39;help&#39; print out a little bit of help.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># text.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>case</span> <span style=color:#c69669>&#34;</span>$1<span style=color:#c69669>&#34;</span> in
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#c69669>&#34;-h&#34;</span> <span style=color:#e8e8a8>|</span> <span style=color:#c69669>&#34;--help&#34;</span> <span style=color:#e8e8a8>|</span> <span style=color:#c69669>&#34;help&#34;</span><span style=color:#ef8080>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>        <span style=color:#ff8ec7>printf</span> <span style=color:#c69669>&#39;Generate kaomojis on request.\n\n&#39;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>        <span style=color:#ff8ec7>printf</span> <span style=color:#c69669>&#39;Usage: %s [kind]\n&#39;</span> <span style=color:#c69669>&#34;</span><span style=color:#0af>$(</span>basename <span style=color:#c69669>&#34;</span>$0<span style=color:#c69669>&#34;</span><span style=color:#0af>)</span><span style=color:#c69669>&#34;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>        <span style=color:#ff8ec7>exit</span> <span style=color:#6eefc0>1</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>        <span style=color:#e8e8a8>;;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>esac</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># The user can pass an argument like &#34;bear&#34; or &#34;angry&#34; to specify the general</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># kind of Kaomoji produced.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>sentiment<span style=color:#ef8080>=</span><span style=color:#c69669>&#34;&#34;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>if</span> <span style=color:#ef8080>[[</span> $1 !<span style=color:#ef8080>=</span> <span style=color:#c69669>&#34;&#34;</span> <span style=color:#ef8080>]]</span><span style=color:#e8e8a8>;</span> <span style=color:#0af>then</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	sentiment<span style=color:#ef8080>=</span><span style=color:#c69669>&#34; </span>$1<span style=color:#c69669>&#34;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>fi</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># Ask mods to generate Kaomojis. Save the output in a variable.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>kaomoji<span style=color:#ef8080>=</span><span style=color:#c69669>&#34;</span><span style=color:#0af>$(</span>mods <span style=color:#c69669>&#34;generate 10</span><span style=color:#c69669>${</span>sentiment<span style=color:#c69669>}</span><span style=color:#c69669> kaomojis. number them and put each one on its own line.&#34;</span><span style=color:#0af>)</span><span style=color:#c69669>&#34;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>if</span> <span style=color:#ef8080>[[</span> $kaomoji <span style=color:#ef8080>==</span> <span style=color:#c69669>&#34;&#34;</span> <span style=color:#ef8080>]]</span><span style=color:#e8e8a8>;</span> <span style=color:#0af>then</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#ff8ec7>exit</span> <span style=color:#6eefc0>1</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>fi</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># Pipe mods output to gum so the user can choose the perfect kaomoji. Save that</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># choice in a variable. Also note that we&#39;re using cut to drop the item number</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># in front of the Kaomoji.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>choice<span style=color:#ef8080>=</span><span style=color:#c69669>&#34;</span><span style=color:#0af>$(</span><span style=color:#ff8ec7>echo</span> <span style=color:#c69669>&#34;</span>$kaomoji<span style=color:#c69669>&#34;</span> <span style=color:#e8e8a8>|</span> gum choose <span style=color:#e8e8a8>|</span> cut -d <span style=color:#c69669>&#39; &#39;</span> -f 2<span style=color:#0af>)</span><span style=color:#c69669>&#34;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>if</span> <span style=color:#ef8080>[[</span> $choice <span style=color:#ef8080>==</span> <span style=color:#c69669>&#34;&#34;</span> <span style=color:#ef8080>]]</span><span style=color:#e8e8a8>;</span> <span style=color:#0af>then</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#ff8ec7>exit</span> <span style=color:#6eefc0>1</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>fi</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># If xsel (X11) or pbcopy (macOS) exists, copy to the clipboard. If not, just</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># print the Kaomoji.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>if</span> <span style=color:#ff8ec7>command</span> -v xsel <span style=color:#e8e8a8>&amp;</span>&gt; /dev/null<span style=color:#e8e8a8>;</span> <span style=color:#0af>then</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#ff8ec7>printf</span> <span style=color:#c69669>&#39;%s&#39;</span> <span style=color:#c69669>&#34;</span>$choice<span style=color:#c69669>&#34;</span> <span style=color:#e8e8a8>|</span> xclip -sel clip <span style=color:#676767># X11</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>elif</span> <span style=color:#ff8ec7>command</span> -v pbcopy <span style=color:#e8e8a8>&amp;</span>&gt; /dev/null<span style=color:#e8e8a8>;</span> <span style=color:#0af>then</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#ff8ec7>printf</span> <span style=color:#c69669>&#39;%s&#39;</span> <span style=color:#c69669>&#34;</span>$choice<span style=color:#c69669>&#34;</span> <span style=color:#e8e8a8>|</span> pbcopy <span style=color:#676767># macOS</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>else</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#676767># We can&#39;t copy, so just print it out.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#ff8ec7>printf</span> <span style=color:#c69669>&#39;Here you go: %s\n&#39;</span> <span style=color:#c69669>&#34;</span>$choice<span style=color:#c69669>&#34;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#ff8ec7>exit</span> <span style=color:#6eefc0>0</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>fi</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># We&#39;re done!</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#ff8ec7>printf</span> <span style=color:#c69669>&#39;Copied %s to the clipboard\n&#39;</span> <span style=color:#c69669>&#34;</span>$choice<span style=color:#c69669>&#34;</span>
</span></span></code></pre><p>To execute the script just call it with <code>bash</code>:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>$ bash kaomoji
</span></span></code></pre><p>Or make the script executable and call it directly:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>$ chmod +x ./kaomoji
</span></span><span style=display:flex;><span style=line-height:1.4em;>$ ./kaomoji
</span></span></code></pre><h2>Going further</h2><p>How would you improve this script? Here are a few ideas:<ul><li>Add an argument to specify the number of Kaomojis generated<li>Let the user choose more than one Kaomoji with <code>gum choose --no-limit</code><li>Colorize the output with <a href=https://github.com/charmbracelet/gum#style target=_blank rel="noopener noreferrer"><code>gum style</code></a><li>Save your fave Kaomojis to a <a href=https://github.com/charmbracelet/skate target=_blank rel="noopener noreferrer">Skate</a> database</ul><h2>Whatcha think?</h2><p>Have some feedback on this post? We’d love to hear. Let us know in
<a href=https://charm.sh/chat>Discord</a> or via email at <a href=mailto:vt100@charm.sh>vt100@charm.sh</a>.]]></content:encoded>
      <author>Christian Rocha</author>
      <guid>Recipe: Let’s Build a Kaomoji Generator</guid>
      <pubDate>Mon, 22 May 2023 00:00:00 +0000</pubDate>
      <source>https://charm.land/blog/kamoji-generator/</source>
    </item>
    <item>
      <title>Writing Bubble Tea Tests</title>
      <link>https://charm.land/blog/teatest/</link>
      <description>Learn how to use x/exp/teatest to write tests for your Bubble Tea apps.</description>
      <content:encoded><![CDATA[<h1>Writing Bubble Tea Tests</h1><p>By Carlos Becker on 8 May 2023<p>Last week we launched our <a href=https://github.com/charmbracelet/x target=_blank rel="noopener noreferrer"><code>github.com/charmbracelet/x</code></a> repository,
which will contain experimental code that we&rsquo;re not ready to promise any
compatibility guarantees on just yet.<p>The first module there is called <a href=https://github.com/charmbracelet/x/tree/main/exp/teatest target=_blank rel="noopener noreferrer"><code>teatest</code></a>.
It is a library that aims to help you test your <a href=https://github.com/charmbracelet/bubbletea target=_blank rel="noopener noreferrer">Bubble Tea</a> apps.<p>You can assert the entire output of your program, parts of it, and/or its
internal <code>tea.Model</code> state.<p>In this post we&rsquo;ll add tests to an existing app using current&rsquo;s <code>teatest</code>
version API.<h2>The app</h2><p>Our example app is a simple <code>sleep</code>-like program, that shows how much time
is left. It is similar to my <a href=https://github.com/caarlos0/timer target=_blank rel="noopener noreferrer"><code>timer</code></a> TUI, if you&rsquo;re interested in
something more complete.<p>Without further ado, let&rsquo;s create the app.<p>First, navigate
<a href=https://github.com/charmbracelet/bubbletea-app-template/generate target=_blank rel="noopener noreferrer">here</a> to
create a new repository based on our
<a href=https://github.com/charmbracelet/bubbletea-app-template target=_blank rel="noopener noreferrer">bubbletea-app-template</a>
repository.<p>Then clone it. In my case, I called it <code>teatest-example</code>:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>gh repo clone caarlos0/teatest-example
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#ff8ec7>cd</span> teatest-example
</span></span><span style=display:flex;><span style=line-height:1.4em;>go mod tidy
</span></span><span style=display:flex;><span style=line-height:1.4em;>$EDITOR .
</span></span></code></pre><p>This example will just sleep until the user presses <code>q</code> to exit.<p>With a few modifications we can get what we want:<ul><li>Add a <code>duration time.Duration</code> field to the <code>model</code>. We&rsquo;ll use this to keep
track of how long we should sleep.<li>Add a <code>start time.Time</code> field to the <code>model</code> to mark when we started the
countdown.<li>The <code>initialModel</code> needs to take the <code>duration</code> as an argument. Set it into the
<code>model</code>, as well as setting <code>start</code> to <code>time.Now()</code>.<li>Add a <code>timeLeft</code> method to the model, which calculates how long we still
need to sleep.<li>In the <code>Update</code> method, we need to check if that <code>timeLeft > 0</code>, and quit
otherwise.<li>In the <code>View</code> method, we need to display how much time is left.<li>Finally, in <code>main</code> we parse <code>os.Args[1]</code> to a <code>time.Duration</code> and pass it down
to <code>initialModel</code>.</ul><p>And that&rsquo;s pretty much it. Here&rsquo;s the
<a href=https://github.com/caarlos0/teatest-example/commit/ff0da36c7c68fef74cf173f5407e70b4347a178b target=_blank rel="noopener noreferrer">link to the full diff</a>.<h2>Imports</h2><p>Before anything else, we need to import the <code>teatest</code> package:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>go get github.com/charmbracelet/x/exp/teatest@latest
</span></span></code></pre><h2>The full output test</h2><p>Next let&rsquo;s create a <code>main_test.go</code> and start with a simple test that asserts
the entire final output of the app.<p>Here&rsquo;s what it looks like:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767>// main_test.go
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767></span><span style=color:#0af>func</span> <span style=color:#00d787>TestFullOutput</span><span style=color:#e8e8a8>(</span>t <span style=color:#ef8080>*</span>testing<span style=color:#e8e8a8>.</span>T<span style=color:#e8e8a8>)</span> <span style=color:#e8e8a8>{</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	m <span style=color:#ef8080>:=</span> <span style=color:#00d787>initialModel</span><span style=color:#e8e8a8>(</span>time<span style=color:#e8e8a8>.</span>Second<span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	tm <span style=color:#ef8080>:=</span> teatest<span style=color:#e8e8a8>.</span><span style=color:#00d787>NewTestModel</span><span style=color:#e8e8a8>(</span>t<span style=color:#e8e8a8>,</span> m<span style=color:#e8e8a8>,</span> teatest<span style=color:#e8e8a8>.</span><span style=color:#00d787>WithInitialTermSize</span><span style=color:#e8e8a8>(</span><span style=color:#6eefc0>300</span><span style=color:#e8e8a8>,</span> <span style=color:#6eefc0>100</span><span style=color:#e8e8a8>))</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	out<span style=color:#e8e8a8>,</span> err <span style=color:#ef8080>:=</span> io<span style=color:#e8e8a8>.</span><span style=color:#00d787>ReadAll</span><span style=color:#e8e8a8>(</span>tm<span style=color:#e8e8a8>.</span><span style=color:#00d787>FinalOutput</span><span style=color:#e8e8a8>(</span>t<span style=color:#e8e8a8>))</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#0af>if</span> err <span style=color:#ef8080>!=</span> <span style=color:#0af>nil</span> <span style=color:#e8e8a8>{</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>		t<span style=color:#e8e8a8>.</span><span style=color:#00d787>Error</span><span style=color:#e8e8a8>(</span>err<span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#e8e8a8>}</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	teatest<span style=color:#e8e8a8>.</span><span style=color:#00d787>RequireEqualOutput</span><span style=color:#e8e8a8>(</span>t<span style=color:#e8e8a8>,</span> out<span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#e8e8a8>}</span>
</span></span></code></pre><ol><li>We created a <code>model</code> that will sleep for 1 second.<li>We passed it to <code>teatest.NewTestModel</code>, also ensuring a fixed terminal size<li>We ask for the <code>FinalOutput</code>, and read it all.<li><code>Final</code> means it will wait for the <code>tea.Program</code> to finish before
returning, so be wary that this will block until that condition is met.<li>We check if the output we got is equal the output in the <em>golden file</em>.</ol><p>If you just run <code>go test ./...</code>, you&rsquo;ll see that it errors. That&rsquo;s because we
don&rsquo;t have a golden file yet. To fix that, run:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>go <span style=color:#ff8ec7>test</span> -v ./... -update
</span></span></code></pre><p>The <code>-update</code> flag comes from the <code>teatest</code> package. It will update the
golden file (or create it if it doesn&rsquo;t exist).<p>You can also <code>cat</code> the golden file to see what it looks like:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>&gt; cat testdata/TestFullOutput.golden
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;>    ⣻  sleeping 0s... press q to quit
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span></code></pre><p>In subsequent tests, you&rsquo;ll want to run <code>go test</code> without the <code>-update</code>, unless
you changed the output portion of your program.<p>Here&rsquo;s the <a href=https://github.com/caarlos0/teatest-example/commit/c42a7180d7a9e39486315bcd9992182f201136a6 target=_blank rel="noopener noreferrer">link to the full diff</a>.<h2>The final model test</h2><p>Bubble Tea returns the final model after it finishes running, so we can also
assert against that final model:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767>// main_test.go
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767></span><span style=color:#0af>func</span> <span style=color:#00d787>TestFinalModel</span><span style=color:#e8e8a8>(</span>t <span style=color:#ef8080>*</span>testing<span style=color:#e8e8a8>.</span>T<span style=color:#e8e8a8>)</span> <span style=color:#e8e8a8>{</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	tm <span style=color:#ef8080>:=</span> teatest<span style=color:#e8e8a8>.</span><span style=color:#00d787>NewTestModel</span><span style=color:#e8e8a8>(</span>t<span style=color:#e8e8a8>,</span> <span style=color:#00d787>initialModel</span><span style=color:#e8e8a8>(</span>time<span style=color:#e8e8a8>.</span>Second<span style=color:#e8e8a8>),</span> teatest<span style=color:#e8e8a8>.</span><span style=color:#00d787>WithInitialTermSize</span><span style=color:#e8e8a8>(</span><span style=color:#6eefc0>300</span><span style=color:#e8e8a8>,</span> <span style=color:#6eefc0>100</span><span style=color:#e8e8a8>))</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	fm <span style=color:#ef8080>:=</span> tm<span style=color:#e8e8a8>.</span><span style=color:#00d787>FinalModel</span><span style=color:#e8e8a8>(</span>t<span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	m<span style=color:#e8e8a8>,</span> ok <span style=color:#ef8080>:=</span> fm<span style=color:#e8e8a8>.(</span>model<span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#0af>if</span> <span style=color:#e8e8a8>!</span>ok <span style=color:#e8e8a8>{</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>		t<span style=color:#e8e8a8>.</span><span style=color:#00d787>Fatalf</span><span style=color:#e8e8a8>(</span><span style=color:#c69669>&#34;final model have the wrong type: %T&#34;</span><span style=color:#e8e8a8>,</span> fm<span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#e8e8a8>}</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#0af>if</span> m<span style=color:#e8e8a8>.</span>duration <span style=color:#ef8080>!=</span> time<span style=color:#e8e8a8>.</span>Second <span style=color:#e8e8a8>{</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>		t<span style=color:#e8e8a8>.</span><span style=color:#00d787>Errorf</span><span style=color:#e8e8a8>(</span><span style=color:#c69669>&#34;m.duration != 1s: %s&#34;</span><span style=color:#e8e8a8>,</span> m<span style=color:#e8e8a8>.</span>duration<span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#e8e8a8>}</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#0af>if</span> m<span style=color:#e8e8a8>.</span>start<span style=color:#e8e8a8>.</span><span style=color:#00d787>After</span><span style=color:#e8e8a8>(</span>time<span style=color:#e8e8a8>.</span><span style=color:#00d787>Now</span><span style=color:#e8e8a8>().</span><span style=color:#00d787>Add</span><span style=color:#e8e8a8>(</span><span style=color:#ef8080>-</span><span style=color:#6eefc0>1</span> <span style=color:#ef8080>*</span> time<span style=color:#e8e8a8>.</span>Second<span style=color:#e8e8a8>))</span> <span style=color:#e8e8a8>{</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>		t<span style=color:#e8e8a8>.</span><span style=color:#00d787>Errorf</span><span style=color:#e8e8a8>(</span><span style=color:#c69669>&#34;m.start should be more than 1 second ago: %s&#34;</span><span style=color:#e8e8a8>,</span> m<span style=color:#e8e8a8>.</span>start<span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#e8e8a8>}</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#e8e8a8>}</span>
</span></span></code></pre><p>The setup is basically the same as the previous test, but instead of the asking
for the <code>FinalOutput</code>, we ask for the <code>FinalModel</code>.<p>We then need to cast it to the concrete type and then, finally, we assert for
the <code>m.duration</code> and <code>m.start</code>.<p>Here&rsquo;s the <a href=https://github.com/caarlos0/teatest-example/commit/d474afa44676a3485623ce6b6966ca78fbdf4829 target=_blank rel="noopener noreferrer">link to the full diff</a>.<h2>Intermediate output and sending messages</h2><p>Another useful test case is to ensure things happen during the test.
We also need to interact with the program while its running.<p>Let&rsquo;s write a quick test exploring these options:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767>// main_test.go
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767></span><span style=color:#0af>func</span> <span style=color:#00d787>TestOuput</span><span style=color:#e8e8a8>(</span>t <span style=color:#ef8080>*</span>testing<span style=color:#e8e8a8>.</span>T<span style=color:#e8e8a8>)</span> <span style=color:#e8e8a8>{</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	tm <span style=color:#ef8080>:=</span> teatest<span style=color:#e8e8a8>.</span><span style=color:#00d787>NewTestModel</span><span style=color:#e8e8a8>(</span>t<span style=color:#e8e8a8>,</span> <span style=color:#00d787>initialModel</span><span style=color:#e8e8a8>(</span><span style=color:#6eefc0>10</span><span style=color:#ef8080>*</span>time<span style=color:#e8e8a8>.</span>Second<span style=color:#e8e8a8>),</span> teatest<span style=color:#e8e8a8>.</span><span style=color:#00d787>WithInitialTermSize</span><span style=color:#e8e8a8>(</span><span style=color:#6eefc0>300</span><span style=color:#e8e8a8>,</span> <span style=color:#6eefc0>100</span><span style=color:#e8e8a8>))</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	teatest<span style=color:#e8e8a8>.</span><span style=color:#00d787>WaitFor</span><span style=color:#e8e8a8>(</span>t<span style=color:#e8e8a8>,</span> tm<span style=color:#e8e8a8>.</span><span style=color:#00d787>Output</span><span style=color:#e8e8a8>(),</span> <span style=color:#0af>func</span><span style=color:#e8e8a8>(</span>bts <span style=color:#e8e8a8>[]</span><span style=color:#6e6ed8>byte</span><span style=color:#e8e8a8>)</span> <span style=color:#6e6ed8>bool</span> <span style=color:#e8e8a8>{</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>		<span style=color:#0af>return</span> bytes<span style=color:#e8e8a8>.</span><span style=color:#00d787>Contains</span><span style=color:#e8e8a8>(</span>bts<span style=color:#e8e8a8>,</span> <span style=color:#e8e8a8>[]</span><span style=color:#ff8ec7>byte</span><span style=color:#e8e8a8>(</span><span style=color:#c69669>&#34;sleeping 8s&#34;</span><span style=color:#e8e8a8>))</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#e8e8a8>},</span> teatest<span style=color:#e8e8a8>.</span><span style=color:#00d787>WithCheckInterval</span><span style=color:#e8e8a8>(</span>time<span style=color:#e8e8a8>.</span>Millisecond<span style=color:#ef8080>*</span><span style=color:#6eefc0>100</span><span style=color:#e8e8a8>),</span> teatest<span style=color:#e8e8a8>.</span><span style=color:#00d787>WithDuration</span><span style=color:#e8e8a8>(</span>time<span style=color:#e8e8a8>.</span>Second<span style=color:#ef8080>*</span><span style=color:#6eefc0>3</span><span style=color:#e8e8a8>))</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	tm<span style=color:#e8e8a8>.</span><span style=color:#00d787>Send</span><span style=color:#e8e8a8>(</span>tea<span style=color:#e8e8a8>.</span>KeyMsg<span style=color:#e8e8a8>{</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>		Type<span style=color:#e8e8a8>:</span>  tea<span style=color:#e8e8a8>.</span>KeyRunes<span style=color:#e8e8a8>,</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>		Runes<span style=color:#e8e8a8>:</span> <span style=color:#e8e8a8>[]</span><span style=color:#ff8ec7>rune</span><span style=color:#e8e8a8>(</span><span style=color:#c69669>&#34;q&#34;</span><span style=color:#e8e8a8>),</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#e8e8a8>})</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	tm<span style=color:#e8e8a8>.</span><span style=color:#00d787>WaitFinished</span><span style=color:#e8e8a8>(</span>t<span style=color:#e8e8a8>,</span> teatest<span style=color:#e8e8a8>.</span><span style=color:#00d787>WithFinalTimeout</span><span style=color:#e8e8a8>(</span>time<span style=color:#e8e8a8>.</span>Second<span style=color:#e8e8a8>))</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#e8e8a8>}</span>
</span></span></code></pre><p>We setup our <code>teatest</code> in the same fashion as the previous test, then we assert
that the app, at some point, is showing <code>sleeping 8s</code>, meaning 2 seconds have
elapsed. We give that condition 3 seconds of time to be met, or else we fail.<p>Finally, we send a <code>tea.KeyMsg</code> with the character <code>q</code> on it, which should cause
the app to quit.<p>To ensure it quits in time, we <code>WaitFinished</code> with a timeout of 1 second.
This way we can be sure we finished because we send a <code>q</code> key press, not because
the program runs its 10 seconds out.<p>Here&rsquo;s the <a href=https://github.com/caarlos0/teatest-example/commit/f63faaa3338af783ac40deb22e7196d7898d052a target=_blank rel="noopener noreferrer">link to the full diff</a>.<h2>The CI is failing. What now?</h2><p>Once you push your commits GitHub Actions will test them and likely fail.<p>The reason for this is because your local golden file was generated with
whatever color profile the terminal <code>go test</code> was run in reported while GitHub
Actions is probably reporting something different.<p>Luckily, we can force everything to use the same color profile:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767>// main_test.go
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767></span><span style=color:#0af>func</span> <span style=color:#00d787>init</span><span style=color:#e8e8a8>()</span> <span style=color:#e8e8a8>{</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	lipgloss<span style=color:#e8e8a8>.</span><span style=color:#00d787>SetColorProfile</span><span style=color:#e8e8a8>(</span>termenv<span style=color:#e8e8a8>.</span>Ascii<span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#e8e8a8>}</span>
</span></span></code></pre><p>In this app we don&rsquo;t need to worry too much about colors, so its fine to use the
<code>Ascii</code> profile, which disables colors.<p>Another thing that might cause tests to fail is line endings. The golden files
look like text, but their line endings shouldn&rsquo;t be messed with—and git might
just do that.<p>To remedy the situation, I recommend adding this to your <code>.gitattributes</code> file:<pre><code class=language-gitattributes>*.golden -text
</code></pre><p>This will keep Git from handling them as text files.<p>Here&rsquo;s the <a href=https://github.com/caarlos0/teatest-example/commit/93d97374a7c9c40711a342c3ab3cc51f3a5a58dd target=_blank rel="noopener noreferrer">link to the full diff</a>.<h2>Final words</h2><p>This is an experimental, work in progress library, hence the
<code>github.com/charmbracelet/x/exp/teatest</code>
package name.<p>We encourage you to try it out in your projects and report back what you find.<p>And, if you&rsquo;re interested, here&rsquo;s the <a href=https://github.com/caarlos0/teatest-example target=_blank rel="noopener noreferrer">link to the repository</a> for this
post.]]></content:encoded>
      <author>Carlos Becker</author>
      <guid>Writing Bubble Tea Tests</guid>
      <pubDate>Mon, 08 May 2023 00:00:00 +0000</pubDate>
      <source>https://charm.land/blog/teatest/</source>
    </item>
    <item>
      <title>Self-hosted Soft Serve</title>
      <link>https://charm.land/blog/self-hosted-soft-serve/</link>
      <description>Discover how to set up and manage your own self-hosted Soft Serve Git server with HTTPS and SSH support in this comprehensive guide.</description>
      <content:encoded><![CDATA[<h1>Self-hosted Soft Serve</h1><p>By Ayman Bagabas on 28 April 2023<p><a href=https://github.com/charmbracelet/soft-serve target=_blank rel="noopener noreferrer">Soft Serve</a> is a self-hostable Git server for the command-line. It supports Git over HTTP(s), SSH, and the <a href=https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols#_the_git_protocol target=_blank rel="noopener noreferrer">Git Protocol</a>. <a href=https://github.com/charmbracelet/soft-serve target=_blank rel="noopener noreferrer">Soft Serve</a> also comes with a simple straight-forward user management interface for teams.<p>In this post, we will go through how to set up your <a href=https://github.com/charmbracelet/soft-serve target=_blank rel="noopener noreferrer">Soft Serve</a> instance. This includes setting up SSH access, HTTPS using <a href=https://certbot.eff.org/ target=_blank rel="noopener noreferrer">Certbot</a>, and how to manage your <a href=https://github.com/charmbracelet/soft-serve target=_blank rel="noopener noreferrer">Soft Serve</a> instance.<h2>Prerequisites</h2><p>In this post, we are assuming that you have a basic knowledge of networking, a general understanding of how to use Linux and the command-line, and are comfortable using <code>git</code> commands.<p>You will need:<ul><li>A server to run <a href=https://github.com/charmbracelet/soft-serve target=_blank rel="noopener noreferrer">Soft Serve</a> on.<li>A domain name to access your server (optional).</ul><p>If you&rsquo;re using a cloud provider, make sure you have the right access settings before proceeding i.e. access tokens. Running <a href=https://github.com/charmbracelet/soft-serve target=_blank rel="noopener noreferrer">Soft Serve</a> locally or on-premise will vary depending on your setup. This post will only cover setting up <a href=https://github.com/charmbracelet/soft-serve target=_blank rel="noopener noreferrer">Soft Serve</a> on the host server using Systemd, reroute OpenSSH traffic, and set up <a href=https://certbot.eff.org/ target=_blank rel="noopener noreferrer">Certbot</a> for HTTPS.<p>We will be using a virtual machine running Ubuntu 22.04 hosted on the cloud. Many cloud providers provide virtual machine services. DigitalOcean calls them Droplets. EC2 if you&rsquo;re using AWS.<blockquote><p><strong>Note:</strong> make sure you enable access to the server and add any firewall rules. Soft Serve uses ports 23231/tcp (SSH), 23232/tcp (HTTP), and 9418/tcp (Git). We will reconfigure Soft Serve to run on port 22/tcp (SSH) and 443/tcp (HTTPS), then use port 2200/tcp for OpenSSH shell access.</blockquote><h2>Setting Up Soft Serve</h2><p>We will start by installing <a href=https://github.com/charmbracelet/soft-serve target=_blank rel="noopener noreferrer">Soft Serve</a> from Charm&rsquo;s APT repository, setting up a Systemd service, getting a <a href=https://letsencrypt.org/ target=_blank rel="noopener noreferrer">Let&rsquo;s Encrypt</a> certificate using <a href=https://certbot.eff.org/ target=_blank rel="noopener noreferrer">Certbot</a>, and lastly, reconfiguring OpenSSH to access the shell on an alternate port (since Soft Serve will be using the default SSH port). Let the fun begin!<h3>Installing Soft Serve</h3><p><a href=https://github.com/charmbracelet/soft-serve target=_blank rel="noopener noreferrer">Soft Serve</a> and all other Charm tools can be installed via APT/RPM repositories. Check out the <a href=https://github.com/charmbracelet/soft-serve#installation target=_blank rel="noopener noreferrer">installation section</a> for more options. Since we&rsquo;re using Ubuntu, we can install <a href=https://github.com/charmbracelet/soft-serve target=_blank rel="noopener noreferrer">Soft Serve</a> from the Charm APT repository:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># Retrieve and import repository key</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>sudo mkdir -p /etc/apt/keyrings
</span></span><span style=display:flex;><span style=line-height:1.4em;>curl -fsSL https://repo.charm.sh/apt/gpg.key <span style=color:#e8e8a8>|</span> sudo gpg --dearmor -o /etc/apt/keyrings/charm.gpg
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># Add APT repository source</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#ff8ec7>echo</span> <span style=color:#c69669>&#34;deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *&#34;</span> <span style=color:#e8e8a8>|</span> sudo tee /etc/apt/sources.list.d/charm.list
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># Install Soft Serve &amp; git</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>sudo apt update <span style=color:#ef8080>&amp;&amp;</span> sudo apt install soft-serve git
</span></span></code></pre><p>You&rsquo;re all set! You should now be able to run the <code>soft</code> binary.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>soft --version
</span></span></code></pre><p>Now that we have <a href=https://github.com/charmbracelet/soft-serve target=_blank rel="noopener noreferrer">Soft Serve</a> installed, let&rsquo;s run it locally.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>soft
</span></span></code></pre><pre><code>Soft Serve is a self-hostable Git server for the command line.

Usage:
  soft [command]

Available Commands:
  help           Help about any command
  serve          Start the server

Flags:
  -h, --help      help for soft
  -v, --version   version for soft

Use &quot;soft [command] --help&quot; for more information about a command.
</code></pre><p>To start the server, we can run <code>soft serve</code>. This will create a data directory that will store the repositories and database.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>2023-04-28 server: Starting Git daemon addr<span style=color:#ef8080>=</span>:9418
</span></span><span style=display:flex;><span style=line-height:1.4em;>2023-04-28 server: Starting HTTP server addr<span style=color:#ef8080>=</span>:23232
</span></span><span style=display:flex;><span style=line-height:1.4em;>2023-04-28 server: Starting SSH server addr<span style=color:#ef8080>=</span>:23231
</span></span><span style=display:flex;><span style=line-height:1.4em;>2023-04-28 server: Starting Stats server addr<span style=color:#ef8080>=</span>:23233
</span></span></code></pre><p>Well, well, we now have a running <a href=https://github.com/charmbracelet/soft-serve target=_blank rel="noopener noreferrer">Soft Serve</a> instance! But, this would be tedious to run each time our server restarts. Luckily, systemd can help us start the process on boot.<h3>Systemd</h3><p>Create a file under <code>/etc/systemd/system/soft-serve.service</code> and put your Systemd service configuration. Here we will be running <a href=https://github.com/charmbracelet/soft-serve target=_blank rel="noopener noreferrer">Soft Serve</a> as <code>root</code> to simplify things. We will place the server&rsquo;s data under <code>/var/local/lib/soft-serve</code>. Make sure you have added your SSH authorized key to the <code>SOFT_SERVE_INITIAL_ADMIN_KEYS</code> environment variables. You can remove this later once the &ldquo;admin&rdquo; user is created and has your key.<p>For a full list of <a href=https://github.com/charmbracelet/soft-serve target=_blank rel="noopener noreferrer">Soft Serve</a> server settings and environment variables refer to the <a href=https://github.com/charmbracelet/soft-serve#server-settings target=_blank rel="noopener noreferrer">Server Settings</a> section.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>[Unit]</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#7a7ae6>Description</span><span style=color:#ef8080>=</span><span style=color:#c69669>Soft Serve git server 🍦</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#7a7ae6>Documentation</span><span style=color:#ef8080>=</span><span style=color:#c69669>https://github.com/charmbracelet/soft-serve</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#7a7ae6>Requires</span><span style=color:#ef8080>=</span><span style=color:#c69669>network-online.target</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#7a7ae6>After</span><span style=color:#ef8080>=</span><span style=color:#c69669>network-online.target</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>[Service]</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#7a7ae6>Type</span><span style=color:#ef8080>=</span><span style=color:#c69669>simple</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#7a7ae6>Restart</span><span style=color:#ef8080>=</span><span style=color:#c69669>always</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#7a7ae6>RestartSec</span><span style=color:#ef8080>=</span><span style=color:#c69669>1</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#7a7ae6>ExecStartPre</span><span style=color:#ef8080>=</span><span style=color:#c69669>mkdir -p /var/local/lib/soft-serve</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#7a7ae6>ExecStart</span><span style=color:#ef8080>=</span><span style=color:#c69669>/usr/bin/soft serve</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#7a7ae6>Environment</span><span style=color:#ef8080>=</span><span style=color:#c69669>SOFT_SERVE_DATA_PATH=/var/local/lib/soft-serve</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#7a7ae6>Environment</span><span style=color:#ef8080>=</span><span style=color:#c69669>SOFT_SERVE_INITIAL_ADMIN_KEYS=&#39;ssh-ed25519 AAAAC3NzaC1lZDI1...&#39;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>[Install]</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#7a7ae6>WantedBy</span><span style=color:#ef8080>=</span><span style=color:#c69669>multi-user.target</span>
</span></span></code></pre><p>Now, reload Systemd configuration, enable and start the service.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>sudo systemctl daemon-reload
</span></span><span style=display:flex;><span style=line-height:1.4em;>sudo systemctl <span style=color:#ff8ec7>enable</span> soft-serve.service
</span></span><span style=display:flex;><span style=line-height:1.4em;>sudo systemctl start soft-serve.service
</span></span></code></pre><p>We can check the logs using <code>journalctl -u soft-serve.service</code>.<blockquote><p><strong>Tip:</strong> add <code>-f</code> flag to &ldquo;tail&rdquo; the logs as they appear. Useful when using tmux to keep an eye on logs <code>journalctl -fu soft-serve.service</code>.</blockquote><h3>HTTPS Certificate</h3><p>To be able to use HTTPS in <a href=https://github.com/charmbracelet/soft-serve target=_blank rel="noopener noreferrer">Soft Serve</a>, we will need to set up TLS certificates so that <a href=https://github.com/charmbracelet/soft-serve target=_blank rel="noopener noreferrer">Soft Serve</a> can use an encrypted connection to communicate with the world. We will be using <a href=https://certbot.eff.org/ target=_blank rel="noopener noreferrer">Certbot</a> to issue us <a href=https://letsencrypt.org/ target=_blank rel="noopener noreferrer">Let&rsquo;s Encrypt</a> certificates. Following <a href=https://certbot.eff.org/instructions target=_blank rel="noopener noreferrer">Certbot instructions</a>, we will choose <code>Other</code> and <code>Ubuntu 20</code> for the instruction options.<blockquote><p><strong>Note:</strong> if you&rsquo;re <em>not</em> using a domain name, you won&rsquo;t be able to issue an HTTPS certificate since <a href=https://letsencrypt.org/ target=_blank rel="noopener noreferrer">Let&rsquo;s Encrypt</a> doesn&rsquo;t allow the use of bare IP addresses. <a href=https://zerossl.com/ target=_blank rel="noopener noreferrer">ZeroSSL</a> is a great alternative that supports bare IP addresses.</blockquote><picture><source srcset=https://stuff.charm.sh/blog/self-hosted-soft-serve/certbot-options.png media="(max-width: 550px)"><img srcset=https://stuff.charm.sh/blog/self-hosted-soft-serve/certbot-options.png></picture><p>Now, make sure you have updated your DNS records to point your server&rsquo;s IP address to your custom domain. This is typically done using a <code>A</code> record. This will vary depending on your DNS domain provider. We will be using <code>git.example.com</code> to demonstrate issuing a certificate for the subdomain <code>git</code>.<p>Install the <code>certbot</code> cli tool using <code>snapd</code> to issue the certificate for our domain.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># Stop Soft Serve</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>sudo systemctl stop soft-serve.service
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># Install certbot from snapd</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>sudo snap install --classic certbot
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># Issue certificate</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>sudo certbot certonly --standalone
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># Enter your email address</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># Agree for terms of service</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># Enter your domain(s): git.example.com</span>
</span></span></code></pre><p>Voilà, we have an HTTPS certificate!<pre><code>Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/git.example.com/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/git.example.com/privkey.pem
This certificate expires on 2023-07-28.
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to automatically renew this certificate in the background.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If you like Certbot, please consider supporting our work by:
 * Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
 * Donating to EFF:                    https://eff.org/donate-le
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
</code></pre><p>Once we have our certificate, we will need to update our <a href=https://github.com/charmbracelet/soft-serve target=_blank rel="noopener noreferrer">Soft Serve</a> configuration to use them and point to our new https:// address.<h3>Server Configuration</h3><p><a href=https://github.com/charmbracelet/soft-serve target=_blank rel="noopener noreferrer">Soft Serve</a> stores its server configuration in the <code>config.yaml</code> file under the <em>data directory</em>. By default, it uses the relative path <code>data</code> as a <em>data directory</em>. You can override this by defining a <code>SOFT_SERVE_DATA_PATH</code> environment variable (as seen above in the systemd service file). This means that our <code>config.yaml</code> file lives under <code>/var/local/lib/soft-serve</code> since we have that as our <em>data directory</em> path.<p>To use HTTPS default port (443), we have to tell <a href=https://github.com/charmbracelet/soft-serve target=_blank rel="noopener noreferrer">Soft Serve</a> about our <a href=https://letsencrypt.org/ target=_blank rel="noopener noreferrer">Let&rsquo;s Encrypt</a> certificates. And since we are using a custom domain now, we need to update the server&rsquo;s public URL. This is the address that will you will be using to manage user access and <code>git clone</code> repositories. Lastly, we will make OpenSSH use a different port, so we can still have shell access on our remote host.<blockquote><p><strong>Info:</strong> you can override configuration settings using environment variables. For example, to override the server&rsquo;s name add <code>SOFT_SERVE_NAME='Git Melon'</code> to your <code>soft-serve.service</code> file.</blockquote><p>Let&rsquo;s edit the file and see what&rsquo;s in there 🤔<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># Make sure you have $EDITOR defined</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># Use vim &lt;3</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># export EDITOR=vim</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>sudo $EDITOR /var/local/lib/soft-serve/config.yaml
</span></span></code></pre><p>Here, we can see the default <a href=https://github.com/charmbracelet/soft-serve/tree/newbase#server-settings target=_blank rel="noopener noreferrer">server configurations</a>.<ul><li>Change the <code>ssh.listen_addr</code> to use the default SSH port (22).<li>Use <code>ssh://git.example.com</code> as our <code>ssh.public_url</code> (this will be used for clones over SSH e.g. <code>git clone git@git.example.com:repo.git</code>).<li>Change the <code>http.listen_addr</code> to use HTTPS default port (443).<li>Update <code>http.public_url</code> to point to use https:// and point to our custom domain <code>https://git.example.com</code>.<li>Set <code>http.tls_key_path</code> and <code>http.tls_cert_path</code> to use the generated <a href=https://letsencrypt.org/ target=_blank rel="noopener noreferrer">Let&rsquo;s Encrypt</a> certificates.</ul><p>The final configurations look like this:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># Soft Serve Server configurations</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># The name of the server.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># This is the name that will be displayed in the UI.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#b083ea>name</span><span style=color:#e8e8a8>:</span> <span style=color:#c69669>&#34;Soft Serve&#34;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># Log format to use. Valid values are &#34;json&#34;, &#34;logfmt&#34;, and &#34;text&#34;.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#b083ea>log_format</span><span style=color:#e8e8a8>:</span> <span style=color:#c69669>&#34;text&#34;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># The SSH server configuration.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#b083ea>ssh</span><span style=color:#e8e8a8>:</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#676767># The address on which the SSH server will listen.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#b083ea>listen_addr</span><span style=color:#e8e8a8>:</span> <span style=color:#c69669>&#34;:22&#34;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#676767># The public URL of the SSH server.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#676767># This is the address that will be used to clone repositories.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#b083ea>public_url</span><span style=color:#e8e8a8>:</span> <span style=color:#c69669>&#34;ssh://git.example.com&#34;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#676767># The path to the SSH server&#39;s private key.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#b083ea>key_path</span><span style=color:#e8e8a8>:</span> <span style=color:#c69669>&#34;ssh/soft_serve_host&#34;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#676767># The path to the server&#39;s client private key.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#676767># This key will be used to authenticate the server to make git requests to</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#676767># ssh remotes.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#b083ea>client_key_path</span><span style=color:#e8e8a8>:</span> <span style=color:#c69669>&#34;ssh/soft_serve_client_ed25519&#34;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#676767># The maximum number of seconds a connection can take.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#676767># A value of 0 means no timeout.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#b083ea>max_timeout</span><span style=color:#e8e8a8>:</span> <span style=color:#6eefc0>0</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#676767># The number of seconds a connection can be idle before it is closed.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#b083ea>idle_timeout</span><span style=color:#e8e8a8>:</span> <span style=color:#6eefc0>0</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># The Git daemon configuration.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#b083ea>git</span><span style=color:#e8e8a8>:</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#676767># The address on which the Git daemon will listen.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#b083ea>listen_addr</span><span style=color:#e8e8a8>:</span> <span style=color:#c69669>&#34;:9418&#34;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#676767># The maximum number of seconds a connection can take.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#676767># A value of 0 means no timeout.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#b083ea>max_timeout</span><span style=color:#e8e8a8>:</span> <span style=color:#6eefc0>0</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#676767># The number of seconds a connection can be idle before it is closed.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#b083ea>idle_timeout</span><span style=color:#e8e8a8>:</span> <span style=color:#6eefc0>3</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#676767># The maximum number of concurrent connections.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#b083ea>max_connections</span><span style=color:#e8e8a8>:</span> <span style=color:#6eefc0>32</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># The HTTP server configuration.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#b083ea>http</span><span style=color:#e8e8a8>:</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#676767># The address on which the HTTP server will listen.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#b083ea>listen_addr</span><span style=color:#e8e8a8>:</span> <span style=color:#c69669>&#34;:443&#34;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#676767># The path to the TLS private key.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#b083ea>tls_key_path</span><span style=color:#e8e8a8>:</span> <span style=color:#c69669>&#34;/etc/letsencrypt/live/git.example.com/privkey.pem&#34;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#676767># The path to the TLS certificate.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#b083ea>tls_cert_path</span><span style=color:#e8e8a8>:</span> <span style=color:#c69669>&#34;/etc/letsencrypt/live/git.example.com/fullchain.pem&#34;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#676767># The public URL of the HTTP server.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#676767># This is the address that will be used to clone repositories.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#676767># Make sure to use https:// if you are using TLS.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#b083ea>public_url</span><span style=color:#e8e8a8>:</span> <span style=color:#c69669>&#34;https://git.example.com&#34;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># The stats server configuration.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#b083ea>stats</span><span style=color:#e8e8a8>:</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#676767># The address on which the stats server will listen.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>  <span style=color:#b083ea>listen_addr</span><span style=color:#e8e8a8>:</span> <span style=color:#c69669>&#34;localhost:23233&#34;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># Additional admin keys.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767>#initial_admin_keys:</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767>#  - &#34;ssh-rsa AAAAB3NzaC1yc2...&#34;</span>
</span></span></code></pre><p>This looks good so far. Now before we start <a href=https://github.com/charmbracelet/soft-serve target=_blank rel="noopener noreferrer">Soft Serve</a> up again, we need to change the port that OpenSSH uses. This is specified in <code>/etc/ssh/sshd_config</code>.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>sudo $EDITOR /etc/ssh/sshd_config
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># Uncomment `#Port 22`</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># Change `Port 22` to `Port 2200`</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># Or use the power of `sed` :)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>sudo sed -i <span style=color:#c69669>&#39;s/^#Port 22/Port 2200/g&#39;</span> /etc/ssh/sshd_config
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># Restart sshd</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>sudo systemctl restart sshd
</span></span></code></pre><p>You can now access your server&rsquo;s shell on port <code>2200</code>. Try it out: <code>ssh -i &lt;my-precious-key> -p 2200 git.example.com</code>.<p>Now, let&rsquo;s start our <a href=https://github.com/charmbracelet/soft-serve target=_blank rel="noopener noreferrer">Soft Serve</a> server again.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>sudo systemctl start soft-serve.service
</span></span></code></pre><p>View logs using <code>journalctl -fu soft-serve.service</code>. Verify the server is indeed running on <code>:22</code> and <code>:443</code>.<pre><code>Apr 28 20:33:39 ip-172-31-84-249 soft[7594]: 2023-04-28 server: Starting Git daemon addr=:9418
Apr 28 20:33:39 ip-172-31-84-249 soft[7594]: 2023-04-28 server: Starting HTTP server addr=:443
Apr 28 20:33:39 ip-172-31-84-249 soft[7594]: 2023-04-28 server: Starting SSH server addr=:22
Apr 28 20:33:39 ip-172-31-84-249 soft[7594]: 2023-04-28 server: Starting Stats server addr=localhost:23233
</code></pre><h2>Manage Soft Serve</h2><p>Now that we successfully set up our <a href=https://github.com/charmbracelet/soft-serve target=_blank rel="noopener noreferrer">Soft Serve</a> server, we want to manage users, server access, and repositories. Since we added our authorized key as an environment variable above, we should be able to see all the admin commands when <code>ssh -i &lt;my-precious-key> git.example.com help</code>.<pre><code>Soft Serve is a self-hostable Git server for the command line.

Usage:
  ssh git.example.com [command]

Available Commands:
  help         Help about any command
  info         Show your info
  pubkey       Manage your public keys
  repo         Manage repositories
  set-username Set your username
  settings     Manage server settings
  user         Manage users

Flags:
  -h, --help   help for this command

Use &quot;ssh git.example.com [command] --help&quot; for more information about a command.
</code></pre><h3>Users and Access</h3><p>You can manage users using the <code>user</code> command. For example, <a href=https://github.com/charmbracelet/soft-serve target=_blank rel="noopener noreferrer">Soft Serve</a> creates a default <code>admin</code> user on the first run that uses the keys defined in <code>SOFT_SERVE_INITIAL_ADMIN_KEYS</code>. Let&rsquo;s verify that using the <code>info</code> command.<pre><code>$ ssh -i &lt;my-precious-key&gt; git.example.com info
Username: admin
Admin: true
Public keys:
  ssh-ed25519 AAAAC3NzaC1lZDI1...
</code></pre><p>We can add more keys using the <code>pubkey</code> command.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>ssh -i &lt;my-precious-key&gt; git.example.com pubkey add ssh-rsa AAAAB3NzaC1yc2...
</span></span></code></pre><p>Use the <code>user</code> command to create more users. Add <code>-a</code> to mark user as admin.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>ssh -i &lt;my-precious-key&gt; git.example.com user create lemon -a
</span></span><span style=display:flex;><span style=line-height:1.4em;>ssh -i &lt;my-precious-key&gt; git.example.com user add-pubkey lemon ssh-rsa AAAAB3NzaC1yc2...
</span></span></code></pre><p>To change the current username, use the <code>set-username</code> command.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>ssh -i &lt;my-precious-key&gt; git.example.com set-username melon
</span></span></code></pre><h3>Repositories</h3><p>Using the <code>repo</code> command, you can create, delete, import, and manage repository settings. You can add/remove collaborators using <code>repo collab</code>. Let&rsquo;s go through an example of creating a new repository, pushing code, and adding a new collaborator who can access the repo.<p>First, we will create a new repository.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>ssh -i &lt;my-precious-key&gt; git.example.com repo create hula-hoop
</span></span></code></pre><p>If you ssh into the server (without any arguments) you should see the TUI and the new repository.</p><picture><source srcset=https://stuff.charm.sh/blog/self-hosted-soft-serve/hula-hoop-tui-selected.png media="(max-width: 550px)"><img srcset=https://stuff.charm.sh/blog/self-hosted-soft-serve/hula-hoop-tui-selected.png></picture><h4>Push to Repository</h4><p>Now, let&rsquo;s push some files to the repository.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># Clone the repository</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>git clone git@git.example.com:hula-hoop.git
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#ff8ec7>cd</span> hula-hoop
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># Change default branch</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>git branch -M main
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># Add content</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>$EDITOR README.md
</span></span><span style=display:flex;><span style=line-height:1.4em;>git add README.md
</span></span><span style=display:flex;><span style=line-height:1.4em;>git commit -m <span style=color:#c69669>&#34;Add README.md&#34;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>git push origin main
</span></span></code></pre><h4>Collaborators</h4><p>Let&rsquo;s add the user <code>lemon</code> that we created earlier as a collaborator. The command <code>add</code> takes a <em>repository</em> name and a <em>username</em> as arguments. <code>repo collab add REPOSITORY USERNAME</code>.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># Add user as a collaborator</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>ssh -i &lt;my-precious-key&gt; git.example.com repo collab add hula-hoop lemon
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># List repository collaborators</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>ssh -i &lt;my-precious-key&gt; git.example.com repo collab list hula-hoop
</span></span></code></pre><h4>Nested Repositories</h4><p><a href=https://github.com/charmbracelet/soft-serve target=_blank rel="noopener noreferrer">Soft Serve</a> also supports nested repositories, you can create repositories with any arbitrary path. Go wild!<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>ssh -i &lt;my-precious-key&gt; git.example.com repo create my/super/nested/new
</span></span><span style=display:flex;><span style=line-height:1.4em;>ssh -i &lt;my-precious-key&gt; git.example.com repo create my/super/nested/new/repository
</span></span></code></pre><h4>Mirrors</h4><p>You can also <em>import</em> repositories from <em>any</em> public remote. Use the <code>repo import</code> command.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>ssh -i &lt;my-precious-key&gt; git.example.com repo import soft-serve https://github.com/charmbracelet/soft-serve
</span></span></code></pre><p>Use <code>--mirror</code> or <code>-m</code> to mark the repository as a <em>pull</em> mirror.<h4>Metadata</h4><p>In <a href=https://github.com/charmbracelet/soft-serve target=_blank rel="noopener noreferrer">Soft Serve</a>, a repository has different properties. Repositories can be <em>hidden</em>, <em>private</em>, or <em>mirrored</em>. Repositories can also have their own descriptions and a <em>project name</em> different from the repository&rsquo;s name. For example, we want the repository we imported above to be presented as <code>Soft Serve</code> rather than <code>soft-serve</code>. To do so, let&rsquo;s set the repository <em>project name</em>.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>ssh -i &lt;my-precious-key&gt; git.example.com repo project-name soft-serve <span style=color:#c69669>&#39;Soft Serve&#39;</span>
</span></span></code></pre><p>Let&rsquo;s also add a description to this repository.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>ssh -i &lt;my-precious-key&gt; git.example.com repo description soft-serve <span style=color:#c69669>&#39;The hackable self-hosted Git server!&#39;</span>
</span></span></code></pre><picture><source srcset=https://stuff.charm.sh/blog/self-hosted-soft-serve/soft-serve-in-da-house-tui-nested.png media="(max-width: 550px)"><img srcset=https://stuff.charm.sh/blog/self-hosted-soft-serve/soft-serve-in-da-house-tui-nested.png></picture><p>For more info on repository commands try <code>repo help</code>.<h2>What&rsquo;s Next?</h2><p>That&rsquo;s it for now. Check out <a href=https://github.com/charmbracelet/soft-serve/blob/main/README.md target=_blank rel="noopener noreferrer">Soft Serve&rsquo;s README</a> for more information on how to use <a href=https://github.com/charmbracelet/soft-serve target=_blank rel="noopener noreferrer">Soft Serve</a>.<p>Till next time, happy hacking!]]></content:encoded>
      <author>Ayman Bagabas</author>
      <guid>Self-hosted Soft Serve</guid>
      <pubDate>Fri, 28 Apr 2023 00:00:00 +0000</pubDate>
      <source>https://charm.land/blog/self-hosted-soft-serve/</source>
    </item>
    <item>
      <title>Charm News March 2023</title>
      <link>https://charm.land/blog/mar2023/</link>
      <description>Everything we got up to in February recapped for March! Inspiring community projects included.</description>
      <content:encoded><![CDATA[<h1>Charm Recap: March 2023</h1><p>By Bashbunni on 13 April 2023<p>Here we are with all the stuff that happened in March!</p><iframe src=https://www.youtube.com/embed/esemay6Bddo title="YouTube video player" frameborder=0 allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe><p>For the full effect, check out the video <a href="https://www.youtube.com/watch?v=esemay6Bddo" target=_blank rel="noopener noreferrer">on YouTube</a>.
For those who prefer to read instead of watch, here&rsquo;s what happened…<hr><h2>Charm Stuff</h2><h3>How-To: Automated Integration Testing with VHS</h3><p>We created <a href=https://youtu.be/dtVezRX8OEM target=_blank rel="noopener noreferrer">another YouTube video</a> all about how
you can simplify your integration testing with VHS. You can use golden files,
or even have a GIF auto-generated when a PR is created, so you can see what
changes were made to your TUI directly in the PR.<h3>Charm Rave!!</h3><figure><video autoplay loop playsinline muted style="max-width: 500px">
<source src=/charm-70k.1cd834e984e9c964.webm><source src=/charm-70k.d5c1bea4fa9506af.mp4></video><figcaption>See u in Ibiza</figcaption></figure><p>We hit 70k stars across our repos! To say thanks for all the love,
<a href=https://x.com/meowgorithm>@meowgorithm</a> crafted
<a href=https://twitter.com/charmcli/status/1625929768829845506 target=_blank rel="noopener noreferrer">another celebratory star mascot animation</a>.
This time we got ~Ibiza vibes~ with a purple hue and rave lights. So thank you
both for the support and for letting us see more creative genius from
Christian.<h3>Introducing ‘Log’</h3><p>This month, we launched <a href=https://github.com/charmbracelet/log target=_blank rel="noopener noreferrer">Log</a>, our new
customizable logger. The goal here is to have structured logging in our
projects without needing to introduce breaking changes to our codebase. With
that goal in mind, logger was born.<figure><img src=https://vhs.charm.sh/vhs-1wBImk2iSIuiiD7Ib9rufi.gif><figcaption>What a cute lil’ bit o’ log output</figcaption></figure><p>It uses Lip Gloss under the hood for styling and supports multiple output
formats including text, JSON, and Logfmt. It will act as a wrapper for slog
once that new standard logger for Go is launched.<p><a href=https://charm.sh/blog/the-charm-logger/>Read all about it on the blog!</a><h3>Meetup, Eh?</h3><p><a href=https://github.com/maaslalani target=_blank rel="noopener noreferrer">Maas</a> from the Charm team attended a Go Toronto
meet up and our libraries were covered by one of the speakers, Jeremy
Foran. They built a neat terminal application called &ldquo;Purple Cow&rdquo;.<h2>Businesses using Charm</h2><p><a href=https://github.com/cased/cased-cli target=_blank rel="noopener noreferrer">Cased</a> is using <a href=https://github.com/charmbracelet/bubbletea target=_blank rel="noopener noreferrer">Bubble Tea</a> in their CLI!<blockquote><p>Cased is an open source enterprise-ready tool for production work. It allows
you to handle critical issues quickly&ndash; with safe, fast access to your
servers over SSH. Reduce mistakes and improve the on-call experience.</blockquote><h2>From the Community</h2><h3>Gum</h3><p><a href=https://twitter.com/Snorremd/status/1622953540112056321 target=_blank rel="noopener noreferrer">Snorre Magnus Davoen</a>
made some custom zsh scripts with Gum. They were shocked at how easy it is to
add interactivity to your scripts! I mean, they&rsquo;re not wrong. Gum is built to
be plug-and-play. Check out their example to see how they used it.<h3>Bubble Tea</h3><p>Post your project in our <code>#projects</code> channel on <a href=https://charm.sh/chat>Discord</a> to be featured!<h4>fabster</h4><p><a href=https://github.com/fabio42/ssl-checker target=_blank rel="noopener noreferrer">SSL Checker</a><blockquote><p>Hello everyone, I wanted to play with the Bubble Tea framework, so integrated
it with a very simple program of mine. I actually like the result, and I&rsquo;m
using it regularly now, so made it public</blockquote><h4>Morphclue</h4><p><a href=https://github.com/Morphclue/ygo-bubble-tea target=_blank rel="noopener noreferrer">ygo-bubble-tea</a><blockquote><p>I wanted to try out a bit of Go, and I&rsquo;ve decided to use the Bubble Tea
framework to start my learning process. The CLI I&rsquo;ve created is a <em>search
engine for Yu-Gi-Oh! cards</em>. You type in the card you want to search for, a
list of results is being displayed and if you select that card you can see
pricing for different versions of that card. Nothing special, but it was a
fun little ride i&rsquo;ve had with go. The code is still a bit messy and can be
probably refactored in many places. If you want to improve something,
refactor or add new features: feel free to do that. Every help is
appreciated!</blockquote><h4>waxcoin</h4><p><a href=https://github.com/waxdred/Term_ChatGPT target=_blank rel="noopener noreferrer">Term ChatGPT</a><blockquote><p>Hey everyone, I created an app in Go for terminal and Neovim for using <em>ChatGPT</em></blockquote><h4>wkw</h4><p><a href=https://github.com/wingkwong/bootstrap-cli target=_blank rel="noopener noreferrer">Repository</a>
<a href=https://discord.com/invite/hGKVsGxMY3 target=_blank rel="noopener noreferrer">Discord</a><blockquote><p>Recently I&rsquo;ve been experiencing Bubble Tea. In order to get my hands dirty, I
started this project. Basically it is <em>a CLI that allows you to bootstrap
projects with few clicks</em>. More features will be added as I get more familiar
with Bubble Tea.</blockquote><h4>Kodder</h4><blockquote><p>Kraven: Bubble Tea Based Twitch Chat (PoC)</blockquote><h4>gzipChrist</h4><p><a href=https://github.com/gzipChrist/driptionary target=_blank rel="noopener noreferrer">driptionary</a><blockquote><p>driptionary: A terminal client for urban dictionary</blockquote><h4>gzipChrist</h4><p><a href=https://github.com/gzipChrist/drexler target=_blank rel="noopener noreferrer">drexler</a><blockquote><p>I wanted to create a TUI project generator that offers a similar dev exp to
frontend tooling that I&rsquo;ve used in the past, so I started hacking on drexler.</blockquote><h4>savannah</h4><p><a href=https://github.com/savannahostrowski/gruyere target=_blank rel="noopener noreferrer">gruyère</a><blockquote><p>Gruyère: A tiny (and pretty) program for viewing + killing listening ports</blockquote><h4>ssllee</h4><blockquote><p>GitHub Calendar Component
To start learning to build stuff with Charm, I ported over a web app I built
to a TUI. The main visual component is a &lsquo;GitHub&rsquo; style contributions
calendar. I&rsquo;m loving building it so far!</blockquote><h4>bbu</h4><p><a href=https://github.com/bueti/bocker target=_blank rel="noopener noreferrer">BockerR</a><blockquote><p>BockerR - Backup and Restore in Docker
I&rsquo;m new to Go and Charm (and I&rsquo;m not really a developer either).
To learn Go I wrote a small tool that creates a PostgreSQL backups, wraps it
in a Docker image and uploads it to Docker Hub for long term storage.
This might not be the smartest thing to do, but it&rsquo;s fun to implement. 😄</blockquote><p>Using:<ul><li><a href=https://github.com/charmbracelet/bubbletea target=_blank rel="noopener noreferrer">Bubble Tea</a><li><a href=https://github.com/charmbracelet/lipgloss target=_blank rel="noopener noreferrer">Lip Gloss</a><li><a href=https://github.com/charmbracelet/log target=_blank rel="noopener noreferrer">Log</a><li><a href=https://github.com/charmbracelet/vhs target=_blank rel="noopener noreferrer">VHS</a></ul><h4>ndsizeif</h4><p><a href=https://github.com/ndsizeif/pulsemanager target=_blank rel="noopener noreferrer">PulseManager</a><blockquote><p>Control your PulseAudio devices with a colorful terminal user interface via Bubble Tea.</blockquote><h3>Wish</h3><h4>selkie</h4><p><a href=https://github.com/cyberselkie/chatbox target=_blank rel="noopener noreferrer">Chatbox</a><blockquote><p>Chatbox or as I have been calling it, epic chat.
I forked someone&rsquo;s project for a basic chat using wish and Lip Gloss and have
been modifying it over the past few days to render in Markdown. There are a
lot of glitches and jankiness (especially considering this is my first time
doing anything in go). I also added some dice roll commands.</blockquote><h2>Charm in the News</h2><h3>TechCrunch</h3><p>TechCrunch featured <a href=https://github.com/charmbracelet/lipgloss target=_blank rel="noopener noreferrer">Soft Serve</a> in their list of fastest growing startups of 2022. Have a look at “<a href=https://techcrunch.com/2023/02/01/which-open-source-startups-rocketed-in-2022/ target=_blank rel="noopener noreferrer">Which Open Source Startups Rocketed in 2022?</a>”<h3>Rattlin&rsquo;</h3><p>The <a href=https://t.co/c0mDCbdDia target=_blank rel="noopener noreferrer">Rattlin&rsquo; blog</a> posted an article to get started with
writing TUI scripts with Clojure and Gum<h3>Linux Pratique</h3><p>Gum was <a href=https://twitter.com/humboldtux/status/1629458986477953024 target=_blank rel="noopener noreferrer">featured in a French Linux magazine</a>!<h2>New Community Videos!</h2><p>There were a couple new videos featuring Glow in our Community YouTube playlist
on our channel. Check ’em out!<ul><li><a href=https://youtu.be/gIGLE16fRfE target=_blank rel="noopener noreferrer">How to Work with Nuclei Output</a><li><a href=https://youtu.be/h9JJjyiHOAw target=_blank rel="noopener noreferrer">Pretty Markdown Rendering in The Terminal With Glow</a></ul>]]></content:encoded>
      <author>Bashbunni</author>
      <guid>Charm News March 2023</guid>
      <pubDate>Thu, 13 Apr 2023 00:00:00 +0000</pubDate>
      <source>https://charm.land/blog/mar2023/</source>
    </item>
    <item>
      <title>Commands in Bubble Tea</title>
      <link>https://charm.land/blog/commands-in-bubbletea/</link>
      <description>Goroutines? No, Bubble Tea uses commands. Come learn all about &#39;em.</description>
      <content:encoded><![CDATA[<h1>Commands in Bubble Tea</h1><p>By Bashbunni on 21 March 2023<p>Because of its <a href=https://elm-lang.org target=_blank rel="noopener noreferrer">Elm</a>-inspired roots,
<a href=https://github.com/charmbracelet/bubbletea target=_blank rel="noopener noreferrer">Bubble Tea</a> has a special approach
to asynchronous operations: commands.<p>Commands are a fundamental part of Bubble Tea and are present whenever I/O needs
to happen. There may built-in commands you&rsquo;re familiar with already such as
the <a href=https://pkg.go.dev/github.com/charmbracelet/bubbletea#Quit%5D target=_blank rel="noopener noreferrer"><code>tea.Quit</code></a>,
<a href=https://pkg.go.dev/github.com/charmbracelet/bubbletea#Tick target=_blank rel="noopener noreferrer"><code>tea.Tick</code></a>, and
so on.<p>Also, if you’re just getting started with Bubble Tea we recommend checking out
the <a href=https://github.com/charmbracelet/bubbletea/tree/master/tutorials/commands target=_blank rel="noopener noreferrer">Bubble Tea Commands tutorial</a>.
It <em>is</em> worth the read.<h2>Rules of thumb</h2><p>There three things to keep in mind with regard to asynchronous stuff in Bubble Tea:<ul><li><strong>Use commands for all I/O.</strong> By doing so your program will stay responsive, snappy, and maintainable. Even something as simple as reading a file from disk could cause a small lock up in your program, and commands are build to handle such cases beautifully.<li><strong>Only use commands for I/O.</strong> Sometimes it&rsquo;s tempting to use a command simply to send a message to another part of the program, however due to the nature of the way data flows in Bubble Tea this is never actually necessary.<li><strong>Never use goroutines within a Bubble Tea program.</strong> Bubble Tea works best when you use commands and messages for communication.</ul><h2>The basics</h2><p>Okay, so how would we write our own commands?<p>A <code>Cmd</code> is simply an alias for <code>func() tea.Msg</code>, so it&rsquo;s just a function that
returns a message. A command could be something as simple as:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>type</span> helloMsg <span style=color:#6e6ed8>string</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>func</span> <span style=color:#00d787>waitASec</span><span style=color:#e8e8a8>()</span> tea<span style=color:#e8e8a8>.</span>Msg <span style=color:#e8e8a8>{</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	time<span style=color:#e8e8a8>.</span><span style=color:#00d787>Sleep</span><span style=color:#e8e8a8>(</span>time<span style=color:#e8e8a8>.</span>Second<span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#0af>return</span> <span style=color:#00d787>helloMsg</span><span style=color:#e8e8a8>(</span><span style=color:#c69669>&#34;Hi, there!&#34;</span><span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#e8e8a8>}</span>
</span></span></code></pre><p>But how would you respond to such a command? You’d match on it in your update
method.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>func</span> <span style=color:#e8e8a8>(</span>m Model<span style=color:#e8e8a8>)</span> <span style=color:#00d787>Update</span><span style=color:#e8e8a8>(</span>msg tea<span style=color:#e8e8a8>.</span>Msg<span style=color:#e8e8a8>)</span> <span style=color:#e8e8a8>(</span>tea<span style=color:#e8e8a8>.</span>Model<span style=color:#e8e8a8>,</span> tea<span style=color:#e8e8a8>.</span>Cmd<span style=color:#e8e8a8>)</span> <span style=color:#e8e8a8>{</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#0af>switch</span> msg <span style=color:#ef8080>:=</span> msg<span style=color:#e8e8a8>.(</span><span style=color:#0af>type</span><span style=color:#e8e8a8>)</span> <span style=color:#e8e8a8>{</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#0af>case</span> helloMsg<span style=color:#e8e8a8>:</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>		<span style=color:#676767>// We caught our message like a Pokémon!
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767></span>		<span style=color:#676767>// From here you could save the output to the model
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767></span>		<span style=color:#676767>// to display it later in your view.
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767></span>		m<span style=color:#e8e8a8>.</span>greeting <span style=color:#e8e8a8>=</span> msg<span style=color:#e8e8a8>;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#0af>default</span><span style=color:#e8e8a8>:</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>		<span style=color:#0af>return</span> m<span style=color:#e8e8a8>,</span> <span style=color:#0af>nil</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#e8e8a8>}</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#e8e8a8>}</span>
</span></span></code></pre><h2>Multiple commands</h2><p>Sometimes you want to fire off more than one command at once. For that, use
<a href=https://pkg.go.dev/github.com/charmbracelet/bubbletea#Batch target=_blank rel="noopener noreferrer"><code>tea.Batch</code></a>.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>func</span> <span style=color:#00d787>choresCmd</span><span style=color:#e8e8a8>()</span> tea<span style=color:#e8e8a8>.</span>Msg <span style=color:#e8e8a8>{</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#0af>return</span> tea<span style=color:#e8e8a8>.</span><span style=color:#00d787>Batch</span><span style=color:#e8e8a8>(</span>getTheLaundry<span style=color:#e8e8a8>,</span> eatDinner<span style=color:#e8e8a8>,</span> petTheCats<span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#e8e8a8>}</span>
</span></span></code></pre><h2>Commands with arguments</h2><p>Okay, so what if you want to make a command that takes an argument? Since <code>Cmd</code>
is a <code>func() tea.Msg</code>, it can&rsquo;t take an argument. So what do you do in that
case? Well, you make a function that returns a command.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>func</span> <span style=color:#00d787>getUser</span><span style=color:#e8e8a8>(</span>id<span style=color:#e8e8a8>)</span> tea<span style=color:#e8e8a8>.</span>Cmd <span style=color:#e8e8a8>{</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#0af>return</span> <span style=color:#0af>func</span><span style=color:#e8e8a8>()</span> tea<span style=color:#e8e8a8>.</span>Msg <span style=color:#e8e8a8>{</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>		user<span style=color:#e8e8a8>,</span> _ <span style=color:#ef8080>:=</span> api<span style=color:#e8e8a8>.</span><span style=color:#00d787>GetUserByID</span><span style=color:#e8e8a8>(</span>id<span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>		<span style=color:#0af>return</span> gotUser<span style=color:#e8e8a8>{</span>user<span style=color:#e8e8a8>}</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#e8e8a8>}</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#e8e8a8>}</span>
</span></span></code></pre><p>In your <code>Update</code> function it would look something like this:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>return</span> m<span style=color:#e8e8a8>,</span> <span style=color:#00d787>getUser</span><span style=color:#e8e8a8>(</span><span style=color:#6eefc0>88</span><span style=color:#e8e8a8>)</span>
</span></span></code></pre><h2>Initial commands</h2><p>Sometimes you&rsquo;ll want to fire off a command right as your Bubble Tea program
starts without waiting for user input. For that, use <code>Model.Init</code>:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>func</span> <span style=color:#e8e8a8>(</span>m Model<span style=color:#e8e8a8>)</span> <span style=color:#00d787>Init</span><span style=color:#e8e8a8>()</span> tea<span style=color:#e8e8a8>.</span>Msg <span style=color:#e8e8a8>{</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#0af>return</span> fetchUsers
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#e8e8a8>}</span>
</span></span></code></pre><p>Or, for multiple commands:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>func</span> <span style=color:#e8e8a8>(</span>m Model<span style=color:#e8e8a8>)</span> <span style=color:#00d787>Init</span><span style=color:#e8e8a8>()</span> tea<span style=color:#e8e8a8>.</span>Msg <span style=color:#e8e8a8>{</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#0af>return</span> tea<span style=color:#e8e8a8>.</span><span style=color:#00d787>Batch</span><span style=color:#e8e8a8>(</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>		fetchUsers<span style=color:#e8e8a8>,</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>		spinner<span style=color:#e8e8a8>.</span>Tick<span style=color:#e8e8a8>,</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#e8e8a8>}</span>
</span></span></code></pre><h2>APIs calls from Bubble Tea</h2><p>A lot of the time you’ll want to make API calls from Bubble Tea. Commands are
great for this.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>func</span> <span style=color:#00d787>getUser</span><span style=color:#e8e8a8>(</span>id <span style=color:#6e6ed8>int</span><span style=color:#e8e8a8>)</span> tea<span style=color:#e8e8a8>.</span>Cmd <span style=color:#e8e8a8>{</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#0af>return</span> <span style=color:#0af>func</span><span style=color:#e8e8a8>()</span> tea<span style=color:#e8e8a8>.</span>Msg <span style=color:#e8e8a8>{</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>		res<span style=color:#e8e8a8>,</span> err <span style=color:#ef8080>:=</span> http<span style=color:#e8e8a8>.</span><span style=color:#00d787>Get</span><span style=color:#e8e8a8>(</span>fmt<span style=color:#e8e8a8>.</span><span style=color:#00d787>Sprintf</span><span style=color:#e8e8a8>(</span><span style=color:#c69669>&#34;http://api.example.com/users?id=%d&#34;</span><span style=color:#e8e8a8>,</span> id<span style=color:#e8e8a8>))</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>		<span style=color:#0af>if</span> err <span style=color:#ef8080>!=</span> <span style=color:#0af>nil</span> <span style=color:#e8e8a8>{</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>			<span style=color:#0af>return</span> apiErrMsg<span style=color:#e8e8a8>{</span>err<span style=color:#e8e8a8>}</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>		<span style=color:#e8e8a8>}</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>		<span style=color:#676767>// ...do unmarshaling and stuff...
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767></span>		<span style=color:#0af>return</span> <span style=color:#00d787>fetchedUserMsg</span><span style=color:#e8e8a8>(</span>u<span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#e8e8a8>}</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#e8e8a8>}</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>func</span> <span style=color:#e8e8a8>(</span>m Model<span style=color:#e8e8a8>)</span> <span style=color:#00d787>Update</span><span style=color:#e8e8a8>(</span>msg tea<span style=color:#e8e8a8>.</span>Msg<span style=color:#e8e8a8>)</span> <span style=color:#e8e8a8>(</span>tea<span style=color:#e8e8a8>.</span>Model<span style=color:#e8e8a8>,</span> tea<span style=color:#e8e8a8>.</span>Cmd<span style=color:#e8e8a8>)</span> <span style=color:#e8e8a8>{</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#0af>switch</span> msg <span style=color:#ef8080>:=</span> msg<span style=color:#e8e8a8>.(</span><span style=color:#0af>type</span><span style=color:#e8e8a8>)</span> <span style=color:#e8e8a8>{</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#0af>case</span> tea<span style=color:#e8e8a8>.</span>KeyMsg<span style=color:#e8e8a8>:</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>		<span style=color:#0af>switch</span> msg<span style=color:#e8e8a8>.</span><span style=color:#00d787>String</span><span style=color:#e8e8a8>()</span> <span style=color:#e8e8a8>{</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>		<span style=color:#0af>case</span> <span style=color:#c69669>&#34;enter&#34;</span><span style=color:#e8e8a8>:</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>			<span style=color:#676767>// Make our API request
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767></span>			<span style=color:#0af>return</span> m<span style=color:#e8e8a8>,</span> <span style=color:#00d787>getUser</span><span style=color:#e8e8a8>(</span>m<span style=color:#e8e8a8>.</span>userID<span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>		<span style=color:#e8e8a8>}</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#0af>case</span> fetchedUserMsg<span style=color:#e8e8a8>:</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>		<span style=color:#676767>// Here&#39;s our API response
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767></span>	<span style=color:#0af>case</span> apiErrMsg<span style=color:#e8e8a8>:</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>		<span style=color:#676767>// Oh no, an API error!
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767></span>	<span style=color:#0af>default</span><span style=color:#e8e8a8>:</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>		<span style=color:#0af>return</span> m<span style=color:#e8e8a8>,</span> <span style=color:#0af>nil</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#e8e8a8>}</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#e8e8a8>}</span>
</span></span></code></pre><h2>Injecting messages from outside the program</h2><p>Commands work great from within a Bubble Tea program, but what if you want to send
messages to <code>Update</code> from outside the program? Enter
<a href=https://pkg.go.dev/github.com/charmbracelet/bubbletea#Program.Send target=_blank rel="noopener noreferrer"><code>Program.Send()</code></a>.
It’s as simple as <code>p.Send(someMsg{})</code>:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>p <span style=color:#ef8080>:=</span> tea<span style=color:#e8e8a8>.</span><span style=color:#00d787>NewProgram</span><span style=color:#e8e8a8>(</span>model<span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767>// Later...
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767></span>p<span style=color:#e8e8a8>.</span><span style=color:#00d787>Send</span><span style=color:#e8e8a8>(</span>someMsg<span style=color:#e8e8a8>{})</span>
</span></span></code></pre><p>For details check out the
<a href=https://github.com/charmbracelet/bubbletea/blob/6b77c8fc10d43195ab29e6e09f93272623ce4e9c/examples/send-msg/main.go#L116 target=_blank rel="noopener noreferrer">full example</a> and
<a href=https://pkg.go.dev/github.com/charmbracelet/bubbletea#Program.Send target=_blank rel="noopener noreferrer"><code>Program.Send</code> in the docs</a>.<h2>Still have questions?</h2><p>Let us know in <a href=https://charm.sh/chat>Discord</a> or in <a href=https://github.com/charmbracelet/bubbletea/discussions target=_blank rel="noopener noreferrer">GitHub Discussions</a>.<h2>Learn More</h2><ul><li><a href=https://github.com/charmbracelet/bubbletea/tree/master/examples/realtime target=_blank rel="noopener noreferrer">Example: real time example</a><li><a href=https://github.com/charmbracelet/bubbletea/issues/25 target=_blank rel="noopener noreferrer">Discussion: injecting messages from outside the program loop</a></ul>]]></content:encoded>
      <author>Bashbunni</author>
      <guid>Commands in Bubble Tea</guid>
      <pubDate>Tue, 21 Mar 2023 00:00:00 +0000</pubDate>
      <source>https://charm.land/blog/commands-in-bubbletea/</source>
    </item>
    <item>
      <title>The Charm Logger</title>
      <link>https://charm.land/blog/the-charm-logger/</link>
      <description>Everything is fine! Just another Go logger.</description>
      <content:encoded><![CDATA[<h1>The Charm Logger</h1><p>By Ayman Bagabas on 21 February 2023<p>There are many logging libraries for Go, and they all have their pros and cons.
Some are more verbose, some are more flexible, some are more performant, and
some are more opinionated. But none of them fit our needs. So we wrote our own
extensible, colorful, and easy-to-use logger.<h2>Background</h2><p>We have been reliably using the standard library&rsquo;s <code>log</code> package. It works great
and has a simple API. However, it doesn&rsquo;t have structured logging, log levels,
and is not customizable. This means that to use structured logging, we
have to introduce breaking changes to our codebase.<p>Another problem we were facing was excessive logging which in turn drove up our
infrastructure cost. We were logging debugging logs on our production instances.
Using leveled logging, we could set different levels per environment and avoid
logging on specific levels thus driving the cost down.<p>Readability was also important to us. We were looking for a library that prints
pretty logs that are easy to read in a development environment. Along with
having a simple, flexible API that accepts different types and values.<h2>The Hunt for a Logger</h2><p>There are many great logging libraries for Go. We tried switching to
<a href=https://github.com/sirupsen/logrus target=_blank rel="noopener noreferrer">logrus</a>, <a href=https://github.com/apex/log target=_blank rel="noopener noreferrer">apex/log</a>, and <a href=https://github.com/go-kit/log target=_blank rel="noopener noreferrer">go-kit/log</a> before coming
up with our own logger.<p><a href=https://github.com/sirupsen/logrus target=_blank rel="noopener noreferrer">Logrus</a> was intuitive, easy to migrate to, and fully
API-compatible with the standard library logger. Given the widespread use of
<a href=https://github.com/sirupsen/logrus target=_blank rel="noopener noreferrer">logrus</a> and the use of structured logging, this was our ideal
option. We can start the migration from the standard library logger by replacing
all the <code>log</code> imports with <code>log "github.com/sirupsen/logrus"</code>. However, given
that the project is now in maintenance mode, meaning they won&rsquo;t be introducing
new features, we continued our search for the most suitable logger.<p>We quickly stumbled upon <a href=https://github.com/apex/log target=_blank rel="noopener noreferrer">apex/log</a>. <a href=https://github.com/apex/log target=_blank rel="noopener noreferrer">Apex/log</a> has a
similar & simpler API to <a href=https://github.com/sirupsen/logrus target=_blank rel="noopener noreferrer">logrus</a>. It was inspired by
<a href=https://github.com/sirupsen/logrus target=_blank rel="noopener noreferrer">logrus</a>. However, it wasn&rsquo;t fully API-compatible with the
standard library logger. At this point, we figured, to support structured
logging and to encourage ourselves to use structured logging, we had to
introduce breaking changes to our codebase.<p>For example, to make the following log example structured, we can write it in
<a href=https://github.com/apex/log target=_blank rel="noopener noreferrer">apex/log</a> using the <code>log.Fields</code> type:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767>// unstructured: golang &#34;log&#34;
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767></span>log<span style=color:#e8e8a8>.</span><span style=color:#00d787>Printf</span><span style=color:#e8e8a8>(</span><span style=color:#c69669>&#34;starting app: %s, env: %s&#34;</span><span style=color:#e8e8a8>,</span> app<span style=color:#e8e8a8>,</span> env<span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>log<span style=color:#e8e8a8>.</span><span style=color:#00d787>Printf</span><span style=color:#e8e8a8>(</span><span style=color:#c69669>&#34;unable to process data: %s&#34;</span><span style=color:#e8e8a8>,</span> err<span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767>// structured &amp; leveled: &#34;apex/log&#34;
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767></span>log<span style=color:#e8e8a8>.</span><span style=color:#00d787>WithFields</span><span style=color:#e8e8a8>(</span>log<span style=color:#e8e8a8>.</span>Fields<span style=color:#e8e8a8>{</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#c69669>&#34;app&#34;</span><span style=color:#e8e8a8>:</span> app<span style=color:#e8e8a8>,</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#c69669>&#34;env&#34;</span><span style=color:#e8e8a8>:</span> env<span style=color:#e8e8a8>,</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#e8e8a8>}).</span><span style=color:#00d787>WithError</span><span style=color:#e8e8a8>(</span>err<span style=color:#e8e8a8>).</span><span style=color:#00d787>Error</span><span style=color:#e8e8a8>(</span><span style=color:#c69669>&#34;unable to process data&#34;</span><span style=color:#e8e8a8>)</span>
</span></span></code></pre><p>This was not what we were looking for in terms of ease of use. We find
introducing more types and methods extraneous and slows the process of writing a
log statement. Plus, <a href=https://github.com/apex/log target=_blank rel="noopener noreferrer">apex/log</a> brings more dependencies given its
centralized approach. That&rsquo;s when we started looking for a minimalistic library
with fewer dependencies.<p><a href=https://github.com/go-kit/log target=_blank rel="noopener noreferrer">Go-kit/log</a> offers a minimal structured logging library, that simple. It offers
one extensible interface:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>type</span> Logger <span style=color:#0af>interface</span> <span style=color:#e8e8a8>{</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#00d787>Log</span><span style=color:#e8e8a8>(</span>keyvals <span style=color:#ef8080>...</span><span style=color:#0af>interface</span><span style=color:#e8e8a8>{})</span> <span style=color:#6e6ed8>error</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#e8e8a8>}</span>
</span></span></code></pre><p>Now, let&rsquo;s write the above example with <a href=https://github.com/go-kit/log target=_blank rel="noopener noreferrer">go-kit/log</a>:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767>// unstructured: golang &#34;log&#34;
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767></span>log<span style=color:#e8e8a8>.</span><span style=color:#00d787>Printf</span><span style=color:#e8e8a8>(</span><span style=color:#c69669>&#34;starting app: %s, env: %s&#34;</span><span style=color:#e8e8a8>,</span> app<span style=color:#e8e8a8>,</span> env<span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>log<span style=color:#e8e8a8>.</span><span style=color:#00d787>Printf</span><span style=color:#e8e8a8>(</span><span style=color:#c69669>&#34;unable to process data: %s&#34;</span><span style=color:#e8e8a8>,</span> err<span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767>// structured: &#34;go-kit/log&#34;
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767></span>logger <span style=color:#ef8080>:=</span> log<span style=color:#e8e8a8>.</span><span style=color:#00d787>NewLogfmtLogger</span><span style=color:#e8e8a8>(</span>log<span style=color:#e8e8a8>.</span><span style=color:#00d787>NewSyncWriter</span><span style=color:#e8e8a8>(</span>os<span style=color:#e8e8a8>.</span>Stderr<span style=color:#e8e8a8>))</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>logger <span style=color:#e8e8a8>=</span> log<span style=color:#e8e8a8>.</span><span style=color:#00d787>With</span><span style=color:#e8e8a8>(</span>logger<span style=color:#e8e8a8>,</span> <span style=color:#c69669>&#34;ts&#34;</span><span style=color:#e8e8a8>,</span> log<span style=color:#e8e8a8>.</span>DefaultTimestamp<span style=color:#e8e8a8>,</span> <span style=color:#c69669>&#34;app&#34;</span><span style=color:#e8e8a8>,</span> app<span style=color:#e8e8a8>,</span> <span style=color:#c69669>&#34;env&#34;</span><span style=color:#e8e8a8>,</span> env<span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>logger<span style=color:#e8e8a8>.</span><span style=color:#00d787>Log</span><span style=color:#e8e8a8>(</span><span style=color:#c69669>&#34;msg&#34;</span><span style=color:#e8e8a8>,</span> <span style=color:#c69669>&#34;unable to process data&#34;</span><span style=color:#e8e8a8>,</span> <span style=color:#c69669>&#34;err&#34;</span><span style=color:#e8e8a8>,</span> err<span style=color:#e8e8a8>)</span>
</span></span></code></pre><p>Wait, I don&rsquo;t see any log levels here?<p>Given the extensible and minimal architecture of <a href=https://github.com/go-kit/log target=_blank rel="noopener noreferrer">go-kit/log</a>, leveled loggers
are just another wrapper around the interface. You can use the <a href=https://godoc.org/github.com/go-kit/log/level target=_blank rel="noopener noreferrer"><code>level</code>
package</a> to add support for
leveled logging.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767>// unstructured: golang &#34;log&#34;
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767></span>log<span style=color:#e8e8a8>.</span><span style=color:#00d787>Printf</span><span style=color:#e8e8a8>(</span><span style=color:#c69669>&#34;starting app: %s, env: %s&#34;</span><span style=color:#e8e8a8>,</span> app<span style=color:#e8e8a8>,</span> env<span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>log<span style=color:#e8e8a8>.</span><span style=color:#00d787>Printf</span><span style=color:#e8e8a8>(</span><span style=color:#c69669>&#34;unable to process data: %s&#34;</span><span style=color:#e8e8a8>,</span> err<span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767>// structured &amp; leveled: &#34;go-kit/log&#34;
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767></span>logger <span style=color:#ef8080>:=</span> log<span style=color:#e8e8a8>.</span><span style=color:#00d787>NewLogfmtLogger</span><span style=color:#e8e8a8>(</span>log<span style=color:#e8e8a8>.</span><span style=color:#00d787>NewSyncWriter</span><span style=color:#e8e8a8>(</span>os<span style=color:#e8e8a8>.</span>Stderr<span style=color:#e8e8a8>))</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>logger <span style=color:#e8e8a8>=</span> level<span style=color:#e8e8a8>.</span><span style=color:#00d787>NewFilter</span><span style=color:#e8e8a8>(</span>logger<span style=color:#e8e8a8>,</span> level<span style=color:#e8e8a8>.</span>AllowInfo<span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>logger <span style=color:#e8e8a8>=</span> log<span style=color:#e8e8a8>.</span><span style=color:#00d787>With</span><span style=color:#e8e8a8>(</span>logger<span style=color:#e8e8a8>,</span> <span style=color:#c69669>&#34;ts&#34;</span><span style=color:#e8e8a8>,</span> log<span style=color:#e8e8a8>.</span>DefaultTimestamp<span style=color:#e8e8a8>,</span> <span style=color:#c69669>&#34;app&#34;</span><span style=color:#e8e8a8>,</span> app<span style=color:#e8e8a8>,</span> <span style=color:#c69669>&#34;env&#34;</span><span style=color:#e8e8a8>,</span> env<span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>level<span style=color:#e8e8a8>.</span><span style=color:#00d787>Error</span><span style=color:#e8e8a8>(</span>logger<span style=color:#e8e8a8>).</span><span style=color:#00d787>Log</span><span style=color:#e8e8a8>(</span><span style=color:#c69669>&#34;msg&#34;</span><span style=color:#e8e8a8>,</span> <span style=color:#c69669>&#34;unable to process data&#34;</span><span style=color:#e8e8a8>,</span> <span style=color:#c69669>&#34;err&#34;</span><span style=color:#e8e8a8>,</span> err<span style=color:#e8e8a8>)</span>
</span></span></code></pre><p>You can see this is starting to get complicated and requires some extra steps to
get it going. Unlike the standard library logger, <a href=https://github.com/sirupsen/logrus target=_blank rel="noopener noreferrer">logrus</a>, and
<a href=https://github.com/apex/log target=_blank rel="noopener noreferrer">apex/log</a>, <a href=https://github.com/go-kit/log target=_blank rel="noopener noreferrer">go-kit/log</a> doesn&rsquo;t offer a global logger instance to
use. Its minimal API and interface require wrapping the logger instance in
multiple layers to get the same out-of-box experience that
<a href=https://github.com/sirupsen/logrus target=_blank rel="noopener noreferrer">logrus</a> or <a href=https://github.com/apex/log target=_blank rel="noopener noreferrer">apex/log</a> offer.<p>Shout out to all of the people behind these awesome libraries:<ul><li><a href=https://github.com/apex/log target=_blank rel="noopener noreferrer">apex/log</a> – A structured logger for Go.<li><a href=https://github.com/coder/slog target=_blank rel="noopener noreferrer">coder/slog</a> – A minimal structured logger.<li><a href=https://github.com/go-kit/log target=_blank rel="noopener noreferrer">go-kit/log</a> – A minimalistic structured logger.<li><a href=https://github.com/golang/glog target=_blank rel="noopener noreferrer">golang/glog</a> – Leveled execution logs for Go.<li><a href=https://github.com/hashicorp/go-hclog target=_blank rel="noopener noreferrer">hashicorp/go-hclog</a> – A simple, structured, leveled logger.<li><a href=https://github.com/rs/zerolog target=_blank rel="noopener noreferrer">rs/zerolog</a> – A zero-allocation JSON logger.<li><a href=https://github.com/sirupsen/logrus target=_blank rel="noopener noreferrer">sirupsen/logrus</a> – A structured, pluggable logger for Go.<li><a href=https://github.com/uber-go/zapp target=_blank rel="noopener noreferrer">uber-go/zap</a> – A fast, structured, leveled logger.</ul><h2>The Charm Logger</h2><figure><video autoplay loop playsinline muted>
<source src=https://stuff.charm.sh/log/LogBlogPost-1080p.webm type=video/webm><source src=https://stuff.charm.sh/log/LogBlogPost-1080p.mp4 type=video/mp4></video><figcaption>This is fine.</figcaption></figure><p>We realized that what we need is a structured, leveled, and easy-to-use logger.
It should come with a global package-wise singleton, offer multiple output
formats such as text, JSON, and Logfmt, and provide an extensible interface. We
also want it to be compatible with the standard library logger.<p>Just like the standard library version, the Charm logger comes with a global
singleton that can be used with the package namespace. It comes with a set of
defaults such as reporting timestamps, filtering out levels less than info, and
formatting logs for the console.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767>// unstructured: golang &#34;log&#34;
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767></span>log<span style=color:#e8e8a8>.</span><span style=color:#00d787>Printf</span><span style=color:#e8e8a8>(</span><span style=color:#c69669>&#34;starting app: %s, env: %s&#34;</span><span style=color:#e8e8a8>,</span> app<span style=color:#e8e8a8>,</span> env<span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>log<span style=color:#e8e8a8>.</span><span style=color:#00d787>Printf</span><span style=color:#e8e8a8>(</span><span style=color:#c69669>&#34;unable to process data: %s&#34;</span><span style=color:#e8e8a8>,</span> err<span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767>// structured &amp; leveled: &#34;charmbracelet/log&#34;
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767></span>log<span style=color:#e8e8a8>.</span><span style=color:#00d787>Error</span><span style=color:#e8e8a8>(</span><span style=color:#c69669>&#34;unable to process data&#34;</span><span style=color:#e8e8a8>,</span> <span style=color:#c69669>&#34;app&#34;</span><span style=color:#e8e8a8>,</span> app<span style=color:#e8e8a8>,</span> <span style=color:#c69669>&#34;env&#34;</span><span style=color:#e8e8a8>,</span> env<span style=color:#e8e8a8>,</span> <span style=color:#c69669>&#34;err&#34;</span> err<span style=color:#e8e8a8>)</span>
</span></span></code></pre><p>Example of the output:
<a href=https://github.com/charmbracelet/log target=_blank rel="noopener noreferrer"><img src=https://vhs.charm.sh/vhs-cP0fObpwIxZKX8ugDLtuK.gif alt=example></a><p>The Charm logger supports 3 different formats, <code>TextFormatter</code> <em>(default)</em>
suitable for console output, <code>JSONFormatter</code> and <code>LogfmtFormatter</code> for
production use. Use <code>log.SetFormatter()</code> or <code>log.Options{Formatter: }</code> option to
set the output format. <code>TextFormatter</code> uses <a href=https://github.com/charmbracelet/lipgloss target=_blank rel="noopener noreferrer">Lip Gloss</a> to style the
output. You can customize the styles by modifying the style definitions in
<a href=https://github.com/charmbracelet/log/blob/main/styles.go target=_blank rel="noopener noreferrer"><code>styles.go</code></a>.<p>You can also create sub-loggers with <code>log.With()</code> and pass fields as key-value pairs.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>logger <span style=color:#ef8080>:=</span> log<span style=color:#e8e8a8>.</span><span style=color:#00d787>With</span><span style=color:#e8e8a8>(</span><span style=color:#c69669>&#34;app&#34;</span><span style=color:#e8e8a8>,</span> app<span style=color:#e8e8a8>,</span> <span style=color:#c69669>&#34;env&#34;</span><span style=color:#e8e8a8>,</span> env<span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>logger<span style=color:#e8e8a8>.</span><span style=color:#00d787>Error</span><span style=color:#e8e8a8>(</span><span style=color:#c69669>&#34;unable to process data&#34;</span><span style=color:#e8e8a8>,</span> <span style=color:#c69669>&#34;err&#34;</span> err<span style=color:#e8e8a8>)</span>
</span></span></code></pre><p>Want to send your logs to a file? No problem:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>f<span style=color:#e8e8a8>,</span> _ <span style=color:#ef8080>:=</span> os<span style=color:#e8e8a8>.</span><span style=color:#00d787>OpenFile</span><span style=color:#e8e8a8>(</span><span style=color:#c69669>&#34;log.txt&#34;</span><span style=color:#e8e8a8>,</span> os<span style=color:#e8e8a8>.</span>O_WRONLY<span style=color:#e8e8a8>|</span>os<span style=color:#e8e8a8>.</span>O_CREATE<span style=color:#e8e8a8>|</span>os<span style=color:#e8e8a8>.</span>O_APPEND<span style=color:#e8e8a8>,</span> <span style=color:#6eefc0>0</span>o644<span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>log<span style=color:#e8e8a8>.</span><span style=color:#00d787>SetOutput</span><span style=color:#e8e8a8>(</span>f<span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>log<span style=color:#e8e8a8>.</span><span style=color:#00d787>SetFormatter</span><span style=color:#e8e8a8>(</span>log<span style=color:#e8e8a8>.</span>JSONFormatter<span style=color:#e8e8a8>)</span> <span style=color:#676767>// Use JSON format
</span></span></span></code></pre><p>The logger also offers different options such as reporting caller location,
changing the log level, setting a prefix, and changing the timestamp format.
Refer to the <a href=https://github.com/charmbracelet/log/blob/main/logger.go target=_blank rel="noopener noreferrer">logger
interface</a> to see all
the available options.<hr><p>That&rsquo;s it! You can find <a href=https://github.com/charmbracelet/log target=_blank rel="noopener noreferrer">Log</a>, our
official logger, on <a href=https://github.com/charmbracelet target=_blank rel="noopener noreferrer">our GitHub</a>. If you have
any thoughts or comments, don&rsquo;t be afraid to <a href=https://charm.sh/chat>share them with us</a>.]]></content:encoded>
      <author>Ayman Bagabas</author>
      <guid>The Charm Logger</guid>
      <pubDate>Tue, 21 Feb 2023 00:00:00 +0000</pubDate>
      <source>https://charm.land/blog/the-charm-logger/</source>
    </item>
    <item>
      <title>Do you even SSH?</title>
      <link>https://charm.land/blog/ssh101/</link>
      <description>Let&#39;s talk about SSH layers and how we use the SSH protocol at Charm</description>
      <content:encoded><![CDATA[<h1>Do you even SSH?</h1><p>By Bashbunni on 2 February 2023<p>This is our first video on SSH! We love the SSH protocol at Charm and are
finding more creative ways it can be leveraged for businesses. We have
libraries for SSH featured in this article, so scroll down or click on the
video to learn more!</p><iframe src=https://www.youtube.com/embed/bo6BVvfvH-k title="YouTube video player" frameborder=0 allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe><p>For details, check out the video above. For those who prefer to read rather
than watch, see the transcript below.<hr><h2>Let&rsquo;s Get Into It</h2><p>Most of you have probably seen SSH either used at work, or to clone GitHub
repos, but maybe you aren’t quite sure how it works or you’re curious to
know why we think it’s so cool. This one’s for you.<p>Don’t forget to like and subscribe if you enjoy SSH and terminal-related
content!<p>Now let’s talk about SSH. SSH is the go-to for authorizing access to remote
servers and services. You authenticate with an SSH private key that has an
authorized public key on the server.<h3>SSH Binary vs SSH Protocol</h3><p>First off, let’s preface this by stating that SSH != OpenSSH. OpenSSH, AKA
the ssh binary, is “a suite of secure networking utilities based on the
Secure Shell (SSH) protocol, which provides a secure channel over an
unsecured network in a client–server architecture“. Interestingly, OpenSSH
actually started out as a fork of SSH, but has since become proprietary
software, according to Wikipedia. OpenSSH specializes in tools for remote
systems administration, while SSH is a flexible protocol with an open
architecture that makes it suitable to various use-cases.<h4>You probably see SSH commonly used for:</h4><ul><li>Administrating a server remotely (with OpenSSH)<li>Authorizing remote processes, SCP for example, which allows you to send files via an SSH tunnel so the data is encrypted in transit.<li>Providing secure access for users<li>Securely mounting a directory on a remote server as a file system<li>Port forwarding
And less commonly used for:<li>Browsing the web through an encrypted proxy connection with SSH clients (extra nerd points for that)<li>But there are plenty more.</ul><h2>SSH Layers</h2><p>Now, before we jump into Charm stuff, we need to break down the layers of SSH.<h4>The Layers:</h4><ul><li>The transport layer<li>The user authentication layer<li>The connection layer</ul><p>The transport layer uses the TCP port to handle initial key exchange as
well as server authentication, and set up encryption, compression, and
integrity verification. It exposes to the upper layer an interface for
sending and receiving plaintext packets.<p>The user authentication layer is responsible for client authentication,.
Because authentication for SSH is client-driven, you may get asked for your
key password by the client while the server responds to authentication
requests. The most common authentication method I’ve seen is public-key
authentication, though there are other methods of authentication supported
by SSH.<p>The connection layer handles different channels in your SSH connections.
You might have multiple channels per connection as they work to transfer
data bi-directionally. Some examples of channels include:<ul><li>Shell for terminal shells, SFTP and exec requests (including SCP transfers)<li>Direct-TCPIP for client-to-server forwarded connections<li>Forwarded-TCPIP for server-to-client forwarded connections</ul><h2>Charm × SSH</h2><p>Now, let’s talk about how we use SSH at Charm. First off, we don’t touch
OpenSSH on our servers, so you are not being given shell access, period. We
serve a TCP connection using the SSH protocol, and you (the app developer)
can do whatever you want with it. It’s pretty much like reading from STDIN and
writing to STDOUT/STDERR. With Wish, you can define what kind of authentication
you might want for your SSH apps, and you can define what users are authorized
to interact with the application, etc.<p>Now, we are also using SSH for identity management with Charm Cloud. How this
works is the first time you run charm it creates an SSH key-pair for you.
From then on, it uses those keys to verify your identify so you can access the
hosted file system, encryption and decryption functions, and your key-value
stores.<p>If you’re ever worried you might lose your SSH keys, fear no more: we have
<a href=https://github.com/charmbracelet/melt target=_blank rel="noopener noreferrer">Melt</a>, which allows you to back up and
recover your SSH key-pairs with seed phrases so you can keep a hard copy in a
safe place.<h4>Learn more about our SSH tools:</h4><ul><li><a href=https://github.com/charmbracelet/wish target=_blank rel="noopener noreferrer">Wish</a><li><a href=https://github.com/charmbracelet/wishlist target=_blank rel="noopener noreferrer">Wishlist</a><li><a href=https://github.com/charmbracelet/melt target=_blank rel="noopener noreferrer">Melt</a><li><a href=https://github.com/charmbracelet/charm target=_blank rel="noopener noreferrer">Charm Cloud</a></ul><p>You can check out our <a href=https://github.com/charmbracelet/soft-serve target=_blank rel="noopener noreferrer">Soft Serve TUI</a>
TUI over ssh by running <code>ssh git.charm.sh</code> from your terminal for an
interactive demo.<p>Ciao!]]></content:encoded>
      <author>Bashbunni</author>
      <guid>Do you even SSH?</guid>
      <pubDate>Thu, 02 Feb 2023 00:00:00 +0000</pubDate>
      <source>https://charm.land/blog/ssh101/</source>
    </item>
    <item>
      <title>What a year!</title>
      <link>https://charm.land/blog/2022-roundup/</link>
      <description>Charm’s 2022 highlights</description>
      <content:encoded><![CDATA[<h1>What a year!</h1><p>Charm’s 2022 highlights<p>By Charm on 21 December 2022<p>Charm’s mission is to make the command line a glamorous platform for creative,
industrial-grade computing.<p>Given that, we continuously strive for clarity and open communication. We’re
excited to share more with our community via this blog, including the recent
posts featuring <a href=blog/ssh-key-marshal/>the nuances of SSH key marshaling</a> and
<a href=/blog/vhs-publish/>the new VHS publish feature</a>. We also want to
keep you all up to date with broader Charm learnings as 2022 has truly been
transformative.<p>In spirit of open source, we’re proud to highlight some milestones achieved:</p><picture class=seamless><source srcset=https://stuff.charm.sh/blog/2022-roundup/stats-mobile.svg media="(max-width: 550px)"><img class=seamless srcset=https://stuff.charm.sh/blog/2022-roundup/stats.svg></picture><h2>Star power</h2><p>In April the team was celebrating crossing 30,000 GitHub stars, a marker in the
open source world! We even open sourced the
<a href=https://twitter.com/charmcli/status/1511397938785886223 target=_blank rel="noopener noreferrer">3D model</a>
that was made in celebration. Yet when <a href=https://twitter.com/jzmusings/status/1554140119963054080 target=_blank rel="noopener noreferrer">Gum</a> and <a href=https://twitter.com/charmcli/status/1586389561219108865 target=_blank rel="noopener noreferrer">VHS</a> launched,
they each hit the global GitHub top 3 within days. These events, coupled with
strong growth of our existing products, pushed us past 65,000 GitHub stars in
record fashion.<h2>Developer love</h2><p><a href=https://github.com/charmbracelet/bubbletea target=_blank rel="noopener noreferrer">Bubble Tea</a>, our TUI framework, crossed 10,000 stars early in
the year. Shortly thereafter, we saw organizational adoption and use cases for
Bubble Tea apps emerge. From <a href=https://github.com/Azure/aztfy target=_blank rel="noopener noreferrer">Microsoft’s Aztify</a>, a tool to bring
Azure resources under Terraform management, to
<a href=https://github.com/aws/amazon-ec2-spot-interrupter target=_blank rel="noopener noreferrer">AWS’s EC2 Spot Interruptor</a>, a tool that triggers EC2 spot interruption
notifications and rebalance recommendations, to
<a href=https://github.com/digitalocean/doctl target=_blank rel="noopener noreferrer">Doctl</a>, Digital Ocean’s
official CLI, and most recently <a href=https://github.com/cockroachdb/cockroach-gen target=_blank rel="noopener noreferrer">Cockroach-Gen</a> from CockroachDB. It&rsquo;s
inspiring to see that developer love for Bubble Tea has grown to the point
that developers are actively integrating it into their organizations! There
have now been 1,750 Bubble Tea apps built to date: that&rsquo;s 3.5x more than that
of last year. And <a href=https://github.com/charmbracelet/glow target=_blank rel="noopener noreferrer">Glow</a>, our markdown reader, continues to be shared
with the wider community with over 31,000 users, almost doubling that of the
past two years.<h2>Connecting with community</h2><p>As an open source startup, the community truly drives what we do. This year,
we prioritized expanding communication channels with our developer users.
In addition to Twitter, Slack, Mastodon, we launched Instagram, YouTube,
Discord for more conversational, produced, and streamlined content. On
<a href=https://www.youtube.com/c/charmcli target=_blank rel="noopener noreferrer">YouTube</a> alone, we’ve published 29 videos, racking over 66,000 views and
2,450 subscribers! Recently, we even <a href=https://twitter.com/charmcli/status/1591122361793024000 target=_blank rel="noopener noreferrer">met our community</a> in person
via a co-hosted Microsoft event!<p>This year, we’re grateful to have been featured by <a href="https://www.youtube.com/watch?v=EazHhUHZZms" target=_blank rel="noopener noreferrer">Microsoft</a>,
<a href=https://twitter.com/github/status/1592236723010179073 target=_blank rel="noopener noreferrer">GitHub</a>, <a href=https://twitter.com/TwitterDev/status/1554496512981286912 target=_blank rel="noopener noreferrer">Twitter</a>, <a href="https://www.youtube.com/watch?v=-w-sfKVVqd0" target=_blank rel="noopener noreferrer">Supabase</a>,
<a href=https://changelog.com/podcast/481 target=_blank rel="noopener noreferrer">Changelog</a>, etc. When one searches for “command line” on Hacker
news, Charm tops the charts.<p><img src=https://stuff.charm.sh/blog/2022-roundup/hn.png alt="A screenshot of the Hacker News website with a search for &lsquo;command line&rsquo;"><p>2022 has been transformative. Thank you for your support!]]></content:encoded>
      <author>Charm</author>
      <guid>What a year!</guid>
      <pubDate>Wed, 21 Dec 2022 00:00:00 +0000</pubDate>
      <source>https://charm.land/blog/2022-roundup/</source>
    </item>
    <item>
      <title>Marshaling SSH Private Keys</title>
      <link>https://charm.land/blog/ssh-key-marshal/</link>
      <description>Why is there always a different block?</description>
      <content:encoded><![CDATA[<h1>Marshaling SSH Private Keys</h1><p>By Carlos Becker on 19 December 2022<p>Not long ago, when I was building <a href=https://github.com/charmbracelet/melt target=_blank rel="noopener noreferrer">melt</a>, I learned something
interesting: if you restore a private key from its seed, and marshal it back to
the OpenSSH Private Key format, you&rsquo;ll always get a different block in the
middle.<h2>Why?</h2><p>That lead to an investigation of how the private key format works. I didn&rsquo;t
find many good references out there, except OpenSSH&rsquo;s source code.<p>Let&rsquo;s start from there, shall we?<p>We can see in the function <code>sshkey_private_to_blob2</code>
in <a href=https://github.com/openssh/openssh-portable/blob/ce3c3e78ce45d68a82c7c8dc89895f297a67f225/sshkey.c#L2812 target=_blank rel="noopener noreferrer"><code>sshkey.c</code></a>,
there&rsquo;s this interesting piece of code:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767>/* Random check bytes */</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>check <span style=color:#ef8080>=</span> arc4random<span style=color:#e8e8a8>();</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>if</span> <span style=color:#e8e8a8>((</span>r <span style=color:#ef8080>=</span> sshbuf_put_u32<span style=color:#e8e8a8>(</span>encrypted<span style=color:#e8e8a8>,</span> check<span style=color:#e8e8a8>))</span> <span style=color:#ef8080>!=</span> <span style=color:#6eefc0>0</span> <span style=color:#ef8080>||</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>    <span style=color:#e8e8a8>(</span>r <span style=color:#ef8080>=</span> sshbuf_put_u32<span style=color:#e8e8a8>(</span>encrypted<span style=color:#e8e8a8>,</span> check<span style=color:#e8e8a8>))</span> <span style=color:#ef8080>!=</span> <span style=color:#6eefc0>0</span><span style=color:#e8e8a8>)</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#0af>goto</span> out<span style=color:#e8e8a8>;</span>
</span></span></code></pre><p>We see there that it creates what seems to be a random <code>uint32</code>, and then calls
<code>sshbuf_put_u32</code> two times, adding it to <code>encrypted</code> and expecting it all to
succeed.<p>Interesting… why?<p>The best clue after that lies in the
<a href=https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key target=_blank rel="noopener noreferrer"><code>PROTOCOL.key</code></a>
file:<pre><code>	uint32	checkint
	uint32	checkint
	byte[]	privatekey1
	string	comment1
</code></pre><p><code>checkint</code>… same name… the answer must be here, right?
Going further:<blockquote><p>Before the key is encrypted, a random integer is assigned to both <code>checkint</code>
fields so successful decryption can be quickly checked by verifying that both
<code>checkint</code> fields hold the same value.</blockquote><p><strong>Aha!</strong> So that&rsquo;s why it&rsquo;s always different! The <code>checkints</code> are used to check
that decryption succeeded. When decrypting the key, we don&rsquo;t know their value,
just that they should be equal.<p><em>Cool!</em><h3>What about Go?</h3><p>When we started this story, I mentioned I was working on <a href=https://github.com/charmbracelet/melt target=_blank rel="noopener noreferrer">melt</a>, which is
written in Go. So far, we&rsquo;ve looked in to C code, but what about Go?<p>Turns out there&rsquo;s an
<a href=https://go-review.googlesource.com/c/crypto/+/218620/6/ssh/keys.go#1480 target=_blank rel="noopener noreferrer">unresolved merge request</a>
adding SSH key marshaling into Go&rsquo;s <code>crypto/ssh</code> package.<p>We can find the same <code>checks</code> there (called <code>Check1</code> and <code>Check2</code>) but the
code might be a bit easier to read:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767>// Random check bytes.
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767></span><span style=color:#0af>var</span> check <span style=color:#6e6ed8>uint32</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#0af>if</span> err <span style=color:#ef8080>:=</span> binary<span style=color:#e8e8a8>.</span><span style=color:#00d787>Read</span><span style=color:#e8e8a8>(</span>rand<span style=color:#e8e8a8>.</span>Reader<span style=color:#e8e8a8>,</span> binary<span style=color:#e8e8a8>.</span>BigEndian<span style=color:#e8e8a8>,</span> <span style=color:#ef8080>&amp;</span>check<span style=color:#e8e8a8>);</span> err <span style=color:#ef8080>!=</span> <span style=color:#0af>nil</span> <span style=color:#e8e8a8>{</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>	<span style=color:#0af>return</span> <span style=color:#0af>nil</span><span style=color:#e8e8a8>,</span> err
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#e8e8a8>}</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>pk1<span style=color:#e8e8a8>.</span>Check1 <span style=color:#e8e8a8>=</span> check
</span></span><span style=display:flex;><span style=line-height:1.4em;>pk1<span style=color:#e8e8a8>.</span>Check2 <span style=color:#e8e8a8>=</span> check
</span></span></code></pre><p><em>P.S. If you want to use this in your Go program, I&rsquo;m keeping a
<a href=https://github.com/caarlos0/sshmarshal target=_blank rel="noopener noreferrer">repository</a> with these changes.</em><h2>Playground</h2><p>We learned that the Private Key file, in the OpenSSH format, will always be
a bit different, even if it&rsquo;s generated with the same parameters. But… is it
still the same key? What happens now?<p>We can verify that using <a href=https://github.com/charmbracelet/melt target=_blank rel="noopener noreferrer">melt</a>!<p>Let&rsquo;s create a new key to play with. You can do so with:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>ssh-keygen -t ed25519 -f post
</span></span></code></pre><p>And then run <code>melt</code> to get a mnemonic:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>melt ./post &gt;seed
</span></span></code></pre><p>For what its worth, here is the mnemonic for the key I created:<pre><code>obey axis lecture satoshi deal comic first unfold bomb control attitude lawsuit
this brown often fault myself rabbit assume miss modify riot around punch
</code></pre><p>Now, let&rsquo;s restore it a couple of times:<pre><code>melt restore ./post1 &amp;lt;seed
melt restore ./post2 &amp;lt;seed
</code></pre><p>Now, let&rsquo;s check a couple of things, starting with the check sum of the private
keys:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>sha256sum post post1 post2
</span></span></code></pre><pre><code>a9a08d6ae71412e0397e1c76d9300002d0cb69e484823dd684d217ee07f32081  post
ec7f45126a4bf96a913b66079c1e1773ca809c6b4a653885a1c49e08d2b4d978  post1
cab1849c9560b6705a335192bcb3991ae2ba8ac479d51659e2325aeeb3ab2476  post2
</code></pre><p>All different…which is expected, due to the <code>checkint</code> we just learned about.<p>Let&rsquo;s now check the public keys:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>sha256sum post*.pub
</span></span></code></pre><pre><code>562de9510ca7278f3284f9f0114e8dc757b557c92f0b1744514c42eb9c1b0d81  post.pub
8dd282d6ad5a0fa6da2a2054b70ac96c257a58ef4c23715f02b9885329094a27  post1.pub
8dd282d6ad5a0fa6da2a2054b70ac96c257a58ef4c23715f02b9885329094a27  post2.pub
</code></pre><p>Except for the first one, they are all equal. So what&rsquo;s the difference between
them?<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>diff -u post.pub post1.pub
</span></span></code></pre><pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#fd5b5b>--- post.pub    2022-12-07 13:28:36.009649296 -0300
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#fd5b5b></span><span style=color:#00d787>+++ post1.pub   2022-12-07 13:31:34.426816707 -0300
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#00d787></span><span style=color:#777>@@ -1 +1 @@
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#777></span><span style=color:#fd5b5b>-ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPA8QtyAE1DLpUIY3otmLILZv9XdRlXv37hHEWTGib7p carlos@darkstar
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#fd5b5b></span><span style=color:#00d787>+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPA8QtyAE1DLpUIY3otmLILZv9XdRlXv37hHEWTGib7p
</span></span></span></code></pre><p>Ah, our original key had a memo, and the restored ones don&rsquo;t. Not a big deal!<p>What about the private keys&rsquo; fingerprints?<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>ssh-keygen -l -f post &gt; post.finger
</span></span><span style=display:flex;><span style=line-height:1.4em;>ssh-keygen -l -f post1 &gt; post1.finger
</span></span><span style=display:flex;><span style=line-height:1.4em;>ssh-keygen -l -f post2 &gt; post2.finger
</span></span><span style=display:flex;><span style=line-height:1.4em;>cat post*.finger
</span></span></code></pre><pre><code>256 SHA256:KfCFwx1/vfcV/XQqdneOMOdgpbVu4Nxz32buks4MLpI carlos@darkstar (ED25519)
256 SHA256:KfCFwx1/vfcV/XQqdneOMOdgpbVu4Nxz32buks4MLpI post1.pub (ED25519)
256 SHA256:KfCFwx1/vfcV/XQqdneOMOdgpbVu4Nxz32buks4MLpI post2.pub (ED25519)
</code></pre><p>Again, the same key. The only difference is the memo.<p>Now let&rsquo;s check how different the private keys really are:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>diff -u post1 post2
</span></span></code></pre><pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#fd5b5b>--- post1       2022-12-07 13:31:34.426816707 -0300
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#fd5b5b></span><span style=color:#00d787>+++ post2       2022-12-07 13:31:37.870839247 -0300
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#00d787></span><span style=color:#777>@@ -1,7 +1,7 @@
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#777></span> -----BEGIN OPENSSH PRIVATE KEY-----
</span></span><span style=display:flex;><span style=line-height:1.4em;> b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtz
</span></span><span style=display:flex;><span style=line-height:1.4em;> c2gtZWQyNTUxOQAAACDwPELcgBNQy6VCGN6LZiyC2b/V3UZV79+4RxFkxom+6QAA
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#fd5b5b>-AIjRy3cc0ct3HAAAAAtzc2gtZWQyNTUxOQAAACDwPELcgBNQy6VCGN6LZiyC2b/V
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#fd5b5b></span><span style=color:#00d787>+AIhYJw2fWCcNnwAAAAtzc2gtZWQyNTUxOQAAACDwPELcgBNQy6VCGN6LZiyC2b/V
</span></span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#00d787></span> 3UZV79+4RxFkxom+6QAAAECX4h38X7OCXJXfaRkl7Dq/Hgw6JmqfklYEN8bo63RD
</span></span><span style=display:flex;><span style=line-height:1.4em;> DfA8QtyAE1DLpUIY3otmLILZv9XdRlXv37hHEWTGib7pAAAAAAECAwQF
</span></span><span style=display:flex;><span style=line-height:1.4em;> -----END OPENSSH PRIVATE KEY-----
</span></span></code></pre><p>Just one line&ndash;about 12 chars&ndash;are different: <code>jRy3cc0ct3HA</code> vs <code>hYJw2fWCcNnw</code>.<p>All that said, for all intents and purposes it&rsquo;s the same key, which shouldn&rsquo;t
be a surprise, but it&rsquo;s good to know anyways!<hr><p>Hope you enjoyed this trip into how OpenSSH private keys are marshaled, and
I&rsquo;ll see you in the next one!]]></content:encoded>
      <author>Carlos Becker</author>
      <guid>Marshaling SSH Private Keys</guid>
      <pubDate>Mon, 19 Dec 2022 00:00:00 +0000</pubDate>
      <source>https://charm.land/blog/ssh-key-marshal/</source>
    </item>
    <item>
      <title>VHS GIF Hosting!</title>
      <link>https://charm.land/blog/vhs-publish/</link>
      <description>Now you can publish your GIFs with VHS.</description>
      <content:encoded><![CDATA[<h1>Host Your GIFs with VHS</h1><p>By Maas Lalani on 19 December 2022<p><a href=https://github.com/charmbracelet/vhs target=_blank rel="noopener noreferrer">VHS</a> now ships with automatic GIF hosting making it easy to share your
VHS creations with your friends, foes, coworkers, and the internet. To publish
your GIFs just add the <code>--publish</code> flag.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>vhs --publish demo.tape
</span></span></code></pre><p>But first things first.<h2>VHS? What&rsquo;s that?</h2><p>In case you missed it, <a href=https://github.com/charmbracelet/vhs target=_blank rel="noopener noreferrer">VHS</a> is a tool that generates GIFs and videos of
terminal GIFs from code. To generate a GIF with VHS, you write a simple
<code>.tape</code> file, which instructs VHS how to interact with the terminal, and pass
it into the <code>vhs</code> binary.<p>Here&rsquo;s what a <code>.tape</code> file could look like:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># Where should we write the GIF?</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#f1f1f1;font-weight:bold>Output</span> demo<span style=color:#ef8080>.</span>gif
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># Set up a 1200x600 terminal with 46px font.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#f1f1f1;font-weight:bold>Set</span> <span style=color:#f1f1f1;font-weight:bold>FontSize</span> <span style=color:#6eefc0>46</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#f1f1f1;font-weight:bold>Set</span> <span style=color:#f1f1f1;font-weight:bold>Width</span> <span style=color:#6eefc0>1200</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#f1f1f1;font-weight:bold>Set</span> <span style=color:#f1f1f1;font-weight:bold>Height</span> <span style=color:#6eefc0>600</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># Type a command in the terminal.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#f1f1f1;font-weight:bold>Type</span> <span style=color:#c69669>&#34;echo &#39;Welcome to VHS!&#39;&#34;</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># Pause for dramatic effect...</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#f1f1f1;font-weight:bold>Sleep</span> <span style=color:#6eefc0>500</span>ms
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># Run the command by pressing enter.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#f1f1f1;font-weight:bold>Enter</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># Admire the output for a bit.</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#f1f1f1;font-weight:bold>Sleep</span> <span style=color:#6eefc0>5</span>s
</span></span></code></pre><p>Once you have written your <code>.tape</code> file, just pass it to VHS:<pre><code>vhs &lt; demo.tape
</code></pre><p><em>Voilà</em>! VHS will output a shiny terminal GIF entirely generated from the
code you wrote!<p><img src=https://vhs.charm.sh/vhs-6PpsQrBPAhhlCQHEGECisU.gif alt="Render of demo.gif"><h2>Publishing GIFs</h2><p>We wanted to make sharing terminal GIFs with VHS as easy as creating them, so
we&rsquo;re pleased to introduce <code>vhs --publish</code>.<p>When you add the <code>--publish</code> flag, VHS will host your GIF on <code>vhs.charm.sh</code> and
provide you with a URL that you can share with your fellow command-line
dwellers.<p>So this&mldr;<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>vhs --publish demo.tape
</span></span></code></pre><p>Will output this&mldr;<p><img src=https://vhs.charm.sh/vhs-4ybGto5iQX3imSFg7JnpFf.gif alt=Demo.gif><p>&mldr;which will then be available at a URL like <a href=https://vhs.charm.sh/vhs-6bx5XJGgNmJLVmvicnXJ9i.gif><code>https://vhs.charm.sh/vhs-6bx5XJGgNmJLVmvicnXJ9i.gif</code></a>.<p>And if you forgot to pass the <code>--publish</code> flag while creating your GIF, worry
not. You can use <code>vhs publish</code> to host the GIF even after it&rsquo;s already been
generated:<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>vhs publish demo.gif
</span></span></code></pre><h2>Bonus: Home Video Recording</h2><p>Want to play around with <code>vhs --publish</code> right now but don&rsquo;t have any <code>.tape</code>
files laying around? Say hello to <code>vhs record</code>.<p><code>vhs record</code>, a relatively new feature, will record your terminal activity and
write the output into a tape file, almost like magic.<pre tabindex=0 style="color:#c4c4c4;background-color:#171717;padding:1.75em 2em;;"><code><span style=display:flex;><span style=line-height:1.4em;>vhs record &gt; file.tape
</span></span><span style=display:flex;><span style=line-height:1.4em;><span style=color:#676767># perform actions, do stuff...</span>
</span></span><span style=display:flex;><span style=line-height:1.4em;>vhs file.tape --publish
</span></span></code></pre><p><img src=https://vhs.charm.sh/vhs-63bcrgfwbaS1kJoZSuUyTE.gif alt><hr><p>We hope that <a href=https://github.com/charmbracelet/vhs target=_blank rel="noopener noreferrer">VHS</a> makes it easy for you
to record and share your terminal applications with others. We <em>love</em> seeing
your terminal GIFs and you&rsquo;re always welcome to <a href=https://charm.sh/chat>share them with us</a>.<p>And, as always, let us know if you have any feedback.]]></content:encoded>
      <author>Maas Lalani</author>
      <guid>VHS GIF Hosting!</guid>
      <pubDate>Mon, 19 Dec 2022 00:00:00 +0000</pubDate>
      <source>https://charm.land/blog/vhs-publish/</source>
    </item>
  </channel>
</rss>
