Compare commits

..

16 Commits

27 changed files with 1942 additions and 1742 deletions

23
.gitignore vendored
View File

@@ -1,2 +1,21 @@
out/
tools/
# build results
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
}

BIN
256colors.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

View File

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

View File

@@ -1,15 +1,20 @@
#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
}
#include "tim.h"
i32 main(void) {
while (tim_run(0)) { // init state and start event loop
tim_scope(A, A, 24, 8) { // centered 24x8 scope
TimStyle c = { // three colors
.brd= TimColor16_Green,
.bg = TimColor16_DarkCyan,
.fg = TimColor16_White
};
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
return 0;
}

View File

@@ -1,10 +1,11 @@
// 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 BG 0xdd
#define BTN (FG << 16 | BG << 8 | FG)
#define NEW 0
#define RUN 1
@@ -13,32 +14,32 @@
typedef union {
struct {
int32_t x;
int32_t y;
i32 x;
i32 y;
};
int64_t xy;
i64 xy;
} point;
static struct {
int state; // game state (NEW RUN PAUSE OVER)
int64_t tick; // updates every 10 ms
int len; // snake length
point body[200]; // snake body
i32 state; // game state (NEW RUN PAUSE OVER)
i64 tick; // updates every 10 ms
i32 len; // snake length
point food; // food position
point look; // active direction
point body[200]; // snake body
} snek;
static void start(void) {
memset(snek.body, -1, sizeof(snek.body));
snek.len = 2;
snek.body[0] = (point){{1, tim.h / 2}};
snek.food = (point){{tim.w / 8, tim.h / 2}};
snek.body[0] = (point){{1, tim->h / 2}};
snek.food = (point){{tim->w / 8, tim->h / 2}};
snek.look = (point){{1, 0}};
}
static void game(void) {
// update game state about every 10 ms
int64_t tick = time_us() / 100000;
i64 tick = tim_time_usec() / (1000000/TPS);
if (snek.tick != tick) {
snek.tick = tick;
// move one unit
@@ -47,84 +48,84 @@ static void game(void) {
snek.body[0].y = snek.body[1].y + snek.look.y;
// self crash
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;
}
// border crash
crash |= snek.body[0].x < 0 || snek.body[0].x >= tim.w / 2 ||
snek.body[0].y < 0 || snek.body[0].y >= tim.h;
crash |= snek.body[0].x < 0 || snek.body[0].x >= tim->w / 2 ||
snek.body[0].y < 0 || snek.body[0].y >= tim->h;
snek.state = crash ? OVER : snek.state;
// food
if (snek.food.xy == snek.body[0].xy) {
snek.len = MIN(snek.len + 2, (int)ARRAY_SIZE(snek.body));
snek.food.x = rand() % (tim.w / 2 - 2) + 1;
snek.food.y = rand() % (tim.h - 2) + 1;
snek.len = MIN(snek.len + 2, (i32)ARRAY_SIZE(snek.body));
snek.food.x = rand() % (tim->w / 2 - 2) + 1;
snek.food.y = rand() % (tim->h - 2) + 1;
}
}
// draw
if (tim.event.type == DRAW_EVENT) {
if (tim->event.type == TimEvent_Draw) {
// food
draw_chr(cell(" ", 0, 0xc5), snek.food.x * 2 + 0, snek.food.y);
draw_chr(cell(" ", 0, 0xc5), snek.food.x * 2 + 1, snek.food.y);
tim_draw_chr(tim_cell(" ", 0, 0xc5), snek.food.x * 2 + 0, snek.food.y);
tim_draw_chr(tim_cell(" ", 0, 0xc5), snek.food.x * 2 + 1, snek.food.y);
// snek
struct cell s = cell(" ", 0, 0);
for (int i = 0; i < snek.len; i++) {
TimCell s = tim_cell(" ", 0, 0);
for (i32 i = 0; i < snek.len; i++) {
s.bg = (i / 2) % 2 ? 0xe3 : 0xea;
int x = snek.body[i].x * 2;
int y = snek.body[i].y;
draw_chr(s, x + 0, y);
draw_chr(s, x + 1, y);
i32 x = snek.body[i].x * 2;
i32 y = snek.body[i].y;
tim_draw_chr(s, x + 0, y);
tim_draw_chr(s, x + 1, y);
}
}
// user input
if (tim.event.type == KEY_EVENT) {
int key = tim.event.key;
if ((key == RIGHT_KEY || key == 'd') && snek.look.x != -1) {
if (tim->event.type == TimEvent_Key) {
i32 key = tim->event.key;
if ((key == TimKey_Right || key == 'd') && snek.look.x != -1) {
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}};
} 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}};
} 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}};
}
else if (key == 'q' || key == TimKey_Escape){
snek.state = PAUSE;
}
}
}
static void menu(void) {
scope(A, A, 20, 13) {
TimStyle style_button = (TimStyle){ .brd= FG, .bg = BG, .fg = FG };
tim_scope(A, A, 20, 13) {
char* lbl = snek.state == OVER ? "GAME OVER" : "SNEK - THE GAME";
char* btn = snek.state == PAUSE ? "Resume" : "Play";
label(lbl, A, 0, A, A, BTN);
if (button(btn, A, 2, 20, 5, BTN) || is_key_press(ENTER_KEY)) {
tim_label(lbl, A, 0, A, A, style_button);
if (tim_button(btn, A, 2, 20, 5, style_button) || tim_is_key_press(TimKey_Enter)) {
if (snek.state != PAUSE) {
start();
}
snek.state = RUN;
}
if (button("Exit", A, 8, 20, 5, BTN) || is_key_press(ESCAPE_KEY)) {
if (tim_button("Exit", A, 8, 20, 5, style_button) || tim_is_key_press('q') || tim_is_key_press(TimKey_Escape)) {
exit(0);
}
}
}
int main(void) {
i32 main(void) {
// draw every 10 ms
while (tim_run(10)) {
struct cell bg = cell(" ", 0, BG);
draw_lot(bg, 0, 0, tim.w, tim.h);
while (tim_run(60)) {
TimCell bg = tim_cell(" ", 0, BG);
tim_draw_lot(bg, 0, 0, tim->w, tim->h);
if (snek.state == RUN) {
game();
} else {
menu();
}
if (is_key_press(ESCAPE_KEY)) {
snek.state = PAUSE;
}
}
return 0;
}

400
include/tim.h Normal file
View File

@@ -0,0 +1,400 @@
#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>
#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 32 // 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
#pragma endregion
#pragma region types
/* first 16 colors from xterm256 supported by any terminal emulator */
enum {
TimColor16_Black = 0x00,
TimColor16_DarkRed = 0x01,
TimColor16_DarkGreen = 0x02,
TimColor16_DarkYellow = 0x03,
TimColor16_DarkBlue = 0x04,
TimColor16_DarkMagenta = 0x05,
TimColor16_DarkCyan = 0x06,
TimColor16_Gray = 0x07,
TimColor16_DarkGray = 0x08,
TimColor16_Red = 0x09,
TimColor16_Green = 0x0a,
TimColor16_Yellow = 0x0b,
TimColor16_Blue = 0x0c,
TimColor16_Magenta = 0x0d,
TimColor16_Cyan = 0x0e,
TimColor16_White = 0x0f,
};
typedef struct TimStyle {
u8 brd; // border
u8 bg; // background
u8 fg; // foreground
} TimStyle;
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 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;
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 {
bool masked; // if true prints '*' instead of buffer content
i32 cursor; // cursor position (utf8)
i32 length; // string length (utf8)
i32 capacity; // buffer size in bytes
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
// for some stupid reason gcc requires more than 3 levels of macros to concat _i_ with line number
#define _tim_cat2(A, B) A##B
#define _tim_scope_line(L) _tim_cat2(_i_, L)
#define _tim_scope_i _tim_scope_line(__LINE__)
// enter layout scope
#define tim_scope(x, y, w, h) for (\
i32 _tim_scope_i = tim_enter_scope((x), (y), (w), (h)); \
_tim_scope_i;\
_tim_scope_i = tim_exit_scope()\
) /* here goes your { code } */
// 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, TimStyle style);
// text label
// str : text - supports multiple lines
// color: background, text
void tim_label(cstr s, i32 x, i32 y, i32 w, i32 h, TimStyle style);
// button with border - returns true on click
// color: frame, background, text
bool tim_button(cstr txt, i32 x, i32 y, i32 w, i32 h, TimStyle style);
// button without border - returns true on click
// color: frame, background, text
bool tim_button_noborder(cstr txt, i32 x, i32 y, i32 w, i32 h, TimStyle style);
// 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, TimStyle style);
// 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, TimStyle style);
/// text edit - value in state
/// @param e persistent edit state, use TimEditState_construct() 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, TimStyle style);
/// @param e uninitialized state
/// @param capacity in bytes
/// @param initial_content may be NULL
void TimEditState_construct(TimEditState* e, i32 capacity, cstr initial_content);
static inline void TimEditState_destroy(TimEditState* e) {
if(!e)
return;
free(e->s);
}
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);
static inline void tim_event_consume(){
tim->event.type = TimEvent_Void;
}
#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

188
project.config Normal file
View File

@@ -0,0 +1,188 @@
#!/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"
EXEC_FILE=""
EXEC_EXT=""
# OS-specific options
case "$OS" in
WINDOWS)
EXEC_EXT=".exe"
SHARED_LIB_FILE="$PROJECT.dll"
INCLUDE="$INCLUDE "
# example: "-lSDL2 -lSDL2_image"
LINKER_LIBS=""
;;
LINUX)
SHARED_LIB_FILE="$PROJECT.so"
INCLUDE="$INCLUDE "
LINKER_LIBS=""
;;
*)
error "operating system $OS has no configuration variants"
;;
esac
# TASKS
case "$TASK" in
# ./ask question?
ask)
EXEC_FILE="ask$EXEC_EXT"
SRC_C="example/ask.c"
LINKER_LIBS="bin/tim.a"
C_ARGS="-O0 -g"
CPP_ARGS="$C_ARGS"
LINKER_ARGS="$CPP_ARGS $LINKER_LIBS"
PRE_TASK_SCRIPT=""
TASK_SCRIPT="@cbuild/default_tasks/build_exec.sh"
POST_TASK_SCRIPT=""
;;
# hello world
hello)
EXEC_FILE="hello$EXEC_EXT"
SRC_C="example/hello.c"
LINKER_LIBS="bin/tim.a"
C_ARGS="-O0 -g"
CPP_ARGS="$C_ARGS"
LINKER_ARGS="$CPP_ARGS $LINKER_LIBS"
PRE_TASK_SCRIPT=""
TASK_SCRIPT="@cbuild/default_tasks/build_exec.sh"
POST_TASK_SCRIPT=""
;;
# snake game
snek)
EXEC_FILE="snek$EXEC_EXT"
SRC_C="example/snek.c"
LINKER_LIBS="bin/tim.a"
C_ARGS="-O0 -g"
CPP_ARGS="$C_ARGS"
LINKER_ARGS="$CPP_ARGS $LINKER_LIBS"
PRE_TASK_SCRIPT=""
TASK_SCRIPT="@cbuild/default_tasks/build_exec.sh"
POST_TASK_SCRIPT=""
;;
# test
test)
EXEC_FILE="test$EXEC_EXT"
SRC_C="test/test.c"
LINKER_LIBS="bin/tim.a"
C_ARGS="-O0 -g"
CPP_ARGS="$C_ARGS"
LINKER_ARGS="$CPP_ARGS $LINKER_LIBS"
PRE_TASK_SCRIPT=""
TASK_SCRIPT="@cbuild/default_tasks/build_exec.sh"
POST_TASK_SCRIPT=""
;;
# color
color)
EXEC_FILE="color$EXEC_EXT"
SRC_C="test/color.c"
LINKER_LIBS="bin/tim.a"
C_ARGS="-O0 -g"
CPP_ARGS="$C_ARGS"
LINKER_ARGS="$CPP_ARGS $LINKER_LIBS"
PRE_TASK_SCRIPT=""
TASK_SCRIPT="@cbuild/default_tasks/build_exec.sh"
POST_TASK_SCRIPT=""
;;
# string
string)
EXEC_FILE="string$EXEC_EXT"
SRC_C="test/string.c"
LINKER_LIBS="bin/tim.a"
C_ARGS="-O0 -g"
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,34 @@
* 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
* quick start ****************************************************************
## build
1. Clone this repository.
```
git clone https://timerix.ddns.net/git/Timerix/tim.git
```
#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
} //
2. Install [cbuild](https://timerix.ddns.net/git/Timerix/cbuild/releases).
Select latest version compatible with the one in `project.config`.
Example: For `2.3.2` download latest `2.3.x`.
* layout *********************************************************************
3. Build static library
```
cd tim
cbuild build_static_lib
```
4. Build tests and examples
```
cbuild ask hello snek test color string
```
## quick start
See [example/hello.c](./example/hello.c)
## layout
The terminal's columns (x) and rows (y) are addressed by their coordinates,
the origin is in the top left corner.
@@ -56,46 +63,28 @@ take the full available space from parent.
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 *********************************************************************
## colors
Colors are stored as 8-bit values.
Most terminals support 16 basic colors. You can see them in TimColor16 enum.
There is also support for xterm-256 colors.
![xterm-256 color chart](./256colors.jpg)
## 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 *******************************************************************
The current event is stored in tim->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
style background, frame
label (str, x, y, w, h, color)
@@ -104,7 +93,7 @@ label (str, x, y, w, h, color)
str zero terminated string
x/y/w/h see layout documentation
color background, text
style background, text
button (str, x, y, w, h, color) -> bool
@@ -113,17 +102,18 @@ button (str, x, y, w, h, color) -> bool
str zero terminated string
x/y/w/h see layout documentation
color frame, background, text
style frame, background, text
edit (state, x, y, w, color) -> bool
edit (state, x, y, w, color) -> int
Draw text edit. Output is stored in state.str. Receives input events when
focused by mouse click. Escape or return relinquish focus. Returns true
when return is pressed.
Draw text edit. Output is stored in state.s.
Receives input events when focused by mouse click or by setting focus manually.
Escape or return relinquish focus.
Returns key id if received key input.
state pointer to persistent edit state struct
x/y/w see layout documentation
color f rame, background, text
style f rame, background, text
check (str, state, x, y, w, color) -> bool
@@ -134,7 +124,7 @@ check (str, state, x, y, w, color) -> bool
str zero terminated string
state pointer to persistent state variable
x/y/w see layout documentation
color check, background, text
style check, background, text
radio (str, state, v, x, y, w, color) -> bool
@@ -146,10 +136,9 @@ radio (str, state, v, x, y, w, color) -> bool
state pointer to persistent state variable
v unique state value
x/y/w see layout documentation
color radio, background, text
* functions ******************************************************************
style radio, background, text
## functions
tim_run (fps) -> bool
Process events and render frame. Blocks until input is received or the
@@ -161,29 +150,27 @@ tim_run (fps) -> bool
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))
if (tim_is_key_press(ESCAPE_KEY))
exit(0);
fps frames per second
is_key_press (key) -> bool
tim_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
tim_time_usec () -> int64
Returns monotonic clock value in microseconds. Not affected by summer
time or leap seconds.
* useful links ***************************************************************
## useful links
https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
https://learn.microsoft.com/en-us/windows/console/
* bugs ***********************************************************************
## bugs
- Double buffering is still new, set ENABLE_DBUF to 0 if you see glitches
- Double width characters like 彁 are not fully supported. Terminals do not
handle these consistently and there is no portable way to reliably
@@ -193,8 +180,7 @@ https://learn.microsoft.com/en-us/windows/console/
- Zero width code points are not supported
- Windows cmd.exe resize events may be delayed
* compatibility **************************************************************
## compatibility
emulator | support | remarks
------------------|---------|----------------------------------
Alacritty | ? |
@@ -222,8 +208,7 @@ https://learn.microsoft.com/en-us/windows/console/
XTerm | full | XTerm is law
Zutty | full |
* license ********************************************************************
## license
MIT License
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;
}
}
}

