diff --git a/readme b/readme.md similarity index 88% rename from readme rename to readme.md index 9d1bcbc..397fac7 100644 --- a/readme +++ b/readme.md @@ -1,9 +1,9 @@ -* about ********************************************************************** +## about tim.h is a portable library to create simple terminal applications Demo video: https://asciinema.org/a/zn3p0dsVCOQOzwY1S9gDfyaxQ -* quick start **************************************************************** +## quick start #include "tim.h" // one header, no lib int main(void) { // @@ -20,7 +20,7 @@ int main(void) { // } // atexit cleanup } // -* layout ********************************************************************* +## layout The terminal's columns (x) and rows (y) are addressed by their coordinates, the origin is in the top left corner. @@ -56,7 +56,7 @@ take the full available space from parent. The layout automatically adopts to terminal window resize events. -* colors ********************************************************************* +## 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. @@ -71,7 +71,7 @@ should be used if consistency is important. xterm-256 color chart https://upload.wikimedia.org/wikipedia/commons/1/15/Xterm_256color_chart.svg -* events ********************************************************************* +## 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. @@ -88,7 +88,7 @@ The current event is stored in tim.event. MOUSE_EVENT | mouse click VOID_EVENT | consumed event -* elements ******************************************************************* +## elements frame (x, y, w, h, color) @@ -115,11 +115,12 @@ button (str, x, y, w, h, color) -> bool x/y/w/h see layout documentation color frame, background, text -edit (state, x, y, w, color) -> bool +edit (state, x, y, w, color) -> int - 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. + Draw text edit. Output is stored in state.s. + Receives input events when focused by mouse click or by setting focus manually. + Escape or return relinquish focus. + Returns key id if received key input. state pointer to persistent edit state struct x/y/w see layout documentation @@ -148,7 +149,7 @@ radio (str, state, v, x, y, w, color) -> bool x/y/w see layout documentation color radio, background, text -* functions ****************************************************************** +## functions tim_run (fps) -> bool @@ -177,12 +178,12 @@ time_us () -> int64 Returns monotonic clock value in microseconds. Not affected by summer time or leap seconds. -* useful links *************************************************************** +## useful links https://invisible-island.net/xterm/ctlseqs/ctlseqs.html https://learn.microsoft.com/en-us/windows/console/ -* bugs *********************************************************************** +## 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 @@ -193,7 +194,7 @@ https://learn.microsoft.com/en-us/windows/console/ - Zero width code points are not supported - Windows cmd.exe resize events may be delayed -* compatibility ************************************************************** +## compatibility emulator | support | remarks ------------------|---------|---------------------------------- @@ -222,7 +223,7 @@ https://learn.microsoft.com/en-us/windows/console/ XTerm | full | XTerm is law Zutty | full | -* license ******************************************************************** +## license MIT License diff --git a/test/string.c b/test/string.c index 6c30031..09eceeb 100644 --- a/test/string.c +++ b/test/string.c @@ -51,7 +51,7 @@ int main(void) { TEST(scan_str("a").width == 1); TEST(scan_str("äß\no").width == 2); - struct line ln = {.str = "foo\nbar"}; + struct line ln = {.s = "foo\nbar"}; TEST(next_line(&ln) == true); TEST(!memcmp(ln.line, "foo", ln.size)); TEST(next_line(&ln) == true); diff --git a/test/test.c b/test/test.c index a39a138..168cb59 100644 --- a/test/test.c +++ b/test/test.c @@ -25,12 +25,12 @@ static inline void test_screen(struct event* e) { 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)); + sprintf(buf, "key : [%d] %s", ke.key, ke.s + (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]); + e->s[0], e->s[1], e->s[2], e->s[3], e->s[4], e->s[5], e->s[6], e->s[7]); label(buf, 2, 4, A, A, 0xf); // lower right @@ -63,13 +63,13 @@ static inline void test_screen(struct event* e) { } // edit - static struct edit ed1 = {.str = "Edit 1"}; + static struct edit ed1 = {.s = "Edit 1"}; static struct edit ed2 = {0}; 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); + label(ed2.s, 2, 17, A, A, 0xf); // checkbox static int chk[2] = {-1, 1}; diff --git a/tim.h b/tim.h index f9f6d2f..331670b 100644 --- a/tim.h +++ b/tim.h @@ -117,9 +117,10 @@ // // 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. +// Draw text edit. Output is stored in state.s. +// Receives input events when focused by mouse click or by setting focus manually. +// Escape or return relinquish focus. +// Returns key id if received key input. // // state pointer to persistent edit state struct // x/y/w see layout documentation @@ -357,7 +358,7 @@ struct text { }; struct line { - const char* str; // input and parse state + const char* s; // input and parse state const char* line; // line strings, not terminated int size; // line size in bytes int width; // line width in glyph @@ -368,13 +369,14 @@ struct event { int32_t key; // used by KEY_EVENT and MOUSE_EVENT int x; // used by MOUSE_EVENT int y; // used by MOUSE_EVENT - char str[32]; // string representation of key + char s[32]; // string representation of key }; struct edit { int cursor; // cursor position (utf8) int length; // string length (utf8) - char str[256]; // zero terminated buffer + int capacity; // buffer size + char* s; // zero terminated buffer }; struct state { @@ -382,7 +384,7 @@ struct state { int h; // screen height int frame; // frame counter struct event event; // current event - uintptr_t focus; // focused element + void* focus; // focused element int loop_stage; // loop stage bool resized; // screen was resized int scope; // current scope @@ -436,8 +438,12 @@ struct state tim = { // like strlen, returns 0 on NULL or int overflow static inline int ztrlen(const char* s) { - size_t n = s ? strlen(s) : 0; - return MAX((int)n, 0); + if(s == NULL) + return 0; + int n = strlen(s); + if(n < 0) + n = 0; + return n; } // bit scan reverse, count leading zeros @@ -494,8 +500,9 @@ static int utfpos(const char* s, int pos) { } // scan string for width and lines -static struct text scan_str(const char* str) { - const char* s = str ? str : ""; +static struct text scan_str(const char* s) { + if(s == NULL) + s = ""; struct text t = { .width = 0, .lines = (s[0] != 0), @@ -514,17 +521,17 @@ static struct text scan_str(const char* str) { // iterate through lines, false when end is reached static bool next_line(struct line* l) { - if (!l->str || !l->str[0]) { + if (!l->s || !l->s[0]) { return false; } - l->line = l->str; + l->line = l->s; l->size = 0; l->width = 0; - for (const char* s = l->str; s[0] && s[0] != '\n'; s++) { + for (const char* s = l->s; s[0] && s[0] != '\n'; s++) { l->size += 1; l->width += (s[0] & 192) != 128 && (uint8_t)s[0] > 31; } - l->str += l->size + !!l->str[l->size]; + l->s += l->size + !!l->s[l->size]; return true; } @@ -563,7 +570,8 @@ static void signal_handler(int signal) { static void update_screen_size(void) { struct winsize ws = {0}; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) != 0) { - return; + printf("ERROR: can't get console buffer size\n"); + exit(1); } int w = ws.ws_col; int h = ws.ws_row; @@ -601,9 +609,9 @@ static void reset_terminal(void) { write_str(S("\33[?1049l")); // exit alternate buffer } -// parse input stored in e->str +// parse input stored in e->s static bool parse_input(struct event* restrict e, int n) { - char* s = e->str; + char* s = e->s; if (n == 1 || s[0] != 27) { // regular key press @@ -694,7 +702,7 @@ static void read_event(int timeout_ms) { if (pfd[1].revents & POLLIN) { // received input - int n = read(STDIN_FILENO, e->str, sizeof(e->str) - 1); + int n = read(STDIN_FILENO, e->s, sizeof(e->s) - 1); if (parse_input(e, n)) { return; } @@ -727,7 +735,8 @@ static void update_screen_size(void) { HANDLE hout = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_SCREEN_BUFFER_INFO csbi = {0}; if (GetConsoleScreenBufferInfo(hout, &csbi) == 0) { - return; + printf("ERROR: can't get console buffer size\n"); + exit(1); } int w = csbi.srWindow.Right - csbi.srWindow.Left + 1; int h = csbi.srWindow.Bottom - csbi.srWindow.Top + 1; @@ -839,7 +848,7 @@ static void read_event(int timeout_ms) { return; } e->key = chr; - WideCharToMultiByte(CP_UTF8, 0, &chr, 1, e->str, sizeof(e->str), + WideCharToMultiByte(CP_UTF8, 0, &chr, 1, e->s, sizeof(e->s), NULL, NULL); return; } @@ -1076,16 +1085,16 @@ static inline void frame(int x, int y, int w, int h, uint64_t color) { // text label // str : text - supports multiple lines // color: background, text -static inline void label(const char* str, int x, int y, int w, int h, +static inline void label(const char* s, int x, int y, int w, int h, uint64_t color) { if (tim.event.type == DRAW_EVENT) { - struct text s = scan_str(str); - w = (w == A) ? s.width : w; - h = (h == A) ? s.lines : h; + struct text t = scan_str(s); + w = (w == A) ? t.width : w; + h = (h == A) ? t.lines : h; struct rect r = abs_xywh(x, y, w, h); struct cell c = cell(" ", color, color >> 8); draw_lot(c, r.x, r.y, r.w, r.h); - struct line l = {.str = str, .line = ""}; + struct line l = {.s = s, .line = ""}; for (int i = 0; next_line(&l); i++) { draw_str(l.line, r.x, r.y + i, l.width, c.fg, c.bg); } @@ -1113,46 +1122,48 @@ static inline bool button(const char* txt, int x, int y, int w, int h, /* edit ***********************************************************************/ static void edit_insert(struct edit* e, const char* s) { - int dst_size = ztrlen(e->str); + int dst_size = ztrlen(e->s); int src_size = ztrlen(s); - if (dst_size + src_size + 1 < (int)sizeof(e->str)) { + if (dst_size + src_size < e->capacity) { int len = utflen(s); // usually 1, except when smashing keys - int cur = utfpos(e->str, e->cursor); - memmove(e->str + cur + src_size, e->str + cur, dst_size - cur); - memmove(e->str + cur, s, src_size); - e->str[dst_size + src_size + 1] = 0; + int cur = utfpos(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; } } static void edit_delete(struct edit* e) { - int size = ztrlen(e->str); - int cur = utfpos(e->str, e->cursor); - int len = utfpos(e->str + cur, 1); + int size = ztrlen(e->s); + int cur = utfpos(e->s, e->cursor); + int len = utfpos(e->s + cur, 1); if (size - cur > 0) { - memmove(e->str + cur, e->str + cur + len, size - cur); + memmove(e->s + cur, e->s + cur + len, size - cur); e->length -= 1; } } -static bool edit_event(struct edit* e, struct rect r) { +/// @return key id or 0 +static int edit_event(struct edit* e, struct rect r) { if (is_click_over(r)) { // take focus - tim.focus = (uintptr_t)e; - return false; + tim.focus = e; + return 0; } - if (tim.focus != (uintptr_t)e || tim.event.type != KEY_EVENT) { + if (tim.focus != e || tim.event.type != KEY_EVENT) { // not focused or no key press - return false; + return 0; } tim.event.type = VOID_EVENT; // consume event switch (tim.event.key) { + case ESCAPE_KEY: case ENTER_KEY: tim.focus = 0; // release focus - return true; + break; case DELETE_KEY: edit_delete(e); break; @@ -1174,40 +1185,42 @@ static bool edit_event(struct edit* e, struct rect r) { case END_KEY: e->cursor = e->length; break; - case ESCAPE_KEY: - tim.focus = 0; // release focus - break; default: if (tim.event.key >= ' ') { - edit_insert(e, tim.event.str); + edit_insert(e, tim.event.s); } break; } - return false; + return tim.event.key; } -// text edit - value in state -// e : persistent edit state -// color: frame, background, text -static inline bool edit(struct edit* e, int x, int y, int w, uint64_t color) { - struct rect r = abs_xywh(x, y, w, 3); +static inline void edit_init(struct edit* e, int capacity, const char* initial_content){ + e->length = utflen(initial_content); + e->cursor = utflen(initial_content); + e->capacity = capacity; + e->s = malloc(capacity + 1); + int byte_len = strlen(initial_content); + memcpy(e->s, initial_content, byte_len); + e->s[byte_len] = 0; +} - // uninitialized edit state - if (e->str[0] && e->cursor == 0 && e->length == 0) { - e->length = utflen(e->str); - e->cursor = e->length; - } +/// text edit - value in state +/// @param e persistent edit state, use edit_init() to create new state +/// @param color frame, background, text +/// @return key id or 0 +static inline int edit(struct edit* e, int x, int y, int w, uint64_t color) { + struct rect r = abs_xywh(x, y, w, 3); if (tim.event.type == DRAW_EVENT) { draw_box(r.x, r.y, r.w, r.h, color >> 16, color >> 8); - if (tim.focus == (uintptr_t)e) { - char* str = e->str + utfpos(e->str, e->cursor - r.w + 4); + if (tim.focus == e) { + char* s = e->s + utfpos(e->s, e->cursor - r.w + 4); int cur = MIN(r.w - 4, e->cursor); - draw_str(str, r.x + 2, r.y + 1, r.w - 3, color, color >> 8); + draw_str(s, r.x + 2, r.y + 1, r.w - 3, color, color >> 8); draw_invert(r.x + cur + 2, r.y + 1, 1); } else { - draw_str(e->str, r.x + 2, r.y + 1, r.w - 3, color, color >> 8); + draw_str(e->s, r.x + 2, r.y + 1, r.w - 3, color, color >> 8); } }