Compare commits

...

11 Commits

Author SHA1 Message Date
161f655492 added dependency config 2026-01-09 06:11:44 +05:00
6e946200a2 TimKey return type 2026-01-09 06:06:01 +05:00
4150a609e2 finished renaming things 2026-01-09 06:03:59 +05:00
a60172ef9a added link to original repository 2026-01-09 05:46:59 +05:00
bd23c66607 alloc big buffers on heap 2026-01-09 05:42:08 +05:00
01df4abcfb finally cbuild instead of make 2026-01-09 05:18:53 +05:00
3f75902aa0 moved code from header to source files 2026-01-09 05:04:35 +05:00
50940e5190 pragma region 2026-01-09 02:47:09 +05:00
025cede850 moved tim.h to include/ 2026-01-09 02:35:38 +05:00
f7b4809db8 renamed types and constants 2026-01-09 02:21:50 +05:00
b725182544 Renamed every 'str' fiend and variable to 's'. Implemented edit_init. 2026-01-09 00:46:42 +05:00
26 changed files with 1755 additions and 1707 deletions

23
.gitignore vendored
View File

@@ -1,2 +1,21 @@
out/ # build results
tools/ bin/
obj/
# IDE files
.vs/
.vshistory/
.editorconfig
*.user
*.vcxproj.filters
# other files
.old*/
old/
tmp/
temp/
*.tmp
*.temp
logs/
log/
*.log

1
.vscode/.gitignore vendored Executable file
View File

@@ -0,0 +1 @@
settings.json

15
.vscode/c_cpp_properties.json vendored Executable file
View File

@@ -0,0 +1,15 @@
{
"configurations": [
{
"name": "all",
"defines": [],
"includePath": [
"src",
"include",
"${default}"
],
"cStandard": "c99"
}
],
"version": 4
}

View File

@@ -1,7 +1,7 @@
// Display a yes/no dialog with a message. Returns with 0 when yes was clicked. // Display a yes/no dialog with a message. Returns with 0 when yes was clicked.
// syntax: ./ask "message" // syntax: ./ask "message"
#include "../tim.h" #include "tim.h"
// colors // colors
#define CTXT 0xf // text black, white #define CTXT 0xf // text black, white
@@ -9,42 +9,41 @@
#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
struct text 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); tim_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 (tim_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 (tim_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(ESCAPE_KEY)) { if (tim_is_key_press('q') || tim_is_key_press(TimKey_Escape)) {
exit(1); exit(2);
} }
} }
} }
} }

View File

@@ -1,15 +1,15 @@
#include "../tim.h" // one header, no lib #include "tim.h"
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
}
i32 main(void) {
while (tim_run(0)) { // init state and start event loop
tim_scope(A, A, 24, 8) { // centered 24x8 scope
u64 c = 0x0a060f; // three colors
tim_frame(0, 0, ~0, ~0, c); // draw frame for scope
tim_label("Greetings!", A, 2, A, A, c); // label in top center
if (tim_button("OK", A, ~1, 8, A, c)) // button in bottom center
return 0; // exit on button click
if (tim_is_key_press('q')) // ctrl-c is masked
return 0; // exit on 'q' press
}
} // atexit() cleanup
}

View File

