code / btui

Lines2.2K C1.2K Python549 Markdown219 make156 Lua49
(265 lines)

BTUI - Bruce's Text User Interface Library

BTUI is a minimal, embeddable, single-header-file Text User Interface (TUI) library and alternative to bloatware like ncurses. BTUI aims to be less than 1% the size of ncurses, while also providing a better API for the programmer who is not afraid to roll up their sleeves and manage their own state. If you want a drop-in TUI library that lets you switch into TUI mode, write some strings to the screen with 24-bit color, and get user keypresses in real-time without line buffering, then this is the library for you! BTUI also has language bindings for both Lua and Python.

BTUI screenshot

Note: Currently, BTUI is around 0.5% the size of ncurses, but some margin for growth is reserved for supporting additional features and constant declarations.

Cleanup by Default

Once BTUI is enabled, you can manually disable TUI Mode (e.g. if you want to temporarily drop back to the regular shell). However, BTUI will always automatically put the terminal back in a normal state when your program exits, even in the event of a segfault or other blockable signal. BTUI can't do anything about unblockable signals like SIGKILL though, so don't use killall -9 myprog unless you want a gunked up terminal (just use kill myprog instead). If your terminal does get gunked up, you can always run the reset shell command to reset it.

Easy 24-bit Colors

BTUI supports 24-bit colors. It's dead easy, just btui_set_fg(bt, r, g, b) (foreground color) or btui_set_bg(bt, r, g, b) (background color). Try make rainbow to see a demo of 24-bit colors in action. This has been supported by most terminals for aeons, but most TUI applications still default to using the same 8 colors. It doesn't have to be that way! (Although you can still use those colors with btui_set_attributes(bt, BTUI_BG_RED | BTUI_FG_BLACK) and so forth.)

Rainbow!

User Input

BTUI lets you get keyboard input for all keypress events handled by your terminal, including mouse clicking and dragging. Warning: BTUI also hooks in to Ctrl-c, Ctrl-d, Ctrl-z, and other keyboard events that normally have very specific behavior in terminal programs. If you want that specific behavior, then you'll have to implement it manually. The upside is that you're able to handle it gracefully and do whatever cleanup you want. If you want to provide Ctrl-z suspend functionality, you can use btui_suspend(bt).

Warning: xterm control sequences do not support all key combinations (e.g. Ctrl-9) and some key combinations map to the same control sequences (e.g. Ctrl-m and Enter, or Ctrl-8 and Backspace). Escape in particular is a little bit odd, since it's only recognized as an escape keypress if some amount of time has passed after pressing it with no new bytes being sent. Most of this oddity is completely unavoidable, that's just the how terminals work. Escape key handling can be improved slightly by polling with zero timeout.

Tips and Tricks

  • For best performance, try to structure your program to take advantage of terminal scrolling. It's much faster than redrawing every line on the screen.

  • Don't assume a string's length (in bytes) is the same as its width on the screen. Unicode characters and terminal escape sequences violate this assumption.

  • Don't clear the screen unless you need to. It causes a lot of unnecessary redrawing.

  • A good pattern to follow is: draw things from left to right, clearing to the right after each item. Then, once everything has been drawn, clear to the bottom of the screen. If you follow this approach, you won't have to worry too much about the screen width of the things you're writing. Also, the terminal won't flicker, since you're never blanking the whole screen, only part of a line at a time.

  • Text wrapping must be done manually, since many terminals do not support wrapping around to any point other than the first column, which is useless for any TUI uses other than text on the far left of the screen.

Language Bindings

BTUI comes with bindings for C, Python, and Lua.

C API

BTUI has the following C function definitions, as well as definitions for some constants, including terminal escape values and keycodes.

