I seem to have forgotten how to blog. (Actual blogging, as opposed to merely linking to new art.) In an attempt to get back on the saddle again:
Outside of art, my project time lately has primarily been swallowed up by some internal tooling changes. I alluded to this back in June, though the plan changed along the way. Rather than merging all those apps into one behemoth conglomerate, I decided it would be better (along at least a few axes) to follow the Unix philosophy and stick with smaller tools that do one thing well. Which conveniently lines up with the set of tools I’ve already built. Fancy that.
In fact, it was so liberating and fun that I plowed onward and decided to ditch Vinci (my internal blog/notebook app) and build a new app, Leaf, using the same technique; the only JS it uses is for keyboard shortcuts. It’s simpler, easier to maintain (I think? it’s still early on), and in a way it feels more in line with the grain of the web.
One other thing I did differently with both apps was to wait to write any CSS until after the functionality was all in place. It was disconcerting and delightful, building something with bare browser styles, and it certainly helped me focus on functionality first rather than getting distracted by layout.
Conclusion: while I doubt I would ever build apps at work this way, this old-school mode was invigorating and absolutely worth it for these personal projects.
I used to use Fabric to deploy my personal apps, but I often ran into issues with it, so several months ago I switched over to simple shell scripts that use ssh. Much more resilient, and far easier to maintain (at least for me).
Here’s a sample of what one of these deploy scripts looks like for a Django app:
I’ve thought about using a CD pipeline instead, but I’m not convinced that introducing an extra dependency — no matter how slick — is actually worth it for something small and personal like this. (CI/CD sure is nice at work, however.)
Storybook is my fiction writing app. It’s a Python app running Django. The name comes from, uh, books with stories in them.
First, the dashboard, which lists weekly writing stats and active stories at top and backburnered stories at bottom:
The writing view has a stats bar at top (showing how close I am to meeting my daily 1,000-word goal) and then the textbox for the actual writing:
The menu has some overall story stats and an outline (with somewhat vague and hopefully unspoilery scene titles), and some admin links:
As you can see from the screenshots, it expects Markdown. I’ve put in a convention hack where h2 tags (##) delineate scenes. Also, scene titles that begin with “Chapter X” create chapter divisions. (Clarification: a story has a flat list of scenes. The chapter divisions are display-only.)
How I use Storybook
On my laptop, I have it open in Firefox as a pinned tab. On my phone, I have it saved to my homescreen as a PWA.
I mostly avoid using Storybook (cough) but somehow still manage to put in a thousand words a day, one word after another.
There’s a payload syntax so I can send writing from Gate or Quill to Storybook, but I never use it.
Same old story: I’m planning to move it to FastAPI and start using plain text files for storage instead of a database.
At some point I want to refactor the outline UI and add search functionality.
Bookshelf is my reading tracking app. It’s a Python app running Django. The name comes from, uh, the thing that holds books.
Behold the books:
At the top there’s the stats panel, which shows how much I read the last six days with color coding for the genre tags (and yes, Wednesday and Friday I didn’t meet my 100-pages-per-day goal), my page total so far this month (932), how many books I’ve finished so far this month (2), and how much of my reading this month has come from each tag (I usually try to read around 50% nonfiction, but I usually fail).
And then there’s the book list itself. Title, progress bar with some extra data (including how long since I started the book), current page number (clicking this opens a panel where I can record the page I’m on along with a comment), and how long it’s been since my last entry.
Each book has a staleness limit (default is five days), where if I haven’t read the book at all in that period of time, it changes the color of the title to a glaring, awful red, and that’s sufficient motivation for me to get back to that book. (To be honest, lately I haven’t seen it come up much since I’ve been reading only a few books at a time, but in those crazy days when I was reading twenty to thirty books at a time, I saw it a lot.)
Also: the sixth book (in case you were wondering) is A Disciple’s Life, which is only visible on Sundays (I reserve it for Sunday reading).
And the mobile view, for the heck of it, and since it’s the one I use almost all of the time:
There’s also a stats page, since the statistics are surprisingly helpful in motivating me to make time for reading:
(Yes, as of a couple days ago I’ve read more this year so far than all of last year in total. This makes me inordinately proud even though it really doesn’t matter.)
And, lastly, the hopefully self-evident history page:
How I use Bookshelf
On my phone, I have it saved to my homescreen as a PWA, and that’s primarily where I use it, since I mainly read on my phone these days. On my laptop, I have it open in Firefox as a pinned tab.
I use Bookshelf every day to track my reading, both for individual books and for my daily/monthly reading goals. It’s handy, too, as a bookmark that toddlers can’t pull out.
The desktop view needs some love, particularly that stats page. (I added those genre tags to it a month or two ago and realize now that I never actually looked at the desktop version. Whoops.)
Also (this should be no surprise by now), I’m planning to switch it to FastAPI along with plain text files for storage, for the same reasons I gave in those other posts.
Momentum is my daily goal app, for keeping a goal chain/streak going. It’s a Python app running Django. The name comes from the momentum that a long streak gives.
Goals can be either binary flags (whether I did it that day or not) or timed (in which case Momentum keeps track of the time spent). The default mode is focus mode, which shows only the top unfinished goal at a time and looks like this (with dummy data):
The thin red line along the top is a progress bar showing how close I am to finishing my Momentum goals for the day. The red boxes show the last few weeks of the streak, the green box at the right is the button for saying I’ve completed that goal for the day, and the blue text under the goal name shows how long the total streak is.
When focus mode is off, it looks like this:
You can see a partially completed timed goal along with a binary goal. Momentum also supports ignoring goals for Saturdays and/or Sundays (the gray boxes among the red), which I use for things I don’t usually do on the weekends.
When the timer on a goal is running, the favicon changes and the page looks like this, with the pink box at right showing the elapsed time for the current session:
(The idea with the timer is that it may take multiple sessions spread throughout the day to meet the daily goal, by the way — if I wanted to make sure I spend an hour writing each day but don’t usually have time to do it all in one block, for example.)
How I use Momentum
On my laptop, I have Momentum open in Firefox as a pinned tab. On my phone, I have it saved to my homescreen as a PWA.
I use Momentum every day for my morning routine, primarily on my phone. The goals I put into it (as opposed to just adding things to my to-do list in Liszt) are things I want to do each day and, to some degree, are things I might not do if I didn’t have a streak pushing me forward (thus “Momentum”).
I’m happy with the app as it is, but I’ve been thinking about merging it into Liszt, since goals like these are fundamentally to-do items. (Every morning Liszt already automatically adds all the items in my ::streak list to my ::today list, so that I can work off my to-do list without necessarily having to go back to Momentum as much.)
Giving Liszt items the ability to be timed is already in place with belt mode, so I’d just need to add the ability to keep track of both partially finished goals and total streaks. Seems worthwhile.
Slash is the engine that runs this blog. It’s just a Python app running Django, but calling it an engine is too satisfying for me to stop anytime soon. The name comes from the ubiquitous forward slash in URLs.
Slash has an internal frontend with some post management pages (see below) along with a small API which is used by Blackbullet (my current website engine, separate from the blog) to pull posts into my site template. The API also publishes the RSS and JSON feeds, which Blackbullet passes right through.
The dashboard lists current drafts and, lower on the page where you can’t see it in this screenshot, recently published posts:
Disclaimer: there’s no guarantee that these particular post drafts will ever see the light of day. I often put ideas in and then decide later that they’re not worth blogging about.
Here’s the post edit page, which is very much a work in progress (last week I added the visual tag controls, since adding tags via the metadata textbox made it impossible to tell whether I’d used a tag before or not):
It’s spartan but works for me.
Other than the notebook specifier, the syntax is pretty much the same as Vinci’s. Posts are written in Markdown. Metadata is specified with the initial-colon syntax.
One thing I realize I forgot to mention in the Vinci post is that in both apps I have a shortcut syntax for including images that looks like this:
I have a page for uploading images to a date-named folder — year and month — and this syntax relies on the image being in the matching folder for the post. A small bit of convention to make things simpler.
How I use Slash
On my laptop, I open Slash when needed. On my phone, I have it saved to my homescreen as a PWA.
Other than that, I use it the way you’d expect — I write blog posts (usually directly in Slash, but occasionally in Gate or Quill), I edit them, I publish them. Months later I finally notice the typos. It’s not too exciting.
As of now, the plan is to replace both Blackbullet and Slash with a new, simpler, consolidated Slash, using plain text for the backend and probably moving to FastAPI. Since I’m in the middle of planning the rewrite right now (and since I’m now working in public), you’ll see posts about it soon.
Vinci is my journal/log app, a private blog of sorts. It’s a Python app running in Django.
Vinci has notebooks which contain entries. Like most blogs, entries are displayed in reverse chronological order. It looks like this, except I usually write in English, har har:
Editing an entry is a modal fullscreen panel, with the main textbox at top, the metadata textbox under that, and some controls at the bottom.
On save, Vinci splices the metadata and the text together and runs it through the payload processor.
Vinci uses the text-based payload idea like Liszt. Its payload syntax looks like this:
:tags foo, bar
Worked on the [foo project](/leaf/3290) for a while. Ran into a few issues.
The first line is the notebook specifier. The second line (and this could have been anywhere, didn’t have to be at the top) has a command with some parameters. And the rest is Markdown.
There’s also a variant syntax where the first line can look like this: /projects/tag/tag2/tag3. I’ve started using that a little more often.
Because of how I like to write in my journal, I’ve also set up a special case for my /journal notebook, where adding an entry will either create a new entry for the day (if there isn’t one yet) or append to the existing entry, so there’s just one entry for each day. (I use Gate or Quill to jot down a paragraph and then append it quickly and easily.)
How I use Vinci
I use Vinci a lot. I maintain my personal journal in it, along with logs for most areas of my life — work, school, writing, projects, church, etc. Sometimes I create notebooks for specific projects, other times I use a higher-level notebook (like /projects) and use tags instead. At some point I’ll probably consolidate.
I have a /thinking notebook where once each morning I think through my current tasks/projects and write out what I need to do for each. Writing things down makes a world of difference for me, across the board.
Each morning I also spend a few minutes reading one of my past journal entries, as mentioned earlier today. (A while back I scanned all my paper journals and I’ve been slowly transcribing them — we’ll get to Ditto soon — and importing them into Vinci.) Lately I’ve been reading through my 2004 college entries. My undergraduate years were great, but I am very, very glad I’m not in that phase of life anymore.
Lastly, I reference these notebooks fairly frequently. (When did we replace our dishwasher, what did I last work on for that project I haven’t worked on in months, etc.)
Vinci currently uses Whoosh for full-text indexing, but it’s unsupported and hasn’t been working as well for me lately. Several months ago I realized that if all my notebook entries are stored in plain text files, I can just use ripgrep or ag for fast and accurate searching, with the further benefit that in the event of my untimely demise, everything would be fairly easy for my family to copy out and preserve. (I really like plain text, can you tell?)
To that end, I started writing a new, smaller app in Go called Leaf. It’s going well, but I’m tempted to switch to FastAPI. Not sure yet if I will or not. (I’ve enjoyed learning Go and have used it on several small projects now, but I’m also thinking more about long-term maintenance across all these apps, and using a single stack would simplify things for me.)
I’m also thinking about adding a small CLI (in the web interface) that would make entry management easier — moving all entries with a specific tag to their own notebook, for example. Truth be told, I don’t know that entry management happens frequently enough to warrant a CLI, but I’m intrigued by the idea of putting a CLI in the interface, and if it goes as well as I hope it will, I see myself doing that in more of my tools. (Right now I see it as an extension of the search interface. Where right now I type dishwasher to search for that keyword, I’d eventually also be able to type something like :move /projects#cardiff /cardiff.)
As with Liszt, I’m also looking forward to moving to a lighter, simpler codebase. Vinci has a moderate amount of vestigial functionality that needs to go.
First in the series introducing my personal productivity tools. Buckle up, this is going to be nerdy. And long.
Liszt is my to-do list app. Named after the composer, though I regret it a little since I butcher his name by pronouncing it lisht to differentiate it from the ordinary list. Heresy. The next version will have a better name, though. It’s a Python app running on Django.
Disclaimer: I don’t think this app is perfect. (Or any of the other tools I’ll talk about, for that matter.) I’ll describe things as they are, acknowledging here that there’s a lot of room for improvement.
This is what the dashboard looks like, populated with some dummy data:
And on mobile, where the controls move to the bottom for easier access:
The top bit is my stats panel, with the data pulled in from my other tools’ APIs. Daily writing counts, daily words left, total words written on the novel (all three from Storybook), daily pages left (from Bookshelf), and daily goals left (from Momentum). I’ll cover those tools later.
There’s also a slide-in menu on the side, with my most commonly used top-level lists:
Double-clicking on a list item opens up an edit panel, which also allows me to move the item to another list with some commonly used lists included as buttons (this whole panel is kind of clunky and needs improvement):
The basic idea behind Liszt (and this is common to many of my apps) is the text-based payload, which enables some nice cross-app integrations (more on this when I cover Gate and Quill). Adding items looks like this:
A Liszt payload (the text entered into the add tray) has one item per line, with optional blank lines and optional list specifiers. If there’s no specifier, it’ll assume the current list if there is one, otherwise it’ll default to ::inbox. (I use the double-colon prefix to mark lists, with a slash to specify sublists.) Here’s a fuller example of the syntax, again with dummy data:
Process email :5
Review the merge requests :10
Read up on Python futures ::: https://docs.python.org/3/library/concurrent.futures.html
Write up the design doc
The first two items (which have belt-mode durations, I’ll explain those in a minute) would go into the ::today list (which is the dashboard list). The last three items would go into the ::work/notifications list, as pictured here:
Of these, the first line sets a subtitle by putting it after the triple-colon marker. I use this all the time.
The second line is a symlink of sorts, pulling in the top item from the linked list (different meaning here) and showing it in place, with the Refactor text shown as the subtitle. I used to use this more often but haven’t as much lately.
You can also see that this list has a child list (::work/notifications/refactor).
If an item has a duration marker included, that triggers belt mode (ala conveyer belts), as evidenced by the new bar at the top of the screen in the image above.
Brief backstory: I initially wrote an Electron app called Belt that did the same sort of thing, then a few months ago ported it to Go as a menubar app. Shortly after I finished that, I realized it would make much more sense in Liszt and brought the functionality in.
And what is that functionality? It’s just an easy way to time tasks from the list. When I hit Start, it switches into belt mode (also changing the favicon so I can tell that it’s running and turning on focus mode so I can only see the task I’m actively working on):
When the timer runs out, it plays a sound and brings up a panel allowing me to continue on to the next item in the list, stop belt mode, or add more time to the timer. (There are also keyboard shortcuts for all of this.)
How I use Liszt
On my laptop, I have Liszt open in Firefox as a pinned tab. On my phone, I have it saved to my homescreen as a PWA.
Every morning I go through the main lists and move the items I’m going to work on to the today list. I then work out of the today list the rest of the day, opening it often.
I use belt mode most days, primarily to help me stop avoiding tasks I don’t really want to work on (but that still need to be done).
Lately I’ve grown enamored of the idea of storing data as plain text files in directories, rather than using an actual database like Postgres or Mongo. There are plenty of apps where this doesn’t make sense, but for personal, small tools, it works nicely, so I’m planning to migrate Liszt off SQLite to plain text, and I am very excited about it. Yes, I am that kind of a person.
While Django is fine (we use it at work and I love it), I’m planning to move to FastAPI, which I’m already using for Ditto and Arc. It’s a bit faster and feels more lightweight. I think in my mind I mostly use Django because of the ORM and admin; once I’ve given that up, the baby goes out with the bathwater.
I’m also looking forward to simplifying things, removing vestigial functionality, and sanding down as many of the friction points as I can.
Brief sidenote: I switched a while back to Python/Django, and I’m very glad I did. I can often get to a working prototype within only an hour or two. Back in my younger days I wanted to write everything myself from the ground up, but I see now that I was foolish. Life is short. I’d rather focus on the interesting parts — the app itself — and let the framework handle the routine grunt work.
Momentum is the app I’d like to introduce today. It’s a web app written in Django, started back in January to help me track my goals.
More specifically, I wanted something that would help me spend more time reading scriptures and writing fiction. I don’t always have a free half-hour, though, so I needed something to track little bits of time throughout the day, and Momentum was born. I’ve been using it pretty much every single day since then.
Here’s what it looks like on my phone (with dummy data):
Some quick notes:
It currently supports tracking minutes, times, or words per day.
When I reach a particular goal, it disappears from the list for the rest of the day so I can focus on the goals I haven’t yet reached. (This is a change I made last night, actually.)
If I don’t make any progress toward a goal at all within a set time period, the goal goes stale and turns red. (There’s a system-wide stale period setting and each goal can also have its own.) I started using this staleness idea in Bookshelf (more on that in a later post) and it’s been motivating enough that I ported it to Momentum.
It supports folders. I have Projects and Health folders, where I have specific projects (stories/apps I’m working on), and things like squats and pushups.
Right now you have to add/edit goals via the Django admin. It works but isn’t as nice as something in-app. I just need to get around to doing this, since this is the main thing keeping it from being releasable.
The code is on GitHub as usual. Again, this is unreleased, in-progress code, YMMV, grain of salt, etc.