Journey into the Terminal: Building the pygamelib UI Module (part 1)

Journey into the Terminal: Building the pygamelib UI Module (part 1)

2023: A Coding Odyssey

·

6 min read

Introduction

A bit of context: I've been working for some years now on a Python library called the pygamelib. A not-so-small library to write games and applications in the terminal in Python. If you are wondering, it has nothing to do with Pygame and I realized way too late that people would mistake the 2... Sorry, I guess?

This library was originally developed for the kids in the coding classes that I'm giving for fun to young kids from 6 to 15 years old. It started as a rough bundle of classes to make simple games in the terminal (to keep the kids interested).

But it got out of hand... Seriously out of hands.

First, I started to be interested in the algorithmic problems and many optimization issues raised by the needs of such a framework.

Then I got interested in game engine theory (read Game Engine Architecture if you're interested in that) and soooo many more things...

Many years later, here I am, starting a brand new module in the library: the pygamelib.gfx.ui module.

This series is about the challenges and my approach to developing that UI module.

I'll start with my philosophy about this whole endeavor and I'll try to have regular entries about my progress and frustrations.

Philosophy: Embracing the Futility

Let’s start with a harsh truth: what I'm building is utterly unnecessary. Established Python libraries like curses, urwid, picotui, and my personal favorite, Textual, have already mastered TUI (Terminal User Interface) frameworks.

So a new one is not only not needed, but more importantly a dilution of the development effort toward a solid TUI (Terminal User Interface) framework.

So of course I embarked on this exercise in futility! Because: why not?

Before I began, I set some ground rules:

  1. I want to solve the problems myself. I'm doing that for fun and to learn, so there's no point in looking for answers.

  2. Errors are ok. I will mark that module as alpha code for a loooong time. So let's fail!

  3. Obviously, contributions and feedback are accepted and even encouraged. However, there are probably very few people who have the same weird drive to implement stuff just for fun.

  4. I like the Qt framework so I am loosely going to get inspiration from it.

PR #221: The Beginning

Well... Not really.

Ok, it's kinda dramatic to call it the beginning because there was already a first draft of a UI toolkit before that PR (Pull Request), and it could already do very nice things:

The pygamelib sprite editor 1/2

The pygamelib sprite editor 2/2

This is the pygamelib sprite editor (yes: we have a sprite system and even a sprite editor!). Obviously, the library is already capable of having menus with floating displays, dialogs, panels, etc.

The issue is that all of these elements are independent and not really working together. It is functional but not really nice. So let's build something that can help bring all these elements together!

PR #221 is actually that beginning.

The basics

The pygamelib has a fairly simple rendering loop, we have a Screen object on which we can place stuff (game board, UI elements, text, etc.) and Screen.update() take care of triggering the rendering loop (if needed). Each object placed on the screen must either be a printable character, a Sprixel (like a pixel but for the terminal), or implement a render_to_buffer(...) method. All of that is rendered into a frame buffer, and finally, update() just push all of that buffer to the terminal's screen.

Obviously, the UI module needs to integrate into that system and take it into consideration. As a matter of fact, it will be crucial in the future when we talk about geometry... But let's keep that for the next issue.

At first, I was very enthusiastic about the amount of work needed, it did not look like it was going to be horribly difficult. The boldness and pretention of that thought...

For a little context, I started to work on PR#221 on the 19th of November 2022 (well, a bit before that, it was the first commit), the PR was merged on the 11th of October 2023! Admittedly I got distracted along the way but 4025 additions and 54 commits later, I kind of realized that it was not going to be a quick endeavor.

The first thing that I implemented was the Widget class. When rendered on screen it's not much, barely a colored rectangle. But when you look at its logic it's more interesting. So the goal is that the Widget class is going to be the base of almost all elements in the UI module. Therefore it needs to be very generic and keep its logic to the minimum. And that it does, the Widget class does only a limited amount of things:

  • it manages its own geometry (max/min height and width, current height and width),

  • it manages the size constraint policies (à la Qt),

  • it keeps track of its parent,

  • it keeps track of its children,

  • it possesses attributes to manage some internal states (like the focus),

  • finally, it can hold a layout (more on that later).

Navigating through default behaviors set by the user was as smooth as finding a cookie jar in a kid's room. Nothing hard, but I have a newfound respect for all the people who give us such sensible default values in the libraries that we use daily...

Now here I am, ending up proudly with a square on my screen... cool cool...

First widget

Not underwhelming but far from exceptional. The code to get there is fairly straightforward:

from pygamelib import engine
from pygamelib.gfx import ui

# Create instances of the game engine and the UI configuration
g = engine.Game.instance()
uic = ui.UiConfig(game=g)

# Place some stuff on the screen
g.screen.place("Here is a widget!", 2, 2)
g.screen.place(ui.Widget(widht=10, height=5), 3, 2)
g.screen.place("Woohooo...", 9, 2)

# Update the screen
g.screen.update()

Now, immediately a certain amount of questions arise:

  • What if I want to put multiple widgets on the screen and have them organized (but I don't want to spend my time calculating height, width, and positions for each and every one of them)?

  • What if I want to build complex widgets with widgets inside widgets?

  • What if I want something else than a bluish square?

All of these are very valid questions, I know: that's the questions I asked myself ;)

We'll talk about that in the next installment!

Conclusion (for now)

This initial installment serves as an introduction, offering context and a glimpse into the inception of my UI module venture. The next article should offer a more technical dive into the details of this development.

Clearly, I'm documenting a year-long development process, so I'll summarize certain aspects and present it more as a report.

In the meantime, I'm noting things in my coding diary for future articles. I hope that this will be an entertaining window into the hobby of a coder for fun and why not: maybe have little interactions with like-minded people along the way!

Have fun and keep coding!

Arnaud.

PS: I used AI to reformulate some parts (sorry but I am not a native English speaker). Tell me if these parts are obvious ;-)