This commit is contained in:
Chu'vok
2024-09-13 13:45:43 +02:00
commit 4dfcf8fe85
9 changed files with 2039 additions and 0 deletions

50
example/ask.c Normal file
View File

@@ -0,0 +1,50 @@
// Display a yes/no dialog with a message. Returns with 0 when yes was clicked.
// syntax: ./ask "message"
#include "../tim.h"
// colors
#define CTXT 0xf // text black, white
#define CFR 0x8 // frame black, gray
#define CYES 0xa000f // yes green, black, white
#define CNO 0x9000f // no red, black white
int main(int argc, char** argv) {
if (argc < 2 || strcmp(argv[1], "-h") == 0) {
printf("syntax: %s message\n", argv[0]);
exit(1);
}
// get text properties
struct text msg = scan_str(argv[1]);
while (tim_run(0)) {
// calculate size of message box
int w = MAX(msg.width + 4, 24);
int h = MAX(msg.lines + 6, 7);
scope (A, A, w, h) {
// draw frame around entire scope
frame(0, 0, ~0, ~0, CFR);
// draw message
label(argv[1], A, 1, msg.width, msg.lines, CTXT);
// draw 'yes' button, return 0 when clicked
if (button("Yes", 2, ~1, A, A, CYES)) {
exit(0);
}
// draw 'no' button, return 1 when clicked
if (button("No ", ~2, ~1, A, A, CNO)) {
exit(1);
}
// return with 1 when q or esc is pressed
if (is_key_press('q') || is_key_press(ESCAPE_KEY)) {
exit(1);
}
}
}
}

14
example/hello.c Normal file
View File

@@ -0,0 +1,14 @@
#include "../tim.h" // one header, no lib
int main(void) { //
while (tim_run(0)) { // event loop
scope (A, A, 24, 8) { // centered scope
uint64_t c = 0x0a060f; // three colors
frame(0, 0, ~0, ~0, c); // draw frame for scope
label("Greetings!", A, 2, A, A, c); // label in top center
if (button("OK", A, ~1, 8, A, c)) // button in bottom center
return 0; // exit on button click
if (is_key_press('q')) //
return 0; // exit on 'q' press
} //
} //
} // automatic cleanup

130
example/snek.c Normal file
View File

@@ -0,0 +1,130 @@
// Simple game of snake to show how to do animation and draw cells.
#include "../tim.h"
#define FG 0x10
#define BG 0xdd
#define BTN (FG << 16 | BG << 8 | FG)
#define NEW 0
#define RUN 1
#define PAUSE 2
#define OVER 3
typedef union {
struct {
int32_t x;
int32_t y;
};
int64_t xy;
} point;
static struct {
int state; // game state (NEW RUN PAUSE OVER)
int64_t tick; // updates every 10 ms
int len; // snake length
point body[200]; // snake body
point food; // food position
point look; // active direction
} snek;
static void start(void) {
memset(snek.body, -1, sizeof(snek.body));
snek.len = 2;
snek.body[0] = (point){{1, tim.h / 2}};
snek.food = (point){{tim.w / 8, tim.h / 2}};
snek.look = (point){{1, 0}};
}
static void game(void) {
// update game state about every 10 ms
int64_t tick = time_us() / 100000;
if (snek.tick != tick) {
snek.tick = tick;
// move one unit
memmove(snek.body + 1, snek.body, sizeof(snek.body) - sizeof(point));
snek.body[0].x = snek.body[1].x + snek.look.x;
snek.body[0].y = snek.body[1].y + snek.look.y;
// self crash
bool crash = false;
for (int i = 1; i < snek.len; i++) {
crash |= snek.body[0].xy == snek.body[i].xy;
}
// border crash
crash |= snek.body[0].x < 0 || snek.body[0].x >= tim.w / 2 ||
snek.body[0].y < 0 || snek.body[0].y >= tim.h;
snek.state = crash ? OVER : snek.state;
// food
if (snek.food.xy == snek.body[0].xy) {
snek.len = MIN(snek.len + 2, ARRAY_SIZE(snek.body));
snek.food.x = rand() % (tim.w / 2 - 2) + 1;
snek.food.y = rand() % (tim.h - 2) + 1;
}
}
// draw
if (tim.event.type == DRAW_EVENT) {
// food
draw_chr(cell(" ", 0, 0xc5), snek.food.x * 2 + 0, snek.food.y);
draw_chr(cell(" ", 0, 0xc5), snek.food.x * 2 + 1, snek.food.y);
// snek
struct cell s = cell(" ", 0, 0);
for (int i = 0; i < snek.len; i++) {
s.bg = (i / 2) % 2 ? 0xe3 : 0xea;
int x = snek.body[i].x * 2;
int y = snek.body[i].y;
draw_chr(s, x + 0, y);
draw_chr(s, x + 1, y);
}
}
// user input
if (tim.event.type == KEY_EVENT) {
int key = tim.event.key;
if ((key == RIGHT_KEY || key == 'd') && snek.look.x != -1) {
snek.look = (point){{1, 0}};
} else if ((key == LEFT_KEY || key == 'a') && snek.look.x != 1) {
snek.look = (point){{-1, 0}};
} else if ((key == DOWN_KEY || key == 's') && snek.look.y != -1) {
snek.look = (point){{0, 1}};
} else if ((key == UP_KEY || key == 'w') && snek.look.y != 1) {
snek.look = (point){{0, -1}};
}
}
}
static void menu(void) {
scope(A, A, 20, 13) {
char* lbl = snek.state == OVER ? "GAME OVER" : "SNEK - THE GAME";
char* btn = snek.state == PAUSE ? "Resume" : "Play";
label(lbl, A, 0, A, A, BTN);
if (button(btn, A, 2, 20, 5, BTN) || is_key_press(ENTER_KEY)) {
if (snek.state != PAUSE) {
start();
}
snek.state = RUN;
}
if (button("Exit", A, 8, 20, 5, BTN) || is_key_press(ESCAPE_KEY)) {
exit(0);
}
}
}
int main(void) {
// draw every 10 ms
while (tim_run(10)) {
struct cell bg = cell(" ", 0, BG);
draw_lot(bg, 0, 0, tim.w, tim.h);
if (snek.state == RUN) {
game();
} else {
menu();
}
if (is_key_press(ESCAPE_KEY)) {
snek.state = PAUSE;
}
}
}