@@ -1,6 +1,8 @@
// Simple game of snake to show how to do animation and draw cells. // Simple game of snake to show how to do animation and draw cells.
#include "../tim.h" #include "tim.h"
#define TPS 10
#define FG 0x10 #define FG 0x10
#define BG 0xdd #define BG 0xdd
@@ -13,32 +15,32 @@
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 food; // food position point food; // food position
point look; // active direction point look; // active direction
point body[200]; // snake body
} snek; } snek;
static void start(void) { static void start(void) {
memset(snek.body, -1, sizeof(snek.body)); memset(snek.body, -1, sizeof(snek.body));
snek.len = 2; snek.len = 2;
snek.body[0] = (point){{1, tim.h / 2}}; snek.body[0] = (point){{1, tim->h / 2}};
snek.food = (point){{tim.w / 8, tim.h / 2}}; snek.food = (point){{tim->w / 8, tim->h / 2}};
snek.look = (point){{1, 0}}; snek.look = (point){{1, 0}};
} }
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() / (1000000/TPS);
if (snek.tick != tick) { if (snek.tick != tick) {
snek.tick = tick; snek.tick = tick;
// move one unit // move one unit
@@ -47,74 +49,77 @@ 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
crash |= snek.body[0].x < 0 || snek.body[0].x >= tim.w / 2 || crash |= snek.body[0].x < 0 || snek.body[0].x >= tim->w / 2 ||
snek.body[0].y < 0 || snek.body[0].y >= tim.h; snek.body[0].y < 0 || snek.body[0].y >= tim->h;
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;
} }
} }
// draw // draw
if (tim.event.type == DRAW_EVENT) { 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
struct cell 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 == RIGHT_KEY || 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 == LEFT_KEY || key == 'a') && snek.look.x != 1) { } else if ((key == TimKey_Left || key == 'a') && snek.look.x != 1) {
snek.look = (point){{-1, 0}}; snek.look = (point){{-1, 0}};
} else if ((key == DOWN_KEY || key == 's') && snek.look.y != -1) { } else if ((key == TimKey_Down || key == 's') && snek.look.y != -1) {
snek.look = (point){{0, 1}}; snek.look = (point){{0, 1}};
} else if ((key == UP_KEY || 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); tim_label(lbl, A, 0, A, A, BTN);
if (button(btn, A, 2, 20, 5, BTN) || is_key_press(ENTER_KEY)) { if (tim_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(ESCAPE_KEY)) { if (tim_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)) {
struct cell 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,9 +127,5 @@ int main(void) {
menu(); menu();
} }
if (is_key_press(ESCAPE_KEY)) {
snek.state = PAUSE;
}
} }
} }

351
include/tim.h Normal file
View File