int     btui_clear(btui_t *bt, int mode);
void    btui_disable(btui_t *bt);
void    btui_draw_linebox(btui_t *bt, int x, int y, int w, int h);
void    btui_draw_shadow(btui_t *bt, int x, int y, int w, int h);
btui_t* btui_create(btui_mode_t mode);
#define btui_enable() btui_create(BTUI_MODE_TUI)
void    btui_fill_box(btui_t *bt, int x, int y, int w, int h);
int     btui_flush(btui_t *bt);
int     btui_getkey(btui_t *bt, int timeout, int *mouse_x, int *mouse_y);
int     btui_hide_cursor(btui_t *bt);
char    *btui_keyname(int key, char *buf);
int     btui_keynamed(const char *name);
int     btui_move_cursor(btui_t *bt, int x, int y);
#define btui_printf(bt, ...) fprintf((bt)->out, __VA_ARGS__)
int     btui_puts(btui_t *bt, const char *s);
int     btui_scroll(btui_t *bt, int firstline, int lastline, int scroll_amount);
int     btui_set_attributes(btui_t *bt, attr_t attrs);
int     btui_set_bg(btui_t *bt, unsigned char r, unsigned char g, unsigned char b);
int     btui_set_bg_hex(btui_t *bt, int hex);
int     btui_set_cursor(btui_t *bt, cursor_t cur);
int     btui_set_fg(btui_t *bt, unsigned char r, unsigned char g, unsigned char b);
int     btui_set_fg_hex(btui_t *bt, int hex);
int     btui_show_cursor(btui_t *bt);
int     btui_suspend(btui_t *bt);

See C/test.c and C/rainbow.c for example usage. You can run make testc to run the C test demo and make rainbow to run the rainbow C demo.

Lua API

The Lua library returns a function that takes one argument: a function that will be called with a BTUI object, which can be used to do TUI actions. Errors will be propagated out of the function, but the terminal will be cleaned up nicely before the error is printed. Here's a simple example program:

require("btui")(function(bt)
    -- Run this code within the TUI, using "bt" as the TUI object
    ...
end)

bt:clear(type="screen") -- Clear the terminal. Options are: "screen", "right", "left", "above", "below", "line"
bt:disable() -- Disables btui
bt:enable() -- Enables btui (if previously disabled)
bt:fillbox(x,y,w,h) -- Fill the given rectangle with space characters
bt:flush() -- Flush the terminal output. Most operations do this anyways.
bt:getkey(timeout=-1) -- Returns a keypress (and optionally, mouse x and y coordinates). The optional timeout argument specifies how long, in tenths of a second, to wait for the next keypress.
bt:height() -- Return the screen height
bt:hidecursor() -- Hide the cursor
bt:linebox(x,y,w,h) -- Draw an outlined box around the given rectangle
bt:move(x, y) -- Move the cursor to the given position. (0,0) is the top left corner.
bt:scroll(firstline, lastline, amount) -- Scroll the given screen region by the given amount.
bt:setattributes(attrs...) -- Set the given attributes
bt:setcursor(type) -- Set the cursor type
bt:shadow(x,y,w,h) -- Draw a shaded shadow to the bottom right of the given rectangle
bt:showcursor() -- Show the cursor
bt:suspend() -- Suspend the current process and drop back into normal terminal mode
bt:unsetattributes(attrs...) -- Unset the given attributes
bt:width() -- Return the scren width
bt:withattributes(attrs..., fn) -- Set the given attributes, call fn, then unset them
-- R,G,B values are in the range [0.0, 1.0]:
bt:withbg(r,g,b, fn) -- Set the background color to (r,g,b), call fn, then reset the background color to default
bt:withdisabled(fn) -- Calls "fn" with btui disabled, then re-enables btui
bt:withfg(r,g,b, fn) -- Set the foreground color to (r,g,b), call fn, then reset the foreground color to default
bt:write() -- Write text to the terminal

See Lua/test.lua for example usage. Run make lua to build the Lua library, and make testlua to run the Lua BTUI demo.

Python API

The Python library has one main function: btui.open(). This is a context manager, which can be used to safely wrap TUI applications.

import btui

with btui.open() as bt:
    # Your code here...

The returned object has the following methods:

