curses is a python wrapper around the
[n|p]curses library, a GUI for linux.
I love that library.
When I started college, the programming languages I was taught were C/C++, Cobol and Java. Java had the reputation to be the language to use if you wanted to create a graphical interface (think about swing).
Disliking Java for being too verbose, I always use C or C++ for my programming needs, even when I needed a graphical interface.
I wrote a checker, a poker and a board game with no issues.
NB: the python
curses is available for unix-based system (linux and mac) as
a standard lib but not on win.
It seems it could be downloaded but I have no experience with it.
Why I'm writing about it
Always looking to do something fun with gamebooks, I thought I could make a
gamebook reader for the terminal. Therefore I needed a
TUI, a Terminal User
curses seemed the obvious choice for me but it is quite too basic
to do what I wanted. I decided to create my wrapper around it, named
I have several objectives with it:
- learn more about object programming in Python because I've never done much OO with that language;
- learn more about TUI for python, as I always used
jsto do all of my GUI needs;
- create a nice TUI module that I can share, even if there are several existing already.
The curses objects
In curses you get to play with mainly 4 types of object: windows, pads, panels and textpads.
Windows are the basic abstraction in curses. You can draw in it, write text but you can't never go outside the area you defined for that window. Also, it might be smaller than your terminal.
A pad is a window which can be extended further than what your terminal size allows and only the visible part will be displayed.
An object I particularly like. Panels are windows with an extra option:
This makes the creation of modals, dropdown menus, etc. so much easier to deal
I've not yet used them. They are windows specialized in text edition.
On top of what exist already for
curses, I created or plan to create several
widgets to use.
The names are temporary. I'm sure I will unify them.
- CurseApp: the main app object gathering and controlling all other widget objects,
- StatusBar: a status bar, displaying information (with rolling text option),
- ScrollWindow: a window with a scroll bar on the right side,
- TextWindow: a window with text displayed in it. It uses
textwrapto avoid crashing the window if the line of text goes over the window row size limit,
- Modal: a modal window,
- ModalQuestion: a modal window with yes / no buttons,
- MenuBar: a dropdown menu bar.
To be implemented
- TabPane: a pane with different tabs you can navigate,
- Music: an odd one, but I would like to be able to play music in the background,
- ImageWindow: a window displaying images. I have a working demo but I need to create the widget which will take an image and transform it for the window.
- VideoWindow: extending on the ImageWindow and display a video since a video is just a series of images ;);
- HistogramWindow: a window to display a histogram;
- CurveWindow: a window to display a curve.
The base widget is quite simple: one constructor method, one rendering method and an update method.
__init__: the app object stores objects in a dictionary where the keys are the given name we give to the widget object;
render: called by the app object whenever we need to redraw the screen, especially after widget updates. All widgets are rendered during a loop. This might induce flickering.
update(key): speaking of the devil, it updates the widget. Usually only one widget is updated at the time during a loop. The method asks for a
keywhich is a keyboard key and depending on its values, it will perform a specific action. It also returns an integer, used to tell the main app to break or not the update cycle for that widget.
class DefaultWin: """Default curse window object. Contains the minimum necessary methods. ... Attributes ---------- None. Methods ------- """ def __init__(self): """Class constructor.""" pass def render(self): """This methods updates the rendering of the window.""" pass def update(self, key): """This methods handles keyboard and/or mouse inputs and returns an int based on the input. Returns ------- An integer related to some I/O input. """ return 0
Refreshing the windows
I talked about refreshing windows. If you do it too frequently, you might induce flickering. To reduce it, you can:
noutrefreshfor each widget, stacking the changes to be done but not yet displayed, then call
doupdateat the end of a cycle; and/or
- mark widgets as to be updated or not, refreshing on the ones needing to.
When you want to work on a specific window, you have to assign a key to that window and then whenever this key is pressed, consider any actions performed after applied to this window.
I had several issues when I made the wrapper and below are my solutions to them.
Curses needs to exit the right way
If the application / script crashes mid-way, curses options will be still impact the terminal setup. There are several solutions to prevent this:
- the ncurses library way with
withcontext manager, or
If your terminal gets all scrambled, you can type
stty sane to go back to a
saner version. It might not reset all your preferences but it's good enough.
with context manager
The two keywords are
with statement context managers. Basically when you use
__enter__ is executed when you enter the context stated by
__exit__ when you exit said context.
It's useful when you want to do specific processing and if you write your own
class and / or module around
It uses a
try statement with
It works well and it is useful when you want to write a quick and short script.
You can't just print lines
Whenever you write text in a window, you use
addstr. Not an issue by itself
but you need to remember the max length enabled by the window. If your text is
longer than that, it won't be displayed. You will need to hyphenate your text
if needed. Luckily Python does it by default via
The menu might not be shown on the top of all the windows and this issue would show randomly. In order to avoid this, we need to render the dropdown menu last, after all other.
- Curses: https://docs.python.org/3.7/howto/curses.html