@@ -0,0 +1,351 @@
#pragma once
#pragma region include
#include <limits.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifdef _WIN32
// windows
#define TIM_WINDOWS
#define _CRT_SECURE_NO_WARNINGS
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
// fix windows.h name clash, coincidentally they have the same values
#undef TimEvent_Key // 0x0001
#undef TimEvent_Mouse // 0x0002
#ifdef _MSC_VER
// disable integer conversion warnings
#pragma warning(disable:4244)
#endif
#else
// unix
#define TIM_UNIX
#include <poll.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <unistd.h>
#endif
typedef int8_t i8;
typedef int16_t i16;
typedef int32_t i32;
typedef int64_t i64;
typedef uint8_t u8;
typedef uint16_t u16;
typedef uint32_t u32;
typedef uint64_t u64;
typedef float f32;
typedef double f64;
typedef const char* cstr;
#if !defined(__cplusplus) && !defined(bool)
# define bool u8
# define false 0
# define true 1
#endif
#ifdef __cplusplus
extern "C" {
#endif
#pragma endregion
#pragma region constants
#define TIM_ENABLE_DBUF 1 // double buffering
#define TIM_MAX_SCOPE 20 // max scope nesting
#define TIM_MAX_CELLS 0x20000 // size of screen buffer
#define TIM_MAX_BUF (TIM_MAX_CELLS * 4) // size of output buffer
#define A INT_MAX // auto center / width / height
typedef enum TimEventType {
TimEvent_Void, // an event was consumed
TimEvent_Draw, // draw screen
TimEvent_Key, // a key was pressed
TimEvent_Mouse, // mouse button, scroll or move
} TimEventType;
enum {
TimKey_MouseButtonLeft = 1,
TimKey_Backspace = 8,
TimKey_Tab = 9,
TimKey_Enter = 13,
TimKey_Escape = 27,
TimKey_Insert = -1,
TimKey_Delete = -2,
TimKey_Home = -3,
TimKey_End = -4,
TimKey_PageUp = -5,
TimKey_PageDown = -6,
TimKey_Up = -7,
TimKey_Down = -8,
TimKey_Left = -9,
TimKey_Right = -10,
};
// key code or 32-bit unicode char
typedef i32 TimKey;
#pragma endregion
#pragma region types
typedef struct TimCell {
u8 fg; // foreground color
u8 bg; // background color
u8 wide; // wide or following wide character
u8 n; // number of bytes in buf
u8 buf[4]; // utf8 code point
} TimCell;
typedef struct TimRect {
i32 x; // x coordinate (left = 0)
i32 y; // y coordinate (top = 0)
i32 w; // width
i32 h; // height
} TimRect;
typedef struct TimText {
i32 size; // size in bytes without terminator
i32 width; // widest line
i32 lines; // number of lines
} TimText;
typedef struct TimLine {
cstr s; // input and parse state
cstr line; // line strings, not terminated
i32 size; // line size in bytes
i32 width; // line width in glyph
} TimLine;
typedef struct TimEvent {
TimEventType type;
TimKey key; // used by TimEvent_Key and TimEvent_Mouse
i32 x; // used by TimEvent_Mouse
i32 y; // used by TimEvent_Mouse
char s[32]; // string representation of key, used by TimEvent_Key
} TimEvent;
typedef struct TimEditState {
i32 cursor; // cursor position (utf8)
i32 length; // string length (utf8)
i32 capacity; // buffer size
char* s; // zero terminated buffer
} TimEditState;
typedef struct TimState {
i32 w; // screen width
i32 h; // screen height
i32 frame; // frame counter
TimEvent event; // current event
void* focus; // focused element
i32 loop_stage; // loop stage
bool resized; // screen was resized
i32 scope; // current scope
TimRect scopes[TIM_MAX_SCOPE]; // scope stack
TimCell* cells; // current screen buffer (first or second half of cells_double_buf)
TimCell* cells_double_buf; // pointer to double buffer
char* buf; // final output buffer
i32 buf_size; // position in write buffer
i64 start_us; // render start time
i32 render_us; // elapsed render time
#ifdef TIM_UNIX
struct termios attr; // initial attributes
i32 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
} TimState;
#pragma endregion
#pragma region 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
#pragma endregion
#pragma region general
// global statem initialized by tim_run()
extern TimState* tim;
bool tim_run(f32 fps);
void tim_reset_terminal(void);
i64 tim_time_usec(void);
#pragma endregion
#pragma region scope
// enter layout scope
#define tim_scope(x, y, w, h) \
for (i32 _i = tim_enter_scope((x), (y), (w), (h)); _i; _i = tim_exit_scope())
// convert relative (scoped) to absolute (screen) coordinates
TimRect tim_scope_rect_to_absolute(i32 x, i32 y, i32 w, i32 h);
// enter scope and push coordinates on stack
i32 tim_enter_scope(i32 x, i32 y, i32 w, i32 h);
// exit scope and pop stack
i32 tim_exit_scope(void);
#pragma endregion
#pragma region widgets
// TODO: create enum TimColor and struct TimStyle
// frame
// color: background, frame
void tim_frame(i32 x, i32 y, i32 w, i32 h, u64 color);
// text label
// str : text - supports multiple lines
// color: background, text
void tim_label(cstr s, i32 x, i32 y, i32 w, i32 h, u64 color);
// button - returns true on click
// color: frame, background, text
bool tim_button(cstr txt, i32 x, i32 y, i32 w, i32 h, u64 color);
// check box - returns true when clicked
// txt : text label
// state: persistent state, 0 unchecked, -1 semi checked, !0: checked
// color: check, background, text
bool tim_checkbox(cstr txt, i32* state, i32 x, i32 y, i32 w, u64 color);
// radio button - return true when clicked
// txt : text label
// state: persistent state, selected if *state == v
// v : value
// color: radio, background, text
bool tim_radiobutton(cstr txt, i32* state, i32 v, i32 x, i32 y, i32 w, u64 color);
/// text edit - value in state
/// @param e persistent edit state, use TimEditState_init() to create new state
/// @param color frame, background, text
/// @return key id or 0
TimKey tim_edit(TimEditState* e, i32 x, i32 y, i32 w, u64 color);
void TimEditState_init(TimEditState* e, i32 capacity, cstr initial_content);
void TimEditState_insert(TimEditState* e, cstr s);
void TimEditState_delete(TimEditState* e);
#pragma endregion
#pragma region event
// returns true if event was of type and key
bool tim_is_event_key(TimEventType type, TimKey key);
// returns true if event was press of key
bool tim_is_key_press(TimKey key);
// returns true if mouse event was over r
bool tim_is_mouse_over(TimRect r);
// returns true if event is mouse left-down and over r
bool tim_is_mouse_click_over(TimRect r);
#pragma endregion
#pragma region drawing
// create cell from utf8 code point with fg and bg colors
TimCell tim_cell(cstr s, u8 fg, u8 bg);
// clear cell buffer
void tim_clear_cells(void);
// draw cell at position
void tim_draw_chr(TimCell cell, i32 x, i32 y);
// draw row of cells
void tim_draw_row(TimCell cell, i32 x, i32 y, i32 w);
// draw column of cells
void tim_draw_col(TimCell cell, i32 x, i32 y, i32 h);
// fill lot (area) of cells
void tim_draw_lot(TimCell cell, i32 x, i32 y, i32 w, i32 h);
// draw string to line, tags potential wide characters
void tim_draw_str(cstr s, i32 x, i32 y, i32 w, u8 fg, u8 bg);
// draw box of ascii cell characters
void tim_draw_box(i32 x, i32 y, i32 w, i32 h, u8 fg, u8 bg);
// invert fg and bg colors of line of cells
void tim_draw_invert(i32 x, i32 y, i32 w);
#pragma endregion
#pragma region string
// like strlen, returns 0 on NULL or i32 overflow
i32 tim_ztrlen(cstr s);
// decode one utf8 code point
i32 tim_utf8_to_i32(cstr s);
// number of utf8 code points
i32 tim_utf8_len(cstr s);
// index of utf8 code point at pos
i32 tim_utf8_pos(cstr s, i32 pos);
// true if utf8 code point could be wide
bool tim_utf8_is_wide_perhaps(const u8* s, i32 n);
// bit scan reverse, count leading zeros
i32 tim_bsr8(u8 x);
// scan string for width and lines
TimText tim_scan_str(cstr s);
// iterate through lines, false when end is reached
bool tim_next_line(TimLine* l);
#pragma endregion
#pragma region internal
void tim_write_str(cstr s, i32 size);
void tim_update_screen_size(void);
void tim_init_terminal(void);
void tim_read_event(i32 timeout_ms);
#pragma endregion
#ifdef __cplusplus
}
#endif