class BTUI:
    @contextmanager
    def attributes(self, *attrs):
    @contextmanager
    def bg(self, r, g, b): # R,G,B values are [0.0, 1.0]
    @contextmanager
    def buffered(self):
    def clear(self, mode='screen'):
    def disable(self):
    @contextmanager
    def disabled(self):
    def draw_shadow(self, x, y, w, h):
    def enable(self):
    @contextmanager
    def fg(self, r, g, b): # R,G,B values are [0.0, 1.0]
    def fill_box(self, x, y, w, h):
    def flush(self):
    def getkey(self, timeout=None):
    @property
    def height(self):
    def hide_cursor(self):
    def move(self, x, y):
    def outline_box(self, x, y, w, h):
    def scroll(self, firstline, lastline=None, amount=None):
    def set_attributes(self, *attrs):
    def set_bg(self, r, g, b): # R,G,B values are [0.0, 1.0]
    def set_cursor(self, cursor_type="default"):
    def set_fg(self, r, g, b): # R,G,B values are [0.0, 1.0]
    def show_cursor(self):
    def suspend(self):
    def unset_attributes(self, *attrs):
    @property
    def width(self):
    def write(self, *args, sep=''):
    def write_bytes(self, b):

See Python/test.py for example code, which can be run with make testpython.

BTUI bed screenshot

BTUI also comes with a demo text editor program, bed (BTUI Editor), at Python/bed.py (around 200 lines of code). It's fairly performant and it showcases a nontrivial example program using BTUI. Usage: ./bed.py <file> (must be run locally).

The Python API also allows you to run BTUI in "debug" mode btui.open_btui(debug=True), which simply adds a timer delay (default: 50ms) after every BTUI write call, which is useful for getting an intuitive feel for where performance bottlenecks are occurring. You can run ./bed.py -d <file> to see this in action.

FAQ

Why not use ncurses? It's already installed almost everywhere!

Ncurses is pretty ubiquitous among amateur TUI developers, however it's a really messy paradigm to work with, and it comes with hundreds of thousands of lines of code bloat. There's also tons of compatibility problems across different versions of ncurses. Using ncurses is a headache, and writing a wrapper around ncurses just adds more bloat on top of a mountain of bloat. So, if you want to write a really nice TUI library for your language of choice, BTUI is a good option for a foundation to build off of. BTUI handles all the low-level C stuff like setting terminal attributes and interrupt handlers, but after that, it mostly just gets out of the way and lets you write the bytes you want, with just a few helper functions that make writing the right bytes easy.

Why not use termbox?

Termbox is an okay library, but its API is built around modifying terminal cells one-at-a-time using function calls. This approach has two major flaws: it's very bad for performance and it makes the API very awkward to use if you want to write strings to the screen. Termbox uses its own custom internal write-buffering to reduce write calls and help performance, but in my experience, there is still noticeable lag when doing large amounts of writes compared to just using plain ol' C fprintf() buffering.

Also, termbox is intended to be used as a system-installed library, but BTUI is much simpler to use: just drop the .h file into your project and #import "btui.h"!

License

BTUI is released under the MIT license. See LICENSE for details.

