Posted on 2026-01-27 16:03 +0000 in TIL
• Tagged with
Python, PyPI, coding
• 1 min read
In the past few months, like a lot of folk in the Python world, I've been
won over by uv. When it comes to managing my
own projects my journey over the past few years as been
pipenv, then
rye, and then when rye was killed off I finally
swapped to uv (later than I should have, I realised in hindsight). At each
step I've found each tool cleaner, easier to work with and more
feature-rich.
There's no doubt in my mind that uv has done the most work to make
installing Python-based tools (or at least PyPI-based tools) as
friction-free an experience as
possible.
Now I've discovered uvh.sh. The thing with uv is the
person installing your application first needs to get and install uv; this
site removes that friction. Now if someone wants to install
obs2nlm (for example) they should be able to
just do:
curl-LsSfuvx.sh/obs2nlm/install.sh|sh
and away they go (assuming they have curl installed,
which is generally going to be far more likely than having uv installed).
Of course, there are the usual caveats and
concerns
about the "just pipe this stuff via sh trust me bro" approach, but if
you are comfortable with suggesting this kind of install method it looks
worth adding this to an application's installation documentation.
I think I'm going to start mentioning it as an option.
Last week I was wrestling with some Textual code, trying to get something to
lay out on the screen "just so". On the whole this isn't too tricky at all,
and for those times where it might feel tricky there's some advice
available on how to go about
it. But in this case
I was trying to do a couple of "on the edge" things and one thing I really
needed to know was what particular part of the display was being "caused" by
what container or widget1.
Now, at the moment anyway, Textual doesn't have a full-blown devtools with
all the bells and whistles; not like in your average web browser. It does
have a devtools, but not
with all the fancy DOM-diving stuff the above would have needed.
What I needed was the equivalent of print-debugging but with a
point-and-ask interface. Now, I actually do often do print-debugging
with Textual apps only I use
notify;
this time though notify wasn't going to cut it.
I needed something that would let me point at a widget and say "show me
stuff about this". Something that happens when the mouse hovers over a
widget. Something like... a
tooltip!
Suddenly I could hover my mouse over a bit of space on the screen and get a
"traceback" of sorts for what "caused" it.
I posted this little hack to #show-and-tell on the Discord
server and someone mentioned it would be
handy if it also showed the CSS for the widget too. That was simple enough
because every widget has a styles.css property that is the CSS for the
widget, as a string.
After that I didn't think much more about it; until today.
Looking back, one thing I realised is that adding the CSS information
on_mount wasn't quite good enough, as it would only show me the state of
CSS when the mount happened, not at the moment I inspect the widget. I
needed the tooltip to be dynamic.
Thing is... Textual tooltips can't be functions (which would be the obvious
approach to make it dynamic); so there was no way to get this on-the-fly
behaviour I wanted.
Except there was! The type of
tooltip
is RenderableType. So that means I could assign it an object that is a
Rich renderable; that in turn means I could write a __rich__ method for a
class that wraps a widget and then reports back what it can see every time
it's called.
In other words, via one step of indirection, I could get the "call a
function each time" approach I was after!
It works a treat too.
All of which is a long-winded way of saying I now have a print-debug-level
DOM inspector tool for when I'm building applications with Textual:
If this sounds handy to you, you can grab the code too. Install it into your
development environment with pip:
$pipinstalltextual-dominfo
and then attach it to your app or screen or some top-level widget you're
interested in via on_mount; for example:
and then hover away with that mouse cursor and inspect all the things!
Whatever you do though, don't make it part of your runtime, and don't keep
it installed; just make it a development dependency.
ObPedant: Containers are widgets, but it's often helpful to make a
distinction between widgets that exist just to control the layout of the
widgets inside them, and widgets that exist to actually do or show
stuff. ↩
The idea for this one popped into my head while on the bus back from Textual
Towers this evening. So after dinner and some nonsense on TV I had to visit
my desk and do a quick hack.
This is textual-countdown, a
subtle but I think useful countdown widget for Textual applications.
The idea is that you compose it somewhere into your screen, and when you
start the countdown the bar highlights and then starts to shrink down to
"nothing" in the middle of its display. When the countdown ends a message is
posted so you can then perform the task that was being waited for in an
event handler.
Not really a novel thing, I've seen this kind of thing before on the web;
I'm sure we all have. I just thought it would be a fun idea for Textual
applications too.
I envisage using this where, perhaps, an application needs to wait for an
API-visiting cooldown period, or perhaps as a subtle countdown for a
question in a quiz; something like that.
Anyway, if this sounds like it's something useful for your Textual
applications, it's now available from
PyPi and, of course, the
source is over on GitHub.
textual-astare is another
Textual-based Python project that I've developed in the last year and I
don't believe I've mentioned on this blog. Simply put, it's a took for
viewing the abstract syntax tree of Python code, in the terminal.
I've just made a small update to it this evening after someone asked for a
sensible change I've been meaning to do for a
while. When I
first read the request I was going to look at it next week, when I have
some time off work, but you know how it is when you sit at your desk and
have a "quick look".
So anyway, yeah, v0.8.0 is out
there and can be installed, with the main changes being:
Made it so you can open a directory to browser from the command line.
Made opening the current working directory the default.
Tweaked the way dark/light mode get toggled so that it's now
command-palette-friendly.
I think the code does need a wee bit of tidying -- this was one of my
earliest apps built with Textual and my approach to writing Textual apps has
changed a fair bit this year, and Textual itself has grown and improved in
that time -- but it's still working well for now.
I don't think I've mentioned it before on this blog, but some time back I
decided it would be fun to use Textual to
write a Mandelbrot explorer (simple Mandelbrot explorers have been another
one of my favourite known problem to try an unknown
thing problems). Doing it in the
terminal seemed like a fun little hack. I started off with creating
textual-canvas and then built
textual-mandelbrot on top
of that.
Not too long back I added a "command palette" to
Textual
(I'd prefer to call it a minibuffer, but I get that that's not fashionable
these days), but so far I've not used it in any of my own projects; earlier
today I thought it could be fun to add it to textual-mandelbrot.
Most of the commands I've added are trivial and really better covered by
(and are covered by) keystrokes, but it was a good test and a way to show
off how to create a command provider.
Having started this I can see some more useful things to add: for example it
might be interesting to add a facility where you can bookmark a specific
location, zoom level, iteration value, etc, and revisit later. The command
palette would feel like a great way to pull back those bookmarks.
What I really liked though was how easy this was to do. The code to make
the commands
available
is pretty trivial and, I believe, easy to follow. Although I do say so
myself I think I managed to design a very accessible API for this.
There's more I'd like to add to that (the Textual command palette itself, I
mean), of course; this was just the start. Support for commands that accept
and prompt for arguments would be a neat and obvious enhancement (especially
if done in a way that's reminiscent of how commands could be defined in
CLIM -- I
remember really liking how you
could create self-documenting and self-completing commands in that).
Since quickly hacking together textual-query-sandbox a few days
back, I've made a bunch of small
changes here and there. While most have been cosmetic and playing with some
ideas, some have also been internal improvements that should make the tool
work better.
The most prominent change is one I pondered in the previous post, where I
thought it might be interesting to have a small collection of playgrounds
grounded together with a TabbedContent. So as of now the tool still has
the original playground which had an emphasis on nested containers:
There's now a playground with an emphasis on selecting widgets within
containers1:
There's also now a playground that has an emphasis on pulling out widgets
based on ID and classes:
The other change you will notice from the original post is the DOM tree
shown in the bottom right corner. Note that that isn't there to show your
query result (that's the bottom left panel), it's there to help picture how
the DOM in the current playground hangs together, and will hopefully help in
picturing the structure for when you write a query.
I sense there's still a lot of fun things I could add to this, and I'm still
keen on the idea of having the playgrounds "soft coded" in some way, so
people can make their own and load them up.
Another thing I want to try and work on is making the display as useful as
possible. While I think it's actually pretty neat and clear, there's not a
lot of space2 available to show the playground and the results. Finding
a good balance is an interesting problem.
For a number of reasons this is turning into a really enjoyable tinker
project.
This is, of course, slightly nonsensical wording. Containers are
widgets in Textual. Pretty much everything you see in your terminal is a
widget, even a Screen is a widget. ↩
A lot of this of course hinges on how big someone's terminal is. I
tend to run a fairly high resolutions with the smallest font I find
readable so my terminal windows are often pretty "big"; other people
tend to have something much smaller in terms of cell with/height. ↩
Sometimes I can have an idea for a Textual widget, library or application on
my ideas list for weeks, months even, before I get around to it -- mostly
just due to not having the clear time to make a run at getting it going --
and then other times an idea can pop into my head and it has to be created
there and then. Has to be!
This happened yesterday evening.
While the tool I built is something I'd thought of before (back around
November last year I think) it hadn't even made it to my "list of stuff I
should make" that I keep in Apple Reminders; not sure why really. But then
yesterday evening a question cropped up on the Textual Discord
server that related to the subject and I was
reminded of it.
The subject being: Textual DOM
queries. I like to think that
DOM queries in Textual are pretty easy to do, and well-explained in the
docs, but it's fair to admit that they need a bit of practice first, just
like any powerful tool. So I was reminded that I'd wanted to write a sandbox
application, that would have a practice DOM inside it, an input field to
type in a query, and a way of displaying the results.
In this very first version (which was really quickly put together -- it
was something like 15 minutes to write the main code and then probably 45
minutes tweaking styles, adding all the admin stuff to allow deployment to
PyPi and writing the
README) there's an Input,
a display of a group of nested containers with different IDs and classes,
and then a Pretty widget
at the bottom to show the
query
result.
If you think this looks like it might be useful to you, it can be installed
using either pip or (ideally) pipx:
$pipxinstalltextual-query-sandbox
and then you can run it with:
$tqs
At which point load up the Textual query docs, type queries into the input
field, hit enter and see what gets highlighted and which widgets end up in
the result set at the bottom of the screen.
Like I say: this was a quick hack yesterday evening, I think there's a lot
more can go into this. For one thing I think a more interesting practice DOM
would be a good idea, with a good mix of widgets; another thing could be
having a collection of different DOM playgrounds that can be switched
between (a TabbedContent of different playgrounds could be fun here); this
could even be taken further such that the user can create their own
playground DOM to practice against.
Eventually it would be neat if this could be turned into a library that can
be included in a Textual application, as a development-time debug tool, so
that on-the-fly test queries can be made.
For now though, it's started, it's under way, and I think the current
version probably covers 90% of the use cases for something like this; making
for a really quick and easy tool to double-check how to query something.
Late on last year I wrote about a
bunch of new things that I'd added to PyPi, things mostly kicked off by an
early dog-fooding session we had at textual
HQ.
Since then I've been slowly doing my best to keep the applications up to
date with Textual.
As much as possible we try and not make breaking changes with the framework,
but at the same time it is still 0.x software and there's still new ways of
doing things being designed so there's going to be the odd break in approach
now and again.
Unbored, my kind of silly
self-populating TODO list application, has been sitting atop Textual 0.20.x
for a while now and earlier today I checked how it was getting in with
0.32.0 and... actually surprisingly okay. Not perfect, there were a couple
of things that had suffered from bitrot, but it wasn't crashing.
The main thing I needed to change was the ability to focus a couple of
containers (they didn't used to receive focus by default, now they do so I
had to tell them not to again), and that was about it.
While the application itself is a bit silly, and likely of no real use to
anyone, I feel it's a pretty good barometer application, helping me check
what the experience is like when it comes to maintaining a Textual
application and the needs to keep on top of changes to Textual.
It goes without saying, I hope, that really you should pin the Textual
dependency for your applications, and upgrade in a controlled and tested
way; for this though it's less crucial and is a good test of the state of
the ecosystem, and on the remote chance that anyone is using it, it'll be
helpful to me if it does break and they yell.
Given that for a good chunk of this year I've been a bit lax about writing
here, there's a couple or so coding projects I've not written about (well,
not on here anyway -- I have spoken lots about them over on
Fosstodon). One such project is
textual-canvas.
As the name might suggest, it's a "canvas" for Textual applications, which
provides a pretty basic interface for drawing pixels, lines and circles --
and of course any other shape you are able to build up from those basics.
Another little thing that's up on PyPi now, which is the final bit of
fallout from the Textual dogfooding
sessions, is a little project I'm
calling OIDIA.
The application is a streak tracker. I'm quite the fan of streak trackers.
I've used a few over the years, both to help keep me motivated and honest,
and also to help me track that I've avoided unhelpful things too. Now, most
of the apps I've used, and use now, tend to have reminders and counts and
stats and are all about "DO NOT BREAK THE STREAK OR ELSE" and that's mostly
fine, but...
To keep things simple and to purely concentrate on how to build Textual
apps, I made this a "non-judgement" streak tracker. It's designed to be
really simple: you add a streak, you bump up/down the number of times you
did (or didn't do) the thing related to that streak, for each day, and
that's it.
No totals. No stats. No reminders and bugging. No judgement.
Here it is in action:
When I started it, I wasn't quite sure how I wanted to store the data.
Throwing it in a SQLite database held some appeal, but that also felt like a
lot of faff for something so simple. Also, I wanted to make the data as easy
to get at, to use elsewhere, and to hack on, as possible. So in the end I
went with a simple JSON file.
On macOS and GNU/Linux streaks.json lives in ~/.local/share/oidia, on
Windows it'll be in... I'm not sure off the top of my head actually; it'll
be in whatever directory the handy xdg
library has chosen. and because it's JSON
that means that something like this:
ends up looking like this:
[{"title":"Hack some Python","days":{"2022-12-02":1,"2022-12-03":1,"2022-12-04":1,"2022-12-05":1,"2022-12-06":1,"2022-12-07":1,"2022-12-08":1,"2022-12-01":1,"2022-11-30":1,"2022-11-29":1,"2022-11-28":1}},{"title":"Brush my teeth","days":{"2022-12-02":2,"2022-12-03":2,"2022-12-04":2,"2022-12-05":2,"2022-12-06":2,"2022-12-07":2,"2022-12-08":1,"2022-12-01":2,"2022-11-30":2,"2022-11-29":2,"2022-11-28":2}},{"title":"Walk","days":{"2022-12-02":1,"2022-12-03":1,"2022-12-04":1,"2022-12-05":1,"2022-12-06":1,"2022-12-07":1,"2022-12-08":1,"2022-12-01":1,"2022-11-30":1,"2022-11-29":1,"2022-11-28":1}},{"title":"Run 5k","days":{"2022-12-03":2,"2022-12-05":1,"2022-11-30":1,"2022-11-28":2}},{"title":"Run 10k","days":{"2022-12-03":1,"2022-11-28":1}}]
Of course, it remains to be seen how well that actually scales; possibly not
so well over a long period of time, but this was written more as another way
to explore Textual than anything else. Even then, it would be pretty trivial
to update to something better for holding the data.
If this seems like your thing (and I will be supporting it and onward
developing it) you can find it over on
PyPi, which means it can be installed with
pip or the ever-handy pipx: