Back to Death Stranding

Posted on 2024-02-05 07:45 +0000 in Gaming • Tagged with Death Stranding, PS5, gaming • 2 min read

Death Stranding

Death Stranding is easily one of my top 5 games ever. I bought the PS4 version, on a whim, back in early 2020, to see what all the fuss was about. I didn't know much about it other than the love/hate it seemed to be getting. I can still remember the first session, being a bit confused about what was going on, and then suddenly hitting a perfect moment in the game where I knew it was for me, and that I was going to love it.

That moment? It's when you're setting out for the incinerator, the camera pulls back, the landscape opens up before you, and Bones by Low Roar kicks in. Perfection!

I went on to finish the story in around 45 to 50 hours (much of that overlapping with the first week or so of lockdown during the pandemic); and then played at least as much again just exploring, building zip lines, roads, etc.

I even kept a photoblog of my time in the game.

About a year back I bought the Director's Cut of the game, this time for PC. I'd decided that I had to play it through again and did start streaming it. Annoyingly though the PC setup wasn't great. The game itself ran just fine on my PC, but the controller setup meant that, as I sat on the sofa, it would drop connection from time to time. The only workaround for this was to sit on my office chair closer to the TV and, really, this wasn't comfortable.

Eventually that play-through fell away.

Fast forward to last week and the release of the 10 minute Death Stranding 2 trailer and I was hooked all over again! While it never totally went away, my obsession with this game was back full force.

I had to play again!

Meanwhile, between the attempt to play through on the PC and now, I'd acquired a PS5 and at some point I'd upgraded my PS4 version of Death Stranding to the PS5 Director's Cut.

So I had to. I just had to. I'd had a great time streaming my play through the story of Cyberpunk 2077 so it made sense to do the same with Death Stranding.

Last night I hit New game on my PS5 and played for just over 2 hours. This play through is likely going to take a long time -- my work situation means I have a longer and more tiring commute -- but I'm determined to do it. I want to experience all of the extra things in the Director's Cut, including the extra story (which I've read there is, but haven't seen or read about).

I hope, eventually, the playlist I started will have the full story plus lots more fun and building and exploring in it. Hopefully I'll be done just in time for the release of DS2! (and GTA6, it's going to be a busy year for gaming next year!)

How not to ask for help

Posted on 2024-02-04 11:27 +0000 in Coding • Tagged with free-software, foss, help • 9 min read

My association with Textual works on two levels: on the one hand, sure, it's currently my day job; on the other hand it's a FOSS project that I'm keen to support so "free time me" tries to work with it and support others working with it too. For this reason you'll often see me being terminally1 online in the Textual Discord, trying to answer questions as they come up, every waking free moment.

Almost without exception the people who ask for help are appreciative and ask in the spirit of wanting help and wanting to work together with whoever is helping them to get an answer. That... that's actually quite a cool thing to be part of. I like the sense of community that comes from someone going "bah I'm trying to do this thing and it isn't working PLEASE HELP!".

And then... well, let's just say that sometimes the odd question will crop up that seems to be asked from a less collaborative position.

