moved code from header to source files

This commit is contained in:
2026-01-09 05:04:35 +05:00
parent 50940e5190
commit 3f75902aa0
20 changed files with 1304 additions and 1207 deletions

View File

@@ -9,40 +9,40 @@
#define CYES 0xa000f // yes green, black, white #define CYES 0xa000f // yes green, black, white
#define CNO 0x9000f // no red, black white #define CNO 0x9000f // no red, black white
int main(int argc, char** argv) { i32 main(i32 argc, char** argv) {
if (argc < 2 || strcmp(argv[1], "-h") == 0) { if (argc < 2 || strcmp(argv[1], "-h") == 0) {
printf("syntax: %s message\n", argv[0]); printf("syntax: %s message\n", argv[0]);
exit(1); exit(1);
} }
// get text properties // get text properties
TimText_t msg = scan_str(argv[1]); TimText msg = tim_scan_str(argv[1]);
while (tim_run(0)) { while (tim_run(0)) {
// calculate size of message box // calculate size of message box
int w = MAX(msg.width + 4, 24); i32 w = MAX(msg.width + 4, 28);
int h = MAX(msg.lines + 6, 7); i32 h = MAX(msg.lines + 6, 7);
scope (A, A, w, h) { tim_scope(A, A, w, h) {
// draw frame around entire scope // draw frame around entire scope
frame(0, 0, ~0, ~0, CFR); tim_frame(0, 0, ~0, ~0, CFR);
// draw message // draw message
label(argv[1], A, 1, msg.width, msg.lines, CTXT); label(argv[1], A, 1, msg.width, msg.lines, CTXT);
// draw 'yes' button, return 0 when clicked // draw 'yes' button, return 0 when clicked
if (button("Yes", 2, ~1, A, A, CYES)) { if (button("[y] Yes", 2, ~1, A, A, CYES) || tim_is_key_press('y')) {
exit(0); exit(0);
} }
// draw 'no' button, return 1 when clicked // draw 'no' button, return 1 when clicked
if (button("No ", ~2, ~1, A, A, CNO)) { if (button("[n] No", ~2, ~1, A, A, CNO) || tim_is_key_press('n')) {
exit(1); exit(1);
} }
// return with 1 when q or esc is pressed // return with 1 when q or esc is pressed
if (is_key_press('q') || is_key_press(TimKey_Escape)) { if (tim_is_key_press('q') || tim_is_key_press(TimKey_Escape)) {
exit(1); exit(2);
} }
} }
} }

View File

@@ -1,14 +1,14 @@
#include "tim.h" #include "tim.h"
int main(void) { i32 main(void) {
while (tim_run(0)) { // event loop while (tim_run(0)) { // event loop
scope (A, A, 24, 8) { // centered 24x8 scope tim_scope(A, A, 24, 8) { // centered 24x8 scope
uint64_t c = 0x0a060f; // three colors u64 c = 0x0a060f; // three colors
frame(0, 0, ~0, ~0, c); // draw frame for scope tim_frame(0, 0, ~0, ~0, c); // draw frame for scope
label("Greetings!", A, 2, A, A, c); // label in top center label("Greetings!", A, 2, A, A, c); // label in top center
if (button("OK", A, ~1, 8, A, c)) // button in bottom center if (button("OK", A, ~1, 8, A, c)) // button in bottom center
return 0; // exit on button click return 0; // exit on button click
if (is_key_press('q')) // ctrl-c is masked if (tim_is_key_press('q')) // ctrl-c is masked
return 0; // exit on 'q' press return 0; // exit on 'q' press
} }
} //TODO: remove atexit cleanup } //TODO: remove atexit cleanup

View File

@@ -13,16 +13,16 @@
typedef union { typedef union {
struct { struct {
int32_t x; i32 x;
int32_t y; i32 y;
}; };
int64_t xy; i64 xy;
} point; } point;
static struct { static struct {
int state; // game state (NEW RUN PAUSE OVER) i32 state; // game state (NEW RUN PAUSE OVER)
int64_t tick; // updates every 10 ms i64 tick; // updates every 10 ms
int len; // snake length i32 len; // snake length
point body[200]; // snake body point body[200]; // snake body
point food; // food position point food; // food position
point look; // active direction point look; // active direction
@@ -38,7 +38,7 @@ static void start(void) {
static void game(void) { static void game(void) {
// update game state about every 10 ms // update game state about every 10 ms
int64_t tick = time_us() / 100000; i64 tick = tim_time_usec() / 100000;
if (snek.tick != tick) { if (snek.tick != tick) {
snek.tick = tick; snek.tick = tick;
// move one unit // move one unit
@@ -47,7 +47,7 @@ static void game(void) {
snek.body[0].y = snek.body[1].y + snek.look.y; snek.body[0].y = snek.body[1].y + snek.look.y;
// self crash // self crash
bool crash = false; bool crash = false;
for (int i = 1; i < snek.len; i++) { for (i32 i = 1; i < snek.len; i++) {
crash |= snek.body[0].xy == snek.body[i].xy; crash |= snek.body[0].xy == snek.body[i].xy;
} }
// border crash // border crash
@@ -56,7 +56,7 @@ static void game(void) {
snek.state = crash ? OVER : snek.state; snek.state = crash ? OVER : snek.state;
// food // food
if (snek.food.xy == snek.body[0].xy) { if (snek.food.xy == snek.body[0].xy) {
snek.len = MIN(snek.len + 2, (int)ARRAY_SIZE(snek.body)); snek.len = MIN(snek.len + 2, (i32)ARRAY_SIZE(snek.body));
snek.food.x = rand() % (tim.w / 2 - 2) + 1; snek.food.x = rand() % (tim.w / 2 - 2) + 1;
snek.food.y = rand() % (tim.h - 2) + 1; snek.food.y = rand() % (tim.h - 2) + 1;
} }
@@ -65,22 +65,22 @@ static void game(void) {
// draw // draw
if (tim.event.type == TimEvent_Draw) { if (tim.event.type == TimEvent_Draw) {
// food // food
draw_chr(cell(" ", 0, 0xc5), snek.food.x * 2 + 0, snek.food.y); tim_draw_chr(tim_cell(" ", 0, 0xc5), snek.food.x * 2 + 0, snek.food.y);
draw_chr(cell(" ", 0, 0xc5), snek.food.x * 2 + 1, snek.food.y); tim_draw_chr(tim_cell(" ", 0, 0xc5), snek.food.x * 2 + 1, snek.food.y);
// snek // snek
TimCell_t s = cell(" ", 0, 0); TimCell s = tim_cell(" ", 0, 0);
for (int i = 0; i < snek.len; i++) { for (i32 i = 0; i < snek.len; i++) {
s.bg = (i / 2) % 2 ? 0xe3 : 0xea; s.bg = (i / 2) % 2 ? 0xe3 : 0xea;
int x = snek.body[i].x * 2; i32 x = snek.body[i].x * 2;
int y = snek.body[i].y; i32 y = snek.body[i].y;
draw_chr(s, x + 0, y); tim_draw_chr(s, x + 0, y);
draw_chr(s, x + 1, y); tim_draw_chr(s, x + 1, y);
} }
} }
// user input // user input
if (tim.event.type == KEY_EVENT) { if (tim.event.type == KEY_EVENT) {
int key = tim.event.key; i32 key = tim.event.key;
if ((key == TimKey_Right || key == 'd') && snek.look.x != -1) { if ((key == TimKey_Right || key == 'd') && snek.look.x != -1) {
snek.look = (point){{1, 0}}; snek.look = (point){{1, 0}};
} else if ((key == TimKey_Left || key == 'a') && snek.look.x != 1) { } else if ((key == TimKey_Left || key == 'a') && snek.look.x != 1) {
@@ -90,31 +90,34 @@ static void game(void) {
} else if ((key == TimKey_Up || key == 'w') && snek.look.y != 1) { } else if ((key == TimKey_Up || key == 'w') && snek.look.y != 1) {
snek.look = (point){{0, -1}}; snek.look = (point){{0, -1}};
} }
else if (key == 'q' || key == TimKey_Escape){
snek.state = PAUSE;
}
} }
} }
static void menu(void) { static void menu(void) {
scope(A, A, 20, 13) { tim_scope(A, A, 20, 13) {
char* lbl = snek.state == OVER ? "GAME OVER" : "SNEK - THE GAME"; char* lbl = snek.state == OVER ? "GAME OVER" : "SNEK - THE GAME";
char* btn = snek.state == PAUSE ? "Resume" : "Play"; char* btn = snek.state == PAUSE ? "Resume" : "Play";
label(lbl, A, 0, A, A, BTN); label(lbl, A, 0, A, A, BTN);
if (button(btn, A, 2, 20, 5, BTN) || is_key_press(TimKey_Enter)) { if (button(btn, A, 2, 20, 5, BTN) || tim_is_key_press(TimKey_Enter)) {
if (snek.state != PAUSE) { if (snek.state != PAUSE) {
start(); start();
} }
snek.state = RUN; snek.state = RUN;
} }
if (button("Exit", A, 8, 20, 5, BTN) || is_key_press(TimKey_Escape)) { if (button("Exit", A, 8, 20, 5, BTN) || tim_is_key_press('q') || tim_is_key_press(TimKey_Escape)) {
exit(0); exit(0);
} }
} }
} }
int main(void) { i32 main(void) {
// draw every 10 ms // draw every 10 ms
while (tim_run(10)) { while (tim_run(60)) {
TimCell_t bg = cell(" ", 0, BG); TimCell bg = tim_cell(" ", 0, BG);
draw_lot(bg, 0, 0, tim.w, tim.h); tim_draw_lot(bg, 0, 0, tim.w, tim.h);
if (snek.state == RUN) { if (snek.state == RUN) {
game(); game();
@@ -122,8 +125,5 @@ int main(void) {
menu(); menu();
} }
if (is_key_press(TimKey_Escape)) {
snek.state = PAUSE;
}
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,53 @@
CFLAGS+=-Iinclude -Isrc -g CFLAGS+=-Iinclude -Isrc -O2
all: bin/test bin/string bin/color bin/hello bin/ask bin/snek all: bin/test bin/string bin/color bin/hello bin/ask bin/snek
bin/test: test/test.c bin bin/test: test/test.c bin/tim.a
$(CC) $< -Wall $(CFLAGS) -o $@ $(CC) $< -Wall $(CFLAGS) bin/tim.a -o $@
bin/string: test/string.c bin bin/string: test/string.c bin/tim.a
$(CC) $< -Wall $(CFLAGS) -o $@ $(CC) $< -Wall $(CFLAGS) bin/tim.a -o $@
bin/color: test/color.c bin bin/color: test/color.c bin/tim.a
$(CC) $< -Wall $(CFLAGS) -o $@ $(CC) $< -Wall $(CFLAGS) bin/tim.a -o $@
bin/hello: example/hello.c bin bin/hello: example/hello.c bin/tim.a
$(CC) $< -Wall $(CFLAGS) -o $@ $(CC) $< -Wall $(CFLAGS) bin/tim.a -o $@
bin/ask: example/ask.c bin bin/ask: example/ask.c bin/tim.a
$(CC) $< -Wall $(CFLAGS) -o $@ $(CC) $< -Wall $(CFLAGS) bin/tim.a -o $@
bin/snek: example/snek.c bin bin/snek: example/snek.c bin/tim.a
$(CC) $< -Wall $(CFLAGS) -o $@ $(CC) $< -Wall $(CFLAGS) bin/tim.a -o $@
bin/tim.a: bin \
obj/drawing.c.o obj/edit.c.o obj/event.c.o obj/loop.c.o obj/render.c.o \
obj/scope.c.o obj/string.c.o obj/unix.c.o obj/widgets.c.o obj/windows.c.o
ar rcs bin/tim.a \
obj/drawing.c.o obj/edit.c.o obj/event.c.o obj/loop.c.o obj/render.c.o \
obj/scope.c.o obj/string.c.o obj/unix.c.o obj/widgets.c.o obj/windows.c.o
obj/drawing.c.o: src/drawing.c obj
$(CC) $< -Wall $(CFLAGS) -c -o $@
obj/edit.c.o: src/edit.c obj
$(CC) $< -Wall $(CFLAGS) -c -o $@
obj/event.c.o: src/event.c obj
$(CC) $< -Wall $(CFLAGS) -c -o $@
obj/loop.c.o: src/loop.c obj
$(CC) $< -Wall $(CFLAGS) -c -o $@
obj/render.c.o: src/render.c obj
$(CC) $< -Wall $(CFLAGS) -c -o $@
obj/scope.c.o: src/scope.c obj
$(CC) $< -Wall $(CFLAGS) -c -o $@
obj/string.c.o: src/string.c obj
$(CC) $< -Wall $(CFLAGS) -c -o $@
obj/unix.c.o: src/unix.c obj
$(CC) $< -Wall $(CFLAGS) -c -o $@
obj/widgets.c.o: src/widgets.c obj
$(CC) $< -Wall $(CFLAGS) -c -o $@
obj/windows.c.o: src/windows.c obj
$(CC) $< -Wall $(CFLAGS) -c -o $@
bin: bin:
mkdir -p bin mkdir -p bin
obj:
mkdir -p obj
clean: clean:
rm -rf bin rm -rf bin obj

View File

@@ -149,18 +149,18 @@ tim_run (fps) -> bool
The Ctrl-C interrupt is masked, so make sure to put an exit condition The Ctrl-C interrupt is masked, so make sure to put an exit condition
like this at the end of the main loop: like this at the end of the main loop:
if (is_key_press(ESCAPE_KEY)) if (tim_is_key_press(ESCAPE_KEY))
exit(0); exit(0);
fps frames per second fps frames per second
is_key_press (key) -> bool tim_is_key_press (key) -> bool
Returns true if key was pressed. Returns true if key was pressed.
key char literal or one of the KEY constants, see constants key char literal or one of the KEY constants, see constants
time_us () -> int64 tim_time_usec () -> int64
Returns monotonic clock value in microseconds. Not affected by summer Returns monotonic clock value in microseconds. Not affected by summer
time or leap seconds. time or leap seconds.

85
src/drawing.c Executable file
View File

@@ -0,0 +1,85 @@
#include "tim.h"
TimCell tim_cell(cstr s, u8 fg, u8 bg) {
TimCell c = {.fg = fg, .bg = bg, .n = 1, .buf = {s[0]}};
while ((s[c.n] & 192) == 128 && c.n < sizeof(c.buf)) {
c.buf[c.n] = s[c.n];
c.n += 1;
}
return c;
}
void tim_clear_cells(void) {
size_t size = sizeof(tim.cells[0]) * tim.w * tim.h;
memset(tim.cells, 0, size);
}
void tim_draw_chr(TimCell cell, i32 x, i32 y) {
if (x >= 0 && x < tim.w && y >= 0 && y < tim.h) {
tim.cells[x + y * tim.w] = cell;
}
}
void tim_draw_row(TimCell cell, i32 x, i32 y, i32 w) {
if (y >= 0 && y < tim.h && w > 0) {
for (i32 i = MAX(x, 0); i < MIN(x + w, tim.w); i++) {
tim.cells[i + y * tim.w] = cell;
}
}
}
void tim_draw_col(TimCell cell, i32 x, i32 y, i32 h) {
if (x >= 0 && x < tim.w && h > 0) {
for (i32 i = MAX(y, 0); i < MIN(y + h, tim.h); i++) {
tim.cells[x + i * tim.w] = cell;
}
}
}
void tim_draw_lot(TimCell cell, i32 x, i32 y, i32 w, i32 h) {
if (w > 0 && h > 0) {
for (i32 iy = MAX(y, 0); iy < MIN(y + h, tim.h); iy++) {
for (i32 ix = MAX(x, 0); ix < MIN(x + w, tim.w); ix++) {
tim.cells[ix + iy * tim.w] = cell;
}
}
}
}
void tim_draw_str(cstr s, i32 x, i32 y, i32 w, u8 fg, u8 bg) {
if (s && y >= 0 && x < tim.w && y < tim.h ) {
i32 end = MIN(x + w, tim.w);
bool wide = false;
for (i32 i = 0; s[i] && x < end; x++) {
TimCell c = tim_cell(&s[i], fg, bg);
wide = wide || tim_utf8_is_wide_perhaps(c.buf, c.n);
if (x >= 0) {
c.wide = wide;
tim.cells[x + y * tim.w] = c;
}
i += c.n;
}
}
}
void tim_draw_box(i32 x, i32 y, i32 w, i32 h, u8 fg, u8 bg) {
tim_draw_chr(tim_cell("", fg, bg), x , y);
tim_draw_chr(tim_cell("", fg, bg), x + w - 1, y);
tim_draw_chr(tim_cell("", fg, bg), x , y + h - 1);
tim_draw_chr(tim_cell("", fg, bg), x + w - 1, y + h - 1);
tim_draw_row(tim_cell("", fg, bg), x + 1 , y , w - 2);
tim_draw_row(tim_cell("", fg, bg), x + 1 , y + h - 1, w - 2);
tim_draw_col(tim_cell("", fg, bg), x , y + 1 , h - 2);
tim_draw_col(tim_cell("", fg, bg), x + w - 1, y + 1 , h - 2);
tim_draw_lot(tim_cell(" ", fg, bg), x + 1 , y + 1 , w - 2, h - 2);
}
void tim_draw_invert(i32 x, i32 y, i32 w) {
if (y >= 0 && y < tim.h && w > 0) {
for (i32 i = MAX(x, 0); i < MIN(x + w, tim.w); i++) {
TimCell c = tim.cells[i + y * tim.w];
tim.cells[i + y * tim.w].fg = c.bg;
tim.cells[i + y * tim.w].bg = c.fg;
}
}
}

104
src/edit.c Executable file
View File

@@ -0,0 +1,104 @@
#include "tim.h"
void TimEditState_init(TimEditState* e, i32 capacity, cstr initial_content){
e->length = tim_utf8_len(initial_content);
e->cursor = tim_utf8_len(initial_content);
e->capacity = capacity;
e->s = (char*)malloc(capacity + 1);
i32 byte_len = strlen(initial_content);
memcpy(e->s, initial_content, byte_len);
e->s[byte_len] = 0;
}
void edit_insert(TimEditState* e, cstr s) {
i32 dst_size = tim_ztrlen(e->s);
i32 src_size = tim_ztrlen(s);
if (dst_size + src_size < e->capacity) {
i32 len = tim_utf8_len(s); // usually 1, except when smashing keys
i32 cur = tim_utf8_pos(e->s, e->cursor);
memmove(e->s + cur + src_size, e->s + cur, dst_size - cur);
memmove(e->s + cur, s, src_size);
e->s[dst_size + src_size] = 0;
e->length += len;
e->cursor += len;
}
}
void edit_delete(TimEditState* e) {
i32 size = tim_ztrlen(e->s);
i32 cur = tim_utf8_pos(e->s, e->cursor);
i32 len = tim_utf8_pos(e->s + cur, 1);
if (size - cur > 0) {
memmove(e->s + cur, e->s + cur + len, size - cur);
e->length -= 1;
}
}
/// @return key id or 0
static i32 edit_event(TimEditState* e, TimRect r) {
if (tim_is_mouse_click_over(r)) {
// take focus
tim.focus = e;
return 0;
}
if (tim.focus != e || tim.event.type != TimEvent_Key) {
// not focused or no key press
return 0;
}
tim.event.type = TimEvent_Void; // consume event
switch (tim.event.key) {
case TimKey_Escape:
case TimKey_Enter:
tim.focus = 0; // release focus
break;
case TimKey_Delete:
edit_delete(e);
break;
case TimKey_Backspace:
if (e->cursor > 0) {
e->cursor -= 1;
edit_delete(e);
}
break;
case TimKey_Left:
e->cursor -= (e->cursor > 0);
break;
case TimKey_Right:
e->cursor += (e->cursor < e->length);
break;
case TimKey_Home:
e->cursor = 0;
break;
case TimKey_End:
e->cursor = e->length;
break;
default:
if (tim.event.key >= ' ') {
edit_insert(e, tim.event.s);
}
break;
}
return tim.event.key;
}
i32 edit(TimEditState* e, i32 x, i32 y, i32 w, u64 color) {
TimRect r = tim_scope_rect_to_absolute(x, y, w, 3);
if (tim.event.type == TimEvent_Draw) {
tim_draw_box(r.x, r.y, r.w, r.h, color >> 16, color >> 8);
if (tim.focus == e) {
char* s = e->s + tim_utf8_pos(e->s, e->cursor - r.w + 4);
i32 cur = MIN(r.w - 4, e->cursor);
tim_draw_str(s, r.x + 2, r.y + 1, r.w - 3, color, color >> 8);
tim_draw_invert(r.x + cur + 2, r.y + 1, 1);
} else {
tim_draw_str(e->s, r.x + 2, r.y + 1, r.w - 3, color, color >> 8);
}
}
return edit_event(e, r);
}

19
src/event.c Executable file
View File

@@ -0,0 +1,19 @@
#include "tim.h"
bool tim_is_event_key(TimEventType type, TimKey key) {
return tim.event.type == type && tim.event.key == key;
}
bool tim_is_key_press(TimKey key) {
return tim_is_event_key(TimEvent_Key, key);
}
bool tim_is_mouse_over(TimRect r) {
i32 x = tim.event.x;
i32 y = tim.event.y;
return x >= r.x && x < r.x + r.w && y >= r.y && y < r.y + r.h;
}
bool tim_is_mouse_click_over(TimRect r) {
return tim_is_event_key(TimEvent_Mouse, TimKey_MouseButtonLeft) && tim_is_mouse_over(r);
}

51
src/loop.c Executable file
View File

@@ -0,0 +1,51 @@
#include "tim.h"
// TODO: remove global variables
static TimCell _tim_cells[TIM_MAX_CELLS << TIM_ENABLE_DBUF];
static char _tim_buf[TIM_MAX_BUF];
TimState tim = {
.cells = _tim_cells,
.cells_double_buf = _tim_cells,
.buf = _tim_buf,
};
bool tim_run(f32 fps) {
i32 timeout = (fps > 0) ? (i32)(1000 / fps) : 0;
while (true) {
switch (tim.loop_stage) {
case 0:
// runs only once
tim_init_terminal();
atexit(tim_reset_terminal);
// fallthru
case 1:
// process input event
tim.start_us = tim_time_usec();
if (tim.event.type != TimEvent_Draw) {
// reset focus on mouse click
if (tim_is_event_key(TimEvent_Mouse, TimKey_MouseButtonLeft)) {
tim.focus = 0;
}
tim.loop_stage = 2;
return true;
}
// fallthru
case 2:
// process draw event
tim_clear_cells();
tim.event.type = TimEvent_Draw;
tim.loop_stage = 3;
return true;
case 3:
// render screen and wait for next event
tim_render();
tim.render_us = tim_time_usec() - tim.start_us;
tim_read_event(timeout); // blocks
// fallthru
default:
tim.loop_stage = 1;
break;
}
} // while
}

114
src/render.c Executable file
View File

@@ -0,0 +1,114 @@
#include "tim.h"
// write character to output buffer
static void tim_put_chr(char c) {
if (tim.buf_size + 1 < TIM_MAX_BUF) {
tim.buf[tim.buf_size] = c;
tim.buf_size += 1;
}
}
// write string to output buffer
static void tim_put_str(cstr s, i32 size) {
if (size > 0 && tim.buf_size + size < TIM_MAX_BUF) {
memmove(&tim.buf[tim.buf_size], s, size);
tim.buf_size += size;
}
}
// write integer as decimal string to output buffer
static void tim_put_int(i32 i) {
// optimized for small positive values, reduces load by a third
char* buf = &tim.buf[tim.buf_size];
if (tim.buf_size + 11 >= TIM_MAX_BUF) {
// not enough space for 32 bit integer
} else if ((u32)i < 10) {
buf[0] = '0' + i;
tim.buf_size += 1;
} else if ((u32)i < 100) {
buf[0] = '0' + i / 10;
buf[1] = '0' + i % 10;
tim.buf_size += 2;
} else if ((u32)i < 1000) {
buf[0] = '0' + i / 100;
buf[1] = '0' + (i / 10) % 10;
buf[2] = '0' + i % 10;
tim.buf_size += 3;
} else {
tim.buf_size += sprintf(buf, "%d", i);
}
}
void tim_render(void) {
i32 fg = -1;
i32 bg = -1;
bool wide = false;
bool skip = false;
// screen buffers
TimCell* new_cells = tim.cells_double_buf;
TimCell* old_cells = tim.cells_double_buf;
#if TIM_ENABLE_DBUF
new_cells += (tim.frame & 1) ? TIM_MAX_CELLS : 0;
old_cells += (tim.frame & 1) ? 0 : TIM_MAX_CELLS;
#endif
tim.buf_size = 0;
for (i32 i = 0; i < tim.w * tim.h; i++) {
TimCell c = new_cells[i];
#if TIM_ENABLE_DBUF
// do nothing if cells in look-ahead are identical
const i32 la = 8; // look-ahead
if (!tim.resized && !(i % la) && (i + la < TIM_MAX_CELLS) &&
!memcmp(new_cells + i, old_cells + i, la * sizeof(c))) {
skip = true;
i = i + la - 1;
continue;
}
#endif
// Set cursor position after a new line, after a string containing wide
// characters or after skipping identical cells.
bool new_line = i % tim.w == 0;
bool wide_spill = wide && (c.n == 0 || c.buf[0] == ' ');
bool wide_flank = wide && !wide_spill && !c.wide;
if (new_line || wide_flank || skip) {
tim_put_str(S("\33["));
tim_put_int((i / tim.w) + 1);
tim_put_chr(';');
tim_put_int((i % tim.w) + 1);
tim_put_chr('H');
}
wide = c.wide || wide_spill;
skip = false;
// change foreground color
if (c.fg != fg) {
fg = c.fg;
tim_put_str(S("\33[38;5;"));
tim_put_int(fg);
tim_put_chr('m');
}
// change background color
if (c.bg != bg) {
bg = c.bg;
tim_put_str(S("\33[48;5;"));
tim_put_int(bg);
tim_put_chr('m');
}
// write character
if (c.n) {
tim_put_str((char*)c.buf, c.n);
} else {
tim_put_chr(' ');
}
}
// duration depends on connection and terminal rendering speed
tim_write_str(tim.buf, tim.buf_size);
tim.resized = false;
tim.frame += 1; // frame counter
tim.cells = old_cells; // swap buffer
}

50
src/scope.c Executable file
View File

@@ -0,0 +1,50 @@
#include "tim.h"
TimRect tim_scope_rect_to_absolute(i32 x, i32 y, i32 w, i32 h) {
TimRect p = tim.scopes[tim.scope]; // parent scope
x = (x == A && w == A) ? 0 : x; // special cases
y = (y == A && h == A) ? 0 : y; //
w = (w == A) ? ~0 : w; //
h = (h == A) ? ~0 : h; //
//
if (w < 0) { //
w += p.w - x + 1; // get w from parent
} //
if (h < 0) { //
h += p.h - y + 1; // get h from parent
} //
if (x == A) { //
x = p.x + (p.w - w) / 2; // center x on parent
} else { //
if (x < 0) { //
x += p.w - w + 1; // anchor x to right
} //
x += p.x; // anchor x to left
} //
if (y == A) { //
y = p.y + (p.h - h) / 2; // center y on parent
} else { //
if (y < 0) { //
y += p.h - h + 1; // anchor y to bottom
} //
y += p.y; // anchor y to top
}
return (TimRect){x, y, w, h};
}
i32 tim_enter_scope(i32 x, i32 y, i32 w, i32 h) {
if (tim.scope + 1 >= TIM_MAX_SCOPE) {
return 0;
}
TimRect r = tim_scope_rect_to_absolute(x, y, w, h);
tim.scope += 1;
tim.scopes[tim.scope] = r;
return 1;
}
i32 tim_exit_scope(void) {
tim.scope -= (tim.scope > 0);
return 0;
}

108
src/string.c Executable file
View File

@@ -0,0 +1,108 @@
#include "tim.h"
i32 tim_ztrlen(cstr s) {
if(s == NULL)
return 0;
i32 n = strlen(s);
if(n < 0)
n = 0;
return n;
}
i32 tim_bsr8(u8 x) {
#if defined __GNUC__ || defined __clang__
unsigned int b = x;
b <<= sizeof(b) * CHAR_BIT - 8;
b |= 1 << (sizeof(b) * CHAR_BIT - 9);
return __builtin_clz(b);
#elif defined _MSC_VER
unsigned long n = 0;
unsigned long b = x;
b <<= sizeof(b) * CHAR_BIT - 8;
b |= 1 << (sizeof(b) * CHAR_BIT - 9);
_BitScanReverse(&n, b);
return n;
#else
i32 n = 0;
for (; n < 8 && !(x & 128); n++, x <<= 1) {}
return n;
#endif
}
i32 tim_utf8_to_i32(cstr s) {
s = s ? s : "";
// use bit magic to mask out leading utf8 1s
u32 c = s[0] & ((1 << (8 - tim_bsr8(~s[0]))) - 1);
for (i32 i = 1; s[0] && s[i] && i < 4; i++) {
c = (c << 6) | (s[i] & 63);
}
return (i32)c;
}
i32 tim_utf8_len(cstr s) {
i32 n = 0;
for (i32 i = 0; s && s[i]; i++) {
n += (s[i] & 192) != 128;
}
return n;
}
i32 tim_utf8_pos(cstr s, i32 pos) {
i32 i = 0;
for (i32 n = 0; pos >= 0 && s && s[i]; i++) {
n += (s[i] & 192) != 128;
if (n == pos + 1) {
return i;
}
}
return i;
}
bool tim_utf8_is_wide_perhaps(const u8* s, i32 n) {
// Character width depends on character, terminal and font. There is no
// reliable method, however most frequently used characters are narrow.
// Zero with characters are ignored, and hope that user input is benign.
if (n < 3 || s[0] < 225) {
// u+0000 - u+1000, basic latin - tibetan
return false;
} else if (s[0] == 226 && s[1] >= 148 && s[1] < 152) {
// u+2500 - u+2600 box drawing, block elements, geometric shapes
return false;
}
return true;
}
TimText tim_scan_str(cstr s) {
if(s == NULL)
s = "";
TimText t = {
.width = 0,
.lines = (s[0] != 0),
};
i32 width = 0;
for (t.size = 0; s[t.size]; t.size++) {
char ch = s[t.size];
i32 newline = (ch == '\n');
width = newline ? 0 : width;
width += (ch & 192) != 128 && (u8)ch > 31;
t.lines += newline;
t.width = MAX(t.width, width);
}
return t;
}
bool tim_next_line(TimLine* l) {
if (!l->s || !l->s[0]) {
return false;
}
l->line = l->s;
l->size = 0;
l->width = 0;
for (cstr s = l->s; s[0] && s[0] != '\n'; s++) {
l->size += 1;
l->width += (s[0] & 192) != 128 && (u8)s[0] > 31;
}
l->s += l->size + !!l->s[l->size];
return true;
}

165
src/unix.c Executable file
View File

@@ -0,0 +1,165 @@
#include "tim.h"
#ifdef TIM_UNIX
i64 tim_time_usec(void) {
struct timespec ts = {0};
clock_gettime(CLOCK_MONOTONIC, &ts);
return ts.tv_sec * 1000000 + ts.tv_nsec / 1000;
}
void tim_write_str(cstr s, i32 size) {
ssize_t _ = write(STDOUT_FILENO, s, size);
(void)_; // remove unused-result warning
}
void signal_handler(i32 signal) {
// signals are written into a fifo pipe and read by event loop
ssize_t _ = write(tim.signal_pipe[1], &signal, sizeof(signal));
(void)_; // remove unused-result warning
}
void tim_update_screen_size(void) {
struct winsize ws = {0};
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) != 0) {
printf("ERROR: can't get console buffer size\n");
exit(1);
}
i32 w = ws.ws_col;
i32 h = ws.ws_row;
tim.resized = (u32)(w * h) <= TIM_MAX_CELLS && (w != tim.w || h != tim.h);
if (tim.resized) {
tim.w = tim.scopes[0].w = w;
tim.h = tim.scopes[0].h = h;
}
}
void tim_init_terminal(void) {
tcgetattr(STDOUT_FILENO, &tim.attr); // store attributes
struct termios attr = tim.attr; //
cfmakeraw(&attr); // configure raw mode
tcsetattr(STDOUT_FILENO, TCSADRAIN, &attr); // set new attributes
tim_write_str(S("\33[?2004l")); // reset bracketed paste mode
tim_write_str(S("\33[?1049h")); // use alternate buffer
tim_write_str(S("\33[?25l")); // hide cursor
tim_write_str(S("\33[?1000h")); // enable mouse
tim_write_str(S("\33[?1002h")); // enable button events
tim_write_str(S("\33[?1006h")); // use mouse sgr protocol
tim_update_screen_size(); // get terminal size
i32 err = pipe(tim.signal_pipe); // create signal pipe
if (!err) { //
signal(SIGWINCH, signal_handler); // terminal size changed
}
}
void tim_reset_terminal(void) {
tcsetattr(STDOUT_FILENO, TCSADRAIN, &tim.attr); // restore attributes
tim_write_str(S("\33[?1000l")); // disable mouse
tim_write_str(S("\33[?1002l")); // disable mouse
tim_write_str(S("\33[m")); // reset colors
tim_write_str(S("\33[?25h")); // show cursor
tim_write_str(S("\33[?1049l")); // exit alternate buffer
}
// parse input stored in e->s
bool parse_input(event* restrict e, i32 n) {
char* s = e->s;
if (n == 1 || s[0] != 27) {
// regular key press
e->type = TimEvent_Key;
e->key = s[0] == 127 ? TimKey_Backspace : tim_utf8_to_i32(s);
return true;
}
if (n >= 9 && !memcmp(s, S("\33[<"))) {
// sgr mouse sequence
e->type = TimEvent_Mouse;
i32 btn = strtol(s + 3, &s, 10);
e->x = strtol(s + 1, &s, 10) - 1;
e->y = strtol(s + 1, &s, 10) - 1;
if (btn == 0 && s[0] == 'M') {
// left button pressed
e->key = TimKey_MouseButtonLeft;
return true;
}
return false;
}
struct {char s[4]; i32 k;} key_table[] = {
{"[A" , TimKey_Up}, //
{"[B" , TimKey_Down}, //
{"[C" , TimKey_Right}, //
{"[D" , TimKey_Left}, //
{"[2~", TimKey_Insert}, //
{"[4h", TimKey_Insert}, // st
{"[3~", TimKey_Delete}, //
{"[P" , TimKey_Delete}, // st
{"[H" , TimKey_Home}, //
{"[1~", TimKey_Home}, // rxvt, lxterm, putty, tmux, screen
{"[7~", TimKey_Home}, // rxvt
{"[F" , TimKey_End}, //
{"[4~", TimKey_End}, // rxvt, lxterm, putty, tmux, screen, st
{"[8~", TimKey_End}, // rxvt
{"[5~", TimKey_PageUp}, //
{"[6~", TimKey_PageDown}, //
};
if ((n == 3 || n == 4) && s[0] == 27) {
// key sequence
for (i32 i = 0; i < (i32)ARRAY_SIZE(key_table); i++) {
if (!memcmp(s + 1, key_table[i].s, n - 1)) {
e->type = TimEvent_Key;
e->key = key_table[i].k;
return true;
}
}
}
return false;
}
void tim_read_event(i32 timeout_ms) {
event* e = &tim.event;
struct pollfd pfd[2] = {
{.fd = tim.signal_pipe[0], .events = POLLIN},
{.fd = STDIN_FILENO, .events = POLLIN},
};
while (true) {
memset(e, 0, sizeof(*e));
i32 r = poll(pfd, 2, timeout_ms > 0 ? timeout_ms : -1);
if (r < 0) {
// poll error, EINTR or EAGAIN
continue;
} else if (r == 0) {
// poll timeout
e->type = TimEvent_Draw;
return;
}
if (pfd[0].revents & POLLIN) {
// received signal
i32 sig = 0;
i32 n = read(tim.signal_pipe[0], &sig, sizeof(sig));
if (n > 0 && sig == SIGWINCH) {
// screen size changed
e->type = TimEvent_Draw;
tim_update_screen_size();
return;
}
}
if (pfd[1].revents & POLLIN) {
// received input
i32 n = read(STDIN_FILENO, e->s, sizeof(e->s) - 1);
if (parse_input(e, n)) {
return;
}
}
} // while
}
#endif // TIM_UNIX

71
src/widgets.c Executable file
View File

@@ -0,0 +1,71 @@
#include "tim.h"
void tim_frame(i32 x, i32 y, i32 w, i32 h, u64 color) {
if (tim.event.type == TimEvent_Draw) {
TimRect r = tim_scope_rect_to_absolute(x, y, w, h);
tim_draw_box(r.x, r.y, r.w, r.h, color, color >> 8);
}
}
void label(cstr s, i32 x, i32 y, i32 w, i32 h, u64 color) {
if (tim.event.type == TimEvent_Draw) {
TimText t = tim_scan_str(s);
w = (w == A) ? t.width : w;
h = (h == A) ? t.lines : h;
TimRect r = tim_scope_rect_to_absolute(x, y, w, h);
TimCell c = tim_cell(" ", color, color >> 8);
tim_draw_lot(c, r.x, r.y, r.w, r.h);
TimLine l = {.s = s, .line = ""};
for (i32 i = 0; tim_next_line(&l); i++) {
tim_draw_str(l.line, r.x, r.y + i, l.width, c.fg, c.bg);
}
}
}
bool button(cstr txt, i32 x, i32 y, i32 w, i32 h, u64 color) {
i32 tw = tim_utf8_len(txt);
w = (w == A) ? (tw + 4) : w;
h = (h == A) ? 3 : h;
TimRect r = tim_scope_rect_to_absolute(x, y, w, h);
if (tim.event.type == TimEvent_Draw) {
tim_draw_box(r.x, r.y, r.w, r.h, color >> 16, color >> 8);
tim_draw_str(txt, r.x + (w - tw) / 2, r.y + h / 2, w, color, color >> 8);
}
return tim_is_mouse_click_over(r);
}
bool check(cstr txt, i32* state, i32 x, i32 y, i32 w, u64 color) {
w = (w == A) ? tim_utf8_len(txt) + 4 : w;
TimRect r = tim_scope_rect_to_absolute(x, y, w, 1);
if (tim.event.type == TimEvent_Draw) {
cstr st = *state == -1 ? "-" : *state ? "x" : " ";
tim_draw_str("[ ] ", r.x, r.y, 4, color, color >> 8);
tim_draw_str(st, r.x + 1, r.y, 1, color >> 16, color >> 8);
tim_draw_str(txt, r.x + 4, r.y, r.w - 4, color, color >> 8);
}
bool click = tim_is_mouse_click_over(r);
*state = click ? !*state : *state;
return click;
}
bool radio(cstr txt, i32* state, i32 v, i32 x, i32 y, i32 w, u64 color) {
w = (w == A) ? tim_utf8_len(txt) + 4 : w;
TimRect r = tim_scope_rect_to_absolute(x, y, w, 1);
if (tim.event.type == TimEvent_Draw) {
cstr st = *state == v ? "o" : " ";
tim_draw_str("( ) ", r.x, r.y, 4, color, color >> 8);
tim_draw_str(st, r.x + 1, r.y, 1, color >> 16, color >> 8);
tim_draw_str(txt, r.x + 4, r.y, r.w - 4, color, color >> 8);
}
bool click = tim_is_mouse_click_over(r);
*state = click ? v : *state;
return click;
}

170
src/windows.c Executable file
View File

@@ -0,0 +1,170 @@
#include "tim.h"
#ifdef TIM_WINDOWS
i64 tim_time_usec(void) {
LARGE_INTEGER ticks = {0};
LARGE_INTEGER freq = {0};
QueryPerformanceCounter(&ticks);
QueryPerformanceFrequency(&freq);
return 1000000 * ticks.QuadPart / freq.QuadPart;
}
void tim_write_str(cstr s, i32 size) {
HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);
WriteFile(h, s, size, NULL, NULL);
FlushFileBuffers(h);
}
void tim_update_screen_size(void) {
HANDLE hout = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_SCREEN_BUFFER_INFO csbi = {0};
if (GetConsoleScreenBufferInfo(hout, &csbi) == 0) {
printf("ERROR: can't get console buffer size\n");
exit(1);
}
i32 w = csbi.srWindow.Right - csbi.srWindow.Left + 1;
i32 h = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
tim.resized = (u32)(w * h) <= TIM_MAX_CELLS && (w != tim.w || h != tim.h);
if (tim.resized) {
tim.w = tim.scopes[0].w = w;
tim.h = tim.scopes[0].h = h;
tim.window = csbi.srWindow;
}
}
void tim_init_terminal(void) {
DWORD mode = 0;
HANDLE hin = GetStdHandle(STD_INPUT_HANDLE);
GetConsoleMode(hin, &tim.mode_in); // get current input mode
mode = tim.mode_in; //
mode &= ~ENABLE_ECHO_INPUT; // disable echo
mode &= ~ENABLE_LINE_INPUT; // disable line buffer
// TODO: enable ctrl-c again
mode &= ~ENABLE_PROCESSED_INPUT; // disable ctrl-c
mode |= ENABLE_WINDOW_INPUT; // enable resize event
mode |= ENABLE_MOUSE_INPUT; // enable mouse event
mode |= ENABLE_EXTENDED_FLAGS; // for ENABLE_QUICK_EDIT
mode &= ~ENABLE_QUICK_EDIT_MODE; // disable select mode
SetConsoleMode(hin, mode); // set input mode
//
HANDLE hout = GetStdHandle(STD_OUTPUT_HANDLE); //
GetConsoleMode(hout, &tim.mode_out); // get current output mode
mode = tim.mode_out; //
mode |= ENABLE_PROCESSED_OUTPUT; // enable ascii sequences
mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; // enable vt sequences
SetConsoleMode(hout, mode); // set output mode
//
tim.cp_in = GetConsoleCP(); // get current code page
tim.cp_out = GetConsoleOutputCP(); //
SetConsoleCP(CP_UTF8); // set utf8 in/out code page
SetConsoleOutputCP(CP_UTF8); //
tim_write_str(S("\33[?1049h")); // use alternate buffer
tim_update_screen_size(); //
}
void tim_reset_terminal(void) {
tim_write_str(S("\33[m")); // reset colors
tim_write_str(S("\33[?25h")); // show cursor
tim_write_str(S("\33[?1049l")); // exit alternate buffer
HANDLE hin = GetStdHandle(STD_INPUT_HANDLE); //
HANDLE hout = GetStdHandle(STD_OUTPUT_HANDLE); //
SetConsoleMode(hin, tim.mode_in); // set original mode
SetConsoleMode(hout, tim.mode_out); //
SetConsoleCP(tim.cp_in); // set original code page
SetConsoleOutputCP(tim.cp_out); //
}
void tim_read_event(i32 timeout_ms) {
TimEvent* e = &tim.event;
HANDLE h = GetStdHandle(STD_INPUT_HANDLE);
const i8 key_table[256] = {
[VK_BACK] = TimKey_Backspace,
[VK_TAB] = TimKey_Tab,
[VK_RETURN] = TimKey_Enter,
[VK_ESCAPE] = TimKey_Escape,
[VK_PRIOR] = TimKey_PageUp,
[VK_NEXT] = TimKey_PageDown,
[VK_END] = TimKey_End,
[VK_HOME] = TimKey_Home,
[VK_LEFT] = TimKey_Left,
[VK_UP] = TimKey_Up,
[VK_RIGHT] = TimKey_Right,
[VK_DOWN] = TimKey_Down,
[VK_INSERT] = TimKey_Insert,
[VK_DELETE] = TimKey_Delete,
};
while (true) {
memset(e, 0, sizeof(*e));
// In cmd.exe the cursor somtimes reappears. This reliably hides it.
tim_write_str(S("\33[?25l"));
DWORD r = WaitForSingleObject(h, timeout_ms);
if (r == WAIT_TIMEOUT) {
e->type = TimEvent_Draw;
tim_update_screen_size(); // workaround, see WINDOW_BUFFER_SIZE_EVENT
return;
} else if (r != WAIT_OBJECT_0) {
continue;
}
// received input
INPUT_RECORD rec = {0};
DWORD n = 0;
ReadConsoleInput(h, &rec, 1, &n);
switch (rec.EventType) {
case KEY_EVENT: {
if (!rec.Event.KeyEvent.bKeyDown) {
// only interested in key press
continue;
}
i32 key = key_table[(u8)rec.Event.KeyEvent.wVirtualKeyCode];
WCHAR chr = rec.Event.KeyEvent.uChar.UnicodeChar;
if (!key && chr < ' ') {
// non printable key
continue;
}
tim_update_screen_size(); // workaround, see WINDOW_BUFFER_SIZE_EVENT
e->type = TimEvent_Key;
if (key) {
e->key = key;
return;
}
e->key = chr;
WideCharToMultiByte(CP_UTF8, 0, &chr, 1, e->s, sizeof(e->s),
NULL, NULL);
return;
}
case MOUSE_EVENT: {
bool move = rec.Event.MouseEvent.dwEventFlags & ~DOUBLE_CLICK;
bool left = rec.Event.MouseEvent.dwButtonState &
FROM_LEFT_1ST_BUTTON_PRESSED;
if (move || !left) {
// ignore move events and buttons other than left
continue;
}
tim_update_screen_size(); // workaround, see WINDOW_BUFFER_SIZE_EVENT
e->type = TimEvent_Mouse;
e->key = TimKey_MouseButtonLeft;
e->x = rec.Event.MouseEvent.dwMousePosition.X - tim.window.Left;
e->y = rec.Event.MouseEvent.dwMousePosition.Y - tim.window.Top;
return;
}
case WINDOW_BUFFER_SIZE_EVENT:
e->type = TimEvent_Draw;
// cmd.exe screen buffer and window size are separate, making this
// event a bit unreliable. Effectively it is only emitted when the
// terminal width changes and not for the height. As a workaround
// the screen size is updated every time an event is emitted.
tim_update_screen_size();
return;
}
} // while
}
#endif // TIM_WINDOWS

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
#include "tim.h" #include "tim.h"
static inline void test_screen(TimEvent_t* e) { static inline void test_screen(TimEvent* e) {
static TimEvent_t me; static TimEvent me;
static TimEvent_t ke; static TimEvent ke;
static int render_us; static i32 render_us;
char buf[64]; char buf[64];
ke = (e->type == KEY_EVENT) ? *e : ke; ke = (e->type == KEY_EVENT) ? *e : ke;
@@ -46,8 +46,8 @@ static inline void test_screen(TimEvent_t* e) {
label("multi\nliñe\nlabël", 24, 1, A, A, 0xf); label("multi\nliñe\nlabël", 24, 1, A, A, 0xf);
// colors // colors
scope (1, 5, 16, 5) { tim_scope(1, 5, 16, 5) {
frame(0, 0, ~0, ~0, 0xf); tim_frame(0, 0, ~0, ~0, 0xf);
label(" Red ", 1, 1, 7, A, 0x0900); label(" Red ", 1, 1, 7, A, 0x0900);
label(" ", 8, 1, 7, A, 0xc400); label(" ", 8, 1, 7, A, 0xc400);
label(" Green ", 1, 2, 7, A, 0x0a00); label(" Green ", 1, 2, 7, A, 0x0a00);
@@ -57,16 +57,16 @@ static inline void test_screen(TimEvent_t* e) {
} }
// button // button
static uint64_t bc = 0x100; static u64 bc = 0x100;
if (button("Click Me", 17, 5, 16, 5, bc)) { if (button("Click Me", 17, 5, 16, 5, bc)) {
bc = (bc + 0x100) & 0xff00; bc = (bc + 0x100) & 0xff00;
} }
// edit // edit
static TimEdit_t ed1; static TimEditState ed1;
static TimEdit_t ed2; static TimEditState ed2;
edit_init(&ed1, 32, "Edit 1"); TimEditState_init(&ed1, 32, "Edit 1");
edit_init(&ed2, 32, ""); TimEditState_init(&ed2, 32, "");
edit(&ed1, 1, 10, 32, 0xff00ff); edit(&ed1, 1, 10, 32, 0xff00ff);
sprintf(buf, "cursor: %d length: %d", ed1.cursor, ed1.length); sprintf(buf, "cursor: %d length: %d", ed1.cursor, ed1.length);
label(buf, 2, 13, A, A, 0xf); label(buf, 2, 13, A, A, 0xf);
@@ -74,46 +74,46 @@ static inline void test_screen(TimEvent_t* e) {
label(ed2.s, 2, 17, A, A, 0xf); label(ed2.s, 2, 17, A, A, 0xf);
// checkbox // checkbox
static int chk[2] = {-1, 1}; static i32 chk[2] = {-1, 1};
check("Check 1", &chk[0], 1, 18, A, 0xa000f); check("Check 1", &chk[0], 1, 18, A, 0xa000f);
check("Check 2", &chk[1], 14, 18, A, 0xa000f); check("Check 2", &chk[1], 14, 18, A, 0xa000f);
// radiobox // radiobox
static int rad = 0; static i32 rad = 0;
radio("Radio 1", &rad, 1, 1, 19, A, 0xa000f); radio("Radio 1", &rad, 1, 1, 19, A, 0xa000f);
radio("Radio 2", &rad, 2, 14, 19, A, 0xa000f); radio("Radio 2", &rad, 2, 14, 19, A, 0xa000f);
radio("Radio 3", &rad, 3, 1, 20, A, 0xa000f); radio("Radio 3", &rad, 3, 1, 20, A, 0xa000f);
radio("Radio 4", &rad, 4, 14, 20, A, 0xa000f); radio("Radio 4", &rad, 4, 14, 20, A, 0xa000f);
// scope nesting // scope nesting
scope(~1, 1, 20, 10) { tim_scope(~1, 1, 20, 10) {
scope(0, 0, 10, 5) { tim_scope(0, 0, 10, 5) {
frame(0, 0, ~0, ~0, 0x9); tim_frame(0, 0, ~0, ~0, 0x9);
} }
scope(~0, 0, 10, 5) { tim_scope(~0, 0, 10, 5) {
frame(0, 0, ~0, ~0, 0xa); tim_frame(0, 0, ~0, ~0, 0xa);
} }
scope(~0, ~0, 10, 5) { tim_scope(~0, ~0, 10, 5) {
frame(0, 0, ~0, ~0, 0xb); tim_frame(0, 0, ~0, ~0, 0xb);
} }
scope(0, ~0, 10, 5) { tim_scope(0, ~0, 10, 5) {
frame(0, 0, ~0, ~0, 0xc); tim_frame(0, 0, ~0, ~0, 0xc);
} }
} }
// funny characters // funny characters
scope (~1, ~3, 11, 5) { tim_scope(~1, ~3, 11, 5) {
frame(0, 0, ~0, ~0, 0xf); tim_frame(0, 0, ~0, ~0, 0xf);
label("123456789", 1, 1, 9, A, 0x0f05); label("123456789", 1, 1, 9, A, 0x0f05);
label("$£ह€𐍈6789", 1, 2, A, A, 0x0f05); label("$£ह€𐍈6789", 1, 2, A, A, 0x0f05);
label("圍棋56789", 1, 3, A, A, 0x0f05); label("圍棋56789", 1, 3, A, A, 0x0f05);
} }
} }
int main(void) { i32 main(void) {
while (tim_run(1.5)) { while (tim_run(1.5)) {
test_screen(&tim.event); test_screen(&tim.event);
if (is_key_press('q') || is_key_press(TimKey_Escape)) { if (tim_is_key_press('q') || tim_is_key_press(TimKey_Escape)) {
break; break;
} }
} }

View File

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