188
readme Normal file
View File

@@ -0,0 +1,188 @@
* about **********************************************************************
tim.h is an immediate mode toolkit for creating simple terminal guis
* quick start ****************************************************************
#include "tim.h" // one header, no lib
int main(void) { //
while (tim_run(0)) { // event loop
scope (A, A, 24, 8) { // centered 28x8 scope
uint64_t c = 0x0a060f; // three colors
frame(0, 0, ~0, ~0, c); // draw frame for scope
label("Greetings!", A, 2, A, A, c); // label in top center
if (button("OK", A, ~1, 8, A, c)) // button in bottom center
return 0; // exit on button click
if (is_key_press('q')) // ctrl-c is masked
return 0; // exit on 'q' press
} //
} //
} // automatic cleanup
* layout *********************************************************************
The terminal's columns (x) and rows (y) are addressed by their coordinates,
the origin is in the top left corner.
Scopes are the primary layout mechanism. They are used to group and place
multiple elements. Scopes can be nested.
The root scope is the full terminal screen. The scope macro is constructed
with a for loop, so statements like break or return inside the scope block
will probably give you a bad time.
Most elements take x/y/w/h arguments to control placement. All positions are
given in relation the element's parent scope.
Automatic (A) width and height are either based on the element's content, or
take the full available space from parent.
arg | value | placement
-----|-------|---------------------------------
x | n | n columns to left
x | ~n | n columns to right
x | A | center horizontally
y | n | n rows to top
y | ~n | n rows to bottom
y | A | center vertically
w | n | n columns wide
w | ~n | fit width to n columns to right
w | A | automatic width
h | n | n rows high
h | ~n | fit height n rows to bottom
h | A | automatic height
* colors *********************************************************************
Most elements have a uint64 color argument which holds up to eight colors.
Typically byte 0 is the text color and byte 1 is the background color.
For example 0x08040f encodes three colors. When used with a button the text
is white (0f), the background is blue (04), and the frame is gray (08).
The terminal should support xterm-256 colors. The TERM variable is ignored.
The lower 16 colors vary across different terminals, so the upper 240 colors
should be used if consistency is important.
xterm-256 color chart
https://upload.wikimedia.org/wikipedia/commons/1/15/Xterm_256color_chart.svg
* events *********************************************************************
tim_run blocks until it observes an event. Mouse and key events are always
immediately followed by a draw event in order to make changes visible.
The event is stored in tim.event.
Some elements need to consume events, for example edit consumes the key
event when focused in order to prevent other key handlers on acting on them.
event | cause
-------------|-----------------------
DRAW_EVENT | input, timeout, resize
KEY_EVENT | key press
MOUSE_EVENT | mouse click
VOID_EVENT | consumed event
* elements *******************************************************************
frame (x, y, w, h, color)
Draw an empty frame and fill area.
x/y/w/h see layout documentation
color background, frame
label (str, x, y, w, h, color)
Draw text label. Automatic width and height are supported. Strings
exceeding width or height are clipped.
str zero terminated string
x/y/w/h see layout documentation
color background, text
button (str, x, y, w, h, color) -> bool
Draw button. Automatic width and height are supported. Strings exceeding
width or height are clipped. Returns true when clicked.
str zero terminated string
x/y/w/h see layout documentation
color frame, background, text
edit (state, x, y, w, color) -> bool
Draw text edit. Output is stored in state.str. Receives input events when
focused by mouse click. Escape or return relinquish focus. Returns true
when return is pressed.
state pointer to persistent edit state struct
x/y/w see layout documentation
color f rame, background, text
check (str, state, x, y, w, color) -> bool
Draw check box. State determines how the box is checked. [x] when state
is non-zero, [ ] when state is zero, [-] when state is -1. A mouse click
toggles the state between one and zero and returns true.
str zero terminated string
state pointer to persistent state variable
x/y/w see layout documentation
color check, background, text
radio (str, state, v, x, y, w, color) -> bool
Draw radio box. If state equals v, the box is selected. Radios are
grouped through a shared state. Within that group, each v must be unique.
A mouse click assigns v to state and returns true.
str zero terminated string
state pointer to persistent state variable
v unique state value
x/y/w see layout documentation
color radio, background, text
* functions ******************************************************************
tim_run (fps) -> bool
Process events and render frame. Blocks until input is received or the
next frame is due. First call also initializes the terminal. When fps is
zero the function blocks until input is received. Key and mouse events
are immediately followed by a draw event, so the actual fps can be
significantly greater than requested. Always returns true. To reset the
terminal after a crash, run "reset".
The Ctrl-C interrupt is masked, so make sure to put an exit condition
like this at the end of the main loop:
if (is_key_press(ESCAPE_KEY))
exit(0);
fps frames per second
is_key_press (key) -> bool
Returns true if key was pressed.
key char literal or one of the KEY constants, see constants
time_us () -> int64
Returns monotonic clock value in microseconds. Not affected by summer
time or leap seconds.
* useful links ***************************************************************
https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
https://learn.microsoft.com/en-us/windows/console/
* bugs ***********************************************************************
- Double buffering is still new, set ENABLE_DBUF to 0 if you see glitches
- Double width characters like 彁 are not fully supported. Terminals do not
handle these consistently and there is no portable way to reliably
determine character width. The renderer can deal with some of the problems
caused by this, but results may vary.
- Decomposed (NFD) UTF-8 is not supported and will cause havoc
- Zero width code points are not supported
- Windows cmd.exe resize events may be delayed

30
test/color.c Normal file
View File

@@ -0,0 +1,30 @@
// Shows xterm-256 color palette.
#include "../tim.h"
static void foo(int x, int y, int c) {
char buf[16] = {0};
sprintf(buf, " %02x ", c);
draw_str(buf, x * 4, y, 4, 0, c);
}
int main(void) {
while (tim_run(0)) {
for (int i = 0; i < 16; i++) {
foo(i % 8, i / 8, i);
}
for (int i = 0; i < 108; i++) {
foo(i % 6, i / 6 + 3, i + 16);
}
for (int i = 0; i < 108; i++) {
foo(i % 6 + 7, i / 6 + 3, i + 124);
}
for (int i = 0; i < 24; i++) {
foo(i % 12, i / 12 + 22, i + 232);
}
if (is_key_press('q') || is_key_press(ESCAPE_KEY)) {
exit(1);
}
}
}

70
test/string.c Normal file
View File

@@ -0,0 +1,70 @@
// Test string functions.
#include "../tim.h"
#define U(s) (uint8_t*)(""s), (sizeof(s) - 1)
#define TEST(t) printf("\33[3%s\33[0m %s\n", (t) ? "2mpass" : "1mfail", #t)
int main(void) {
(void)tim_run;
TEST(ztrlen(NULL) == 0);
TEST(ztrlen("") == 0);
TEST(ztrlen("$") == 1);
TEST(ztrlen("£") == 2);
TEST(ztrlen("") == 3);
TEST(ztrlen("𐍈") == 4);
TEST(bsr8(128) == 0);
TEST(bsr8(64) == 1);
TEST(bsr8(1) == 7);
TEST(bsr8(0) == 8);
TEST(utfchr(NULL) == 0);
TEST(utfchr("") == 0);
TEST(utfchr("$") == 0x24);
TEST(utfchr("£") == 0xA3);
TEST(utfchr("И") == 0x418);
TEST(utfchr("") == 0x939);
TEST(utfchr("") == 0x20AC);
TEST(utfchr("") == 0xD55C);
TEST(utfchr("𐍈") == 0x10348);
TEST(utflen(NULL) == 0);
TEST(utflen("") == 0);
TEST(utflen("$") == 1);
TEST(utflen("$$") == 2);
TEST(utflen("") == 2);
TEST(utflen("$€𐍈") == 3);
TEST(utfpos(NULL, 0) == 0);
TEST(utfpos("äbc", 0) == 0);
TEST(utfpos("äbc", 1) == 2);
TEST(utfpos("äbc", 2) == 3);
TEST(utfpos("äbc", 9) == 4);
TEST(scan_str(NULL).lines == 0);
TEST(scan_str("").lines == 0);
TEST(scan_str("abc").lines == 1);
TEST(scan_str("a\no").lines == 2);
TEST(scan_str("a").width == 1);
TEST(scan_str("äß\no").width == 2);
struct line ln = {.str = "foo\nbar"};
TEST(next_line(&ln) == true);
TEST(!memcmp(ln.line, "foo", ln.size));
TEST(next_line(&ln) == true);
TEST(!memcmp(ln.line, "bar", ln.size));
TEST(next_line(&ln) == false);
TEST(is_wide_perhaps(NULL, 0) == false);
TEST(is_wide_perhaps(U("")) == false);
TEST(is_wide_perhaps(U("$")) == false);
TEST(is_wide_perhaps(U("£")) == false);
TEST(is_wide_perhaps(U("")) == false);
TEST(is_wide_perhaps(U("")) == true);
TEST(is_wide_perhaps(U("")) == false);
TEST(is_wide_perhaps(U("")) == true);
TEST(is_wide_perhaps(U("𐍈")) == true);
}

119
test/test.c Normal file
View File

@@ -0,0 +1,119 @@
#include "../tim.h"
static inline void test_screen(struct event* e) {
static struct event me;
static struct event ke;
static int render_us;
char buf[64];
ke = (e->type == KEY_EVENT) ? *e : ke;
me = (e->type == MOUSE_EVENT) ? *e : me;
// positioning
label("+", 0, 0, A, A, 0xf);
label("+", ~0, 0, A, A, 0xf);
label("+", 0, ~0, A, A, 0xf);
label("+", ~0, ~0, A, A, 0xf);
label("+", A, A, A, A, 0xf);
label("-", 0, A, A, A, 0xf);
label("-", ~0, A, A, A, 0xf);
label("|", A, 0, A, A, 0xf);
label("|", A, ~0, A, A, 0xf);
// some information
sprintf(buf, "screen: %dx%d", tim.w, tim.h);
label(buf, 2, 0, A, A, 0xf);
sprintf(buf, "frame : [%c] %d", ": "[tim.frame & 1], tim.frame);
label(buf, 2, 1, A, A, 0xf);
sprintf(buf, "key : [%d] %s", ke.key, ke.str + (ke.key < 32));
label(buf, 2, 2, A, A, 0xf);
sprintf(buf, "mouse : [%d] %d:%d", me.key, me.x, me.y);
label(buf, 2, 3, A, A, 0xf);
sprintf(buf, "input : %02hhx %02hhx %02hhx %02hhx %02hhx %02hhx %02hhx %02hhx",
e->str[0], e->str[1], e->str[2], e->str[3], e->str[4], e->str[5], e->str[6], e->str[7]);
label(buf, 2, 4, A, A, 0xf);
// lower right
render_us += tim.render_us;
sprintf(buf, "%d µs (Ø %d µs)", tim.render_us, render_us / MAX(tim.frame, 1));
label(buf, ~2, ~2, A, A, 0xf);
sprintf(buf, "%d cells (%.0f %%)", tim.w * tim.h, 100.0 * tim.w * tim.h / MAX_CELLS);
label(buf, ~2, ~1, A, A, 0xf);
sprintf(buf, "%d bytes (%.0f %%)", tim.buf_size, 100.0 * tim.buf_size / MAX_BUF);
label(buf, ~2, ~0, A, A, 0xf);
// multi line label
label("multi\nliñe\nlabël", 24, 1, A, A, 0xf);
// colors
scope (1, 5, 16, 5) {
frame(0, 0, ~0, ~0, 0xf);
label(" Red ", 1, 1, 7, A, 0x0900);
label(" ", 8, 1, 7, A, 0xc400);
label(" Green ", 1, 2, 7, A, 0x0a00);
label(" ", 8, 2, 7, A, 0x2e00);
label(" Blue ", 1, 3, 7, A, 0x0c00);
label(" ", 8, 3, 7, A, 0x1500);
}
// button
static uint64_t bc = 0x100;
if (button("Click Me", 17, 5, 16, 5, bc)) {
bc = (bc + 0x100) & 0xff00;
}
// edit
static struct edit ed1 = {.str = "Edit 1"};
static struct edit ed2 = {};
edit(&ed1, 1, 10, 32, 0xff00ff);
sprintf(buf, "cursor: %d length: %d", ed1.cursor, ed1.length);
label(buf, 2, 13, A, A, 0xf);
edit(&ed2, 1, 14, 32, 0xff00ff);
label(ed2.str, 2, 17, A, A, 0xf);
// checkbox
static int chk[2] = {-1, 1};
check("Check 1", &chk[0], 1, 18, A, 0xa000f);
check("Check 2", &chk[1], 14, 18, A, 0xa000f);
// radiobox
static int rad = 0;
radio("Radio 1", &rad, 1, 1, 19, A, 0xa000f);
radio("Radio 2", &rad, 2, 14, 19, A, 0xa000f);
radio("Radio 3", &rad, 3, 1, 20, A, 0xa000f);
radio("Radio 4", &rad, 4, 14, 20, A, 0xa000f);
// scope nesting
scope(~1, 1, 20, 10) {
scope(0, 0, 10, 5) {
frame(0, 0, ~0, ~0, 0x9);
}
scope(~0, 0, 10, 5) {
frame(0, 0, ~0, ~0, 0xa);
}
scope(~0, ~0, 10, 5) {
frame(0, 0, ~0, ~0, 0xb);
}
scope(0, ~0, 10, 5) {
frame(0, 0, ~0, ~0, 0xc);
}
}
// funny characters
scope (~1, ~3, 11, 5) {
frame(0, 0, ~0, ~0, 0xf);
label("123456789", 1, 1, 9, A, 0x0f05);
label("$£ह€𐍈6789", 1, 2, A, A, 0x0f05);
label("圍棋56789", 1, 3, A, A, 0x0f05);
}
};
int main(void) {
while (tim_run(1.5)) {
test_screen(&tim.event);
if (is_key_press('q') || is_key_press(ESCAPE_KEY)) {
break;
}
}
}

69
test/width.c Normal file
View File

@@ -0,0 +1,69 @@
// Test character width.
#include <assert.h>
#include "../tim.h"
static int cp_to_utf8(int32_t cp, char* s) {
assert(cp > 0 && cp < 0x110000);
if (cp < 0x80) {
s[0] = cp;
return 1;
} else if (cp < 0x800) {
s[0] = (cp >> 6) | 0xc0;
s[1] = (cp & 0x3f) | 0x80;
return 2;
} else if (cp < 0x10000) {
s[0] = (cp >> 12) | 0xe0;
s[1] = ((cp >> 6) & 0x3f) | 0x80;
s[2] = (cp & 0x3f) | 0x80;
return 3;
} else {
s[0] = (cp >> 18) | 0xf0;
s[1] = ((cp >> 12) & 0x3f) | 0x80;
s[2] = ((cp >> 6) & 0x3f) | 0x80;
s[3] = (cp & 0x3f) | 0x80;
return 4;
}
return -1;
}
static int cursor_pos() {
write(STDOUT_FILENO, S("\33[6n"));
char buf[64] = {0};
int n = read(STDIN_FILENO, buf, 64);
if (n < 6 || buf[0] != '\33' || buf[n - 1] != 'R') {
return -1;
}
int r = atoi(buf + 2);
int c = atoi(buf + 4 + (r > 9));
return c;
}
int main(int argc, char** argv) {
assert(argc == 2);
(void)tim_run;
FILE* f = fopen(argv[1], "w");
assert(f);
init_terminal();
for (int i = 32; i < 0x110000; i++) {
write(STDOUT_FILENO, S("\33[0;0H"));
char buf[5] = {0};
int n = cp_to_utf8(i, buf);
write(STDOUT_FILENO, buf, n);
int w = cursor_pos() - 1;
if (w) {
fprintf(f, "u+%06x %d %s\n", i, w, buf);
} else {
fprintf(f, "u+%06x %d\n", i, w);
}
}
reset_terminal();
fclose(f);
}

1369
tim.h Normal file

File diff suppressed because it is too large Load Diff