View File

@@ -1,20 +0,0 @@
all: out/test out/string out/color out/hello out/ask out/snek
out/test: test/test.c out
$(CC) $< -Wall $(CFLAGS) -o $@
out/string: test/string.c out
$(CC) $< -Wall $(CFLAGS) -o $@
out/color: test/color.c out
$(CC) $< -Wall $(CFLAGS) -o $@
out/hello: example/hello.c out
$(CC) $< -Wall $(CFLAGS) -o $@
out/ask: example/ask.c out
$(CC) $< -Wall $(CFLAGS) -o $@
out/snek: example/snek.c out
$(CC) $< -Wall $(CFLAGS) -o $@
out:
mkdir -p out
clean:
rm -rf out

140
project.config Normal file
View File

@@ -0,0 +1,140 @@
#!/usr/bin/env bash
CBUILD_VERSION=2.3.2
PROJECT="tim"
CMP_C="gcc"
CMP_CPP="g++"
STD_C="c99"
STD_CPP="c++11"
WARN_C="-Wall -Wextra
-Wduplicated-branches
-Wduplicated-cond
-Wformat=2
-Wmissing-include-dirs
-Wshadow
-Werror=return-type
-Werror=pointer-arith
-Werror=init-self
-Werror=incompatible-pointer-types"
WARN_CPP="$WARN_C"
SRC_C="$(find src -name '*.c')"
SRC_CPP="$(find src -name '*.cpp')"
# Directory with dependency configs.
# See cbuild/example_dependency_configs
DEPENDENCY_CONFIGS_DIR='dependencies'
# List of dependency config files in DEPENDENCY_CONFIGS_DIR separated by space.
ENABLED_DEPENDENCIES=''
# OBJDIR structure:
# ├── objects/ - Compiled object files. Cleans on each call of build task
# ├── static_libs/ - Symbolic links to static libraries used by linker. Cleans on each call of build task.
# ├── static_libs/ - Symbolic links to dynamic libraries used by linker. Cleans on each call of build task.
# └── profile/ - gcc *.gcda profiling info files
OBJDIR="obj"
OUTDIR="bin"
STATIC_LIB_FILE="$PROJECT.a"
# example: "-I./include -I$DEPENDENCIES_DIR/libexample"
INCLUDE="-Iinclude -Isrc"
# OS-specific options
case "$OS" in
WINDOWS)
EXEC_FILE="snek.exe"
SHARED_LIB_FILE="$PROJECT.dll"
INCLUDE="$INCLUDE "
# example: "-lSDL2 -lSDL2_image"
LINKER_LIBS=""
;;
LINUX)
EXEC_FILE="snek"
SHARED_LIB_FILE="$PROJECT.so"
INCLUDE="$INCLUDE "
LINKER_LIBS=""
;;
*)
error "operating system $OS has no configuration variants"
;;
esac
# TASKS
case "$TASK" in
# creates executable using profiling info if it exists
build_exec)
SRC_C="$SRC_C example/snek.c"
# -flto applies more optimizations across object files
# -flto=auto is needed to multithreaded copilation
# -fuse-linker-plugin is required to use static libs with lto
# -fprofile-use enables compiler to use profiling info files to optimize executable
# -fprofile-prefix-path sets path where profiling info about objects are be saved
# -fdata-sections -ffunction-sections -Wl,--gc-sections removes unused code
C_ARGS="-O2 -flto=auto -fuse-linker-plugin -fprofile-use -fprofile-prefix-path=$(realpath $OBJDIR)/objects -fdata-sections -ffunction-sections -Wl,--gc-sections"
CPP_ARGS="$C_ARGS"
LINKER_ARGS="$CPP_ARGS $LINKER_LIBS"
PRE_TASK_SCRIPT=""
TASK_SCRIPT="@cbuild/default_tasks/build_exec.sh"
POST_TASK_SCRIPT="@cbuild/default_tasks/strip_exec.sh"
;;
# creates executable with debug info and no optimizations
build_exec_dbg)
SRC_C="$SRC_C example/snek.c"
C_ARGS="-O0 -g3"
CPP_ARGS="$C_ARGS"
LINKER_ARGS="$CPP_ARGS $LINKER_LIBS"
PRE_TASK_SCRIPT=""
TASK_SCRIPT="@cbuild/default_tasks/build_exec.sh"
POST_TASK_SCRIPT=""
;;
# creates shared library
build_shared_lib)
C_ARGS="-O2 -fpic -flto -shared"
CPP_ARGS="$C_ARGS"
LINKER_ARGS="$CPP_ARGS $LINKER_LIBS -Wl,-soname,$SHARED_LIB_FILE"
PRE_TASK_SCRIPT=""
TASK_SCRIPT="@cbuild/default_tasks/build_shared_lib.sh"
POST_TASK_SCRIPT=""
;;
# creates shared library with debug symbols and no optimizations
build_shared_lib_dbg)
C_ARGS="-O0 -g3 -fpic -shared"
CPP_ARGS="$C_ARGS"
LINKER_ARGS="$CPP_ARGS $LINKER_LIBS -Wl,-soname,$SHARED_LIB_FILE"
PRE_TASK_SCRIPT=""
TASK_SCRIPT="@cbuild/default_tasks/build_shared_lib.sh"
POST_TASK_SCRIPT=""
;;
# creates static library
build_static_lib)
C_ARGS="-O2 -fpic -fdata-sections -ffunction-sections"
CPP_ARGS="$C_ARGS"
PRE_TASK_SCRIPT=""
TASK_SCRIPT="@cbuild/default_tasks/build_static_lib.sh"
POST_TASK_SCRIPT=""
;;
# creates static library with debug symbols and no optimizations
build_static_lib_dbg)
C_ARGS="-O0 -g3"
CPP_ARGS="$C_ARGS"
PRE_TASK_SCRIPT=""
TASK_SCRIPT="@cbuild/default_tasks/build_static_lib.sh"
POST_TASK_SCRIPT=""
;;
# rebuilds specified dependencies
# EXAMPLE: `cbuild rebuild_dependencies=libexample1,fonts`
# 'all' can be specified to rebuild all dependencies
rebuild_dependencies)
TASK_SCRIPT="@cbuild/default_tasks/rebuild_dependencies.sh"
;;
# deletes generated files
clean)
TASK_SCRIPT="@cbuild/default_tasks/clean.sh"
;;
# nothing to do
"" | no_task)
;;
# unknown task
*)
error "task <$PROJECT/$TASK> not found"
;;
esac