123
src/edit.c Executable file
View File

@@ -0,0 +1,123 @@
#include "tim.h"
void TimEditState_construct(TimEditState* e, i32 capacity, cstr initial_content){
e->masked = false;
e->length = initial_content ? tim_utf8_len(initial_content) : 0;
e->cursor = e->length;
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_consume();
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, TimStyle style) {
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, style.brd, style.bg);
i32 s_x = r.x + 2, s_y = r.y + 1, s_w = r.w - 3, s_w_sub1 = s_w - 1;
char* s = e->s;
bool focused = tim->focus == e;
if (focused) {
// rewind long buffer so cursor is at the most right cell
s = e->s + tim_utf8_pos(e->s, e->cursor - s_w_sub1);
}
if(e->masked){
// draw '*' instead of buffer characters
TimCell cell_masked = tim_cell("*", style.fg, style.bg);
i32 w_mask = MIN(s_w, e->length);
tim_draw_row(cell_masked, s_x, s_y, w_mask);
}
else {
// draw part of buffer content
tim_draw_str(s, s_x, s_y, s_w, style.fg, style.bg);
}
if (focused) {
// invert color of last cell
i32 cur = MIN(s_w_sub1, e->cursor);
tim_draw_invert(s_x + cur, s_y, 1);
}
}
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

