Files
tim/tim.h

1422 lines
47 KiB
C

/* about **********************************************************************/
// tim.h is a portable library to create simple terminal applications
// Demo video: https://asciinema.org/a/zn3p0dsVCOQOzwY1S9gDfyaxQ
/* quick start ****************************************************************/
// #include "tim.h" // one header, no lib
// int main(void) { //
// while (tim_run(0)) { // event loop
// scope (A, A, 24, 8) { // centered 24x8 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
// } //
// } // atexit 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.
//
// Elements (widget, control, component) are elements of user interaction, such
// as a button or edit box. 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
//
// The layout automatically adopts to terminal window resize events.
/* 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.
//
// 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.
//
// The current event is stored in tim.event.
//
// 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
/* compatibility **************************************************************/
// emulator | support | remarks
// ------------------|---------|----------------------------------
// Alacritty | ? |
// cmd.exe | good | resize may lag
// Cool Retro Term | good | wide character spill
// Deepin Terminal | good | wide character spill
// Eterm | abysmal | garbled output
// GNOME Terminal | full |
// GNUstep Terminal | abysmal | garbled output
// iTerm2 | ? |
// kitty | full |
// Konsole | full |
// LXTerminal | full |
// macOS Terminal | ? |
// PuTTY | full |
// QTerminal | good | wide character spill
// rxvt-unicode | full |
// GNU Screen | good | no alternate buffer, double esc
// st | full |
// Terminator | full |
// Terminology | full |
// tmux | good | esc lags
// Windows Terminal | full |
// Xfce Terminal | full |
// XTerm | full | XTerm is law
// Zutty | full |
/* license ********************************************************************/
// MIT License
//
// Copyright (c) MMXXIV Chu'vok <chuvok@maeppi.e4ward.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// The software is provided "as is", without warranty of any kind, express or
// implied, including but not limited to the warranties of merchantability,
// fitness for a particular purpose and noninfringement. In no event shall the
// authors or copyright holders be liable for any claim, damages or other
// liability, whether in an action of contract, tort or otherwise, arising from,
// out of or in connection with the software or the use or other dealings in the
// software.
/* includes *******************************************************************/
// unix-like
#if defined __unix__ || defined __unix || defined __APPLE__ || defined __ELF__
#define TIM_UNIX
#include <poll.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <unistd.h>
#endif
// windows
#ifdef _WIN32
#define TIM_WINDOWS
#define _CRT_SECURE_NO_WARNINGS
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#endif
// libc
#include <limits.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
/* workaround *****************************************************************/
#ifdef __cplusplus
#error "C++ is not supported. Sorry."
#endif
#ifdef _MSC_VER
// disable integer conversion warnings
#pragma warning(disable:4244)
#endif
#ifdef _WIN32
// fix windows.h name clash, coincidentally they have the same values
#undef KEY_EVENT // 0x0001
#undef MOUSE_EVENT // 0x0002
#endif
#ifdef __PCC__
// Guard to identify dynamic shared objects during global destruction. Not sure
// if this is a good idea. pcc and tcc may require this.
int __dso_handle;
#endif
/* constants ******************************************************************/
#define ENABLE_DBUF 1 // double buffering
#define MAX_SCOPE 20 // max scope nesting
#define MAX_CELLS 0x20000 // size of screen buffer
#define MAX_BUF (MAX_CELLS * 4) // size of output buffer
#define A INT_MAX // auto center / width / height
// tim.event.type
enum {
DRAW_EVENT, // draw screen
KEY_EVENT, // a key was pressed
MOUSE_EVENT, // mouse button, scroll or move
VOID_EVENT, // set when an event was consumed
};
// tim.event.key
enum {
LEFT_BUTTON = 1,
BACKSPACE_KEY = 8,
TAB_KEY = 9,
ENTER_KEY = 13,
ESCAPE_KEY = 27,
INSERT_KEY = -1,
DELETE_KEY = -2,
HOME_KEY = -3,
END_KEY = -4,
PAGEUP_KEY = -5,
PAGEDOWN_KEY = -6,
UP_KEY = -7,
DOWN_KEY = -8,
LEFT_KEY = -9,
RIGHT_KEY = -10,
};
/* types **********************************************************************/
struct cell {
uint8_t fg; // foreground color
uint8_t bg; // background color
uint8_t wide; // wide or following wide character
uint8_t n; // number of bytes in buf
uint8_t buf[4]; // utf8 code point
};
struct rect {
int x; // x coordinate (left = 0)
int y; // y coordinate (top = 0)
int w; // width
int h; // height
};
struct text {
int size; // size in bytes without terminator
int width; // widest line
int lines; // number of lines
};
struct line {
const char* str; // input and parse state
const char* line; // line strings, not terminated
int size; // line size in bytes
int width; // line width in glyph
};
struct event {
int type; // event type
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
};
struct edit {
int cursor; // cursor position (utf8)
int length; // string length (utf8)
char str[256]; // zero terminated buffer
};
struct state {
int w; // screen width
int h; // screen height
int frame; // frame counter
struct event event; // current event
uintptr_t focus; // focused element
int loop_stage; // loop stage
bool resized; // screen was resized
int scope; // current scope
struct rect scopes[MAX_SCOPE]; // scope stack
struct cell* cells; // screen buffer
char* buf; // final output buffer
int buf_size; // position in write buffer
int64_t start_us; // render start time
int render_us; // elapsed render time
#ifdef TIM_UNIX //
struct termios attr; // initial attributes
int signal_pipe[2]; // signal fifo pipe
#endif //
#ifdef TIM_WINDOWS //
SMALL_RECT window; // screen buffer window size
DWORD mode_in; // initial input mode
DWORD mode_out; // initial output mode
UINT cp_in; // initial input code page
UINT cp_out; // initial output code page
#endif
};
/* macros *********************************************************************/
#define MAX(a, b) ((a) > (b) ? (a) : (b)) //
#define MIN(a, b) ((a) < (b) ? (a) : (b)) //
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) // number of items in array
#define S(s) (""s), (sizeof(s) - 1) // expand to s, sizeof(s) - 1
/* global variables ***********************************************************/
// These buffers were part of tim struct but caused the linker to produce very
// large binaries.
static struct cell tim_cells[MAX_CELLS << ENABLE_DBUF]; // screen buffer
static char tim_buf[MAX_BUF]; // output buffer
// global state
#ifdef TIM_EXTERN_STATE
extern struct state tim;
#else
// Intentionally not declared as static to trigger a linker error when used in
// multiple compilation units. If that happens, #define TIM_EXTERN_STATE before
// including this header in all but one compilation unit.
struct state tim = {
.cells = tim_cells,
.buf = tim_buf,
};
#endif
/* string *********************************************************************/
// 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);
}
// bit scan reverse, count leading zeros
static inline int bsr8(uint8_t 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
int n = 0;
for (; n < 8 && !(x & 128); n++, x <<= 1) {}
return n;
#endif
}
// decode one utf8 code point
static int32_t utfchr(const char* s) {
s = s ? s : "";
// use bit magic to mask out leading utf8 1s
uint32_t c = s[0] & ((1 << (8 - bsr8(~s[0]))) - 1);
for (int i = 1; s[0] && s[i] && i < 4; i++) {
c = (c << 6) | (s[i] & 63);
}
return (int32_t)c;
}
// number of utf8 code points
static int utflen(const char* s) {
int n = 0;
for (int i = 0; s && s[i]; i++) {
n += (s[i] & 192) != 128;
}
return n;
}
// index of utf8 code point at pos
static int utfpos(const char* s, int pos) {
int i = 0;
for (int n = 0; pos >= 0 && s && s[i]; i++) {
n += (s[i] & 192) != 128;
if (n == pos + 1) {
return i;
}
}
return i;
}
// scan string for width and lines
static struct text scan_str(const char* str) {
const char* s = str ? str : "";
struct text t = {
.width = 0,
.lines = (s[0] != 0),
};
int width = 0;
for (t.size = 0; s[t.size]; t.size++) {
char ch = s[t.size];
int newline = (ch == '\n');
width = newline ? 0 : width;
width += (ch & 192) != 128 && (uint8_t)ch > 31;
t.lines += newline;
t.width = MAX(t.width, width);
}
return t;
}
// iterate through lines, false when end is reached
static bool next_line(struct line* l) {
if (!l->str || !l->str[0]) {
return false;
}
l->line = l->str;
l->size = 0;
l->width = 0;
for (const char* s = l->str; 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];
return true;
}
// true if utf8 code point could be wide
static bool is_wide_perhaps(const uint8_t* s, int 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;
}
/* unix ***********************************************************************/
// Unix-like terminal IO. Osx is missing ppoll and __unix__. Come on, fix it!
#ifdef TIM_UNIX
static void write_str(const char* s, int size) {
ssize_t _ = write(STDOUT_FILENO, s, size);
(void)_; // remove unused-result warning
}
static void signal_handler(int 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
}
static void update_screen_size(void) {
struct winsize ws = {0};
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) != 0) {
return;
}
int w = ws.ws_col;
int h = ws.ws_row;
tim.resized = (unsigned)(w * h) <= 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;
}
}
static void 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
write_str(S("\33[?2004l")); // reset bracketed paste mode
write_str(S("\33[?1049h")); // use alternate buffer
write_str(S("\33[?25l")); // hide cursor
write_str(S("\33[?1000h")); // enable mouse
write_str(S("\33[?1002h")); // enable button events
write_str(S("\33[?1006h")); // use mouse sgr protocol
update_screen_size(); // get terminal size
int err = pipe(tim.signal_pipe); // create signal pipe
if (!err) { //
signal(SIGWINCH, signal_handler); // terminal size changed
}
}
static void reset_terminal(void) {
tcsetattr(STDOUT_FILENO, TCSADRAIN, &tim.attr); // restore attributes
write_str(S("\33[?1000l")); // disable mouse
write_str(S("\33[?1002l")); // disable mouse
write_str(S("\33[m")); // reset colors
write_str(S("\33[?25h")); // show cursor
write_str(S("\33[?1049l")); // exit alternate buffer
}
// parse input stored in e->str
static bool parse_input(struct event* restrict e, int n) {
char* s = e->str;
if (n == 1 || s[0] != 27) {
// regular key press
e->type = KEY_EVENT;
e->key = s[0] == 127 ? BACKSPACE_KEY : utfchr(s);
return true;
}
if (n >= 9 && !memcmp(s, S("\33[<"))) {
// sgr mouse sequence
e->type = MOUSE_EVENT;
int 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 = LEFT_BUTTON;
return true;
}
return false;
}
static struct {char s[4]; int k;} key_table[] = {
{"[A" , UP_KEY}, //
{"[B" , DOWN_KEY}, //
{"[C" , RIGHT_KEY}, //
{"[D" , LEFT_KEY}, //
{"[2~", INSERT_KEY}, //
{"[4h", INSERT_KEY}, // st
{"[3~", DELETE_KEY}, //
{"[P" , DELETE_KEY}, // st
{"[H" , HOME_KEY}, //
{"[1~", HOME_KEY}, // rxvt, lxterm, putty, tmux, screen
{"[7~", HOME_KEY}, // rxvt
{"[F" , END_KEY}, //
{"[4~", END_KEY}, // rxvt, lxterm, putty, tmux, screen, st
{"[8~", END_KEY}, // rxvt
{"[5~", PAGEUP_KEY}, //
{"[6~", PAGEDOWN_KEY}, //
};
if ((n == 3 || n == 4) && s[0] == 27) {
// key sequence
for (int i = 0; i < (int)ARRAY_SIZE(key_table); i++) {
if (!memcmp(s + 1, key_table[i].s, n - 1)) {
e->type = KEY_EVENT;
e->key = key_table[i].k;
return true;
}
}
}
return false;
}
static void read_event(int timeout_ms) {
struct 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));
int 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 = DRAW_EVENT;
return;
}
if (pfd[0].revents & POLLIN) {
// received signal
int sig = 0;
int n = read(tim.signal_pipe[0], &sig, sizeof(sig));
if (n > 0 && sig == SIGWINCH) {
// screen size changed
e->type = DRAW_EVENT;
update_screen_size();
return;
}
}
if (pfd[1].revents & POLLIN) {
// received input
int n = read(STDIN_FILENO, e->str, sizeof(e->str) - 1);
if (parse_input(e, n)) {
return;
}
}
} // while
}
static inline int64_t time_us(void) {
struct timespec ts = {0};
clock_gettime(CLOCK_MONOTONIC, &ts);
return ts.tv_sec * 1000000 + ts.tv_nsec / 1000;
}
#endif // TIM_UNIX
/* windows ********************************************************************/
// Windows terminal IO. Win32 is actually not that horrible as many say. Quirky
// but well documented.
#ifdef TIM_WINDOWS
static void write_str(const char* s, int size) {
HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);
WriteFile(h, s, size, NULL, NULL);
FlushFileBuffers(h);
}
static void update_screen_size(void) {
HANDLE hout = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_SCREEN_BUFFER_INFO csbi = {0};
if (GetConsoleScreenBufferInfo(hout, &csbi) == 0) {
return;
}
int w = csbi.srWindow.Right - csbi.srWindow.Left + 1;
int h = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
tim.resized = (unsigned)(w * h) <= 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;
}
}
static void 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
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); //
write_str(S("\33[?1049h")); // use alternate buffer
update_screen_size(); //
}
static void reset_terminal(void) {
write_str(S("\33[m")); // reset colors
write_str(S("\33[?25h")); // show cursor
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); //
}
static void read_event(int timeout_ms) {
struct event* e = &tim.event;
HANDLE h = GetStdHandle(STD_INPUT_HANDLE);
static const int8_t key_table[256] = {
[VK_BACK] = BACKSPACE_KEY,
[VK_TAB] = TAB_KEY,
[VK_RETURN] = ENTER_KEY,
[VK_ESCAPE] = ESCAPE_KEY,
[VK_PRIOR] = PAGEUP_KEY,
[VK_NEXT] = PAGEDOWN_KEY,
[VK_END] = END_KEY,
[VK_HOME] = HOME_KEY,
[VK_LEFT] = LEFT_KEY,
[VK_UP] = UP_KEY,
[VK_RIGHT] = RIGHT_KEY,
[VK_DOWN] = DOWN_KEY,
[VK_INSERT] = INSERT_KEY,
[VK_DELETE] = DELETE_KEY,
};
while (true) {
memset(e, 0, sizeof(*e));
// In cmd.exe the cursor somtimes reappears. This reliably hides it.
write_str(S("\33[?25l"));
DWORD r = WaitForSingleObject(h, timeout_ms);
if (r == WAIT_TIMEOUT) {
e->type = DRAW_EVENT;
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;
ReadConsoleInputW(h, &rec, 1, &n);
switch (rec.EventType) {
case KEY_EVENT: {
if (!rec.Event.KeyEvent.bKeyDown) {
// only interested in key press
continue;
}
int key = key_table[(uint8_t)rec.Event.KeyEvent.wVirtualKeyCode];
WCHAR chr = rec.Event.KeyEvent.uChar.UnicodeChar;
if (!key && chr < ' ') {
// non printable key
continue;
}
update_screen_size(); // workaround, see WINDOW_BUFFER_SIZE_EVENT
e->type = KEY_EVENT;
if (key) {
e->key = key;
return;
}
e->key = chr;
WideCharToMultiByte(CP_UTF8, 0, &chr, 1, e->str, sizeof(e->str),
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;
}
update_screen_size(); // workaround, see WINDOW_BUFFER_SIZE_EVENT
e->type = MOUSE_EVENT;
e->key = LEFT_BUTTON;
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 = DRAW_EVENT;
// 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.
update_screen_size();
return;
}
} // while
}
static inline int64_t time_us(void) {
LARGE_INTEGER ticks = {0};
LARGE_INTEGER freq = {0};
QueryPerformanceCounter(&ticks);
QueryPerformanceFrequency(&freq);
return 1000000 * ticks.QuadPart / freq.QuadPart;
}
#endif // TIM_WINDOWS
/* events *********************************************************************/
// returns true if event was of type and key
static inline bool is_event_key(int type, int32_t key) {
return tim.event.type == type && tim.event.key == key;
}
// returns true if event was press of key
static inline bool is_key_press(int32_t key) {
return is_event_key(KEY_EVENT, key);
}
// returns true if mouse event was over r
static inline bool is_mouse_over(struct rect r) {
int x = tim.event.x;
int y = tim.event.y;
return x >= r.x && x < r.x + r.w && y >= r.y && y < r.y + r.h;
}
// returns true if event is mouse left-down and over r
static inline bool is_click_over(struct rect r) {
return is_event_key(MOUSE_EVENT, LEFT_BUTTON) && is_mouse_over(r);
}
/* drawing ********************************************************************/
// create cell from utf8 code point with fg and bg colors
static inline struct cell cell(const char* s, uint8_t fg, uint8_t bg) {
struct cell 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;
}
// clear cell buffer
static void clear_cells(void) {
size_t size = sizeof(tim.cells[0]) * tim.w * tim.h;
memset(tim.cells, 0, size);
}
// draw cell at position
static void draw_chr(struct cell cell, int x, int y) {
if (x >= 0 && x < tim.w && y >= 0 && y < tim.h) {
tim.cells[x + y * tim.w] = cell;
}
}
// draw row of cells
static void draw_row(struct cell cell, int x, int y, int w) {
if (y >= 0 && y < tim.h && w > 0) {
for (int i = MAX(x, 0); i < MIN(x + w, tim.w); i++) {
tim.cells[i + y * tim.w] = cell;
}
}
}
// draw column of cells
static void draw_col(struct cell cell, int x, int y, int h) {
if (x >= 0 && x < tim.w && h > 0) {
for (int i = MAX(y, 0); i < MIN(y + h, tim.h); i++) {
tim.cells[x + i * tim.w] = cell;
}
}
}
// fill lot (area) of cells
static void draw_lot(struct cell cell, int x, int y, int w, int h) {
if (w > 0 && h > 0) {
for (int iy = MAX(y, 0); iy < MIN(y + h, tim.h); iy++) {
for (int ix = MAX(x, 0); ix < MIN(x + w, tim.w); ix++) {
tim.cells[ix + iy * tim.w] = cell;
}
}
}
}
// draw string to line, tags potential wide characters
static void draw_str(const char* s, int x, int y, int w,
uint8_t fg, uint8_t bg) {
if (s && y >= 0 && x < tim.w && y < tim.h ) {
int end = MIN(x + w, tim.w);
bool wide = false;
for (int i = 0; s[i] && x < end; x++) {
struct cell c = cell(&s[i], fg, bg);
wide = wide || is_wide_perhaps(c.buf, c.n);
if (x >= 0) {
c.wide = wide;
tim.cells[x + y * tim.w] = c;
}
i += c.n;
}
}
}
// draw box of ascii cell characters
static void draw_box(int x, int y, int w, int h, uint8_t fg, uint8_t bg) {
draw_chr(cell("", fg, bg), x , y);
draw_chr(cell("", fg, bg), x + w - 1, y);
draw_chr(cell("", fg, bg), x , y + h - 1);
draw_chr(cell("", fg, bg), x + w - 1, y + h - 1);
draw_row(cell("", fg, bg), x + 1 , y , w - 2);
draw_row(cell("", fg, bg), x + 1 , y + h - 1, w - 2);
draw_col(cell("", fg, bg), x , y + 1 , h - 2);
draw_col(cell("", fg, bg), x + w - 1, y + 1 , h - 2);
draw_lot(cell(" ", fg, bg), x + 1 , y + 1 , w - 2, h - 2);
}
// invert fg and bg colors of line of cells
static void draw_invert(int x, int y, int w) {
if (y >= 0 && y < tim.h && w > 0) {
for (int i = MAX(x, 0); i < MIN(x + w, tim.w); i++) {
struct cell 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;
}
}
}
/* scope **********************************************************************/
// enter layout scope
#define scope(x, y, w, h) \
for (int _i = enter_scope((x), (y), (w), (h)); _i; _i = exit_scope())
// convert relative (scoped) to absolute (screen) coordinates
static struct rect abs_xywh(int x, int y, int w, int h) {
struct rect 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 (struct rect){x, y, w, h};
}
// enter scope and push coordinates on stack
static inline int enter_scope(int x, int y, int w, int h) {
if (tim.scope + 1 >= MAX_SCOPE) {
return 0;
}
struct rect r = abs_xywh(x, y, w, h);
tim.scope += 1;
tim.scopes[tim.scope] = r;
return 1;
}
// exit scope and pop stack
static inline int exit_scope(void) {
tim.scope -= (tim.scope > 0);
return 0;
}
/* frame **********************************************************************/
// frame
// color: background, frame
static inline void frame(int x, int y, int w, int h, uint64_t color) {
if (tim.event.type == DRAW_EVENT) {
struct rect r = abs_xywh(x, y, w, h);
draw_box(r.x, r.y, r.w, r.h, color, color >> 8);
}
}
/* label **********************************************************************/
// 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,
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 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 = ""};
for (int i = 0; next_line(&l); i++) {
draw_str(l.line, r.x, r.y + i, l.width, c.fg, c.bg);
}
}
}
/* button *********************************************************************/
// button - returns true on click
// color: frame, background, text
static inline bool button(const char* txt, int x, int y, int w, int h,
uint64_t color) {
int tw = utflen(txt);
w = (w == A) ? (tw + 4) : w;
h = (h == A) ? 3 : h;
struct rect r = abs_xywh(x, y, w, h);
if (tim.event.type == DRAW_EVENT) {
draw_box(r.x, r.y, r.w, r.h, color >> 16, color >> 8);
draw_str(txt, r.x + (w - tw) / 2, r.y + h / 2, w, color, color >> 8);
}
return is_click_over(r);
}
/* edit ***********************************************************************/
static void edit_insert(struct edit* e, const char* s) {
int dst_size = ztrlen(e->str);
int src_size = ztrlen(s);
if (dst_size + src_size + 1 < (int)sizeof(e->str)) {
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;
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);
if (size - cur > 0) {
memmove(e->str + cur, e->str + cur + len, size - cur);
e->length -= 1;
}
}
static bool edit_event(struct edit* e, struct rect r) {
if (is_click_over(r)) {
// take focus
tim.focus = (uintptr_t)e;
return false;
}
if (tim.focus != (uintptr_t)e || tim.event.type != KEY_EVENT) {
// not focused or no key press
return false;
}
tim.event.type = VOID_EVENT; // consume event
switch (tim.event.key) {
case ENTER_KEY:
tim.focus = 0; // release focus
return true;
case DELETE_KEY:
edit_delete(e);
break;
case BACKSPACE_KEY:
if (e->cursor > 0) {
e->cursor -= 1;
edit_delete(e);
}
break;
case LEFT_KEY:
e->cursor -= (e->cursor > 0);
break;
case RIGHT_KEY:
e->cursor += (e->cursor < e->length);
break;
case HOME_KEY:
e->cursor = 0;
break;
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);
}
break;
}
return false;
}
// 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);
// uninitialized edit state
if (e->str[0] && e->cursor == 0 && e->length == 0) {
e->length = utflen(e->str);
e->cursor = e->length;
}
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);
int cur = MIN(r.w - 4, e->cursor);
draw_str(str, 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);
}
}
return edit_event(e, r);
}
/* check **********************************************************************/
// check box - returns true when clicked
// txt : text label
// state: persistent state, 0 unchecked, -1 semi checked, !0: checked
// color: check, background, text
static inline bool check(const char* txt, int* state, int x, int y, int w,
uint64_t color) {
w = (w == A) ? utflen(txt) + 4 : w;
struct rect r = abs_xywh(x, y, w, 1);
if (tim.event.type == DRAW_EVENT) {
const char* st = *state == -1 ? "-" : *state ? "x" : " ";
draw_str("[ ] ", r.x, r.y, 4, color, color >> 8);
draw_str(st, r.x + 1, r.y, 1, color >> 16, color >> 8);
draw_str(txt, r.x + 4, r.y, r.w - 4, color, color >> 8);
}
bool click = is_click_over(r);
*state = click ? !*state : *state;
return click;
}
/* radio **********************************************************************/
// radio button - return true when clicked
// txt : text label
// state: persistent state, selected if *state == v
// v : value
// color: radio, background, text
static inline bool radio(const char* txt, int* state, int v, int x, int y,
int w, uint64_t color) {
w = (w == A) ? utflen(txt) + 4 : w;
struct rect r = abs_xywh(x, y, w, 1);
if (tim.event.type == DRAW_EVENT) {
const char* st = *state == v ? "o" : " ";
draw_str("( ) ", r.x, r.y, 4, color, color >> 8);
draw_str(st, r.x + 1, r.y, 1, color >> 16, color >> 8);
draw_str(txt, r.x + 4, r.y, r.w - 4, color, color >> 8);
}
bool click = is_click_over(r);
*state = click ? v : *state;
return click;
}
/* rendering ******************************************************************/
// write character to output buffer
static inline void put_chr(char c) {
if (tim.buf_size + 1 < MAX_BUF) {
tim.buf[tim.buf_size] = c;
tim.buf_size += 1;
}
}
// write string to output buffer
static inline void put_str(const char* s, int size) {
if (size > 0 && tim.buf_size + size < MAX_BUF) {
memmove(&tim.buf[tim.buf_size], s, size);
tim.buf_size += size;
}
}
// write integer as decimal string to output buffer
static inline void put_int(int i) {
// optimized for small positive values, reduces load by a third
char* buf = &tim.buf[tim.buf_size];
if (tim.buf_size + 11 >= MAX_BUF) {
// not enough space for 32 bit integer
} else if ((unsigned)i < 10) {
buf[0] = '0' + i;
tim.buf_size += 1;
} else if ((unsigned)i < 100) {
buf[0] = '0' + i / 10;
buf[1] = '0' + i % 10;
tim.buf_size += 2;
} else if ((unsigned)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);
}
}
static void render(void) {
int fg = -1;
int bg = -1;
bool wide = false;
bool skip = false;
// screen buffers
struct cell* new_cells = tim_cells;
struct cell* old_cells = tim_cells;
#if ENABLE_DBUF
new_cells += (tim.frame & 1) ? MAX_CELLS : 0;
old_cells += (tim.frame & 1) ? 0 : MAX_CELLS;
#endif
tim.buf_size = 0;
for (int i = 0; i < tim.w * tim.h; i++) {
struct cell c = new_cells[i];
#if ENABLE_DBUF
// do nothing if cells in look-ahead are identical
const int la = 8; // look-ahead
if (!tim.resized && !(i % la) && (i + la < 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) {
put_str(S("\33["));
put_int((i / tim.w) + 1);
put_chr(';');
put_int((i % tim.w) + 1);
put_chr('H');
}
wide = c.wide || wide_spill;
skip = false;
// change foreground color
if (c.fg != fg) {
fg = c.fg;
put_str(S("\33[38;5;"));
put_int(fg);
put_chr('m');
}
// change background color
if (c.bg != bg) {
bg = c.bg;
put_str(S("\33[48;5;"));
put_int(bg);
put_chr('m');
}
// write character
if (c.n) {
put_str((char*)c.buf, c.n);
} else {
put_chr(' ');
}
}
// duration depends on connection and terminal rendering speed
write_str(tim.buf, tim.buf_size);
tim.resized = false;
tim.frame += 1; // frame counter
tim.cells = old_cells; // swap buffer
}
/* event loop *****************************************************************/
static bool tim_run(float fps) {
int timeout = (fps > 0) ? (int)(1000 / fps) : 0;
while (true) {
switch (tim.loop_stage) {
case 0:
// runs only once
init_terminal();
atexit(reset_terminal);
// fallthru
case 1:
// process input event
tim.start_us = time_us();
if (tim.event.type != DRAW_EVENT) {
// reset focus on mouse click
if (is_event_key(MOUSE_EVENT, LEFT_BUTTON)) {
tim.focus = 0;
}
tim.loop_stage = 2;
return true;
}
// fallthru
case 2:
// process draw event
clear_cells();
tim.event.type = DRAW_EVENT;
tim.loop_stage = 3;
return true;
case 3:
// render screen and wait for next event
render();
tim.render_us = time_us() - tim.start_us;
read_event(timeout); // blocks
// fallthru
default:
tim.loop_stage = 1;
break;
}
} // while
}