11
project.config.user.default Executable file
View File

@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# Project user config is ignored by git.
# Here you can add variables that users might want to change
# on their local machine, without commiting to the repository.
# Directory where you install dependencies.
# Do not confuse with DEPENDENCY_CONFIGS_DIR
# Example:
# libexample source code is at `../libexample`, and dependency config
# that specifies how to build this lib is at `dependencies/libexample.config`
DEPENDENCIES_DIR=".."

View File

@@ -1,27 +1,14 @@
* about ********************************************************************** # tim
Fork of https://codeberg.org/chuvok/tim.h
tim.h is a portable library to create simple terminal applications ## about
tim is a portable library to create simple terminal applications
Demo video: https://asciinema.org/a/zn3p0dsVCOQOzwY1S9gDfyaxQ Demo video: https://asciinema.org/a/zn3p0dsVCOQOzwY1S9gDfyaxQ
* quick start **************************************************************** ## quick start
See [example/hello.c](./example/hello.c)
#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 *********************************************************************
## layout
The terminal's columns (x) and rows (y) are addressed by their coordinates, The terminal's columns (x) and rows (y) are addressed by their coordinates,
the origin is in the top left corner. the origin is in the top left corner.
@@ -56,8 +43,7 @@ take the full available space from parent.
The layout automatically adopts to terminal window resize events. The layout automatically adopts to terminal window resize events.
* colors ********************************************************************* ## colors
Most elements have a uint64 color argument which holds up to eight 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. Typically byte 0 is the text color and byte 1 is the background color.
@@ -71,15 +57,14 @@ should be used if consistency is important.
xterm-256 color chart xterm-256 color chart
https://upload.wikimedia.org/wikipedia/commons/1/15/Xterm_256color_chart.svg 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 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. immediately followed by a draw event in order to make changes visible.
Some elements need to consume events, for example edit consumes the key Some elements need to consume events, for example edit consumes the key
event when focused in order to prevent other key handlers on acting on them. event when focused in order to prevent other key handlers on acting on them.
The current event is stored in tim.event. The current event is stored in tim->event.
event | cause event | cause
-------------|----------------------- -------------|-----------------------
@@ -88,8 +73,7 @@ The current event is stored in tim.event.
MOUSE_EVENT | mouse click MOUSE_EVENT | mouse click
VOID_EVENT | consumed event VOID_EVENT | consumed event
* elements ******************************************************************* ## elements
frame (x, y, w, h, color) frame (x, y, w, h, color)
Draw an empty frame and fill area. Draw an empty frame and fill area.
@@ -115,11 +99,12 @@ button (str, x, y, w, h, color) -> bool
x/y/w/h see layout documentation x/y/w/h see layout documentation
color frame, background, text 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 Draw text edit. Output is stored in state.s.
focused by mouse click. Escape or return relinquish focus. Returns true Receives input events when focused by mouse click or by setting focus manually.
when return is pressed. Escape or return relinquish focus.
Returns key id if received key input.
state pointer to persistent edit state struct state pointer to persistent edit state struct
x/y/w see layout documentation x/y/w see layout documentation
@@ -148,8 +133,7 @@ radio (str, state, v, x, y, w, color) -> bool
x/y/w see layout documentation x/y/w see layout documentation
color radio, background, text color radio, background, text
* functions ****************************************************************** ## functions
tim_run (fps) -> bool tim_run (fps) -> bool
Process events and render frame. Blocks until input is received or the Process events and render frame. Blocks until input is received or the
@@ -161,29 +145,27 @@ 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.
* useful links *************************************************************** ## useful links
https://invisible-island.net/xterm/ctlseqs/ctlseqs.html https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
https://learn.microsoft.com/en-us/windows/console/ 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 buffering is still new, set ENABLE_DBUF to 0 if you see glitches
- Double width characters like 彁 are not fully supported. Terminals do not - Double width characters like 彁 are not fully supported. Terminals do not
handle these consistently and there is no portable way to reliably handle these consistently and there is no portable way to reliably
@@ -193,8 +175,7 @@ https://learn.microsoft.com/en-us/windows/console/
- Zero width code points are not supported - Zero width code points are not supported
- Windows cmd.exe resize events may be delayed - Windows cmd.exe resize events may be delayed
* compatibility ************************************************************** ## compatibility
emulator | support | remarks emulator | support | remarks
------------------|---------|---------------------------------- ------------------|---------|----------------------------------
Alacritty | ? | Alacritty | ? |
@@ -222,8 +203,7 @@ https://learn.microsoft.com/en-us/windows/console/
XTerm | full | XTerm is law XTerm | full | XTerm is law
Zutty | full | Zutty | full |
* license ******************************************************************** ## license
MIT License MIT License
Copyright (c) MMXXIV Chu'vok <chuvok@maeppi.e4ward.com> Copyright (c) MMXXIV Chu'vok <chuvok@maeppi.e4ward.com>

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 TimEditState_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 TimEditState_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 TimKey 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:
TimEditState_delete(e);
break;
case TimKey_Backspace:
if (e->cursor > 0) {
e->cursor -= 1;
TimEditState_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 >= ' ') {
TimEditState_insert(e, tim->event.s);
}
break;
}
return tim->event.key;
}
TimKey tim_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);
}