81
src/widgets.c Executable file
View File

@@ -0,0 +1,81 @@
#include "tim.h"
void tim_frame(i32 x, i32 y, i32 w, i32 h, TimStyle style) {
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, style.brd, style.bg);
}
}
void tim_label(cstr s, i32 x, i32 y, i32 w, i32 h, TimStyle style) {
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(" ", style.fg, style.bg);
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, TimStyle style) {
i32 txt_w = tim_utf8_len(txt);
w = (w == A) ? (txt_w + 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, style.brd, style.bg);
tim_draw_str(txt, r.x + (w - txt_w) / 2, r.y + h / 2, w, style.fg, style.bg);
}
return tim_is_mouse_click_over(r);
}
bool tim_button_noborder(cstr txt, i32 x, i32 y, i32 w, i32 h, TimStyle style) {
i32 txt_w = tim_utf8_len(txt);
w = (w == A) ? txt_w : w;
h = (h == A) ? 1 : h;
TimRect r = tim_scope_rect_to_absolute(x, y, w, h);
if (tim->event.type == TimEvent_Draw) {
tim_draw_str(txt, r.x + (w - txt_w) / 2, r.y + h / 2, w, style.fg, style.bg);
}
return tim_is_mouse_click_over(r);
}
bool tim_checkbox(cstr txt, i32* state, i32 x, i32 y, i32 w, TimStyle style) {
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, style.fg, style.bg);
tim_draw_str(st, r.x + 1, r.y, 1, style.brd, style.bg);
tim_draw_str(txt, r.x + 4, r.y, r.w - 4, style.fg, style.bg);
}
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, TimStyle style) {
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, style.fg, style.bg);
tim_draw_str(st, r.x + 1, r.y, 1, style.brd, style.bg);
tim_draw_str(txt, r.x + 4, r.y, r.w - 4, style.fg, style.bg);
}
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;
ReadConsoleInputW(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,30 @@
// 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};
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)) {
for (int i = 0; i < 16; i++) {
for (i32 i = 0; i < 16; 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);
}
for (int i = 0; i < 108; i++) {
for (i32 i = 0; i < 108; i++) {
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);
}
if (is_key_press('q') || is_key_press(ESCAPE_KEY)) {
if (tim_is_key_press('q') || tim_is_key_press(TimKey_Escape)) {
exit(1);
}
}
return 0;
}

