textual-dominfo

Posted on 2024-01-15 21:20 +0100 in Python • Tagged with PyPi, Python, coding, Textual • 3 min read

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!

So that was easy:

def on_mount(self) -> None:
    for widget in [self, *self.query("*")]:
        widget.tooltip = "\n".join(
            f"{node!r}" for node in widget.ancestors_with_self
        )

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:

textual-dominfo in action

If this sounds handy to you, you can grab the code too. Install it into your development environment with pip:

$ pip install textual-dominfo

and then attach it to your app or screen or some top-level widget you're interested in via on_mount; for example:

def on_mount(self) -> None:
    from textual_dominfo import DOMInfo
    DOMInfo.attach_to(self)

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.

The source can be found over on GitHub and the package is, as mentioned above, over on PyPi.


  1. 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. 


textual-countdown

Posted on 2024-01-11 22:52 +0100 in Python • Tagged with PyPi, Python, coding, Textual • 1 min read

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.

Textual Countdown in action

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.


Orange Site Hit v0.2.0

Posted on 2024-01-07 09:50 +0000 in Coding • Tagged with Python, terminal, textual • 3 min read

This is actually the second release of OSHit since I first announced it a week back, and I'll get to that other release in a moment.

I've just published v0.2.0, which isn't a very substantial release, but which bumps the required version of Textual to v0.47.1 and has some fun with the new nested CSS feature.

Underlying the point of this release was me taking a "real world" application of mine and nesting as much of the CSS within it as possible, in part to get a feel for how and when it's useful, but also to give it a proper test in a "proper" application. In doing so I think I've found one bug.

Dogfooding is always a good idea.

The main visible change in this release is I've played around with the look of the comments dialog a bit:

OSHit you have comments

I'm still narrowing this down, but I think I prefer this look to what I started out with.

Another change I made was also to the comments dialog. Before, if you performed the "expand comments" action on a comment card that already had its comments expanded, it would move focus to the first child comment; this was a deliberate choice that felt right at the time. Having used the app for a few days now I've realised that making it an open/close toggle is far more useful. So that's what I've done.

Now... as for the previous release I mentioned above. That was a fun one.

Back when I released v0.1.0 some joker decided that it would be fun to submit the blog post about it to the Orange Site. The comments there went as you'd expected:

  • Some riffed off the opening paragraph, ignoring the tool itself.
  • Some riffed off the opening paragraph in self-reflective way.
  • Some riffed off the opening paragraph in a "I never see the problem" way.
  • One or two did the usual "why even bother building that when $TOOL_OF_CHOICE exists?" dance to show their terminal purity.
  • One or two posted genuinely useful links to other similar projects.
  • The biggest tree of comments was kind of a fight.

One comment caught my eye though; someone reported having a problem running it. My initial thought on reading it was "my dude, seriously, you're going to report the problem in some random comment on HN rather than raise an issue with the author?!?".

For once I was wrong to be so cynical.

So, yeah, that was the reminder I needed that I'd been intentionally reckless while writing the original code, and hadn't gone back to the API code I'd written and made it behave before doing the initial release.

All of which is to say: if you run into a problem with some FOSS project, be like @mihaitodor. Issue that thing so the developer gets to know about it; don't assume they'll be reading some random comment section, social media site or Discord server!

That and don't make 500+ HTTP requests at once; that might not end well for some.


Orange Site Hit

Posted on 2024-01-01 10:17 +0000 in Coding • Tagged with Python, terminal, textual • 3 min read

I know I'm not alone in having a relationship with the orange site that is... weird. I generally dislike the culture there, it's almost impossible to read any of the comments without being frustrated about the industry I work in or am adjacent to and some of the people who inhabit it; but as a link aggregator of stuff I might find interesting... I honestly can't think of anywhere better. So, yes, I've been a fairly avid reader of HackerNews for many years, and have even had an account there for over 4 years.

Given this, for a wee while now, I've been meaning to knock up a terminal-based client for it using Textual.