1 # BTUI - Bruce's Text User Interface Library
3 BTUI is a minimal, embeddable, single-header-file Text User Interface (TUI)
4 library and alternative to bloatware like ncurses. BTUI aims to be less than 1%
5 the size of ncurses, while also providing a better API for the programmer who
6 is not afraid to roll up their sleeves and manage their own state. If you want
7 a drop-in TUI library that lets you switch into TUI mode, write some strings to
8 the screen with 24-bit color, and get user keypresses in real-time without line
9 buffering, then this is the library for you! BTUI also has language bindings for
10 both [Lua](#markdown-header-lua-api) and [Python](#markdown-header-python-api).
12 ![BTUI screenshot](btui.png)
14 Note: Currently, BTUI is around 0.5% the size of ncurses, but some margin for
15 growth is reserved for supporting additional features and constant
16 declarations.
18 ## Cleanup by Default
20 Once BTUI is enabled, you can manually disable TUI Mode (e.g. if you want to
21 temporarily drop back to the regular shell). However, BTUI will always
22 automatically put the terminal back in a normal state when your program exits,
23 even in the event of a segfault or other blockable signal. BTUI can't do
24 anything about unblockable signals like SIGKILL though, so don't use `killall
25 -9 myprog` unless you want a gunked up terminal (just use `kill myprog`
26 instead). If your terminal *does* get gunked up, you can always run the `reset`
27 shell command to reset it.
29 ## Easy 24-bit Colors
31 BTUI supports 24-bit colors. It's dead easy, just `btui_set_fg(bt, r, g, b)`
32 (foreground color) or `btui_set_bg(bt, r, g, b)` (background color). Try `make
33 rainbow` to see a demo of 24-bit colors in action. This has been supported by
34 most terminals for aeons, but most TUI applications still default to using the
35 same 8 colors. It doesn't have to be that way! (Although you can still use
36 those colors with `btui_set_attributes(bt, BTUI_BG_RED | BTUI_FG_BLACK)` and so
37 forth.)
39 ![Rainbow!](rainbow.png)
41 ## User Input
43 BTUI lets you get keyboard input for all keypress events handled by your
44 terminal, including mouse clicking and dragging. Warning: BTUI also hooks in to
45 `Ctrl-c`, `Ctrl-d`, `Ctrl-z`, and other keyboard events that normally have very
46 specific behavior in terminal programs. If you want that specific behavior,
47 then you'll have to implement it manually. The upside is that you're able to
48 handle it gracefully and do whatever cleanup you want. If you want to provide
49 `Ctrl-z` suspend functionality, you can use `btui_suspend(bt)`.
51 Warning: xterm control sequences do not support all key combinations (e.g.
52 `Ctrl-9`) and some key combinations map to the same control sequences (e.g.
53 `Ctrl-m` and `Enter`, or `Ctrl-8` and `Backspace`). `Escape` in particular is a
54 little bit odd, since it's only recognized as an escape keypress if some amount
55 of time has passed after pressing it with no new bytes being sent. Most of this
56 oddity is completely unavoidable, that's just the how terminals work. Escape
57 key handling can be improved slightly by polling with zero timeout.
59 ## Tips and Tricks
61 * For best performance, try to structure your program to take advantage of
62 terminal scrolling. It's much faster than redrawing every line on the screen.
64 * Don't assume a string's length (in bytes) is the same as its width on the
65 screen. Unicode characters and terminal escape sequences violate this
66 assumption.
68 * Don't clear the screen unless you need to. It causes a lot of unnecessary
69 redrawing.
71 * A good pattern to follow is: draw things from left to right, clearing to the
72 right after each item. Then, once everything has been drawn, clear to the
73 bottom of the screen. If you follow this approach, you won't have to worry
74 too much about the screen width of the things you're writing. Also, the
75 terminal won't flicker, since you're never blanking the whole screen, only
76 part of a line at a time.
78 * Text wrapping must be done manually, since many terminals do not support
79 wrapping around to any point other than the first column, which is useless
80 for any TUI uses other than text on the far left of the screen.
82 ## Language Bindings
84 BTUI comes with bindings for C, Python, and Lua.
86 ### C API
88 BTUI has the following C function definitions, as well as definitions for some
89 constants, including terminal escape values and keycodes.
91 ```c
92 int btui_clear(btui_t *bt, int mode);
93 void btui_disable(btui_t *bt);
94 void btui_draw_linebox(btui_t *bt, int x, int y, int w, int h);
95 void btui_draw_shadow(btui_t *bt, int x, int y, int w, int h);
96 btui_t* btui_create(btui_mode_t mode);
97 #define btui_enable() btui_create(BTUI_MODE_TUI)
98 void btui_fill_box(btui_t *bt, int x, int y, int w, int h);
99 int btui_flush(btui_t *bt);
100 int btui_getkey(btui_t *bt, int timeout, int *mouse_x, int *mouse_y);
101 int btui_hide_cursor(btui_t *bt);
102 char *btui_keyname(int key, char *buf);
103 int btui_keynamed(const char *name);
104 int btui_move_cursor(btui_t *bt, int x, int y);
105 #define btui_printf(bt, ...) fprintf((bt)->out, __VA_ARGS__)
106 int btui_puts(btui_t *bt, const char *s);
107 int btui_scroll(btui_t *bt, int firstline, int lastline, int scroll_amount);
108 int btui_set_attributes(btui_t *bt, attr_t attrs);
109 int btui_set_bg(btui_t *bt, unsigned char r, unsigned char g, unsigned char b);
110 int btui_set_bg_hex(btui_t *bt, int hex);
111 int btui_set_cursor(btui_t *bt, cursor_t cur);
112 int btui_set_fg(btui_t *bt, unsigned char r, unsigned char g, unsigned char b);
113 int btui_set_fg_hex(btui_t *bt, int hex);
114 int btui_show_cursor(btui_t *bt);
115 int btui_suspend(btui_t *bt);
116 ```
118 See [C/test.c](C/test.c) and [C/rainbow.c](C/rainbow.c) for example usage. You
119 can run `make testc` to run the C test demo and `make rainbow` to run the
120 rainbow C demo.
122 ### Lua API
124 The Lua library returns a function that takes one argument: a function that will
125 be called with a `BTUI` object, which can be used to do TUI actions. Errors will
126 be propagated out of the function, but the terminal will be cleaned up nicely
127 before the error is printed. Here's a simple example program:
129 ```lua
130 require("btui")(function(bt)
131 -- Run this code within the TUI, using "bt" as the TUI object
132 ...
133 end)
135 bt:clear(type="screen") -- Clear the terminal. Options are: "screen", "right", "left", "above", "below", "line"
136 bt:disable() -- Disables btui
137 bt:enable() -- Enables btui (if previously disabled)
138 bt:fillbox(x,y,w,h) -- Fill the given rectangle with space characters
139 bt:flush() -- Flush the terminal output. Most operations do this anyways.
140 bt:getkey(timeout=-1) -- Returns a keypress (and optionally, mouse x and y coordinates). The optional timeout argument specifies how long, in tenths of a second, to wait for the next keypress.
141 bt:height() -- Return the screen height
142 bt:hidecursor() -- Hide the cursor
143 bt:linebox(x,y,w,h) -- Draw an outlined box around the given rectangle
144 bt:move(x, y) -- Move the cursor to the given position. (0,0) is the top left corner.
145 bt:scroll(firstline, lastline, amount) -- Scroll the given screen region by the given amount.
146 bt:setattributes(attrs...) -- Set the given attributes
147 bt:setcursor(type) -- Set the cursor type
148 bt:shadow(x,y,w,h) -- Draw a shaded shadow to the bottom right of the given rectangle
149 bt:showcursor() -- Show the cursor
150 bt:suspend() -- Suspend the current process and drop back into normal terminal mode
151 bt:unsetattributes(attrs...) -- Unset the given attributes
152 bt:width() -- Return the scren width
153 bt:withattributes(attrs..., fn) -- Set the given attributes, call fn, then unset them
154 -- R,G,B values are in the range [0.0, 1.0]:
155 bt:withbg(r,g,b, fn) -- Set the background color to (r,g,b), call fn, then reset the background color to default
156 bt:withdisabled(fn) -- Calls "fn" with btui disabled, then re-enables btui
157 bt:withfg(r,g,b, fn) -- Set the foreground color to (r,g,b), call fn, then reset the foreground color to default
158 bt:write() -- Write text to the terminal
159 ```
161 See [Lua/test.lua](Lua/test.lua) for example usage. Run `make lua` to build the
162 Lua library, and `make testlua` to run the Lua BTUI demo.
164 ### Python API
166 The Python library has one main function: `btui.open()`. This is a context
167 manager, which can be used to safely wrap TUI applications.
169 ```python
170 import btui
172 with btui.open() as bt:
173 # Your code here...
174 ```
176 The returned object has the following methods:
178 ```python
179 class BTUI:
180 @contextmanager
181 def attributes(self, *attrs):
182 @contextmanager
183 def bg(self, r, g, b): # R,G,B values are [0.0, 1.0]
184 @contextmanager
185 def buffered(self):
186 def clear(self, mode='screen'):
187 def disable(self):
188 @contextmanager
189 def disabled(self):
190 def draw_shadow(self, x, y, w, h):
191 def enable(self):
192 @contextmanager
193 def fg(self, r, g, b): # R,G,B values are [0.0, 1.0]
194 def fill_box(self, x, y, w, h):
195 def flush(self):
196 def getkey(self, timeout=None):
197 @property
198 def height(self):
199 def hide_cursor(self):
200 def move(self, x, y):
201 def outline_box(self, x, y, w, h):
202 def scroll(self, firstline, lastline=None, amount=None):
203 def set_attributes(self, *attrs):
204 def set_bg(self, r, g, b): # R,G,B values are [0.0, 1.0]
205 def set_cursor(self, cursor_type="default"):
206 def set_fg(self, r, g, b): # R,G,B values are [0.0, 1.0]
207 def show_cursor(self):
208 def suspend(self):
209 def unset_attributes(self, *attrs):
210 @property
211 def width(self):
212 def write(self, *args, sep=''):
213 def write_bytes(self, b):
214 ```
216 See [Python/test.py](Python/test.py) for example code, which can be run with
217 `make testpython`.
219 ![BTUI bed screenshot](Python/bed.png)
221 BTUI also comes with a demo text editor program, bed (BTUI Editor), at
222 [Python/bed.py](Python/bed.py) (around 200 lines of code). It's fairly
223 performant and it showcases a nontrivial example program using BTUI. Usage:
224 `./bed.py <file>` (must be run locally).
226 The Python API also allows you to run BTUI in "debug" mode
227 `btui.open_btui(debug=True)`, which simply adds a timer delay (default: 50ms)
228 after every BTUI write call, which is useful for getting an intuitive feel for
229 where performance bottlenecks are occurring. You can run `./bed.py -d <file>`
230 to see this in action.
232 ## FAQ
234 ### Why not use ncurses? It's already installed almost everywhere!
236 [Ncurses](https://invisible-island.net/ncurses/) is pretty ubiquitous among
237 amateur TUI developers, however it's a really messy paradigm to work with, and
238 it comes with hundreds of thousands of lines of code bloat. There's also tons
239 of compatibility problems across different versions of ncurses. Using ncurses
240 is a headache, and writing a wrapper around ncurses just adds more bloat on top
241 of a mountain of bloat. So, if you want to write a really nice TUI library for
242 your language of choice, BTUI is a good option for a foundation to build off
243 of. BTUI handles all the low-level C stuff like setting terminal attributes and
244 interrupt handlers, but after that, it mostly just gets out of the way and lets
245 you write the bytes you want, with just a few helper functions that make
246 writing the right bytes easy.
248 ### Why not use termbox?
250 [Termbox](https://github.com/nsf/termbox) is an okay library, but its API is
251 built around modifying terminal cells one-at-a-time using function calls. This
252 approach has two major flaws: it's very bad for performance and it makes the
253 API very awkward to use if you want to write strings to the screen. Termbox
254 uses its own custom internal write-buffering to reduce write calls and help
255 performance, but in my experience, there is still noticeable lag when doing
256 large amounts of writes compared to just using plain ol' C `fprintf()`
257 buffering.
259 Also, termbox is intended to be used as a system-installed library, but BTUI is
260 much simpler to use: just drop the .h file into your project and `#import
261 "btui.h"`!
263 ## License
265 BTUI is released under the MIT license. See [LICENSE](LICENSE) for details.