2022-11-26 DevLog from davep

This commit is contained in:
Dave Pearson
2022-11-26 11:19:29 +00:00
parent 6bd652e240
commit a25466e0d4
5 changed files with 337 additions and 0 deletions

View File

@@ -0,0 +1,337 @@
---
draft: false
date: 2022-11-26
categories:
- DevLog
authors:
- davep
---
# On dog food, the (original) Metaverse, and (not) being bored
## Introduction
!!! quote
Cutler, armed with a schedule, was urging the team to "eat its own dog
food". Part macho stunt and part common sense, the "dog food diet" was the
cornerstone of Cutlers philosophy.
<cite>G. Pascal Zachary &mdash; Show-Stopper!</cite>
I can't remember exactly when it was -- it was likely late in 1994 or some
time in 1995 -- when I first came across the concept of, or rather the name
for the concept of, *"eating your own dog food"*. The idea and the name
played a huge part in the book [*Show-Stopper!* by G. Pascal
Zachary](https://www.gpascalzachary.com/showstopper__the_breakneck_race_to_create_windows_nt_and_the_next_generation_at_m_50101.htm).
The idea wasn't new to me of course; I'd been writing code for over a decade
by then and plenty of times I'd built things and then used those things to
do things, but it was fascinating to a mostly-self-taught 20-something me to
be reading this (excellent -- go read it if you care about the history of
your craft) book and to see the idea written down and named.
While [Textualize](https://www.textualize.io/) isn't (thankfully -- really,
I do recommend reading the book) anything like working on the team building
Windows NT, the idea of taking a little time out from working *on* Textual,
and instead work *with* Textual, makes a lot of sense. It's far too easy to
get focused on adding things and improving things and tweaking things while
losing sight of the fact that people will want to build **with** your
product.
So you can imagine how pleased I was when
[Will](https://mastodon.social/@willmcgugan) announced that he wanted [all
of us](https://www.textualize.io/about-us) to spend a couple or so weeks
building something with Textual. I had, of course, already written [one
small application with the
library](https://github.com/Textualize/textual/blob/main/examples/five_by_five.py),
and had plans for another (in part [it's how I ended up working
here](https://blog.davep.org/2022/10/05/on-to-something-new-redux.html)),
but I'd yet to really dive in and try and build something more involved.
Giving it some thought: I wasn't entirely sure what I wanted to build
though. I do want to use Textual to build a brand new terminal-based Norton
Guide reader ([not my first](https://github.com/davep/eg), not by [a long
way](https://github.com/davep/eg-OS2)) but I felt that was possibly a bit
too niche, and actually could take a bit too long anyway. Maybe not, it
remains to be seen.
Eventually I decided on this approach: try and do a quick prototype of some
daft idea each day or each couple of days, do that for a week or so, and
then finally try and settle down on something less trivial. This approach
should work well in that it'll help introduce me to more of Textual, help
try out a few different parts of the library, and also hopefully discover
some real pain-points with working with it and highlight a list of issues we
should address -- as seen from the perspective of a developer working with
the library.
So, here I am, at the end of week one. What I want to try and do is briefly
(yes yes, I know, this introduction is the antithesis of brief) talk about
what I built and perhaps try and highlight some lessons learnt, highlight
some patterns I think are useful, and generally do an end-of-week version of
a [TIL](https://simonwillison.net/2022/Nov/6/what-to-blog-about/). TWIL?
Yeah. I guess this is a TWIL.
## gridinfo
I started the week by digging out a quick hack I'd done a couple of weeks
earlier, with a view to cleaning it up. It started out as a fun attempt to
do something with [Rich Pixels](https://github.com/darrenburns/rich-pixels)
while also making a terminal-based take on
[`slstats.el`](https://github.com/davep/slstats.el). I'm actually pleased
with the result and how quickly it came together.
The point of the application itself is to show some general information
about the current state of the Second Life grid (hello to any fellow
residents of [the original
Metaverse](https://wiki.secondlife.com/wiki/History_of_Second_Life)!), and
to also provide a simple region lookup screen that, using Rich Pixels, will
display the object map (albeit in pretty low resolution -- but that's the
fun of this!).
So the opening screen looks like this:
![The initial screen of gridinfo, showing the main SL stats](../images/2022-11-26-davep-devlog/gridinfo-1.png)
and a lookup of a region looks like this:
![Looking up the details of the first even region](../images/2022-11-26-davep-devlog/gridinfo-2.png)
Here's a wee video of the whole thing in action:
<div class="video-wrapper">
<iframe
width="560" height="315"
src="https://www.youtube.com/embed/dzpGgVPD2aM"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen>
</iframe>
</div>
### Worth a highlight
Here's a couple of things from the code that I think are worth a highlight,
as things to consider when building Textual apps:
#### Don't use the default screen
Use of the default `Screen` that's provided by the `App` is handy enough,
but I feel any non-trivial application should really put as much code as
possible in screens that relate to key "work". Here's the entirety of my
application code:
```python
class GridInfo( App[ None ] ):
"""TUI app for showing information about the Second Life grid."""
CSS_PATH = "gridinfo.css"
"""The name of the CSS file for the app."""
TITLE = "Grid Information"
"""str: The title of the application."""
SCREENS = {
"main": Main,
"region": RegionInfo
}
"""The collection of application screens."""
def on_mount( self ) -> None:
"""Set up the application on startup."""
self.push_screen( "main" )
```
You'll notice there's no work done in the app, other than to declare the
screens, and to set the `main` screen running when the app is mounted.
#### Don't work hard `on_mount`
My initial version of the application had it loading up the data from the
Second Life and GridSurvey APIs in `Main.on_mount`. This obviously wasn't a
great idea as it made the startup appear slow. That's when I realised just
how handy
[`call_after_refresh`](https://textual.textualize.io/api/message_pump/#textual.message_pump.MessagePump.call_after_refresh)
is. This meant I could show some placeholder information and then fire off
the requests (3 of them: one to get the main grid information, one to get
the grid concurrency data, and one to get the grid size data), keeping the
application looking active and updating the display when the replies came
in.
### Pain points
While building this app I think there was only really the one pain-point,
and I suspect it's mostly more on me than on Textual itself: getting a good
layout and playing whack-a-mole with CSS. I suspect this is going to be down
to getting more and more familiar with CSS and the terminal (which is
different from laying things out for the web), while also practising with
various layout schemes -- which is where the [revamped `Placeholder`
class](https://textual.textualize.io/blog/2022/11/22/what-i-learned-from-my-first-non-trivial-pr/#what-i-learned-from-my-first-non-trivial-pr)
is going to be really useful.
## unbored
The next application was initially going to be a very quick hack, but
actually turned into a less-trivial build that I'd initially envisaged; not
in a negative way though. The more I played with it the more I explored and
I feel that this ended up being my first really good exploration of some
useful (personal -- your kilometerage may vary) patterns and approaches when
working with Textual.
The application itself is a terminal client for [the
Bored-API](https://www.boredapi.com/). I had initially intended to roll my
own code for working with the API, but I noticed that [someone had done a
nice library for it](https://pypi.org/project/bored-api/) and it seemed
silly to not build on that. Not needing to faff with that, I could
concentrate on the application itself.
At first I was just going to let the user click away at a button that showed
a random activity, but this quickly morphed into a *"why don't I make this
into a sort of TODO list builder app, where you can add things to do when
you are bored, and delete things you don't care for or have done"* approach.
Here's a view of the main screen:
![The main Unbored screen](../images/2022-11-26-davep-devlog/unbored-1.png)
and here's a view of the filter pop-over:
![Setting filters for activities](../images/2022-11-26-davep-devlog/unbored-2.png)
### Worth a highlight
#### Don't put all your `BINDINGS` in one place
This came about from me overloading the use of the `escape` key. I wanted it
to work more or less like this:
- If you're inside an activity, move focus up to the activity type selection
buttons.
- If the filter pop-over is visible, close that.
- Otherwise exit the application.
It was easy enough to do, and I had an action in the `Main` screen that
`escape` was bound to (again, in the `Main` screen) that did all this logic
with some `if`/`elif` work but it didn't feel elegant. Moreover, it meant
that the `Footer` always displayed the same description for the key.
That's when I realised that it made way more sense to have a `BINDING` for
`escape` in every widget that was the actual context for escape's use. So I
went from one top-level binding to...
```python
...
class Activity( Widget ):
"""A widget that holds and displays a suggested activity."""
BINDINGS = [
...
Binding( "escape", "deselect", "Switch to Types" )
]
...
class Filters( Vertical ):
"""Filtering sidebar."""
BINDINGS = [
Binding( "escape", "close", "Close Filters" )
]
...
class Main( Screen ):
"""The main application screen."""
BINDINGS = [
Binding( "escape", "quit", "Close" )
]
"""The bindings for the main screen."""
```
This was so much cleaner **and** I got better `Footer` descriptions too. I'm
going to be leaning hard on this approach from now on.
#### Messages are awesome
Until I wrote this application I hadn't really had a need to define or use
my own `Message`s. During work on this I realised how handy they really are.
In the code I have an `Activity` widget which takes care of the job of
moving itself amongst its siblings if the user asks to move an activity up
or down. When this happens I also want the `Main` screen to save the
activities to the filesystem as things have changed.
Thing is: I don't want the screen to know what an `Activity` is capable of
and I don't want an `Activity` to know what the screen is capable of;
especially the latter as I really don't want a child of a screen to know
what the screen can do (in this case *"save stuff"*).
This is where messages come in. Using a message I could just set things up
so that the `Activity` could shout out **"HEY I JUST DID A THING THAT CHANGES
ME"** and not care who is listening and not care what they do with that
information.
So, thanks to this bit of code in my `Activity` widget...
```python
class Moved( Message ):
"""A message to indicate that an activity has moved."""
def action_move_up( self ) -> None:
"""Move this activity up one place in the list."""
if self.parent is not None and not self.is_first:
parent = cast( Widget, self.parent )
parent.move_child(
self, before=parent.children.index( self ) - 1
)
self.emit_no_wait( self.Moved( self ) )
self.scroll_visible( top=True )
```
...the `Main` screen can do this:
```python
def on_activity_moved( self, _: Activity.Moved ) -> None:
"""React to an activity being moved."""
self.save_activity_list()
```
### Pain points
On top the issues of getting to know terminal-based-CSS that I mentioned
earlier:
- Textual currently lacks any sort of selection list or radio-set widget.
This meant that I couldn't quite do the activity type picking how I would
have wanted. Of course I could have rolled my own widgets for this, but I
think I'd sooner wait until such things [are in Textual
itself](https://textual.textualize.io/roadmap/#widgets).
- Similar to that, I could have used some validating `Input` widgets. They
too are on the roadmap but I managed to cobble together fairly good
working versions for my purposes. In doing so though I did further
highlight that the [reactive attribute
facility](https://textual.textualize.io/tutorial/#reactive-attributes)
needs a wee bit more attention as I ran into some
([already-known](https://github.com/Textualize/textual/issues/1216)) bugs.
Thankfully in my case [it was a very easy
workaround](https://github.com/davep/unbored/blob/d46f7959aeda0996f39d287388c6edd2077be935/unbored#L251-L255).
- Scrolling in general seems a wee bit off when it comes to widgets that are
more than one line tall. While there's nothing really obvious I can point
my finger at, I'm finding that scrolling containers sometimes get confused
about what should be in view. This becomes very obvious when forcing
things to scroll from code. I feel this deserves a dedicated test
application to explore this more.
## Conclusion
The first week of *"dogfooding"* has been fun and I'm more convinced than
ever that it's an excellent exercise for Textualize to engage in. I didn't
quite manage my plan of *"one silly trivial prototype per day"*, which means
I've ended up with two (well technically one and a half I guess given that
`gridinfo` already existed as a prototype) applications rather than four.
I'm okay with that. I got a **lot** of utility out of this.
Now to look at the list of ideas I have going and think about what I'll kick
next week off with...