So after work on Tinboard settled down I got the urge to start a new pet project (not abandoning Tinboard, I'm still going to be tweaking and extending it of course) and finally knocking up that client seemed like the one.

Orange Site Hit is the result.

OShit

It's worth making clear from the very start: this is a read-only reader. There is no logging in, there is no voting, there is no posting of things. This is a client built with their own API and it doesn't provide such a thing; at least not now and despite me seeing past promises that this will change, there's no API for doing that sort of thing.1

The idea of this application is you can run it up in the terminal, check the top, best and latest from the categories provided by the API, perhaps dive off into your web browser if needed, and then got on with other things.

It's there for when you're in the terminal you just need your hit of the orange site.

The main screen of the app revolves around the index of items, most of which are going to be stories. You can see an example of that above. For people who prefer things to be slightly less cramped, I've also added a "relaxed layout" mode too:

The index in relaxed mode

From the index you can head off into your web-browser by hitting Enter on any item; if the item is a story that links to somewhere that link will be opened; if it's something more like AskHN, or a job, it'll open the related page on HackerNews itself.

Pressing u with an item selected will let you view the details for the user who posted the item:

Viewing the details of a user

If you're the sort of person who wants to torture themselves by reading the comments (oh come on we all do it!), there's a comment reader/navigator too. With an item selected press c and the comment dialog will open:

Viewing the real meat of HackerNews

I think the navigation within that dialog is fine; although I can see some scope for improvement. At the moment it uses a widget-per-comment (actually, it's at least 4 widgets per comment), which is fine and Textual handles that without an issue, even on items with lots of comments, but longer-term I can see me having some fun using the line API to build a super-efficient comment presentation and navigation widget.

That's it for now; it feels like a good v0.1.0 spot to be in. There are a bunch of things I still want to do with it (better cleaning up of the text, perhaps with some markup support so links get handled, etc; plus lots of ways of searching for stuff), but I felt it was in a place where I could start using it.

Anyway, if this sounds like your sort of thing, it can be installed with pip or (ideally) pipx from PyPi. The source is available over on GitHub.


  1. Yes, there are lots of clients that do all sorts of HTML-scraping of the actual website to make this possible; this ain't that. This ain't going to be that. 


Tinboard v0.4.0

Posted on 2023-12-25 10:43 +0000 in Coding • Tagged with Python, terminal, textual • 1 min read

Although it's not planned this way, so far it looks like I'm on a "every other day" release cycle with Tinboard right now! Today's release is a small but handy one, I think.

Thanks to the handy little library pyperclip I've added:

  • The ability to copy a bookmark's URL to the clipboard.
  • URL field autofill if you go to add a new bookmark and the clipboard appears to have a valid URL in it.

The code for copying to the clipboard

At the moment the copy facility is just a straightforward copy of the URL, nothing else. At some point I may add an extended copy option, which will open a dialog with a bunch of options of what to copy from the bookmark, and perhaps also how to format it or something. Like, often, if I'm copying a bookmark's URL, it's because I want to paste it into some Markdown document, or some location that will handle Markdown markup.

Perhaps that'll turn up in v0.5.0 in a couple of days? ;-)

Tinboard can be installed with pip or (ideally) pipx from PyPi. The source is available on GitHub.


Tinboard v0.3.0

Posted on 2023-12-23 08:49 +0000 in Coding • Tagged with Python, terminal, textual • 2 min read

It looks like I'm in a wee period of small incremental changes and release of Tinboard. This morning I've release v0.3.0, which has a couple of small but useful changes.

The first is more of a cosmetic thing. The Footer widget in Textual is handy for showing the current keyboard bindings in a given context, but it can get massively cluttered very quickly (we do have plans to revisit this); in Tinboard this clutter creep was turning into a thing.

So I've removed almost every binding from being displayed in the Footer, and have placed an emphasis on the user pressing F1 to get context-sensitive help, and have also left the most useful bindings in the footer with very minimal descriptions.

Given that this is a keyboard-first application, and I've tried to make the bindings easy to remember, I think it's going to make more sense to do it like this, and will make for a tidier UI too.

There is one disadvantage here of course: by removing the display of bindings from the footer, the mouse-heavy user becomes disadvantaged; if a particular binding doesn't have a UI feature that favours the mouse to cover it too there's no way to initiate that action with the mouse. I'm going to think on this a little. Again, Tinboard is designed for me first and foremost, and my preference is to be keyboard-first when using the application; but finding a good compromise would be advantageous when it comes to advising people asking about Textual application design.

The second change is a simple but useful one. I've added a toggle of the sort order of the tags menu in the left-hand column (bound to F4). Right now it simply toggles between alphabetical order, or bookmark count order (most to least). At some point I might make it more of a cycle than a toggle, but this serves my purposes for now.

Tinboard can be installed with pip or (ideally) pipx from PyPi. The source is available on GitHub.


Tinboard v0.2.0

Posted on 2023-12-21 09:29 +0000 in Coding • Tagged with Python, terminal, textual • 1 min read

Following on from the initial full release a couple of days ago, I've just released v0.2.0 of Tinboard. There's just one small change in this, but I think it's a really useful one.

In the top-left corner of the screen there's a menu of main filters, letting you switch between seeing all bookmarks, or a combination of read, unread, public, private, etc... In using the application I quickly realised that it would be handy to have bookmark counts in that menu.

So I added that:

Counts in the filters menu

These counts are a little different from those in the tags list, in that they always show the number of matching bookmarks amongst all recorded bookmarks, not just those currently on display (and so subject to any sort of filter that's in play).

I think this is the right approach here. I know for sure that that's what I want from this, and I am writing this for me after all...

What's handy about this is that it makes it easier for me to see how many bookmarks haven't been tagged, and also how many I haven't looked back over and marked as read. This is already helping me get to untagged-zero.

Tinboard can be installed with pip or (ideally) pipx from PyPi. The source is available on GitHub.


Tinboard

Posted on 2023-12-19 09:47 +0000 in Coding • Tagged with Python, terminal, textual • 2 min read

Over the past few weeks I've been working on a new pet project, in part done as a Textual "dogfooding" project, but also because this is a tool I've been wanting for a while now: a terminal-based client for the Pinboard bookmarking service.

The dogfooding side of the development has been helping, uncovering a couple of fun bugs in Textual; plus the act of building this has let me try out a few of the newer features we've recently added to the framework.

What's really important though is this is a tool I actually wanted, and I'm using pretty often. I've written a lot of Textual-based applications over the past year, most small examples, some quite a bit bigger, but none of them really form part of my daily workflow. This changes with Tinboard.

Tinboard in action

Tinboard is designed as a fully-featured client, allowing for the creation of new Bookmarks, complete with tag suggestion support:

Adding a new bookmark

Not only are tag suggestions pulled from Pinboard, but entry of tags can auto-complete, taking completions from both the suggested tags and also tags used amongst your own bookmarks:

Auto-complete of tags

That feature was really easy to add thanks to the Suggester API.

Thanks to the recently-added TextArea widget the add/edit dialog allows for proper full editing of the extended text description of the bookmark too:

Editing a bookmark

One caveat here is a lack of word-wrapping; but this will be arriving in an update to Textual early in the new year.

As well as all the usual add/edit/delete facilities, Tinboard is also designed to make it pretty easy to find bookmarks too. There are filtering options for seeing all read/unread, public/private and tagged/untagged bookmarks; this makes bookmark management really easy for me because I can filter for all the untagged and private bookmarks, which are likely the ones that need editing and expanding on, and tidy up my bookmark library.

There is also, of course, full text search too.

Text search entry

When a filter or search is in operation, the related tags and the like react too:

A search result being shown

Another thing I've made a point of doing in Tinboard is leaning pretty hard on the Command Palette. No functionality is only available by it (I've done my best to make sure that keyboard is the primary input device here, with keyboard shortcuts for as much as possible). Initially I approached this as a "for the sake of completeness" feature, but already I'm finding that it's a pretty quick method of pulling up a tag filter.

The command palette in action

To help make all the features as discoverable possible, I've also ensured there's a pretty comprehensive help screen:

Help

Anyway; that's v0.1.0 out in the wild. I'm pleased with how it's turned out and there's a few more things I'd like to add. It's licensed GPL-3.0 and available via GitHub and also via PyPi. If you have an environment that has pipx installed you should be able to get up and going with:

$ pipx install tinboard

I hope this is useful to someone else. :-)


Evolve Words

Posted on 2023-10-31 21:39 +0100 in Coding • Tagged with Python, evolution, biology, terminal, textual • 2 min read

This follows on from my previous post. If you've not read that, it's worth having a dive in first for the background.

The Ruby code I mention, that was written back in 2008, was actually a pair of scripts. The first one, called selection, did what visual-selection does, only visual-selection does it with a nice TUI interface: it takes a random collection of letters and symbols and evolves them into a target phrase.

As covered before: I don't remember all of the details of the conversation that was going on at the time, but I do seem to remember something along the lines of "yes, but you start out and end up with something the same length" and "nothing more complex is made" (let's gloss over the whole "complex" thing for now... well okay let's just gloss over it, end of story; this is just a fun coding exercise).

What I do remember is that the seed of an idea was planted. Fine: how about I start off with one small word, and using a list of English words as the "fitness landscape" that the mutations had to survive in, mutate a population over and over and see what happens. Would I "randomly" create known words, with fewer letters, with the same letters, with more letters?

So this version of the code randomly did three forms of mutation: it would randomly flip a letter, or randomly delete a letter, or randomly insert a random letter. It would do this over and over and eliminate words that aren't in the original list (the simple form of selecting for survival within the landscape).

Like I said last time: never going to convince anyone of anything, but fun to write some code.

This version became selection2.

So, having turned selection into a TUI application with Textual, I had to do the same with this code...

Evolve Words

As before, because it's fun to do so, this leans heavily on the worker API and textual-plotext.

If you want to check out the app itself there's a GitHub repo and it can also be installed from PyPi using pipx.


Visual Selection

Posted on 2023-10-26 18:50 +0100 in Coding • Tagged with Python, evolution, biology, terminal, textual • 4 min read

Over the last few weeks I've had a couple of sessions of working on a library to wrap Plotext -- a popular terminal-based plotting library for Python -- so that it can easily be used in Textual apps; textual-plotext is the result.

I feel it's come together pretty well

But... I've been itching to find a reason to use it in a project of my own.

Meanwhile...

Back in the mid-2000s, when phpBB systems were still the fashion, I used to hang out on a site that was chiefly aimed at the atheist and secular humanist crowd. We'd get a good number of drive-by YEC types who'd want to argue (sorry... debate) and often talk nonsense about biology and the like.

Now, I'm no biologist, I'm no scientist, I'm just a hacker who likes to write code for fun and profit; so any time there was a chance to write some code to help illustrate an idea I'd jump at the chance. I forget the detail now -- this was back in 2008; 15 years ago as of the time of writing -- but one time I remember a conversation was taking place where someone was just flat out claiming that "random mutation" can only cause "loss of information" and could never lead to a "desired result", or some such thing.

If you've ever had, read or watched those debates, you'll know the sort of thing I mean.

So that got me thinking back then, could I write something that could give a simple illustration of how this doesn't quite make sense?

So I had a little hacking session and came up with some Ruby code1 that did what I felt was the job. You'd give it a phrase you wanted it to generate (a stand-in for the current "fitness landscape", in effect), it would then generate a totally random string of that length, and then would set about mutating it, finding mutations that were "fitter" than others (a stand in for selection), breed the best two so far (randomly copy one chunk from another to create a child), then repeat over and over.

When I first wrote it I wasn't sure what to expect; would it ever finish given a reasonably large target string?

It did.

It was fun to code.

It got posted to the BB and of course wasn't in any way persuasive to them (honestly I never expected it would be). I seem to recall it being hand-waved away with calls of there obviously being an intelligent designer involved2.

Anyway, the "meanwhile..." in this: a few times this year I've thought it could be fun to rework this in Python (it's really not that complex after all; just a string-chopping loop really) and use Textual to put a fun UI on it.