View File

@@ -1,71 +1,71 @@
// 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)
int main(void) {
i32 main(void) {
(void)tim_run;
TEST(ztrlen(NULL) == 0);
TEST(ztrlen("") == 0);
TEST(ztrlen("$") == 1);
TEST(ztrlen("£") == 2);
TEST(ztrlen("") == 3);
TEST(ztrlen("𐍈") == 4);
TEST(tim_ztrlen(NULL) == 0);
TEST(tim_ztrlen("") == 0);
TEST(tim_ztrlen("$") == 1);
TEST(tim_ztrlen("£") == 2);
TEST(tim_ztrlen("") == 3);
TEST(tim_ztrlen("𐍈") == 4);
TEST(bsr8(128) == 0);
TEST(bsr8(64) == 1);
TEST(bsr8(1) == 7);
TEST(bsr8(0) == 8);
TEST(tim_bsr8(128) == 0);
TEST(tim_bsr8(64) == 1);
TEST(tim_bsr8(1) == 7);
TEST(tim_bsr8(0) == 8);
TEST(utfchr(NULL) == 0);
TEST(utfchr("") == 0);
TEST(utfchr("$") == 0x24);
TEST(utfchr("£") == 0xA3);
TEST(utfchr("И") == 0x418);
TEST(utfchr("") == 0x939);
TEST(utfchr("") == 0x20AC);
TEST(utfchr("") == 0xD55C);
TEST(utfchr("𐍈") == 0x10348);
TEST(tim_utf8_to_i32(NULL) == 0);
TEST(tim_utf8_to_i32("") == 0);
TEST(tim_utf8_to_i32("$") == 0x24);
TEST(tim_utf8_to_i32("£") == 0xA3);
TEST(tim_utf8_to_i32("И") == 0x418);
TEST(tim_utf8_to_i32("") == 0x939);
TEST(tim_utf8_to_i32("") == 0x20AC);
TEST(tim_utf8_to_i32("") == 0xD55C);
TEST(tim_utf8_to_i32("𐍈") == 0x10348);
TEST(utflen(NULL) == 0);
TEST(utflen("") == 0);
TEST(utflen("$") == 1);
TEST(utflen("$$") == 2);
TEST(utflen("") == 2);
TEST(utflen("$€𐍈") == 3);
TEST(tim_utf8_len(NULL) == 0);
TEST(tim_utf8_len("") == 0);
TEST(tim_utf8_len("$") == 1);
TEST(tim_utf8_len("$$") == 2);
TEST(tim_utf8_len("") == 2);
TEST(tim_utf8_len("$€𐍈") == 3);
TEST(utfpos(NULL, 0) == 0);
TEST(utfpos("ab", -1) == 0);
TEST(utfpos("äbc", 0) == 0);
TEST(utfpos("äbc", 1) == 2);
TEST(utfpos("äbc", 2) == 3);
TEST(utfpos("äbc", 9) == 4);
TEST(tim_utf8_pos(NULL, 0) == 0);
TEST(tim_utf8_pos("ab", -1) == 0);
TEST(tim_utf8_pos("äbc", 0) == 0);
TEST(tim_utf8_pos("äbc", 1) == 2);
TEST(tim_utf8_pos("äbc", 2) == 3);
TEST(tim_utf8_pos("äbc", 9) == 4);
TEST(scan_str(NULL).lines == 0);
TEST(scan_str("").lines == 0);
TEST(scan_str("abc").lines == 1);
TEST(scan_str("a\no").lines == 2);
TEST(scan_str("a").width == 1);
TEST(scan_str("äß\no").width == 2);
TEST(tim_scan_str(NULL).lines == 0);
TEST(tim_scan_str("").lines == 0);
TEST(tim_scan_str("abc").lines == 1);
TEST(tim_scan_str("a\no").lines == 2);
TEST(tim_scan_str("a").width == 1);
TEST(tim_scan_str("äß\no").width == 2);
struct line ln = {.str = "foo\nbar"};
TEST(next_line(&ln) == true);
TimLine ln = {.s = "foo\nbar"};
TEST(tim_next_line(&ln) == true);
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(next_line(&ln) == false);
TEST(tim_next_line(&ln) == false);
TEST(is_wide_perhaps(NULL, 0) == false);
TEST(is_wide_perhaps(U("")) == false);
TEST(is_wide_perhaps(U("$")) == false);
TEST(is_wide_perhaps(U("£")) == false);
TEST(is_wide_perhaps(U("")) == false);
TEST(is_wide_perhaps(U("")) == true);
TEST(is_wide_perhaps(U("")) == false);
TEST(is_wide_perhaps(U("")) == true);
TEST(is_wide_perhaps(U("𐍈")) == true);
TEST(tim_utf8_is_wide_perhaps(NULL, 0) == false);
TEST(tim_utf8_is_wide_perhaps(U("")) == false);
TEST(tim_utf8_is_wide_perhaps(U("$")) == false);
TEST(tim_utf8_is_wide_perhaps(U("£")) == false);
TEST(tim_utf8_is_wide_perhaps(U("")) == false);
TEST(tim_utf8_is_wide_perhaps(U("")) == true);
TEST(tim_utf8_is_wide_perhaps(U("")) == false);
TEST(tim_utf8_is_wide_perhaps(U("")) == true);
TEST(tim_utf8_is_wide_perhaps(U("𐍈")) == true);
return 0;
}

View File

@@ -1,119 +1,133 @@
#include "../tim.h"
#include "tim.h"
static inline void test_screen(struct event* e) {
static struct event me;
static struct event ke;
static int render_us;
char buf[64];
static TimEditState ed1;
static TimEditState ed2;
ke = (e->type == KEY_EVENT) ? *e : ke;
me = (e->type == MOUSE_EVENT) ? *e : me;
static inline void test_screen(TimEvent* e) {
static TimEvent me = {0};
static TimEvent ke = {0};
static i32 render_us = 0;
char buf[64] = {0};
if(e->type == TimEvent_Key)
ke = *e;
if(e->type == TimEvent_Mouse)
me = *e;
// positioning
label("+", 0, 0, A, A, 0xf);
label("+", ~0, 0, A, A, 0xf);
label("+", 0, ~0, A, A, 0xf);
label("+", ~0, ~0, A, A, 0xf);
label("+", A, A, A, A, 0xf);
label("-", 0, A, A, A, 0xf);
label("-", ~0, A, A, A, 0xf);
label("|", A, 0, A, A, 0xf);
label("|", A, ~0, A, A, 0xf);
static TimStyle style_default = { .fg = TimColor16_White };
tim_label("+", 0, 0, A, A, style_default);
tim_label("+", ~0, 0, A, A, style_default);
tim_label("+", 0, ~0, A, A, style_default);
tim_label("+", ~0, ~0, A, A, style_default);
tim_label("+", A, A, A, A, style_default);
tim_label("-", 0, A, A, A, style_default);
tim_label("-", ~0, A, A, A, style_default);
tim_label("|", A, 0, A, A, style_default);
tim_label("|", A, ~0, A, A, style_default);
// some information
sprintf(buf, "screen: %dx%d", tim.w, tim.h);
label(buf, 2, 0, A, A, 0xf);
sprintf(buf, "frame : [%c] %d", ": "[tim.frame & 1], tim.frame);
label(buf, 2, 1, A, A, 0xf);
sprintf(buf, "key : [%d] %s", ke.key, ke.str + (ke.key < 32));
label(buf, 2, 2, A, A, 0xf);
sprintf(buf, "screen: %dx%d", tim->w, tim->h);
tim_label(buf, 2, 0, A, A, style_default);
sprintf(buf, "frame : [%c] %d", ": "[tim->frame & 1], tim->frame);
tim_label(buf, 2, 1, A, A, style_default);
sprintf(buf, "key : [%d] %s", ke.key, ke.s + (ke.key < 32));
tim_label(buf, 2, 2, A, A, style_default);
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, style_default);
sprintf(buf, "input : %02hhx %02hhx %02hhx %02hhx %02hhx %02hhx %02hhx %02hhx",
e->str[0], e->str[1], e->str[2], e->str[3], e->str[4], e->str[5], e->str[6], e->str[7]);
label(buf, 2, 4, A, A, 0xf);
e->s[0], e->s[1], e->s[2], e->s[3], e->s[4], e->s[5], e->s[6], e->s[7]);
tim_label(buf, 2, 4, A, A, style_default);
// lower right
render_us += tim.render_us;
sprintf(buf, "%d µs (Ø %d µs)", tim.render_us, render_us / MAX(tim.frame, 1));
label(buf, ~2, ~2, A, A, 0xf);
sprintf(buf, "%d cells (%.0f %%)", tim.w * tim.h, 100.0 * tim.w * tim.h / MAX_CELLS);
label(buf, ~2, ~1, A, A, 0xf);
sprintf(buf, "%d bytes (%.0f %%)", tim.buf_size, 100.0 * tim.buf_size / MAX_BUF);
label(buf, ~2, ~0, A, A, 0xf);
render_us += tim->render_us;
sprintf(buf, "%d µs (Ø %d µs)", tim->render_us, render_us / MAX(tim->frame, 1));
tim_label(buf, ~2, ~2, A, A, style_default);
sprintf(buf, "%d cells (%.0f %%)", tim->w * tim->h, 100.0 * tim->w * tim->h / TIM_MAX_CELLS);
tim_label(buf, ~2, ~1, A, A, style_default);
sprintf(buf, "%d bytes (%.0f %%)", tim->buf_size, 100.0 * tim->buf_size / TIM_MAX_BUF);
tim_label(buf, ~2, ~0, A, A, style_default);
// 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, style_default);
// colors
scope (1, 5, 16, 5) {
frame(0, 0, ~0, ~0, 0xf);
label(" Red ", 1, 1, 7, A, 0x0900);
label(" ", 8, 1, 7, A, 0xc400);
label(" Green ", 1, 2, 7, A, 0x0a00);
label(" ", 8, 2, 7, A, 0x2e00);
label(" Blue ", 1, 3, 7, A, 0x0c00);
label(" ", 8, 3, 7, A, 0x1500);
tim_scope(1, 5, 16, 5) {
tim_frame(0, 0, ~0, ~0, style_default);
tim_label(" Red ", 1, 1, 7, A, (TimStyle){ .bg = 0x09 });
tim_label(" ", 8, 1, 7, A, (TimStyle){ .bg = 0xc4 });
tim_label(" Green ", 1, 2, 7, A, (TimStyle){ .bg = 0x0a });
tim_label(" ", 8, 2, 7, A, (TimStyle){ .bg = 0x2e });
tim_label(" Blue ", 1, 3, 7, A, (TimStyle){ .bg = 0x0c });
tim_label(" ", 8, 3, 7, A, (TimStyle){ .bg = 0x15 });
}
// button
static uint64_t bc = 0x100;
if (button("Click Me", 17, 5, 16, 5, bc)) {
bc = (bc + 0x100) & 0xff00;
static TimStyle style_button = { .bg = 0x01 };
if (tim_button("Click Me", 17, 5, 16, 5, style_button)) {
style_button.bg = (style_button.bg + 1) & 0xff;
}
// edit
static struct edit ed1 = {.str = "Edit 1"};
static struct edit ed2 = {0};
edit(&ed1, 1, 10, 32, 0xff00ff);
static TimStyle style_edit = { .brd = 0xff, .fg = 0xff };
tim_edit(&ed1, 1, 10, 32, style_edit);
sprintf(buf, "cursor: %d length: %d", ed1.cursor, ed1.length);
label(buf, 2, 13, A, A, 0xf);
edit(&ed2, 1, 14, 32, 0xff00ff);
label(ed2.str, 2, 17, A, A, 0xf);
tim_label(buf, 2, 13, A, A, style_default);
tim_edit(&ed2, 1, 14, 32, style_edit);
tim_label(ed2.s, 2, 17, A, A, style_default);
// checkbox
static int chk[2] = {-1, 1};
check("Check 1", &chk[0], 1, 18, A, 0xa000f);
check("Check 2", &chk[1], 14, 18, A, 0xa000f);
static TimStyle style_checkbox = { .brd = TimColor16_Cyan, .fg = TimColor16_White };
static i32 chk[2] = {-1, 1};
tim_checkbox("Check 1", &chk[0], 1, 18, A, style_checkbox);
tim_checkbox("Check 2", &chk[1], 14, 18, A, style_checkbox);
// radiobox
static int rad = 0;
radio("Radio 1", &rad, 1, 1, 19, A, 0xa000f);
radio("Radio 2", &rad, 2, 14, 19, A, 0xa000f);
radio("Radio 3", &rad, 3, 1, 20, A, 0xa000f);
radio("Radio 4", &rad, 4, 14, 20, A, 0xa000f);
static i32 rad = 0;
tim_radiobutton("Radio 1", &rad, 1, 1, 19, A, style_checkbox);
tim_radiobutton("Radio 2", &rad, 2, 14, 19, A, style_checkbox);
tim_radiobutton("Radio 3", &rad, 3, 1, 20, A, style_checkbox);
tim_radiobutton("Radio 4", &rad, 4, 14, 20, A, style_checkbox);
// scope nesting
scope(~1, 1, 20, 10) {
scope(0, 0, 10, 5) {
frame(0, 0, ~0, ~0, 0x9);
tim_scope(~1, 1, 20, 10) {
tim_scope(0, 0, 10, 5) {
tim_frame(0, 0, ~0, ~0, (TimStyle){ .brd = TimColor16_Red });
}
scope(~0, 0, 10, 5) {
frame(0, 0, ~0, ~0, 0xa);
tim_scope(~0, 0, 10, 5) {
tim_frame(0, 0, ~0, ~0, (TimStyle){ .brd = TimColor16_Green });
}
scope(~0, ~0, 10, 5) {
frame(0, 0, ~0, ~0, 0xb);
tim_scope(~0, ~0, 10, 5) {
tim_frame(0, 0, ~0, ~0, (TimStyle){ .brd = TimColor16_Yellow });
}
scope(0, ~0, 10, 5) {
frame(0, 0, ~0, ~0, 0xc);
tim_scope(0, ~0, 10, 5) {
tim_frame(0, 0, ~0, ~0, (TimStyle){ .brd = TimColor16_Blue });
}
}
// funny characters
scope (~1, ~3, 11, 5) {
frame(0, 0, ~0, ~0, 0xf);
label("123456789", 1, 1, 9, A, 0x0f05);
label("$£ह€𐍈6789", 1, 2, A, A, 0x0f05);
label("圍棋56789", 1, 3, A, A, 0x0f05);
static TimStyle style_funny = { .bg = TimColor16_White, .fg = TimColor16_Magenta };
tim_scope(~1, ~3, 11, 5) {
tim_frame(0, 0, ~0, ~0, style_default);
tim_label("123456789", 1, 1, 9, A, style_funny);
tim_label("$£ह€𐍈6789", 1, 2, A, A, style_funny);
tim_label("圍棋56789", 1, 3, A, A, style_funny);
}
}
int main(void) {
i32 main(void) {
TimEditState_construct(&ed1, 32, "Edit 1");
TimEditState_construct(&ed2, 32, "");
while (tim_run(1.5)) {
test_screen(&tim.event);
if (is_key_press('q') || is_key_press(ESCAPE_KEY)) {
test_screen(&tim->event);
if (tim_is_key_press('q') || tim_is_key_press(TimKey_Escape)) {
break;
}
}
}
TimEditState_destroy(&ed1);
TimEditState_destroy(&ed2);
return 0;
}

View File

@@ -1,9 +1,9 @@
// Test character width.
#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);
if (cp < 0x80) {
@@ -29,33 +29,33 @@ static int cp_to_utf8(int32_t cp, char* s) {
return -1;
}
static int cursor_pos() {
static i32 cursor_pos() {
write(STDOUT_FILENO, S("\33[6n"));
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') {
return -1;
}
int r = atoi(buf + 2);
int c = atoi(buf + 4 + (r > 9));
i32 r = atoi(buf + 2);
i32 c = atoi(buf + 4 + (r > 9));
return c;
}
int main(int argc, char** argv) {
i32 main(i32 argc, char** argv) {
assert(argc == 2);
(void)tim_run;
FILE* f = fopen(argv[1], "w");
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"));
char buf[5] = {0};
int n = cp_to_utf8(i, buf);
i32 n = cp_to_utf8(i, buf);
write(STDOUT_FILENO, buf, n);
int w = cursor_pos() - 1;
i32 w = cursor_pos() - 1;
if (w) {
fprintf(f, "u+%06x %d %s\n", i, w, buf);
} else {
@@ -63,7 +63,8 @@ int main(int argc, char** argv) {
}
}
reset_terminal();
tim_reset_terminal();
fclose(f);
return 0;
}

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