182
src/loop.c Executable file
View File

@@ -0,0 +1,182 @@
#include "tim.h"
static void tim_render(void);
TimState* tim = NULL;
static void tim_init(void){
tim = (TimState*)malloc(sizeof(TimState));
memset(tim, 0, sizeof(TimState));
size_t cdb_size = (TIM_MAX_CELLS << TIM_ENABLE_DBUF);
tim->cells_double_buf = (TimCell*)malloc(cdb_size * sizeof(TimCell));
tim->cells = tim->cells_double_buf;
tim->buf = (char*)malloc(TIM_MAX_BUF);
}
static void tim_deinit(void){
if(!tim)
return;
free(tim->cells_double_buf);
free(tim->buf);
free(tim);
tim = NULL;
}
bool tim_run(f32 fps) {
if(tim == NULL){
tim_init();
atexit(tim_deinit);
}
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
}
// 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);
}
}
static 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 tim_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 tim_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 tim_checkbox(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 tim_radiobutton(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

@@ -1,30 +1,29 @@
// Shows xterm-256 color palette. // Shows xterm-256 color palette.
#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(ESCAPE_KEY)) { if (tim_is_key_press('q') || tim_is_key_press(TimKey_Escape)) {
exit(1); exit(1);
} }
} }
} }

View File