So, that's what I did, complete with textual-plotext plot:

Visual Selection in action

While, 15 years on, this isn't going to convince anyone of the underlying point, I think it does serve a good educational purpose. It shows that you can create a fun UI for the terminal, with Textual, with not a lot of code. It also shows off how you can easily create dynamic plots. Plus -- and I think this might be the really important one -- it shows you can write "traditional" tight-loop code in a Textual application and still have a responsive UI; all thanks to the worker API.

The heart of the code for this application is this:

environment = Environment("This is the target string we want to create!")
while not environment.best_fit_found:
    environment.shit_happens()

Sure, there's some detail in the Environment class, but you get the idea: while we've not hit the target, let life find a way. A loop like that would totally bog down an application with a UI without some other work taking place. With Textual and workers the resulting method in the application, complete with code to send updates to the UI, really doesn't look much different:

@work(thread=True, exclusive=True)
def run_world(self, target: str) -> None:
    worker = get_current_worker()
    environment = Environment(target)
    iterations = 0
    self.post_message(self.WorldUpdate(environment, iterations, *environment.best))
    while not worker.is_cancelled and not environment.best_fit_found:
        environment.shit_happens()
        iterations += 1
        if (iterations % 1000) == 0 or environment.best_fit_found:
            self.post_message(
                self.WorldUpdate(environment, iterations, *environment.best)
            )
    if environment.best_fit_found:
        self.post_message(self.Finished(iterations))

I honestly think the worker API is one of the coolest things added to Textual and I so often see people have real "woah!" moments when they get to grips with it.

Anyway... I've covered science, religion, and how Ruby is better than Python, so I'm sure I've annoyed almost everyone. Job done I guess. ;-)

If you want to check out the app itself there's a GitHub repo and it can also be installed from PyPi using pipx.

Expect it to be my tinker project of choice for a wee while; there's a couple of other things I'd like to add to it.


  1. Possibly unpopular opinion with some folk who will read this, but I've long been a fan of Ruby as a language and actually generally prefer it to Python. 

  2. Me, the coder. While utterly missing the point of a simple illustration, while apparently not understanding the concept of an analogy, I guess at least they felt I was intelligent?