Without wanting to appear to dunk on an individual (I don't wish to), I want to break down an example that happened yesterday. For some background, I'd been AFK all day, having a wonderful time in town with a friend, shopping, lunch, a movie, that sort of thing. A nicely-chilled day where I didn't even look at the Discord notifications that had popped up on my watch and phone.

However, later on that evening, finally home and flopped on the sofa, I saw a question pop up that, while lacking any useful detail and possibly suffering a wee bit from being an XY problem, the immediate answer was clear:

BadIdentifier: 'test.udp_json_client-input' is an invalid id; identifiers must contain only letters, numbers, underscores, or hyphens, and must not begin with a number.

????????????????? a dot isn't allowed?

Like I say: it lacks context and detail, and the number of question marks doesn't really clarify much, but the core question that seems to be at play here is "is it true that a full stop can't be used as part of the ID of a widget?".

The answer is: no, it can't. There's a reason for that too, and if someone were to take a step back for a moment and think about how IDs play a part in queries and how they'd be used in a stylesheet, the reason for that might pop out. So, to help the person asking the question walk in the direction of the answer, I reply:

When you come to query that how would the parser know it’s not ID “test” combined with a class, if dot was allowed?

Before we go on, to illustrate my point, consider this ID: When you come to query that back, or use it in a stylesheet, how would look? Is it a widget with the ID; or is it a widget with the id foo and the class bar?

As far as Textual's CSS is concerned, it would be the latter.

But at this point it didn't seem necessary to get into all that detail; I like to try and assume knowledge on the part of the person asking the question, sans any other evidence, so for the moment I'll assume a "oh, right, yeah, that's a damn good point" kind of reply. Or if not, perhaps a "I don't quite follow, could you explain?" reply, in which case I'm happy to go into all the detail.

The reply was neither:

why are element ID and classes co-mingled?

this was previously allowed so your question doesn't really make sense to me

Now I'm confused. Asking why element IDs and classes are co-mingled seems odd; but I'm used to chatting with people who don't have English as a first language so I'm going to assume it's just a wording choice; but the latter part is very odd: this has never been allowed. Or, more to the point... without any proper context I can't really appreciate what claim is being made here.

You see... I did notice a bug in Textual recently, when it came to widget IDs. Long story short: when you set your ID for the widget in your code, no actual validation of the ID was being done. This was an oversight that was fixed in the latest release.

But knowing that that's the case would be guesswork on my part; I'm also fresh at my desk after a day out; I'm probably not quite in the coding/Textual zone yet, so rather than try and guess half of the conversation, it's easier to just ask the person who is asking. So I ask them to restate the question, and give some more background.

The reply is:

the objective is to put a string like test.udp_json_client-input as the label of a tab, which previously just used the ID property. from skimming release notes, is it better to explicitly set the label, and then assign something compliant for the ID separately?

Wait... what? I thought we were talking about valid widget IDs, now we're talking about tabs and labels? Do we mean TabbeContent and the labels of a TabPane? This is a bit different. So I'm sat there trying to figure out this person's thought process so I can offer the help they're after and this follows:

okay @davep, you have a real bug. --content-tab- prefix is not ephemeral. if I create a tab and grab .id, that prefix comes with it, so if you save it for later and try to set .active, assigning .active doesn't agree that there is a tab --content-tab-thing

so I need to de-mangle the name manually before assigning .active I guess?

Wut? Like... wut? Okay, we do seem to be talking about TabbedContent, I recognise the values being mentioned here; we did some work late on last year that added some namespacing to parts of the TabbedContent widget in an effort to reduce some foot-gun situations.

But... there's no . being used in the IDs as part of that; why are we now proclaiming a bug in an unrelated PR? That's quite the leap with zero evidence. Like... sure, I'm all for being alerted to bugs and fixing them, but this doesn't seem like that.

And then there's the "so I need to..." conclusion that also seems to have no connection to the original question.

Anyone who has ever done support will recognise this situation, I'm sure. Someone has seen a problem, they've dug around a little and reached a conclusion about what the cause is, and turns up looking for help with the conclusion they've reached (very much a variant of a XY problem).

That almost never gets us where we want to go, so I do the obvious thing; I try and reboot the question; I try and get us back to the start and try and get some clarity; I try and encourage asking the question with zero assumptions:

I'm afraid I'm still not really understanding your question, as it now no longer seems to relate to what you very first asked. Perhaps you could start again, ideally with an MRE of what you're looking at and trying to do, for clarity?

I figure, whatever the problem is, it can be illustrated with like a dozen lines of code. Also, when asking people to do this, it often actually helps them rubber-duck their own problem. There's been plenty of times on Discord where someone's "found a bug" in Textual, they're asked to make an MRE of it, and they come back and go "oh, shoot, right, I did that and realised the bug was in my code". It's cool when they happens; everyone learns something.

So... no MRE comes back, but this is the reply:

I'm trying to fix multiple breakages in my application from some recent changes. Right now I can't wrap my head around what to assign a for it to work how it did before (where if you have a tab with ID sample, you can assign = "sample", but you can't do that anymore)

While not an MRE, I can work with this. It seems clear that they have a TabbedContent where they have a TabPane with the ID "sample" and they are struggling to make it the active tab by setting active to "sample". That seems hugely unlikely, this is what TabbedContent is all about, I think we'd have noticed (I'm petty sure we've got unit tests that cover this), but I'm game. I can test this. And the MRE I write will illustrate there isn't a problem.

So I reply:

Again, I can only suggest that you make an MRE of the issue you're seeing. For example, here's me making a set of tabs, the last of which has the ID "four", and I set the active to "four":

and provide the code:

from import App, ComposeResult
from textual.widgets import TabbedContent, TabPane, Label

class TabbedContentApp(App[None]):

    def compose(self) -> ComposeResult:
        with TabbedContent():
            with TabPane("One", id="one"):
                yield Label("One")
            with TabPane("Two", id="two"):
                yield Label("Two")
            with TabPane("Three", id="three"):
                yield Label("Three")
            with TabPane("Four", id="four"):
                yield Label("Four")

    def on_mount(self) -> None:
        self.query_one(TabbedContent).active = "four"

if __name__ == "__main__":

Based on what they've most-recently said is the problem, I'm confident they'll see that this MRE is their situation in a nutshell, and we can work out from there and figure out what the problem is they're seeing and where this . in their IDs is coming from (because I'm very confident it isn't coming from the work that was done on TabbedContent).

This is good. We're getting close to heading down a good path; I can feel it!

I was wrong.

there's no way you can deny you just added a metric ton of shenanigans with the tab ID stuff. I can't get it to work at all anymore (assigning .active), but yes I will either come up with an MRE or find the bug and let you know

So, rather than back up a wee bit, work with the MRE I wrote for them so we can take a walk through the problem, they instead decide to tell me that the PR I did last year (which still isn't implicated in any of this outwith of them seemingly assuming it's the cause of all the issues, presented with zero evidence that it is) was simply "a metric ton of shenanigans".



This is not how you ask for help.

This isn't how you ask for help from a product or service you pay for. This really isn't how you ask for help from a Free Software project, where the people who are offering you help are doing so in their free time because they want people to be able to build cool things with it.

It really isn't hard at all to show just a wee bit of respect for people's time and willingness to try and help you.

Now... I get it. I can imagine a scenario where someone has just updated Textual and their application suddenly starts throwing all sorts of weird and new errors. That happens. That happened to me on Thursday evening just gone. But that's no reason for approaching getting help like this.

The way to approach it is this: pin the problem dependency, perhaps publish a new version of your application so there's no accidental update of the dependency, then head to any of the help resources for the dependency has and work with people who want to help you to find the cause of the problem. Trust me, it'll go a lot faster if you work with them, take on board suggestions (no matter how odd they might first appear), and really don't call their code "a metric ton of shenanigans".

The conclusion to all of this? The person asking the question eventually found they were setting some widget's ID to an invalid ID; one with a . in it. So as I suspected and wanted to walk them to: they had invalid IDs all along and they only found out about this because ID validation was fixed.

Perhaps one day they'll retract the claim that my actually-unrelated code that wasn't "just" released but was from last year is "a metric ton of shenanigans". ¯\_(ツ)_/¯

  1. Geddit? GEDDIT? 


Posted on 2024-01-29 21:30 +0000 in Coding • Tagged with Python, terminal, textual • 2 min read

I feel like I'm on a bit of a roll when it comes to building applications for the terminal at the moment; while I'm still tinkering and improving tinboard and OSHit, I had the urge to tackle another idea that's been on my TODO list for a while.

This is something I did for Emacs back in 2017 and I felt it was a perfect candidate for a Textual-based project. It's a terminal-based trivia quiz game, using the Open Trivia Database as the source of questions.


I've just published an early version to PyPI; it still needs some polish and I have a few other ideas for it, but as it stands I feel it's a fun little game to mess around with.

The idea is pretty straightforward: you can run it up and create lots of different quizzes, there are various parameters you can use to create lots of different kinds of challenges:

Building a new quiz

Once you're created a quiz, you can run it and answer away:

An example question

Right now the idea is that you answer by pressing either 1, 2, 3 or 4 (or just 1 or 2 for true/false questions); when I get a moment I'll also enable mouse support for selecting an answer too (honestly I feel keyboard-answering feels far more natural).

Once the quiz is done you can review your answers and see which were right and which were wrong:

Viewing results

As I say: there's a bunch of other things I want to add to this (keeping track of scores, adding session token support to reduce the chances of repeat questions, etc), but this felt like a good spot to make a v0.1.0 available if anyone else wanted to have a play.

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.

PS: Now you can see why I made textual-countdown.

Orange Site Hit v0.5.0

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

Just a wee catch-up post about OSHit, my terminal-based HackerNews browser. Over the past couple of weeks I've made some small changes, so I thought I'd make mention of what I've done.

As of v0.5.0, which I released earlier today, I've:

  • Added a quick way of following links while viewing a comment. While a comment is highlighted you can press l to follow a link; if there's more than one link in the comment a menu will be shown and you can select which one to follow.
  • Added support for viewing polls. Polls seem to be few and far between on HackerNews, so when I published the first version of OSHit I didn't have one to hand to test any code against. Eventually one turned up and broke OSHit (on purpose; I wanted to see when that happened) so I could then add the code to load polls and show them. Right now it just shows scores; I might do actual charts at some point.
  • Added optional item numbers in the lists; turned on/off with F4.

So far all small things, but handy little improvements. There's still a nice TODO list in the README and I will slowly work through it. Along with tinboard these are two applications that have absolutely turned into "daily drivers", so they're going to get a lot of tweaking over the next few weeks, probably even months.


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

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. 


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.


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.