@@ -1,71 +1,70 @@
// Test string functions. // Test string functions.
#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);
struct line ln = {.str = "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,119 +1,120 @@
#include "../tim.h" #include "tim.h"
static inline void test_screen(struct event* e) { static inline void test_screen(TimEvent* e) {
static struct event me; static TimEvent me;
static struct event 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;
me = (e->type == MOUSE_EVENT) ? *e : me; me = (e->type == MOUSE_EVENT) ? *e : me;
// positioning // positioning
label("+", 0, 0, A, A, 0xf); tim_label("+", 0, 0, A, A, 0xf);
label("+", ~0, 0, A, A, 0xf); tim_label("+", ~0, 0, A, A, 0xf);
label("+", 0, ~0, A, A, 0xf); tim_label("+", 0, ~0, A, A, 0xf);
label("+", ~0, ~0, A, A, 0xf); tim_label("+", ~0, ~0, A, A, 0xf);
label("+", A, A, A, A, 0xf); tim_label("+", A, A, A, A, 0xf);
label("-", 0, A, A, A, 0xf); tim_label("-", 0, A, A, A, 0xf);
label("-", ~0, A, A, A, 0xf); tim_label("-", ~0, A, A, A, 0xf);
label("|", A, 0, A, A, 0xf); tim_label("|", A, 0, A, A, 0xf);
label("|", A, ~0, A, A, 0xf); tim_label("|", A, ~0, A, A, 0xf);
// some information // some information
sprintf(buf, "screen: %dx%d", tim.w, tim.h); sprintf(buf, "screen: %dx%d", tim->w, tim->h);
label(buf, 2, 0, A, A, 0xf); tim_label(buf, 2, 0, A, A, 0xf);
sprintf(buf, "frame : [%c] %d", ": "[tim.frame & 1], tim.frame); sprintf(buf, "frame : [%c] %d", ": "[tim->frame & 1], tim->frame);
label(buf, 2, 1, A, A, 0xf); tim_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); tim_label(buf, 2, 2, A, A, 0xf);
sprintf(buf, "mouse : [%d] %d:%d", me.key, me.x, me.y); sprintf(buf, "mouse : [%d] %d:%d", me.key, me.x, me.y);
label(buf, 2, 3, A, A, 0xf); tim_label(buf, 2, 3, A, A, 0xf);
sprintf(buf, "input : %02hhx %02hhx %02hhx %02hhx %02hhx %02hhx %02hhx %02hhx", 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); tim_label(buf, 2, 4, A, A, 0xf);
// lower right // lower right
render_us += tim.render_us; render_us += tim->render_us;
sprintf(buf, "%d µs (Ø %d µs)", tim.render_us, render_us / MAX(tim.frame, 1)); sprintf(buf, "%d µs (Ø %d µs)", tim->render_us, render_us / MAX(tim->frame, 1));
label(buf, ~2, ~2, A, A, 0xf); tim_label(buf, ~2, ~2, A, A, 0xf);
sprintf(buf, "%d cells (%.0f %%)", tim.w * tim.h, 100.0 * tim.w * tim.h / MAX_CELLS); sprintf(buf, "%d cells (%.0f %%)", tim->w * tim->h, 100.0 * tim->w * tim->h / TIM_MAX_CELLS);
label(buf, ~2, ~1, A, A, 0xf); tim_label(buf, ~2, ~1, A, A, 0xf);
sprintf(buf, "%d bytes (%.0f %%)", tim.buf_size, 100.0 * tim.buf_size / MAX_BUF); sprintf(buf, "%d bytes (%.0f %%)", tim->buf_size, 100.0 * tim->buf_size / TIM_MAX_BUF);
label(buf, ~2, ~0, A, A, 0xf); tim_label(buf, ~2, ~0, A, A, 0xf);
// multi line label // multi line label
label("multi\nliñe\nlabël", 24, 1, A, A, 0xf); tim_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); tim_label(" Red ", 1, 1, 7, A, 0x0900);
label(" ", 8, 1, 7, A, 0xc400); tim_label(" ", 8, 1, 7, A, 0xc400);
label(" Green ", 1, 2, 7, A, 0x0a00); tim_label(" Green ", 1, 2, 7, A, 0x0a00);
label(" ", 8, 2, 7, A, 0x2e00); tim_label(" ", 8, 2, 7, A, 0x2e00);
label(" Blue ", 1, 3, 7, A, 0x0c00); tim_label(" Blue ", 1, 3, 7, A, 0x0c00);
label(" ", 8, 3, 7, A, 0x1500); tim_label(" ", 8, 3, 7, A, 0x1500);
} }
// button // button
static uint64_t bc = 0x100; static u64 bc = 0x100;
if (button("Click Me", 17, 5, 16, 5, bc)) { if (tim_button("Click Me", 17, 5, 16, 5, bc)) {
bc = (bc + 0x100) & 0xff00; bc = (bc + 0x100) & 0xff00;
} }
// edit // edit
static struct edit ed1 = {.str = "Edit 1"}; static TimEditState ed1;
static struct edit ed2 = {0}; static TimEditState ed2;
edit(&ed1, 1, 10, 32, 0xff00ff); TimEditState_init(&ed1, 32, "Edit 1");
TimEditState_init(&ed2, 32, "");
tim_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); tim_label(buf, 2, 13, A, A, 0xf);
edit(&ed2, 1, 14, 32, 0xff00ff); tim_edit(&ed2, 1, 14, 32, 0xff00ff);
label(ed2.str, 2, 17, A, A, 0xf); tim_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); tim_checkbox("Check 1", &chk[0], 1, 18, A, 0xa000f);
check("Check 2", &chk[1], 14, 18, A, 0xa000f); tim_checkbox("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); tim_radiobutton("Radio 1", &rad, 1, 1, 19, A, 0xa000f);
radio("Radio 2", &rad, 2, 14, 19, A, 0xa000f); tim_radiobutton("Radio 2", &rad, 2, 14, 19, A, 0xa000f);
radio("Radio 3", &rad, 3, 1, 20, A, 0xa000f); tim_radiobutton("Radio 3", &rad, 3, 1, 20, A, 0xa000f);
radio("Radio 4", &rad, 4, 14, 20, A, 0xa000f); tim_radiobutton("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); tim_label("123456789", 1, 1, 9, A, 0x0f05);
label("$£ह€𐍈6789", 1, 2, A, A, 0x0f05); tim_label("$£ह€𐍈6789", 1, 2, A, A, 0x0f05);
label("圍棋56789", 1, 3, A, A, 0x0f05); tim_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(ESCAPE_KEY)) { if (tim_is_key_press('q') || tim_is_key_press(TimKey_Escape)) {
break; break;
} }
} }
} }

View File

@@ -1,9 +1,9 @@
// Test character width. // Test character width.
#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);
} }

19
tim.config Executable file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# This is a dependency config.
# You can copy it to another cbuild project to add this lib as dependency.
DEP_WORKING_DIR="$DEPENDENCIES_DIR/tim"
if [[ "$TASK" = *_dbg ]]; then
dep_build_target="build_static_lib_dbg"
else
dep_build_target="build_static_lib"
fi
DEP_PRE_BUILD_COMMAND=""
DEP_BUILD_COMMAND="cbuild $dep_build_target"
DEP_POST_BUILD_COMMAND=""
DEP_CLEAN_COMMAND="cbuild clean"
DEP_DYNAMIC_OUT_FILES=""
DEP_STATIC_OUT_FILES="bin/tim.a"
DEP_OTHER_OUT_FILES=""
PRESERVE_OUT_DIRECTORY_STRUCTURE=false

1421
tim.h

File diff suppressed because it is too large Load Diff