<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Darren Burns</title>
    <link>//darren.codes/</link>
    <description>Recent content on Darren Burns</description>
    <generator>Hugo</generator>
    <language>en-gb</language>
    <copyright>© Darren Burns</copyright>
    <lastBuildDate>Thu, 13 Mar 2025 10:09:06 +0000</lastBuildDate>
    <atom:link href="//darren.codes/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Improving Posting&#39;s startup time by over 40%</title>
      <link>//darren.codes/posts/python-startup-time/</link>
      <pubDate>Thu, 13 Mar 2025 10:09:06 +0000</pubDate>
      <guid>//darren.codes/posts/python-startup-time/</guid>
      <description>&lt;p&gt;I&amp;rsquo;ve been working on improving the startup time of Posting recently.&#xA;&lt;a href=&#34;https://github.com/darrenburns/posting&#34;&gt;Posting&lt;/a&gt; is a TUI application built with &lt;a href=&#34;https://textual.textualize.io/&#34;&gt;Textual&lt;/a&gt;, and written in Python, for managing and sending HTTP requests.&lt;/p&gt;&#xA;&lt;figure class=&#34;video&#34;&gt;&#xA;&lt;video controls preload=&#34;metadata&#34; width=&#34;100%&#34;  autoplay=&#34;true&#34; loop=&#34;true&#34;&gt;&#xA;&lt;source src=&#34;//darren.codes/posts/python-startup-time/posting-overview.mp4&#34; type=&#34;video/mp4&#34;&gt;&#xA;&lt;/video&gt;&#xA;&lt;/figure&gt;&#xA;&lt;p&gt;This post is an overview of some of the techniques (and hacks) I used and the gains made, including:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Using &lt;code&gt;python -X importtime&lt;/code&gt; to measure import times.&lt;/li&gt;&#xA;&lt;li&gt;Reducing the scope of import statements, so that they don&amp;rsquo;t run when they&amp;rsquo;re not needed.&lt;/li&gt;&#xA;&lt;li&gt;Patching dependencies to prune the import tree.&lt;/li&gt;&#xA;&lt;li&gt;Using &lt;code&gt;pyinstrument&lt;/code&gt; and it&amp;rsquo;s new flamegraph view to find improvements.&lt;/li&gt;&#xA;&lt;li&gt;Extracting expensive-to-import classes into their own modules, isolating other code from them.&lt;/li&gt;&#xA;&lt;li&gt;Lazy loading of Textual widgets.&lt;/li&gt;&#xA;&lt;li&gt;Using C-based extensions where possible.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Most of the techniques are Python-specific, and almost all of them boil down to deferring work until it&amp;rsquo;s actually required.&lt;/p&gt;&#xA;&lt;p&gt;The values in this post are measured on an M4 Macbook Pro - the absolute gains on slower systems will likely be greater.&lt;/p&gt;&#xA;&lt;h2 id=&#34;deferring-openapi_pydantic-import-63ms-gained&#34; class=&#34;heading-anchor&#34;&gt;Deferring openapi_pydantic import (~63ms gained)&lt;a href=&#34;#deferring-openapi_pydantic-import-63ms-gained&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Deferring openapi_pydantic import (~63ms gained)&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;If you run a Python application with &lt;code&gt;python -X importtime&lt;/code&gt;, Python will print a table showing how long it takes to import each module. The &amp;ldquo;imported package&amp;rdquo; column contains the name of the module being imported, and if you look at the indentation of it, you&amp;rsquo;ll see it forms a tree structure.&lt;/p&gt;&#xA;&lt;p&gt;The table is best read bottom-up, because parent modules are listed below child modules. The &lt;code&gt;cumulative&lt;/code&gt; column contains the time, in microseconds, to import the module in it&amp;rsquo;s entirety, including all descendent imports.&lt;/p&gt;&#xA;&lt;div class=&#34;code-panel&#34;&gt;&#xA;  &lt;div class=&#34;code-bar&#34;&gt;&#xA;    &lt;span class=&#34;code-label&#34;&gt;bash&lt;/span&gt;&#xA;    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34;&gt;copy&lt;/button&gt;&#xA;  &lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;import time: self &lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;us&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; cumulative &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; imported package&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  ... trimmed ...&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;import time:      &lt;span class=&#34;m&#34;&gt;3505&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt;      &lt;span class=&#34;m&#34;&gt;29292&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt;         openapi_pydantic.v3.v3_1&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;import time:      &lt;span class=&#34;m&#34;&gt;4138&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt;      &lt;span class=&#34;m&#34;&gt;62771&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt;       openapi_pydantic.v3.parser&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;import time:        &lt;span class=&#34;m&#34;&gt;84&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt;      &lt;span class=&#34;m&#34;&gt;62854&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt;     openapi_pydantic.v3&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;import time:        &lt;span class=&#34;m&#34;&gt;99&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt;      &lt;span class=&#34;m&#34;&gt;62952&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt;   openapi_pydantic&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;import time:        &lt;span class=&#34;m&#34;&gt;97&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt;      &lt;span class=&#34;m&#34;&gt;63048&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; posting.importing.open_api&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;The table above shows that &lt;code&gt;posting.importing.open_api&lt;/code&gt; and it&amp;rsquo;s descendants take a total of 63ms to import. However, this part of Posting is specific to a single CLI command, &lt;code&gt;posting import&lt;/code&gt;, which is not used as part of the main TUI. By moving this import into the local scope of the import command handler, we can ensure we only pay the price when the &lt;code&gt;posting import&lt;/code&gt; runs.&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nd&#34;&gt;@cli.command&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;import&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# ...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;def&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;import_spec&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;spec_path&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;str&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;output&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;str&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;|&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;None&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;None&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s2&#34;&gt;&amp;#34;&amp;#34;&amp;#34;Import an OpenAPI specification into a Posting collection.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;# ...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kn&#34;&gt;from&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;posting.importing.open_api&lt;/span&gt; &lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;import_openapi_spec&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;# ...&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#xA;&lt;p&gt;The difference was immediately noticeable to me, and startup time measurement code in Posting reported a 63ms improvement. Simply by moving a single import statement. Nice!&lt;/p&gt;&#xA;&lt;p&gt;In Python CLI apps, it&amp;rsquo;s common to have multiple command handlers in one file, with dependencies imported at the module level, even when they&amp;rsquo;re only needed by specific handlers. This is like importing the whole world when you only need an island. By moving imports inside the handlers, we can avoid this, loading just what&amp;rsquo;s necessary for the executed command.&lt;/p&gt;&#xA;&lt;h2 id=&#34;patching-httpx--20-25ms-gained&#34; class=&#34;heading-anchor&#34;&gt;Patching httpx  (20-25ms gained)&lt;a href=&#34;#patching-httpx--20-25ms-gained&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Patching httpx  (20-25ms gained)&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;I found that the fantastic &lt;a href=&#34;https://www.python-httpx.org/&#34;&gt;&lt;code&gt;httpx&lt;/code&gt; module&lt;/a&gt; was taking around 51 milliseconds to import, which felt excessive. Almost 30 milliseconds of that was &lt;code&gt;httpx._main&lt;/code&gt;, which contains a CLI tool for &lt;code&gt;httpx&lt;/code&gt;. On diving into that module&amp;rsquo;s code, I found that if your environment contains &lt;code&gt;click&lt;/code&gt; and &lt;code&gt;rich&lt;/code&gt; (both common libraries), &lt;code&gt;httpx&lt;/code&gt; &amp;ldquo;assumes&amp;rdquo;, that you&amp;rsquo;ve installed the &lt;code&gt;httpx[cli]&lt;/code&gt; extras and will perform a bunch of expensive imports that are required for the &lt;code&gt;httpx&lt;/code&gt; CLI. End-user apps are unlikely to require CLI, but will still pay a ~30ms price to import it.&lt;/p&gt;&#xA;&lt;div class=&#34;code-panel&#34;&gt;&#xA;  &lt;div class=&#34;code-bar&#34;&gt;&#xA;    &lt;span class=&#34;code-label&#34;&gt;bash&lt;/span&gt;&#xA;    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34;&gt;copy&lt;/button&gt;&#xA;  &lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;import time: self &lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;us&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; cumulative &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; imported package&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  ... trimmed ...&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;import time:       &lt;span class=&#34;m&#34;&gt;292&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt;      &lt;span class=&#34;m&#34;&gt;28409&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt;       httpx._main&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;import time:       &lt;span class=&#34;m&#34;&gt;195&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt;      &lt;span class=&#34;m&#34;&gt;51101&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt;     httpx&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;To avoid running through this expensive and unnecessary import chain, I manually patch the &lt;code&gt;httpx._main&lt;/code&gt; module at runtime, before importing &lt;code&gt;httpx&lt;/code&gt; itself:&lt;/p&gt;&#xA;&lt;div class=&#34;code-panel&#34;&gt;&#xA;  &lt;div class=&#34;code-bar&#34;&gt;&#xA;    &lt;span class=&#34;code-label&#34;&gt;python&lt;/span&gt;&#xA;    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34;&gt;copy&lt;/button&gt;&#xA;  &lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;sys&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;sys&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;modules&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;httpx._main&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;None&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# later...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;httpx&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;Checking the import times again, I found the cumulative time for &lt;code&gt;httpx&lt;/code&gt; dropped from 51ms to 23ms:&lt;/p&gt;&#xA;&lt;div class=&#34;code-panel&#34;&gt;&#xA;  &lt;div class=&#34;code-bar&#34;&gt;&#xA;    &lt;span class=&#34;code-label&#34;&gt;bash&lt;/span&gt;&#xA;    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34;&gt;copy&lt;/button&gt;&#xA;  &lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;import time: self &lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;us&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; cumulative &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; imported package&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  ... trimmed ...&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;import time:         &lt;span class=&#34;m&#34;&gt;2&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt;          &lt;span class=&#34;m&#34;&gt;2&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt;       httpx._main&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;import time:       &lt;span class=&#34;m&#34;&gt;156&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt;      &lt;span class=&#34;m&#34;&gt;23305&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt;     httpx&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;About 28ms saved. Not bad!&lt;/p&gt;&#xA;&lt;p&gt;Modules are cached once loaded, so some of the modules that were imported transitively by &lt;code&gt;httpx._main&lt;/code&gt; and &lt;em&gt;actually&lt;/em&gt; required somewhere in Posting will be still be loaded elsewhere. This means a portion of these savings are likely just due to the import cost being shifted elsewhere. However, many expensive imports such as &lt;code&gt;pygments&lt;/code&gt; that were required by &lt;code&gt;httpx._main&lt;/code&gt; and &lt;em&gt;not&lt;/em&gt; required by Posting have been eliminated.&lt;/p&gt;&#xA;&lt;p&gt;I feel I should emphasise that this is a &lt;em&gt;hack&lt;/em&gt; - and if you&amp;rsquo;re going to do it you should be comfortable that the module is truly unused.&lt;/p&gt;&#xA;&lt;p&gt;All of this makes me wish Python had a means of checking &lt;em&gt;which&lt;/em&gt; extras were explicitly installed by the user or application. This would allow &lt;code&gt;httpx&lt;/code&gt; to only perform those expensive imports when they&amp;rsquo;re asked for rather than if they happen to be installed in the environment.&lt;/p&gt;&#xA;&lt;h2 id=&#34;using-pyinstruments-flamegraph-view&#34; class=&#34;heading-anchor&#34;&gt;Using pyinstrument&amp;rsquo;s flamegraph view&lt;a href=&#34;#using-pyinstruments-flamegraph-view&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Using pyinstrument&amp;rsquo;s flamegraph view&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://github.com/joerick/pyinstrument&#34;&gt;&lt;code&gt;pyinstrument&lt;/code&gt;&lt;/a&gt; is one of my favourite tools, ever. It&amp;rsquo;s a sampling profiler, which recently added a flamegraph viewer.&lt;/p&gt;&#xA;&lt;p&gt;On running the command below, Posting starts up, and when I close the app, &lt;code&gt;pyinstrument&lt;/code&gt; will open a browser window with profiling results.&lt;/p&gt;&#xA;&lt;div class=&#34;code-panel&#34;&gt;&#xA;  &lt;div class=&#34;code-bar&#34;&gt;&#xA;    &lt;span class=&#34;code-label&#34;&gt;bash&lt;/span&gt;&#xA;    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34;&gt;copy&lt;/button&gt;&#xA;  &lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;pyinstrument -r html -m src.posting.__main__&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;A couple of things stood out when I examined the flamegraph tab:&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;./startup-flame.png&#34; alt=&#34;startup-flame.png&#34;&gt;&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;watchfiles&lt;/code&gt; is being initialised before the app starts, but we don&amp;rsquo;t actually need to setup file watchers until after the app has started.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;help_screen&lt;/code&gt; is being imported before the app starts, which is ultimately importing &lt;code&gt;markdown_it&lt;/code&gt;. We don&amp;rsquo;t actually need the Markdown rendering stuff until the user opens the help popup - so we don&amp;rsquo;t need to pay the import cost until then.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;I made a couple of changes, noted below, based on these findings.&#xA;It sliced another ~16ms off of the startup time.&#xA;Not amazing, but small improvements like this add up!&lt;/p&gt;&#xA;&lt;h3 id=&#34;moving-helpdata-class-to-its-own-module-10ms-gained&#34; class=&#34;heading-anchor&#34;&gt;Moving HelpData class to its own module (10ms gained)&lt;a href=&#34;#moving-helpdata-class-to-its-own-module-10ms-gained&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Moving HelpData class to its own module (10ms gained)&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;If you have code which is required on startup which is currently housed in a module alongside expensive-to-import and &lt;em&gt;not&lt;/em&gt; required on startup code, you can extract the code that is required on startup into its own module and defer the expensive imports into the local scope(s) where it&amp;rsquo;s required.&lt;/p&gt;&#xA;&lt;p&gt;In other words, try to isolate expensive-to-import code by splitting it into a separate module, so that it can be imported on its own, and won&amp;rsquo;t get pulled in alongside cheaper to import code.&lt;/p&gt;&#xA;&lt;p&gt;In Posting, the &lt;code&gt;HelpData&lt;/code&gt; class was contained alongside the &lt;code&gt;HelpScreen&lt;/code&gt; class. &lt;code&gt;HelpData&lt;/code&gt; is required on startup and, on it&amp;rsquo;s own, would be cheap to import. However, because it&amp;rsquo;s bundled into the same module as &lt;code&gt;HelpScreen&lt;/code&gt;, importing &lt;code&gt;HelpData&lt;/code&gt; also imports all the dependencies of &lt;code&gt;HelpScreen&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;I extracted &lt;code&gt;HelpData&lt;/code&gt; into a separate &lt;code&gt;help_data&lt;/code&gt; module, meaning it could be imported at the module level without the overhead of also importing all of the other stuff housed alongside &lt;code&gt;HelpScreen&lt;/code&gt;. &lt;code&gt;HelpScreen&lt;/code&gt; is now imported &amp;ldquo;on demand&amp;rdquo;, when the user opens the help popup.&lt;/p&gt;&#xA;&lt;p&gt;This change shaved another 10ms off of the startup time.&lt;/p&gt;&#xA;&lt;h3 id=&#34;deferred-loading-of-watchfiles-library-6ms-gained&#34; class=&#34;heading-anchor&#34;&gt;Deferred loading of watchfiles library (6ms gained)&lt;a href=&#34;#deferred-loading-of-watchfiles-library-6ms-gained&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Deferred loading of watchfiles library (6ms gained)&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Posting watches for changes to external files using &lt;code&gt;watchfiles&lt;/code&gt;. The watching happens in another &lt;code&gt;asyncio.Task&lt;/code&gt;, so by moving the import into the task which isn&amp;rsquo;t kicked off until the app starts, Posting started ~6ms faster on my machine.&lt;/p&gt;&#xA;&lt;h2 id=&#34;using-textuals-lazy-widget-for-tab-panes-100ms-gained&#34; class=&#34;heading-anchor&#34;&gt;Using Textual&amp;rsquo;s Lazy widget for tab panes (100ms gained)&lt;a href=&#34;#using-textuals-lazy-widget-for-tab-panes-100ms-gained&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Using Textual&amp;rsquo;s Lazy widget for tab panes (100ms gained)&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Posting&amp;rsquo;s user interface relies heavily on tabs to hide content. By default, Textual will load the content of all tabs before on startup, even though only two tabs can be visible and interactive.&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;./posting-screen.png&#34; alt=&#34;posting-screen.png&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;I used Textual&amp;rsquo;s &lt;code&gt;Lazy&lt;/code&gt; widget to defer loading of the hidden content of these tabs until after the initial paint, and this had a substantial impact on the time taken to first paint.&lt;/p&gt;&#xA;&lt;p&gt;My approach to testing this is rather primitive and slightly flawed, but good enough.&#xA;I record a video using Quicktime Player, and scrub through it - taking note of the timestamp at which I pressed &lt;code&gt;Enter&lt;/code&gt; to run Posting (Launch time (s)), and the time at which the UI is first painted (First paint time (s)). The difference between these timestamps is the Time To First Paint (TTFP (ms)).&#xA;This allows me to measure the full end-to-end time from when I press &lt;code&gt;Enter&lt;/code&gt; to when the UI is first painted.&lt;/p&gt;&#xA;&lt;h3 id=&#34;without-lazy-before&#34; class=&#34;heading-anchor&#34;&gt;Without Lazy (before)&lt;a href=&#34;#without-lazy-before&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Without Lazy (before)&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th style=&#34;text-align: left&#34;&gt;Run&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: left&#34;&gt;Launch (s)&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: left&#34;&gt;First paint (s)&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: left&#34;&gt;TTFP (ms)&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;1&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;2.48&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;2.98&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;500&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;2&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;4.37&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;4.86&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;490&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;3&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;6.88&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;7.38&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;500&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;4&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;10.17&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;10.65&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;480&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h3 id=&#34;with-lazy-after&#34; class=&#34;heading-anchor&#34;&gt;With Lazy (after)&lt;a href=&#34;#with-lazy-after&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: With Lazy (after)&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th style=&#34;text-align: left&#34;&gt;Run&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: left&#34;&gt;Launch (s)&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: left&#34;&gt;First paint (s)&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: left&#34;&gt;TTFP (ms)&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;1&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;17.58&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;17.98&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;400&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;2&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;19.62&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;20.00&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;380&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;3&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;21.50&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;21.87&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;370&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;4&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;23.55&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;23.92&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;370&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;This approach is quite time consuming, and it&amp;rsquo;s not particularly accurate, since QuickTime records at 60fps (each frame captures ~17ms of time). However, in this case I didn&amp;rsquo;t require a high degree of accuracy in order to determine the magnitude of improvement.&lt;/p&gt;&#xA;&lt;h2 id=&#34;using-pyyamls-cloader-5ms-gained&#34; class=&#34;heading-anchor&#34;&gt;Using PyYAML&amp;rsquo;s CLoader (5ms gained)&lt;a href=&#34;#using-pyyamls-cloader-5ms-gained&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Using PyYAML&amp;rsquo;s CLoader (5ms gained)&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Posting uses PyYAML to parse request files and theme files. Requests are stored in YAML files on disk, and when you load a collection into Posting, it&amp;rsquo;ll load all of the request YAML files into memory.&lt;/p&gt;&#xA;&lt;p&gt;PyYAML has a C implementation, which can be used if &lt;code&gt;libyaml&lt;/code&gt; is available on the host system.&lt;/p&gt;&#xA;&lt;div class=&#34;code-panel&#34;&gt;&#xA;  &lt;div class=&#34;code-bar&#34;&gt;&#xA;    &lt;span class=&#34;code-label&#34;&gt;python&lt;/span&gt;&#xA;    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34;&gt;copy&lt;/button&gt;&#xA;  &lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;yaml&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;data&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;yaml&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;load&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;file&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Loader&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;yaml&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;CLoader&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;Switching to this load time when loading a collection of 18 requests from 72ms to 67ms. The time taken loading and parsing the YAML files specifically reduced from 8ms to 3ms.&lt;/p&gt;&#xA;&lt;h2 id=&#34;final-results&#34; class=&#34;heading-anchor&#34;&gt;Final results&lt;a href=&#34;#final-results&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Final results&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;I re-ran the test where I record a video and examine the timestamps where I run the command vs when the UI is first painted. This time, a collection of 18 requests was loaded into the app on startup as well as a couple of environment files.&lt;/p&gt;&#xA;&lt;div class=&#34;code-panel&#34;&gt;&#xA;  &lt;div class=&#34;code-bar&#34;&gt;&#xA;    &lt;span class=&#34;code-label&#34;&gt;bash&lt;/span&gt;&#xA;    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34;&gt;copy&lt;/button&gt;&#xA;  &lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;posting --collection tests/sample-collections/ &lt;span class=&#34;se&#34;&gt;\&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;        --env tests/sample-envs/sample_base.env &lt;span class=&#34;se&#34;&gt;\&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;        --env tests/sample-envs/sample_extra.env&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;Overall, I found a reduction of startup time of over 200ms!&#xA;Although the end result is still not quite as speedy as I&amp;rsquo;d like - I&amp;rsquo;m chuffed with the improvements.&lt;/p&gt;&#xA;&lt;h3 id=&#34;before&#34; class=&#34;heading-anchor&#34;&gt;Before&lt;a href=&#34;#before&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Before&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Time between launching &lt;code&gt;posting&lt;/code&gt; and the UI being painted was approximately 580-590ms.&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th style=&#34;text-align: left&#34;&gt;Run&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: left&#34;&gt;Launch (s)&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: left&#34;&gt;First paint (s)&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: left&#34;&gt;TTFP (ms)&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;1&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;0.93&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;1.52&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;590&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;2&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;4.01&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;4.59&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;580&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;3&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;6.25&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;6.83&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;580&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;4&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;8.45&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;9.03&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;580&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h3 id=&#34;after&#34; class=&#34;heading-anchor&#34;&gt;After&lt;a href=&#34;#after&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: After&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Time between launching &lt;code&gt;posting&lt;/code&gt; and the UI being painted was approximately&#xA;360-390ms.&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th style=&#34;text-align: left&#34;&gt;Run&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: left&#34;&gt;Launch (s)&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: left&#34;&gt;First paint (s)&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: left&#34;&gt;TTFP (ms)&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;1&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;1.07&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;1.45&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;380&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;2&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;6.36&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;6.75&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;390&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;3&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;9.24&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;9.60&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;360&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;4&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;11.17&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;11.55&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;380&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;hr&gt;&#xA;&lt;h4 id=&#34;some-other-ideas&#34; class=&#34;heading-anchor&#34;&gt;Some other ideas&lt;a href=&#34;#some-other-ideas&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Some other ideas&#34;&gt;#&lt;/a&gt;&lt;/h4&gt;&#xA;&lt;p&gt;A couple of other ideas which may or may not be worth pursuing:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;If the user is using one of the built-in themes (i.e. they haven&amp;rsquo;t specified a different theme in their config, then we could lazily load their themes directory when the command palette is opened rather than at startup).&lt;/li&gt;&#xA;&lt;li&gt;There&amp;rsquo;s also &lt;a href=&#34;https://github.com/Textualize/textual/pull/5639&#34;&gt;an improvement&lt;/a&gt; I&amp;rsquo;m looking to merge into Textual itself, which will lazily load &lt;code&gt;tree-sitter&lt;/code&gt; parsers only when they&amp;rsquo;re actually needed.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;hr&gt;&#xA;</description>
    </item>
    <item>
      <title>Goodbye Textualize</title>
      <link>//darren.codes/posts/goodbye-textualize/</link>
      <pubDate>Wed, 01 Jan 2025 13:40:01 +0100</pubDate>
      <guid>//darren.codes/posts/goodbye-textualize/</guid>
      <description>&lt;p&gt;After three years of working full-time on Textual, it&amp;rsquo;s finally time to move on and find a new challenge.&lt;/p&gt;&#xA;&lt;p&gt;Over the course of three years, I&amp;rsquo;ve worked with some amazing colleagues, been part of a fantastic community of open-source developers, and had the opportunity to work on the most interesting projects of my career so far. We took Textual from a proof-of-concept to version 1.0 - a fully-featured open-source TUI framework - which can be used to build apps that push the boundaries of what is possible in the terminal. I feel incredibly privileged to have been involved in such a project. Despite no longer working full-time on it, I&amp;rsquo;ll likely remain active in the community, albeit at a far reduced capacity.&lt;/p&gt;&#xA;&lt;p&gt;What&amp;rsquo;s next for me? Frankly, I have no idea! I&amp;rsquo;ll likely be taking a little bit of time off to relax, spend time with family, and figure out what I want to do.&lt;/p&gt;&#xA;</description>
    </item>
    <item>
      <title>Introducing Posting 2.0</title>
      <link>//darren.codes/posts/posting2/</link>
      <pubDate>Sat, 19 Oct 2024 13:40:01 +0100</pubDate>
      <guid>//darren.codes/posts/posting2/</guid>
      <description>&lt;p&gt;Posting is a powerful HTTP client that runs entirely inside your terminal.&lt;/p&gt;&#xA;&lt;p&gt;Version 2.0 has just arrived, bringing some powerful new features to Posting, including scripting, keymaps, and more!&lt;/p&gt;&#xA;&lt;figure&gt;&lt;img src=&#34;//darren.codes/posts/posting2/posting-screenshot.svg&#34;&#xA;    alt=&#34;posting-2&#34;&gt;&lt;figcaption&gt;&#xA;      &lt;h4&gt;Posting 2.0 - a TUI HTTP client&lt;/h4&gt;&#xA;    &lt;/figcaption&gt;&#xA;&lt;/figure&gt;&#xA;&#xA;&lt;h2 id=&#34;pre-request-and-post-response-scripts&#34; class=&#34;heading-anchor&#34;&gt;Pre-request and post-response scripts&lt;a href=&#34;#pre-request-and-post-response-scripts&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Pre-request and post-response scripts&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;A common feature request has been around adding scripting capabilities to Posting.&#xA;With 2.0, you can now attach Python functions to requests, and have them run before or after the request is sent.&#xA;All output from these scripts is captured and displayed in the new &amp;ldquo;Scripts&amp;rdquo; tab.&#xA;This makes it easy to see the results of your script executions and troubleshoot any issues.&lt;/p&gt;&#xA;&lt;figure&gt;&lt;img src=&#34;//darren.codes/posts/posting2/script-output.png&#34;&#xA;    alt=&#34;Script output&#34;&gt;&lt;figcaption&gt;&#xA;      &lt;h4&gt;Viewing the status and output of scripts in Posting&amp;#39;s new &amp;#39;Scripts&amp;#39; tab.&lt;/h4&gt;&#xA;    &lt;/figcaption&gt;&#xA;&lt;/figure&gt;&#xA;&#xA;&lt;p&gt;This is an extremely versatile feature.&#xA;You can use scripts to perform setup, set variables which requests and other scripts can access, modify outgoing requests, and much more.&#xA;Here&amp;rsquo;s an example of some Python code which you can attach to a request:&lt;/p&gt;&#xA;&lt;div class=&#34;code-panel&#34;&gt;&#xA;  &lt;div class=&#34;code-bar&#34;&gt;&#xA;    &lt;span class=&#34;code-label&#34;&gt;python&lt;/span&gt;&#xA;    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34;&gt;copy&lt;/button&gt;&#xA;  &lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;def&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;on_request&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;request&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;RequestModel&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;posting&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Posting&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;None&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;request&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;headers&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;append&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Header&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;X-Custom-Header&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;value&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;foo&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;request&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;auth&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Auth&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;basic_auth&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;username&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;password&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nb&#34;&gt;print&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;Request is being sent!&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;posting&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;notify&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;Request is being sent!&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;The API is currently a bit rough around the edges, but it&amp;rsquo;ll be improved in future releases.&lt;/p&gt;&#xA;&lt;p&gt;Attached scripts can even be edited in an external editor, and Posting will automatically reload them when they are changed.&lt;/p&gt;&#xA;&lt;figure&gt;&lt;img src=&#34;//darren.codes/posts/posting2/auto-reload.png&#34;&#xA;    alt=&#34;auto-reload&#34;&gt;&lt;figcaption&gt;&#xA;      &lt;h4&gt;Automatic reloading&lt;/h4&gt;&#xA;    &lt;/figcaption&gt;&#xA;&lt;/figure&gt;&#xA;&#xA;&lt;p&gt;There&amp;rsquo;s also a convenience keyboard shortcut which allows you to quickly open a request&amp;rsquo;s scripts in your preferred &lt;code&gt;$EDITOR&lt;/code&gt;,&#xA;so you could quickly swap over to &lt;code&gt;vim&lt;/code&gt; or &lt;code&gt;nano&lt;/code&gt; to make changes, then return to Posting to continue working.&lt;/p&gt;&#xA;&lt;p&gt;Check out the &lt;a href=&#34;https://posting.sh/guide/scripting&#34;&gt;scripting docs&lt;/a&gt; for more details!&lt;/p&gt;&#xA;&lt;h2 id=&#34;keymaps&#34; class=&#34;heading-anchor&#34;&gt;Keymaps&lt;a href=&#34;#keymaps&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Keymaps&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Posting 2.0 also allows you to declare custom keybindings, thanks to an update in the underlying &lt;a href=&#34;https://textual.textualize.io/&#34;&gt;Textual&lt;/a&gt; framework.&#xA;For 2.0 only &amp;ldquo;global&amp;rdquo; keybindings can be remapped, but this will be expanded in future releases to include most keybindings.&lt;/p&gt;&#xA;&lt;p&gt;You can customise your keymap by adding a &lt;code&gt;keymap&lt;/code&gt; section to your Posting config.&#xA;Here&amp;rsquo;s a short example which changes the default for sending the request to &lt;code&gt;ctrl+r&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;div class=&#34;code-panel&#34;&gt;&#xA;  &lt;div class=&#34;code-bar&#34;&gt;&#xA;    &lt;span class=&#34;code-label&#34;&gt;yaml&lt;/span&gt;&#xA;    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34;&gt;copy&lt;/button&gt;&#xA;  &lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;keymap&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;send-request&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;ctrl+r&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;Hopefully this will help you feel more at home in Posting, and avoid awkward clashes with emulators and multiplexers!&lt;/p&gt;&#xA;&lt;p&gt;See the &lt;a href=&#34;https://posting.sh/guide/keymap&#34;&gt;keymap docs&lt;/a&gt; for more details!&lt;/p&gt;&#xA;&lt;h2 id=&#34;customisable-hostnames&#34; class=&#34;heading-anchor&#34;&gt;Customisable hostnames&lt;a href=&#34;#customisable-hostnames&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Customisable hostnames&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Posting is a TUI, which means it works great over SSH.&#xA;However, when working inside a TUI, it&amp;rsquo;s sometimes easy to forget which host you&amp;rsquo;re on.&lt;/p&gt;&#xA;&lt;p&gt;In 2.0, there&amp;rsquo;s a new config option which allows you to customise the hostnames which are displayed in the header, including styling them with Rich markup to make them stand out.&lt;/p&gt;&#xA;&lt;p&gt;This is useful if you work with Posting in multiple environments, such as your local machine, a staging server, and a production server.&lt;/p&gt;&#xA;&lt;figure&gt;&lt;img src=&#34;//darren.codes/posts/posting2/header-local.png&#34;&#xA;    alt=&#34;header-local&#34;&gt;&lt;figcaption&gt;&#xA;      &lt;h4&gt;Running Posting locally&lt;/h4&gt;&#xA;    &lt;/figcaption&gt;&#xA;&lt;/figure&gt;&#xA;&#xA;&lt;figure&gt;&lt;img src=&#34;//darren.codes/posts/posting2/production-blink.gif&#34;&#xA;    alt=&#34;header-production&#34;&gt;&lt;figcaption&gt;&#xA;      &lt;h4&gt;Running Posting on a production server&lt;/h4&gt;&#xA;    &lt;/figcaption&gt;&#xA;&lt;/figure&gt;&#xA;&#xA;&lt;p&gt;Here&amp;rsquo;s the config I use on my &amp;ldquo;production&amp;rdquo; server to achieve the effect above:&lt;/p&gt;&#xA;&lt;div class=&#34;code-panel&#34;&gt;&#xA;  &lt;div class=&#34;code-bar&#34;&gt;&#xA;    &lt;span class=&#34;code-label&#34;&gt;yaml&lt;/span&gt;&#xA;    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34;&gt;copy&lt;/button&gt;&#xA;  &lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;heading&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;hostname&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;[black on #ff0000 bold blink]PRODUCTION[/]&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;h2 id=&#34;documentation&#34; class=&#34;heading-anchor&#34;&gt;Documentation&lt;a href=&#34;#documentation&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Documentation&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Posting&amp;rsquo;s documentation has been expanded, making it easier than ever to get started!&lt;/p&gt;&#xA;&lt;p&gt;The additions include:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Duplicating and &amp;ldquo;quick duplicating&amp;rdquo; requests.&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://posting.sh/guide/scripting&#34;&gt;Scripting&lt;/a&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://posting.sh/guide/keymap&#34;&gt;Keymaps&lt;/a&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://posting.sh/guide/external_tools&#34;&gt;Integrating Posting with external tools&lt;/a&gt;.&lt;/li&gt;&#xA;&lt;li&gt;An improved &lt;a href=&#34;https://posting.sh/guide/&#34;&gt;getting started guide&lt;/a&gt; which better showcases some quality-of-life features that may not be obvious at first glance :)&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;whats-next&#34; class=&#34;heading-anchor&#34;&gt;What&amp;rsquo;s next?&lt;a href=&#34;#whats-next&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: What&amp;rsquo;s next?&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Textual 1.0 is around the corner, and will unlock a better theming system for Posting as well as a more powerful command palette!&lt;/p&gt;&#xA;&lt;p&gt;I&amp;rsquo;ve also braindumped some features I&amp;rsquo;d like to implement in Posting &lt;a href=&#34;https://posting.sh/roadmap&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;The new features mentioned in this post and those on the roadmap are very much driven by users.&#xA;If you&amp;rsquo;d like to suggest new features, please open a &lt;a href=&#34;https://github.com/darrenburns/posting/discussions&#34;&gt;discussion on GitHub&lt;/a&gt;!&lt;/p&gt;&#xA;</description>
    </item>
    <item>
      <title>Custom keymaps in Textual</title>
      <link>//darren.codes/posts/textual-keymaps/</link>
      <pubDate>Thu, 03 Oct 2024 10:42:09 +0100</pubDate>
      <guid>//darren.codes/posts/textual-keymaps/</guid>
      <description>&lt;p&gt;&lt;a href=&#34;https://github.com/Textualize/textual/releases/tag/v0.82.0&#34;&gt;Textual 0.82.0&lt;/a&gt; adds support for &lt;em&gt;keymaps&lt;/em&gt;, which make it easy to customise keybindings based on config or user preferences.&lt;/p&gt;&#xA;&lt;p&gt;Keymaps allow you to swap the keys associated with a &lt;code&gt;Binding&lt;/code&gt; at runtime with a single method call.&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;You could switch to a Vim keymap if a user of your app enables &amp;ldquo;Vim mode&amp;rdquo;.&lt;/li&gt;&#xA;&lt;li&gt;You could read a keymap from a file on the users drive and swap it in on app startup.&lt;/li&gt;&#xA;&lt;li&gt;You could swap the increment and decrement keys on April 1st!&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;The possibilities are endless!&lt;/p&gt;&#xA;&lt;p&gt;Keymaps are simple to use - just give an &lt;code&gt;id&lt;/code&gt; to any &lt;code&gt;Binding&lt;/code&gt; you wish to permit overrides for, then call the &lt;code&gt;App.set_keymap&lt;/code&gt; method when your app starts, passing in a dict mapping that binding ID to the new key.&lt;/p&gt;&#xA;&lt;h2 id=&#34;vim-keys&#34; class=&#34;heading-anchor&#34;&gt;Vim keys&lt;a href=&#34;#vim-keys&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Vim keys&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Here&amp;rsquo;s an example of how you&amp;rsquo;d define a binding with an overridable key:&lt;/p&gt;&#xA;&lt;div class=&#34;code-panel&#34;&gt;&#xA;  &lt;div class=&#34;code-bar&#34;&gt;&#xA;    &lt;span class=&#34;code-label&#34;&gt;python&lt;/span&gt;&#xA;    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34;&gt;copy&lt;/button&gt;&#xA;  &lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kn&#34;&gt;from&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;textual.binding&lt;/span&gt; &lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Binding&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kn&#34;&gt;from&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;textual.reactive&lt;/span&gt; &lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;reactive&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kn&#34;&gt;from&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;textual.widgets&lt;/span&gt; &lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Static&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;Counter&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Static&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;can_focus&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;True&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;):&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;BINDINGS&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;# The id is a string used to identify this binding in the keymap.&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;# You may wish to namespace them to make managing them easier!&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;Binding&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;up&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;change_count(1)&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Increment&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;counter.increment&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;Binding&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;down&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;change_count(-1)&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Decrement&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;counter.decrement&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#x9;&lt;span class=&#34;n&#34;&gt;count&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;reactive&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s2&#34;&gt;&amp;#34;&amp;#34;&amp;#34;Stores the current value of the counter.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#x9;&lt;span class=&#34;k&#34;&gt;def&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;action_change_count&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;delta&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;None&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;s2&#34;&gt;&amp;#34;&amp;#34;&amp;#34;Change the counter by delta.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#x9;    &lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;count&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;delta&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;Pressing &lt;code&gt;up&lt;/code&gt; or &lt;code&gt;down&lt;/code&gt; on your keyboard will increment or decrement the counter.&lt;/p&gt;&#xA;&lt;p&gt;Since we&amp;rsquo;ve given these bindings IDs, they can now be &lt;em&gt;overridden&lt;/em&gt; by a keymap.&#xA;Let&amp;rsquo;s override these two bindings so that we can also use Vim keys to change the counter.&lt;/p&gt;&#xA;&lt;p&gt;When our &lt;code&gt;App&lt;/code&gt; launches, we just need call &lt;code&gt;App.set_keymap&lt;/code&gt; to specify our overrides.&#xA;A good place to do this would be in &lt;code&gt;App.on_mount&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;div class=&#34;code-panel&#34;&gt;&#xA;  &lt;div class=&#34;code-bar&#34;&gt;&#xA;    &lt;span class=&#34;code-label&#34;&gt;python&lt;/span&gt;&#xA;    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34;&gt;copy&lt;/button&gt;&#xA;  &lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kn&#34;&gt;from&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;textual.app&lt;/span&gt; &lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;App&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;MyCounterApp&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;App&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;None&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]):&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;def&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;on_mount&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;None&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#x9;    &lt;span class=&#34;n&#34;&gt;vim_keymap&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#x9;        &lt;span class=&#34;s2&#34;&gt;&amp;#34;counter.increment&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;k,up&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#x9;        &lt;span class=&#34;s2&#34;&gt;&amp;#34;counter.decrement&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;j,down&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#x9;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;set_keymap&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;vim_keymap&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;Now users will be able to use &lt;code&gt;k&lt;/code&gt; &lt;em&gt;or&lt;/em&gt; &lt;code&gt;up&lt;/code&gt; to increment the counter, and &lt;code&gt;j&lt;/code&gt; or &lt;code&gt;down&lt;/code&gt; to decrement it!&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;It&amp;rsquo;s important to note that if we still wish for the &lt;code&gt;up&lt;/code&gt; and &lt;code&gt;down&lt;/code&gt; keys to work, &lt;em&gt;we need to include them in the keymap too!&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;That&amp;rsquo;s all there is to it!&lt;/p&gt;&#xA;&lt;p&gt;Keymaps should make it easier for developers to support users who want to customise their keybindings, and reduce the impact of a default&#xA;keybinding choice clashing with the terminal emulator or multiplexer.&lt;/p&gt;&#xA;&lt;p&gt;Check out the API documentation for keymaps &lt;a href=&#34;https://textual.textualize.io/api/app/#textual.app.App.set_keymap&#34;&gt;here&lt;/a&gt;!&lt;/p&gt;&#xA;</description>
    </item>
    <item>
      <title>Copying and pasting in Textual</title>
      <link>//darren.codes/posts/textual-copy-paste/</link>
      <pubDate>Wed, 02 Oct 2024 22:36:57 +0100</pubDate>
      <guid>//darren.codes/posts/textual-copy-paste/</guid>
      <description>&lt;p&gt;Terminal emulators are in general quite limited when it comes to copying and pasting.&#xA;Fortunately, we have a few options when writing Textual apps which allow us to add support for copy and paste.&#xA;Each has its own trade-offs.&#xA;Let&amp;rsquo;s explore these options and look at how we can integrate them into our apps.&lt;/p&gt;&#xA;&lt;h2 id=&#34;appcopy_to_clipboard&#34; class=&#34;heading-anchor&#34;&gt;&lt;code&gt;App.copy_to_clipboard&lt;/code&gt;&lt;a href=&#34;#appcopy_to_clipboard&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: App.copy_to_clipboard&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Textual&amp;rsquo;s &lt;code&gt;App&lt;/code&gt; class has a method called &lt;a href=&#34;https://textual.textualize.io/api/app/#textual.app.App.copy_to_clipboard&#34;&gt;&lt;code&gt;copy_to_clipboard&lt;/code&gt;&lt;/a&gt;.&#xA;Pass a string into it, and it gets copied to the clipboard on the machine the emulator is running on.&#xA;There is a caveat here: If the terminal doesn&amp;rsquo;t support the required protocol then nothing will happen!&lt;/p&gt;&#xA;&lt;h3 id=&#34;aside-how-does-it-work&#34; class=&#34;heading-anchor&#34;&gt;Aside: How does it work?&lt;a href=&#34;#aside-how-does-it-work&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Aside: How does it work?&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;&lt;code&gt;App.copy_to_clipboard&lt;/code&gt; uses the &lt;a href=&#34;https://en.wikipedia.org/wiki/ANSI_escape_code#OSC_(Operating_System_Command)_sequences&#34;&gt;OSC 52 ANSI escape sequence&lt;/a&gt; to tell the terminal emulator to copy text to the system clipboard.&#xA;The terminal emulator doesn&amp;rsquo;t consider the &lt;em&gt;source&lt;/em&gt; of the text, it justs sends it to the system clipboard.&#xA;Running an application inside tmux over SSH? The text can still be copied to your local system&amp;rsquo;s clipboard!&lt;/p&gt;&#xA;&lt;p&gt;This sequence has a reasonable degree of support, with MacOS terminal being the notable exception.&#xA;If a terminal supports OSC 52, then &lt;code&gt;App.copy_to_clipboard&lt;/code&gt; will work.&lt;/p&gt;&#xA;&lt;p&gt;Here&amp;rsquo;s a table showing OSC 52 support for common terminal emulators:&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th style=&#34;text-align: left&#34;&gt;Terminal Emulator&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: left&#34;&gt;OSC 52 Support&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;iTerm2&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;Yes&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;Kitty&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;Yes&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;Alacritty&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;Yes&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;Windows Terminal&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;Yes&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;WezTerm&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;Yes&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;Ghostty&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;Yes&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;Gnome Terminal&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;No&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;Konsole&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;No (Coming soon)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;MacOS Terminal&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;No&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;consider-pyperclip&#34; class=&#34;heading-anchor&#34;&gt;Consider &lt;code&gt;pyperclip&lt;/code&gt;&lt;a href=&#34;#consider-pyperclip&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Consider pyperclip&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;If you need broader clipboard support you can try &lt;code&gt;pyperclip&lt;/code&gt;. &lt;code&gt;pyperclip&lt;/code&gt; is a cross-platform library for interacting with the clipboard.&lt;/p&gt;&#xA;&lt;p&gt;It&amp;rsquo;s important to note that &lt;code&gt;pyperclip&lt;/code&gt; copies to the clipboard of the machine that the &lt;em&gt;app&lt;/em&gt; is running on.&#xA;This is an important distinction.&#xA;If your app is running over SSH, &lt;code&gt;pyperclip&lt;/code&gt; copies to the clipboard of the &lt;em&gt;remote host&lt;/em&gt;!&lt;/p&gt;&#xA;&lt;h2 id=&#34;integrating-pyperclip-with-textual&#34; class=&#34;heading-anchor&#34;&gt;Integrating &lt;code&gt;pyperclip&lt;/code&gt; with Textual&lt;a href=&#34;#integrating-pyperclip-with-textual&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Integrating pyperclip with Textual&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Let&amp;rsquo;s look at an example of how we can create a read-only &lt;code&gt;TextArea&lt;/code&gt;, which we can copy some text from.&#xA;The example assumes you&amp;rsquo;ve installed &lt;code&gt;pyperclip&lt;/code&gt; into your environment.&lt;/p&gt;&#xA;&lt;h3 id=&#34;subclassing-textarea-and-adding-a-new-binding&#34; class=&#34;heading-anchor&#34;&gt;Subclassing &lt;code&gt;TextArea&lt;/code&gt; and adding a new binding&lt;a href=&#34;#subclassing-textarea-and-adding-a-new-binding&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Subclassing TextArea and adding a new binding&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;First, subclass &lt;code&gt;TextArea&lt;/code&gt;, and add a binding to call the &lt;code&gt;action_copy_selection&lt;/code&gt; method:&lt;/p&gt;&#xA;&lt;div class=&#34;code-panel&#34;&gt;&#xA;  &lt;div class=&#34;code-bar&#34;&gt;&#xA;    &lt;span class=&#34;code-label&#34;&gt;python&lt;/span&gt;&#xA;    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34;&gt;copy&lt;/button&gt;&#xA;  &lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;ReadOnlyTextArea&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;TextArea&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;):&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;BINDINGS&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Binding&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;y,c&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;action&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;copy_selection&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;description&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;Copy selection&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)]&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;def&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;on_mount&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;None&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;read_only&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;True&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;def&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;action_copy_selection&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;):&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;s2&#34;&gt;&amp;#34;&amp;#34;&amp;#34;Copy to the clipboard of the machine the app is running on&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;This links the &lt;code&gt;y&lt;/code&gt; and &lt;code&gt;c&lt;/code&gt; keys on your keyboard to the &lt;code&gt;action_copy_selection&lt;/code&gt; method.&#xA;Press one of those keys, and &lt;code&gt;action_copy_selection&lt;/code&gt; runs.&#xA;Let&amp;rsquo;s implement that method now.&lt;/p&gt;&#xA;&lt;h3 id=&#34;implementing-the-action_copy_selection-method&#34; class=&#34;heading-anchor&#34;&gt;Implementing the &lt;code&gt;action_copy_selection&lt;/code&gt; method&lt;a href=&#34;#implementing-the-action_copy_selection-method&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Implementing the action_copy_selection method&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;The snippet below implements &lt;code&gt;action_copy_selection&lt;/code&gt; using &lt;code&gt;pyperclip&lt;/code&gt;.&#xA;It gets the &lt;code&gt;selected_text&lt;/code&gt; from the &lt;code&gt;TextArea&lt;/code&gt;, then tries to import and call &lt;code&gt;pyperclip&lt;/code&gt;.&#xA;There is a gotcha here: clipboard functionality is not guaranteed.&#xA;On Ubuntu, the clipboard is provided by &lt;code&gt;xclip&lt;/code&gt; or &lt;code&gt;xsel&lt;/code&gt;.&#xA;If these are unavailable, &lt;code&gt;pyperclip&lt;/code&gt; will raise an exception which we must handle.&#xA;Lack of clipboard support shouldn&amp;rsquo;t be a fatal error, so graceful handling is important.&#xA;I opted to notify the user about the problem using &lt;code&gt;self.notify&lt;/code&gt;, which shows a toast popup at the bottom right of the screen.&lt;/p&gt;&#xA;&lt;div class=&#34;code-panel&#34;&gt;&#xA;  &lt;div class=&#34;code-bar&#34;&gt;&#xA;    &lt;span class=&#34;code-label&#34;&gt;python&lt;/span&gt;&#xA;    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34;&gt;copy&lt;/button&gt;&#xA;  &lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;def&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;action_copy_selection&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;):&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;s2&#34;&gt;&amp;#34;&amp;#34;&amp;#34;Copy to the clipboard of the machine the app is running on&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;text_to_copy&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;selected_text&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;try&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;pyperclip&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;pyperclip&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;copy&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;text_to_copy&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;except&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;pyperclip&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;PyperclipException&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;as&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;exc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;c1&#34;&gt;# Show a toast popup if we fail to copy.&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;notify&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;nb&#34;&gt;str&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;exc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;n&#34;&gt;title&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;Clipboard error&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;n&#34;&gt;severity&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;error&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;notify&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;sa&#34;&gt;f&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;Copied &lt;/span&gt;&lt;span class=&#34;si&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;len&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;text_to_copy&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt; characters!&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;title&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;Copied selection&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;With our action method in place, we can now select text and press &lt;code&gt;y&lt;/code&gt; to copy it to the clipboard.&lt;/p&gt;&#xA;&lt;h3 id=&#34;using-our-new-widget&#34; class=&#34;heading-anchor&#34;&gt;Using our new widget&lt;a href=&#34;#using-our-new-widget&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Using our new widget&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Here&amp;rsquo;s a simple Textual app that demonstrates the use of our &lt;code&gt;ReadOnlyTextArea&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;div class=&#34;code-panel&#34;&gt;&#xA;  &lt;div class=&#34;code-bar&#34;&gt;&#xA;    &lt;span class=&#34;code-label&#34;&gt;python&lt;/span&gt;&#xA;    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34;&gt;copy&lt;/button&gt;&#xA;  &lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kn&#34;&gt;from&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;textual.app&lt;/span&gt; &lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;App&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ComposeResult&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kn&#34;&gt;from&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;textual.widgets&lt;/span&gt; &lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Header&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Footer&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;ClipboardDemoApp&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;App&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;):&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;def&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;compose&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ComposeResult&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;yield&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Header&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;yield&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ReadOnlyTextArea&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;Select this text and press &amp;#39;y&amp;#39; to copy!&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;yield&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Footer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;vm&#34;&gt;__name__&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;__main__&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;app&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ClipboardDemoApp&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;app&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;run&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;If you run the app and select some text using the mouse or by holding shift and moving the cursor, you&amp;rsquo;ll see a toast popup indicating that the text was copied.&lt;/p&gt;&#xA;&lt;p&gt;Switch over to another application and try pasting to confirm that it works.&lt;/p&gt;&#xA;&lt;h2 id=&#34;which-should-i-use&#34; class=&#34;heading-anchor&#34;&gt;Which should I use?&lt;a href=&#34;#which-should-i-use&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Which should I use?&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;If you&amp;rsquo;re distributing your app, &lt;code&gt;pyperclip&lt;/code&gt; is likely the better choice due to broader support.&#xA;However, if your app might reasonably be used over SSH, you may consider supporting &lt;em&gt;both&lt;/em&gt;.&lt;/p&gt;&#xA;&lt;p&gt;You could override &lt;code&gt;App.copy_to_clipboard&lt;/code&gt; with a &lt;code&gt;pyperclip&lt;/code&gt; version, or make it configurable.&#xA;You could choose to have different keybindings for &amp;ldquo;copy via terminal&amp;rdquo; and &amp;ldquo;copy via host.&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;Another option is to try some smarter detection and choose which one to used based on the user&amp;rsquo;s environment.&#xA;For example, if the user is using the default MacOS terminal (which lacks support for copy/paste), you may opt to fall-back to &lt;code&gt;pyperclip&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;div class=&#34;code-panel&#34;&gt;&#xA;  &lt;div class=&#34;code-bar&#34;&gt;&#xA;    &lt;span class=&#34;code-label&#34;&gt;python&lt;/span&gt;&#xA;    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34;&gt;copy&lt;/button&gt;&#xA;  &lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kn&#34;&gt;from&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;textual.app&lt;/span&gt; &lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;App&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;MyApp&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;App&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;None&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]):&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;def&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;copy_to_clipboard&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;text&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;str&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;None&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;s2&#34;&gt;&amp;#34;&amp;#34;&amp;#34;Copy text to the clipboard&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;is_apple_terminal&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;os&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;environ&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;get&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;TERM_PROGRAM&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Apple_Terminal&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;is_apple_terminal&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;copy_with_pyperclip&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;text&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;nb&#34;&gt;super&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;copy_to_clipboard&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;text&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;def&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;copy_with_pyperclip&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;text&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;str&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;None&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;s2&#34;&gt;&amp;#34;&amp;#34;&amp;#34;Copy text to the clipboard using pyperclip&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;# don&amp;#39;t forget error-handling!&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;pyperclip&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;pyperclip&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;copy&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;text&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;This is of course rather naive, as &lt;code&gt;TERM_PROGRAM&lt;/code&gt; alone does not provide a complete picture of the user&amp;rsquo;s environment.&#xA;A more robust solution might check the XTerm version, &lt;a href=&#34;https://github.com/emacs-mirror/emacs/blob/f18af6cd5cb7dbbf7420ec2d3efed4e202c4f0dd/lisp/term/xterm.el#L713&#34;&gt;like Emacs does&lt;/a&gt;.&lt;/p&gt;&#xA;</description>
    </item>
    <item>
      <title>Let&#39;s get fractional! Textual layouts with fr units</title>
      <link>//darren.codes/posts/textual-layout-fr-units/</link>
      <pubDate>Tue, 01 Oct 2024 22:22:47 +0100</pubDate>
      <guid>//darren.codes/posts/textual-layout-fr-units/</guid>
      <description>&lt;p&gt;Fractional units (&lt;code&gt;fr&lt;/code&gt;) are a tool in &lt;a href=&#34;https://textual.textualize.io/guide/CSS/&#34;&gt;Textual&amp;rsquo;s CSS system&lt;/a&gt; which lets you distribute space between child widgets inside a container in a really intuitive way.&lt;/p&gt;&#xA;&lt;p&gt;I use them &lt;em&gt;everywhere&lt;/em&gt; in my Textual apps, but I get the feeling that they may be a little under-appreciated.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;fr&lt;/code&gt; units make it easy to implement many common layout patterns, and have some less obvious benefits too!&lt;/p&gt;&#xA;&lt;p&gt;Let&amp;rsquo;s explore how fractional units work in Textual, along with some practical examples!&lt;/p&gt;&#xA;&lt;h2 id=&#34;a-half-and-half-layout&#34; class=&#34;heading-anchor&#34;&gt;A half-and-half layout&lt;a href=&#34;#a-half-and-half-layout&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: A half-and-half layout&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Let&amp;rsquo;s start with a simple example.&#xA;Given a horizontal container with two child widgets &lt;code&gt;Left&lt;/code&gt; and &lt;code&gt;Right&lt;/code&gt;, give each widget half of the available width.&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;Left&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Static&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;):&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;pass&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;Right&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Static&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;):&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;pass&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;MyApp&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;App&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;None&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]):&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;def&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;compose&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ComposeResult&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;with&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Horizontal&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;():&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;yield&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Left&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;yield&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Right&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#xA;&lt;p&gt;We can assign a width of &lt;code&gt;1fr&lt;/code&gt; to each of these widgets to achieve the desired layout:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-scss&#34; data-lang=&#34;scss&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;Horizontal&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&#34;nt&#34;&gt;Left&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line hl&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;na&#34;&gt;width&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;fr&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&#34;nt&#34;&gt;Right&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line hl&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;na&#34;&gt;width&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;fr&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#xA;&lt;p&gt;Both &lt;code&gt;Left&lt;/code&gt; and &lt;code&gt;Right&lt;/code&gt; widgets have width &lt;code&gt;1fr&lt;/code&gt; out of a total of 2 assigned &lt;code&gt;fr&lt;/code&gt; units on the horizontal axis, meaning they&amp;rsquo;ll each be given &lt;code&gt;1/2&lt;/code&gt; of the available space inside the container (25 cells each in this case).&lt;/p&gt;&#xA;&lt;aside class=&#34;callout&#34;&gt;&#xA;  &lt;span class=&#34;callout-icon&#34;&gt;&lt;svg class=&#34;icon&#34; viewBox=&#34;0 0 24 24&#34; aria-hidden=&#34;true&#34; fill=&#34;none&#34; stroke=&#34;currentColor&#34; stroke-width=&#34;2&#34; stroke-linecap=&#34;round&#34; stroke-linejoin=&#34;round&#34;&gt;&lt;circle cx=&#34;12&#34; cy=&#34;12&#34; r=&#34;10&#34;/&gt;&lt;path d=&#34;M12 16v-4M12 8h.01&#34;/&gt;&lt;/svg&gt;&lt;/span&gt;&#xA;  &lt;div class=&#34;callout-body&#34;&gt;&lt;code&gt;Horizontal&lt;/code&gt; is a Textual utility container which itself has width &lt;code&gt;1fr&lt;/code&gt;.&lt;/div&gt;&#xA;&lt;/aside&gt;&#xA;&#xA;&lt;p&gt;&lt;img src=&#34;./FrUnitsEqual.svg&#34; alt=&#34;FrUnitsEqual.svg&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;In my terminal-based HTTP client, &lt;a href=&#34;https://github.com/darrenburns/posting&#34;&gt;Posting&lt;/a&gt;, I use &lt;code&gt;height: 1fr&lt;/code&gt; to evenly split the request and response sections inside a &lt;code&gt;Vertical&lt;/code&gt; container:&lt;/p&gt;&#xA;&lt;figure&gt;&lt;img src=&#34;//darren.codes/posts/textual-layout-fr-units/posting.svg&#34;&#xA;    alt=&#34;A screenshot of Posting, showing request and response sections split 50/50.&#34;&gt;&lt;figcaption&gt;&#xA;      &lt;h4&gt;The request and response sections are inside a Vertical container, and both have height: 1fr, creating a 50/50 split.&lt;/h4&gt;&#xA;    &lt;/figcaption&gt;&#xA;&lt;/figure&gt;&#xA;&#xA;&lt;p&gt;That&amp;rsquo;s all well and good, but it&amp;rsquo;s not really offering anything new over using &lt;code&gt;%&lt;/code&gt; units, is it?&lt;/p&gt;&#xA;&lt;h2 id=&#34;changing-the-distribution&#34; class=&#34;heading-anchor&#34;&gt;Changing the distribution&lt;a href=&#34;#changing-the-distribution&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Changing the distribution&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;If one of our child widgets, say, the &lt;code&gt;Left&lt;/code&gt; one, is updated to &lt;code&gt;width: 2fr&lt;/code&gt;, then that widget will take up &lt;em&gt;two thirds&lt;/em&gt; of the space, and the other widget will take up the remaining one third.&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;./FrUnitsTwoThirds.svg&#34; alt=&#34;FrUnitsTwoThirds.svg&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;This is because we&amp;rsquo;ve now assigned a total of 3 &lt;code&gt;fr&lt;/code&gt; units along the horizontal axis.&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;The &lt;code&gt;Left&lt;/code&gt; widget was assigned 2 units on the horizontal axis, so receives 2/3 of the available space.&lt;/li&gt;&#xA;&lt;li&gt;The &lt;code&gt;Right&lt;/code&gt; widget was assigned 1 unit, so receives 1/3 of the available space.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Note that in this example, the parent &lt;code&gt;Horizontal&lt;/code&gt; widget has a width of 50, which is &lt;em&gt;not&lt;/em&gt; divisible by 3. However, when using fractional units, Textual takes care of that for you, ensuring that the space is fully distributed with no remainder!&lt;/p&gt;&#xA;&lt;p&gt;This also shows another subtle benefit - we only need to update a single value to &lt;code&gt;2fr&lt;/code&gt; and the other children will automatically adjust to fill the remaining space. When using percentages, we&amp;rsquo;d need to update all children and ensure they sum to 100% to get (almost) the same effect.&lt;/p&gt;&#xA;&lt;p&gt;The &lt;code&gt;fr&lt;/code&gt; unit guarantees that we&amp;rsquo;ll always use &lt;em&gt;precisely&lt;/em&gt; 100% of the remaining space, while also handling rounding issues you might encounter when dealing in percentages&amp;hellip; because 33.3% + 66.6% != 100%.&lt;/p&gt;&#xA;&lt;h2 id=&#34;mixing-concrete-and-fractional-units&#34; class=&#34;heading-anchor&#34;&gt;Mixing concrete and fractional units&lt;a href=&#34;#mixing-concrete-and-fractional-units&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Mixing concrete and fractional units&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Fractional units also interact wonderfully with concrete values like &lt;code&gt;width: 10;&lt;/code&gt;, and this combo has several practical use-cases.&#xA;For example, it lets us model a fixed-width &amp;ldquo;sidebar&amp;rdquo;, and have the rest of the content fill out the remaining space.&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;./FrUnitsFill.svg&#34; alt=&#34;FrUnitsFill.svg&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;The CSS for this might look something like this:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-scss&#34; data-lang=&#34;scss&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;Horizontal&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&#34;nt&#34;&gt;Sidebar&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;na&#34;&gt;width&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;10&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&#34;nt&#34;&gt;Content&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;na&#34;&gt;width&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;fr&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#xA;&lt;p&gt;Having the blue &lt;code&gt;Content&lt;/code&gt; widget in the example above fill out like it does is not possible using &lt;code&gt;%&lt;/code&gt;, as the value we&amp;rsquo;d need to set it to would change if the parent container was resized.&lt;/p&gt;&#xA;&lt;p&gt;We can again see this in action in Posting, this time in the URL bar:&lt;/p&gt;&#xA;&lt;figure&gt;&lt;img src=&#34;//darren.codes/posts/textual-layout-fr-units/PostingUrlBar.png&#34;&#xA;    alt=&#34;A screenshot of Posting, showing the URL bar with a 50/50 split between the path and query parameters.&#34;&gt;&lt;figcaption&gt;&#xA;      &lt;h4&gt;The &amp;#39;URLBar&amp;#39; widget in Posting.&lt;/h4&gt;&#xA;    &lt;/figcaption&gt;&#xA;&lt;/figure&gt;&#xA;&#xA;&lt;p&gt;The method selector dropdown on the left and the &amp;ldquo;Send&amp;rdquo; button on the right have fixed widths, while the URL input has &lt;code&gt;width: 1fr&lt;/code&gt;, allowing it to expand to fill the remaining space between them.&lt;/p&gt;&#xA;&lt;p&gt;I&amp;rsquo;ve found this to be a recurring pattern in my apps: a &lt;code&gt;Horizontal&lt;/code&gt; container with a few children having fixed or &lt;code&gt;auto&lt;/code&gt;-calculated widths, with one (or sometimes more) children having &lt;code&gt;width: 1fr&lt;/code&gt; to fill the remaining space.&lt;/p&gt;&#xA;&lt;p&gt;Here&amp;rsquo;s the Python code directly taken from Posting which shows this in practice:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;def&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;compose&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ComposeResult&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;with&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Horizontal&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;():&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;yield&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;MethodSelector&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;method-selector&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;# fixed width&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;yield&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UrlInput&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;  &lt;span class=&#34;c1&#34;&gt;# width: 1fr&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;placeholder&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;Enter a URL...&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;nb&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;url-input&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;yield&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Label&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;trace-markers&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;  &lt;span class=&#34;c1&#34;&gt;# fixed width&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;yield&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;SendRequestButton&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;Send&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;  &lt;span class=&#34;c1&#34;&gt;# fixed width&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#xA;&lt;h2 id=&#34;conclusion&#34; class=&#34;heading-anchor&#34;&gt;Conclusion&lt;a href=&#34;#conclusion&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Conclusion&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;I&amp;rsquo;ve found fractional units to be a really powerful and practical tool in Textual, and I hope this post has given you a few ideas for how you can use them in your own apps!&lt;/p&gt;&#xA;</description>
    </item>
    <item>
      <title>Learning</title>
      <link>//darren.codes/learning/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>//darren.codes/learning/</guid>
      <description>&lt;p&gt;Interactive explainers I&amp;rsquo;ve made while learning how things work.&lt;/p&gt;&#xA;&lt;h2 id=&#34;how-terminals-work&#34; class=&#34;heading-anchor&#34;&gt;How terminals work&lt;a href=&#34;#how-terminals-work&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: How terminals work&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;a href=&#34;//darren.codes/learning/how-terminals-work/&#34;&gt;&lt;strong&gt;Anatomy of a keystroke →&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;Follow a single key press from the keyboard, through the terminal emulator, the kernel&amp;rsquo;s PTY and line discipline, to your shell and back again as pixels on screen. Includes a working simulator, a step-through VT escape sequence parser, and a job control playground.&lt;/p&gt;&#xA;</description>
    </item>
    <item>
      <title>My projects</title>
      <link>//darren.codes/projects/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>//darren.codes/projects/</guid>
      <description>&lt;p&gt;Over the years I&amp;rsquo;ve developed several open source projects.&lt;/p&gt;&#xA;&lt;p&gt;The majority are written in Python, and they&amp;rsquo;re typically terminal-based developer tools.&lt;/p&gt;&#xA;&lt;h2 id=&#34;posting&#34; class=&#34;heading-anchor&#34;&gt;Posting&lt;a href=&#34;#posting&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Posting&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://posting.sh&#34;&gt;Posting&lt;/a&gt; is a powerful TUI for working with HTTP APIs.&#xA;It&amp;rsquo;s a bit like Postman, except it runs in the terminal and is designed around keyboard-centric workflows.&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;./home-image-ad-15aug24.svg&#34; alt=&#34;A screenshot of Posting&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;It&amp;rsquo;s a feature-rich tool that tries to work for both terminal power-users and newcomers who are more accustomed to modern web UIs.&lt;/p&gt;&#xA;&lt;p&gt;Written in Python using &lt;a href=&#34;https://textual.textualize.io/&#34;&gt;Textual&lt;/a&gt;, a TUI framework I help develop and maintain.&lt;/p&gt;&#xA;&lt;h2 id=&#34;elia&#34; class=&#34;heading-anchor&#34;&gt;Elia&lt;a href=&#34;#elia&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Elia&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://github.com/darrenburns/elia&#34;&gt;Elia&lt;/a&gt; is a TUI for interacting with large language models.&#xA;It can interact with proprietary models such as Claude, ChatGPT, as well as local models like Llama.&lt;/p&gt;&#xA;&lt;figure&gt;&lt;img src=&#34;//darren.codes/projects/elia.svg&#34;&#xA;    alt=&#34;A screenshot of Elia&#34; width=&#34;900px&#34;&gt;&lt;figcaption&gt;&#xA;      &lt;h4&gt;Elia has a simple but powerful interface. It uses a SQLite backend to store your conversations locally, keeping your data private.&lt;/h4&gt;&#xA;    &lt;/figcaption&gt;&#xA;&lt;/figure&gt;&#xA;&#xA;&lt;h2 id=&#34;dunk&#34; class=&#34;heading-anchor&#34;&gt;Dunk&lt;a href=&#34;#dunk&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Dunk&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://github.com/darrenburns/dunk&#34;&gt;Dunk&lt;/a&gt; makes your diffs beautiful and easy to read.&#xA;Just pipe your &lt;code&gt;git diff&lt;/code&gt; output into Dunk and it&amp;rsquo;ll generate a colourful diff like this:&lt;/p&gt;&#xA;&lt;figure&gt;&lt;img src=&#34;//darren.codes/projects/dunk.png&#34;&#xA;    alt=&#34;A screenshot of Dunk&#34; width=&#34;900px&#34;&gt;&lt;figcaption&gt;&#xA;      &lt;h4&gt;Dunk makes your diffs easy to read. I alias this as `gdd`, which runs `git diff | dunk | less -R`, resulting in a beautiful paged diff.&lt;/h4&gt;&#xA;    &lt;/figcaption&gt;&#xA;&lt;/figure&gt;&#xA;&#xA;&lt;h2 id=&#34;textuals-textarea-widget&#34; class=&#34;heading-anchor&#34;&gt;Textual&amp;rsquo;s &lt;code&gt;TextArea&lt;/code&gt; widget&lt;a href=&#34;#textuals-textarea-widget&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Textual&amp;rsquo;s TextArea widget&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;I developed Textual&amp;rsquo;s &lt;code&gt;TextArea&lt;/code&gt; widget, which is a multi-line input widget that supports &lt;code&gt;tree-sitter&lt;/code&gt; syntax highlighting, wrapping, undo/redo, full mouse support, and much more.&lt;/p&gt;&#xA;&lt;figure&gt;&lt;img src=&#34;//darren.codes/projects/text-area-json.gif&#34;&#xA;    alt=&#34;A screenshot of Textual&amp;#39;s TextArea widget&#34; width=&#34;400px&#34;&gt;&lt;figcaption&gt;&#xA;      &lt;h4&gt;The TextArea widget in action - running completely inside the terminal.&lt;/h4&gt;&#xA;    &lt;/figcaption&gt;&#xA;&lt;/figure&gt;&#xA;&#xA;&lt;p&gt;I wrote a blog post about it &lt;a href=&#34;https://textual.textualize.io/blog/2023/09/18/things-i-learned-while-building-textuals-textarea/&#34;&gt;here&lt;/a&gt;, which includes a whole lot of videos of it in action, and covers some of my learnings from that project.&lt;/p&gt;&#xA;&lt;h2 id=&#34;ward-archived&#34; class=&#34;heading-anchor&#34;&gt;Ward (archived)&lt;a href=&#34;#ward-archived&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Ward (archived)&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://github.com/darrenburns/ward&#34;&gt;Ward&lt;/a&gt; was a test framework I developed for Python.&#xA;It had a strong focus on readability.&#xA;Test descriptions were written in plain language rather than function names,&#xA;and it output very readable diffs when tests failed.&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;./ward.png&#34; alt=&#34;A screenshot of Ward&#34;&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;autocomplete&#34; class=&#34;heading-anchor&#34;&gt;Autocomplete&lt;a href=&#34;#autocomplete&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Autocomplete&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;I developed &lt;a href=&#34;https://github.com/darrenburns/textual-autocomplete&#34;&gt;textual-autocomplete&lt;/a&gt;, which let&amp;rsquo;s you add autocompletion to your Textual-based TUI apps.&lt;/p&gt;&#xA;&lt;p&gt;I&amp;rsquo;m currently (slowly) working on the next version which will be used in &lt;a href=&#34;https://posting.sh&#34;&gt;Posting&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;./textual-autocomplete.png&#34; alt=&#34;A screenshot of &#34;&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;trogon&#34; class=&#34;heading-anchor&#34;&gt;Trogon&lt;a href=&#34;#trogon&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Trogon&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://github.com/Textualize/trogon&#34;&gt;Trogon&lt;/a&gt; was built to solve the problem of learning and discovering features of CLIs.&#xA;In just a few lines of code it lets you create a fully interactive TUI which can be used in place of a CLI.&#xA;Here&amp;rsquo;s an example of Trogon converting the feature-rich &lt;code&gt;sqlite-utils&lt;/code&gt; CLI into a TUI:&lt;/p&gt;&#xA;&lt;figure&gt;&lt;img src=&#34;//darren.codes/projects/trogon.png&#34;&#xA;    alt=&#34;A screenshot of Trogon&#34; width=&#34;900px&#34;&gt;&lt;figcaption&gt;&#xA;      &lt;h4&gt;Trogon gives you a sidebar menu containing all of the commands in your CLI. Clicking on an item shows you a form with all of the arguments and options for the selected command.&lt;/h4&gt;&#xA;    &lt;/figcaption&gt;&#xA;&lt;/figure&gt;&#xA;&#xA;&lt;h2 id=&#34;radiant&#34; class=&#34;heading-anchor&#34;&gt;Radiant&lt;a href=&#34;#radiant&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Radiant&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;I developed (but never released) an iOS and Android app using Flutter and the Dart programming language.&lt;/p&gt;&#xA;&lt;figure&gt;&lt;img src=&#34;//darren.codes/projects/radiant.png&#34;&#xA;    alt=&#34;A screenshot of Radiant&#34; width=&#34;300px&#34;&gt;&lt;figcaption&gt;&#xA;      &lt;h4&gt;Screenshot of Radiant&lt;/h4&gt;&#xA;    &lt;/figcaption&gt;&#xA;&lt;/figure&gt;&#xA;&#xA;</description>
    </item>
    <item>
      <title>Things I really like</title>
      <link>//darren.codes/likes/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>//darren.codes/likes/</guid>
      <description>&lt;p&gt;An incomplete collection of things I really like.&lt;/p&gt;&#xA;&lt;h2 id=&#34;books&#34; class=&#34;heading-anchor&#34;&gt;Books&lt;a href=&#34;#books&#34; class=&#34;anchor&#34; title=&#34;Copy link to this section&#34; aria-label=&#34;Link to section: Books&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;I mostly read fantasy and sci-fi.&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;em&gt;The Will of the Many&lt;/em&gt; | James Islington | Fantasy&lt;/li&gt;&#xA;&lt;li&gt;&lt;em&gt;Powder Mage Series&lt;/em&gt; | Brian McClellan | Fantasy&lt;/li&gt;&#xA;&lt;li&gt;&lt;em&gt;Children of Time&lt;/em&gt; | Adrian Tchaikovsky  | Sci-fi&lt;/li&gt;&#xA;&lt;li&gt;&lt;em&gt;Hyperion (book 1)&lt;/em&gt; | Dan Simmons | Sci-fi&lt;/li&gt;&#xA;&lt;li&gt;&lt;em&gt;Stormlight Archive Series&lt;/em&gt; | Brandon Sanderson | Fantasy&lt;/li&gt;&#xA;&lt;li&gt;&lt;em&gt;Mistborn First Trilogy&lt;/em&gt; | Brandon Sanderson | Fantasy&lt;/li&gt;&#xA;&lt;li&gt;&lt;em&gt;Piranesi&lt;/em&gt; | Susanna Clarke | Fantasy&lt;/li&gt;&#xA;&lt;li&gt;&lt;em&gt;Ender&amp;rsquo;s Game&lt;/em&gt; | Orson Scott Card | Sci-fi&lt;/li&gt;&#xA;&lt;li&gt;&lt;em&gt;The Expanse Series&lt;/em&gt; (the first 4 books) | James S.A. Corey | Sci-fi&lt;/li&gt;&#xA;&lt;li&gt;&lt;em&gt;Project Hail Mary&lt;/em&gt; (Audiobook) | Andy Weir | Sci-fi&lt;/li&gt;&#xA;&lt;li&gt;&lt;em&gt;Childhood&amp;rsquo;s End&lt;/em&gt; | Arthur C. Clarke | Sci-fi&lt;/li&gt;&#xA;&lt;li&gt;&lt;em&gt;Flowers for Algernon&lt;/em&gt; | Daniel Keyes | Sci-fi&lt;/li&gt;&#xA;&lt;li&gt;&lt;em&gt;The Name of the Wind&lt;/em&gt; | Patrick Rothfuss | Fantasy&lt;/li&gt;&#xA;&lt;li&gt;&lt;em&gt;Blood Song&lt;/em&gt; | Anthony Ryan | Fantasy&lt;/li&gt;&#xA;&lt;li&gt;&lt;em&gt;Old Man&amp;rsquo;s War&lt;/em&gt; | John Scalzi | Sci-fi&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&amp;hellip;&lt;/p&gt;&#xA;</description>
    </item>
  </channel>
</rss>