blog - git - desktop - contact
2026-05-29
Making programs with some sort of GUI (or TUI) has been a bit unsatisfactory for me for a very long time now. Libraries come and go, trends come and go. You constantly have to chase upstream's new decisions and adjust your code. Sometimes you don't agree with upstream's decisions at all and then you have to find a new framework. It's a bit tiring. It's not rare that I keep my projects alive for 5 or 10 years or more, and a lot can change in that time.
So, after last Advent of Code in late December 2025, I decided to start making my own TUI framework. This wasn't an easy decision, because I knew that it was going to be a lot of work. I looked for alternatives but couldn't find any that I liked -- or that were fast enough. Performance really appears to have tanked lately, with some frameworks requiring two seconds just to initialize.
This blog post is nothing but a little tour of the current state of movwin, because I've decided that I won't publish this code for now. The situation isn't too great at the moment: Everything I publish will get sucked up by an "AI" company and then they will sell it, disregarding any licensed attached to the code. I'm not okay with that.
The basics
It's a Python library. I'm not the biggest fan of Python anymore, but it does have its advantages, mainly a big standard library that lets me easily do a lot of things.
movwin -- which stands for "movq's windows and widgets" -- sits on top of ncurses. ncurses does the heavy lifting when it comes to terminal compability. What movwin does not make use of is ncurses' subwindows or pads. Instead, ncurses acts as some kind of (intelligent) framebuffer that I can draw to, and it acts as a source of keyboard and mouse input.
One major goal is "acceptable" Unicode support. It's highly unlikely that movwin will ever support things like right-to-left, but I do not want to put an emoji somewhere and see the entire layout explode. In other words, movwin must know how many cells in the terminal a Unicode sequence will (probably) occupy. The big problem here is that this depends on the terminal, so it can't ever be perfect.
There is only one dependency (besides Python 3.14 itself):
wcwidth and its wcswidth()
function. This is used to measure the "apparent size" of text: "♀️" is
two cells wide, for example.
From very early on, movwin had the notion of a "Window" and a "Window Manager". What I had in mind while making the whole thing was old DOS TUIs:
It would have been lovely to truly recreate this experience, and to some degree I have, but mouse support in UNIX terminals is not that great. In my tests, no terminal reported mouse motion events by default (only "mouse down / up") and I had to tweak terminfo files. Worse, in large terminal windows, some mouse events don't register at all. As such, mouse support in movwin is quite limited at the moment. Maybe I'll even remove it entirely, because, well, being keyboard-driven is actually a good thing after all. For some things like moving windows or controlling scroll areas having mouse support is very nice, though.
Another major goal is "acceptable" performance, meaning: "Around 200-300
ms of startup time on my tiny little 10-year-old Intel NUC with a
Celeron CPU is okay, but it shouldn't be more than that." Startup time
really is an issue with Python, because import is super slow. I had to
make some sacrifices here, like not using dataclasses. This is on the
NUC where the import times alone are a killer:
$ time python -c 'exit(0)'
real 0m0.061s
user 0m0.048s
sys 0m0.011s
$ time python -c 'from dataclasses import dataclass'
real 0m0.151s
user 0m0.115s
sys 0m0.027s
Applications
What started this whole thing was a little program called tracktivity:
I use it to track activities and events, like caffeine consumption or
weather events. It operates on CSV files:
rfc3339datetime,Food[coffee;blacktea;greentea],Comment
2026-05-29T14:04:00+00:00,coffee,at home
2026-05-29T14:04:43+00:00,blacktea,just some dummy entry :-)
2026-05-29T14:04:51+00:00,blacktea,more tea
The Food column is configured to have several options to choose from,
the Comment column is free-form text. tracktivity builds a UI form
based on that file, it looks like this:
It's nothing fancy and some widget types are still missing, for example there's no proper table or list widget yet.
tracktivity is a simple single-window fullscreen program, but it still
uses the Window class (like all movwin programs should), so if I
wanted to I could resize and move this window around (click for a
video):
"Popups" are also implemented as new windows. movwin comes with a couple of stock popups, like a "yes/no box", an "input box", or a "message box".
bine is another program that uses movwin: It's a basic hex editor.
What I really wanted to have is a simple hex editor with good
performance but also that little info panel at the bottom (click for a
video):
Like, "the byte under the cursor has the following value when interpreted as a signed 8 bit integer" or the same thing for 2, 4, 8 bytes, signed and unsigned. Or try to interpret the bytes (starting at the cursor position) as UTF-8.
Here's me scrolling through the menu to show some more available features (click for a video):
Two more demos, first showing the "Edit binary" function which uses a custom popup with application-defined widgets, and the second video shows undo/redo (click for videos):
bine makes use of mmap() a lot and being written in Python does not
harm the performance. As you can see in the first video, searching an
ASCII string in a 2 GB file took maybe a second. That's more than good
enough for me and I didn't spend any time trying to optimize this.
Python's
bytes.find()
operates on the mmap object and it does all the heavy lifting.
bine can also make good use of the window system: You can open another
file in a new window, leaving the first window untouched. When opening
several files, the WindowManager class will apply a tiling layout (or
you can switch to "put the focused window in fullscreen mode" -- or move
them around freely, if you're so inclined):
Lastly, I recently made a very simple time-tracking program:
It has a start/stop button and shows a simple list of "bookings": In that screenshot, I was active from 4:59 o'clock until 10:08, then a break, and then I started working again at 12:21. By the time I made this screenshot, this amounted to a total of 9 hours and 28 minutes.
What you can't see on that screenshot is that the tool also displays this 9:28 on a little 7-segment display connected to an Arduino:
I use this whole thing at work to keep an eye on how many hours I've already worked. :-) And when I'm done for the day, I can upload this data to our cloud-based time tracking service thingie (that's what the "Transfer" menu is for).
While we're at it: Timer events can be implemented by using
timerfd
on Linux. movwin's main loop is select() based and you can register
arbitrary file descriptors. A timerfd fires in regular intervals, so
this allows for regular updates of the UI and the Arduino display.
More goodies
Theming
movwin comes with two built-in themes:
The "amber" theme sparks a lot of nostalgia for me because it reminds me of DOS programs on my first amber CRT monitor.
If you want to, you can put color definitions in
~/.config/movwin/colors.json and roll your own themes, but that can
obviously be a bit tricky when applications happen to define their own
colors as well (you can override their colors in that file).
By default, movwin selects the current theme depending on the month of
the year. If you're on the northern hemisphere, you'll see the "amber"
theme in the months around December and the brighter "bluegray" theme
during other months. The auto-selection can be switched to "southern
hemisphere" or you can just select one theme directly by setting
$MOVWIN_THEME.
I recently wrote this someplace else:
I've come to appreciate the concept of Menus a lot in the past few weeks.
They provide easy discoverability of functions. No need to read a manual.
They are organized by categories. I won't open a "Format" menu in a text editor when I look for the program's global settings, for example.
They are keyboard-driven thanks to the "accelerators". And when a menu item has a direct shortcut like
Ctrl+U, it says so right there in the menu.(Not all menu systems implement all of that, of course.)
We used to take menus for granted, because they were just a normal thing that GUI programs did. But it feels like they have been lost somewhere along the way ... ? Especially in (UNIX) terminal programs and in websites. And smartphones.
I particularly like how the hotkeys work in movwin:
menu_tree = MenuRoot(
[
MenuSub(
'&File',
children=[
MenuItem(
'&Quit',
cb_quit,
hotkey='KEY_F3',
),
],
),
MenuSub(
'&Edit',
children=[
MenuItem(
'Edit &raw file',
cb_edit_raw,
hotkey='KEY_F5',
arg=state,
),
],
),
...
],
)
So when you define a menu structure, you can specify a hotkey for a menu
item right there. I love this because it's self-documenting. No need for
a special F3 handler and no need to put F3 somewhere into a help page,
because the menu already tells you what this key does. (I could make
more use of this in bine.)
As for the accelerators: In the example above, Alt+f, q would call
cb_quit() and Alt+e, r would call cb_edit_raw().
Menus can be arbitrarily nested, although I haven't had the need yet to actually use this:
Menus lack mouse support, though. For now, I was too lazy to implement this. And as I said, maybe I'll remove mouse support anyway.
And menus don't cast a shadow on the items below them. Again, I was too lazy for now. :-)
Edit boxes and Unicode
Edit boxes understand just enough Unicode to allow for horizontal scrolling without glitches (click for a video):
The scrolling isn't perfect when the two-cell penguin leaves on the left of the screen, but the main thing here is that the widget won't draw the penguin outside of the allotted area.
(This kind of clipping applies to all text, not just edit boxes. Edit boxes are a bit special, though, because they implement their own horizontal scrolling.)
Where to go from here?
First of all, I'm pretty happy with the current state. There are a few TODOs in the code, but it's already quite usable. It feels nice to have a framework that does what I need and want, and that won't change unexpectedly in the next release. ncurses is super stable, it won't do funny things. wcwidth is something I could fork, should the need arise. Python itself is also relatively stable and I think they have learned from the hard 2-to-3 transition and won't do that again anytime soon? Not sure. Either way, movwin feels like something that could still work in 5, 10, 15 years -- without too much drama.
There are a couple of "bigger" things that I'd like to implement, like a proper list view, maybe a tree view, and most certainly a proper file selection dialog.
And then ... we'll see.
















