123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271 | # A tomo library for Terminal User Interfaces
use ./btui.c
enum Color(Normal, Black, Red, Green, Yellow, Blue, Magenta, Cyan, White, Color256(n:Byte), RGB(r,g,b:Byte))
func is_dark(c:Color -> Bool)
when c is Black
return yes
is Color256(n)
return n == 0 or n == 16 or n == 17 or (232 <= n and n <= 237)
is RGB(r,g,b)
return Int(r)+Int(g)+Int(b) < 127*3
else
return no
func is_light(c:Color -> Bool)
when c is White
return yes
is Color256, RGB
return not c.is_dark()
else
return no
struct ScreenVec2(x,y:Int)
func divided_by(v:ScreenVec2, divisor:Int -> ScreenVec2)
return ScreenVec2(v.x/divisor, v.y/divisor)
func scaled_by(v:ScreenVec2, scale:Int -> ScreenVec2)
return ScreenVec2(v.x*scale, v.y*scale)
func plus(a,b:ScreenVec2 -> ScreenVec2)
return ScreenVec2(a.x+b.x, a.y+b.y)
func minus(a,b:ScreenVec2 -> ScreenVec2)
return ScreenVec2(a.x-b.x, a.y-b.y)
enum BTUIMode(Disabled, Normal, TUI)
func set_mode(mode:BTUIMode)
if C_code:Bool`bt.mode == BTUI_MODE_UNINITIALIZED`
at_cleanup(disable)
when mode is Disabled
C_code `btui_set_mode(BTUI_MODE_DISABLED);`
is Normal
C_code `btui_set_mode(BTUI_MODE_NORMAL);`
is TUI
C_code `btui_set_mode(BTUI_MODE_TUI);`
func disable()
C_code `btui_disable();`
func force_close()
C_code `btui_force_close();`
enum ClearMode(Screen, Above, Below, Line, Left, Right)
func clear(mode:ClearMode=Screen, pos:ScreenVec2?=none)
if pos
move_cursor(pos)
when mode is Screen then C_code `btui_clear(BTUI_CLEAR_SCREEN);`
is Above then C_code `btui_clear(BTUI_CLEAR_ABOVE);`
is Below then C_code `btui_clear(BTUI_CLEAR_BELOW);`
is Line then C_code `btui_clear(BTUI_CLEAR_LINE);`
is Left then C_code `btui_clear(BTUI_CLEAR_LEFT);`
is Right then C_code `btui_clear(BTUI_CLEAR_RIGHT);`
enum TextAlign(Left, Center, Right)
func write(text:Text, pos:ScreenVec2?=none, align:TextAlign=Left)
if pos
when align is Left then pass
is Center then pos -= ScreenVec2(text.width()/2, 0)
is Right then pos -= ScreenVec2(text.width(), 0)
move_cursor(pos)
C_code `
btui_puts(@(text.as_c_string()));
btui_flush();
`
enum CursorMode(Default, BlinkingBlock, SteadyBlock, BlinkingUnderline, SteadyUnderline, BlinkingBar, SteadyBar)
func set_cursor(cursor:CursorMode)
when cursor is Default then C_code`btui_set_cursor(CURSOR_DEFAULT);`
is BlinkingBlock then C_code`btui_set_cursor(CURSOR_BLINKING_BLOCK);`
is SteadyBlock then C_code`btui_set_cursor(CURSOR_STEADY_BLOCK);`
is BlinkingUnderline then C_code`btui_set_cursor(CURSOR_BLINKING_UNDERLINE);`
is SteadyUnderline then C_code`btui_set_cursor(CURSOR_STEADY_UNDERLINE);`
is BlinkingBar then C_code`btui_set_cursor(CURSOR_BLINKING_BAR);`
is SteadyBar then C_code`btui_set_cursor(CURSOR_STEADY_BAR);`
func move_cursor(pos:ScreenVec2, relative=no)
if relative
C_code `btui_move_cursor_relative(@(Int32(pos.x)), @(Int32(pos.y)));`
else
C_code `btui_move_cursor(@(Int32(pos.x)), @(Int32(pos.y)));`
func hide_cursor()
C_code `btui_hide_cursor();`
func show_cursor()
C_code `btui_show_cursor();`
func get_size(->ScreenVec2)
return ScreenVec2(
Int(C_code:Int32`bt.width`),
Int(C_code:Int32`bt.height`),
)
func get_key(mouse_pos:&ScreenVec2?=none, timeout:Int?=none -> Text)
timeout_i32 := if timeout then Int32(timeout) else Int32(-1)
mouse_x : Int32
mouse_y : Int32
key := C_code:Text `
int key = btui_getkey(@timeout_i32, &@mouse_x, &@mouse_y);
char buf[256];
char *end = btui_keyname(key, buf);
Text$from_strn(buf, (int64_t)(end - buf));
`
if mouse_pos
mouse_pos[] = ScreenVec2(Int(mouse_x), Int(mouse_y))
return key
func flush()
C_code `btui_flush();`
func draw_linebox(pos:ScreenVec2, size:ScreenVec2)
C_code `btui_draw_linebox(@(Int32(pos.x)), @(Int32(pos.y)), @(Int32(size.x)), @(Int32(size.y)));`
func draw_shadow(pos:ScreenVec2, size:ScreenVec2)
C_code `btui_draw_shadow(@(Int32(pos.x)), @(Int32(pos.y)), @(Int32(size.x)), @(Int32(size.y)));`
func fill_box(pos:ScreenVec2, size:ScreenVec2)
C_code `btui_fill_box(@(Int32(pos.x)), @(Int32(pos.y)), @(Int32(size.x)), @(Int32(size.y)));`
func scroll(first_line:Int, last_line:Int, scroll_amount:Int)
C_code `btui_scroll(@(Int32(first_line)), @(Int32(last_line)), @(Int32(scroll_amount)));`
func style(
fg:Color?=none, bg:Color?=none,
normal:Bool?=none, bold:Bool?=none, faint:Bool?=none, italic:Bool?=none,
underline:Bool?=none, blink_slow:Bool?=none, blink_fast:Bool?=none,
reverse:Bool?=none, conceal:Bool?=none, strikethrough:Bool?=none,
fraktur:Bool?=none, double_underline:Bool?=none, framed:Bool?=none,
encircled:Bool?=none, overlined:Bool?=none,
)
if fg
when fg is RGB(r,g,b) then C_code `btui_set_fg(@r, @g, @b);`
is Color256(n) then C_code `btui_set_fg256(@n);`
is Normal then C_code `btui_set_attributes(BTUI_FG_NORMAL);`
is Black then C_code `btui_set_attributes(BTUI_FG_BLACK);`
is Red then C_code `btui_set_attributes(BTUI_FG_RED);`
is Green then C_code `btui_set_attributes(BTUI_FG_GREEN);`
is Yellow then C_code `btui_set_attributes(BTUI_FG_YELLOW);`
is Blue then C_code `btui_set_attributes(BTUI_FG_BLUE);`
is Magenta then C_code `btui_set_attributes(BTUI_FG_MAGENTA);`
is Cyan then C_code `btui_set_attributes(BTUI_FG_CYAN);`
is White then C_code `btui_set_attributes(BTUI_FG_WHITE);`
if bg
when bg is RGB(r,g,b) then C_code `btui_set_bg(@r, @g, @b);`
is Color256(n) then C_code `btui_set_bg256(@n);`
is Normal then C_code `btui_set_attributes(BTUI_BG_NORMAL);`
is Black then C_code `btui_set_attributes(BTUI_BG_BLACK);`
is Red then C_code `btui_set_attributes(BTUI_BG_RED);`
is Green then C_code `btui_set_attributes(BTUI_BG_GREEN);`
is Yellow then C_code `btui_set_attributes(BTUI_BG_YELLOW);`
is Blue then C_code `btui_set_attributes(BTUI_BG_BLUE);`
is Magenta then C_code `btui_set_attributes(BTUI_BG_MAGENTA);`
is Cyan then C_code `btui_set_attributes(BTUI_BG_CYAN);`
is White then C_code `btui_set_attributes(BTUI_BG_WHITE);`
C_code `uint64_t attr = 0;`
# Disable attributes
if bold == no or faint then C_code `attr |= BTUI_NO_BOLD_OR_FAINT;`
if italic == no or fraktur == no then C_code `attr |= BTUI_NO_ITALIC_OR_FRAKTUR;`
if underline == no then C_code `attr |= BTUI_NO_UNDERLINE;`
if blink_slow == no or blink_fast == no then C_code `attr |= BTUI_NO_BLINK;`
if reverse == no then C_code `attr |= BTUI_NO_REVERSE;`
if conceal == no then C_code `attr |= BTUI_NO_CONCEAL;`
if strikethrough == no then C_code `attr |= BTUI_NO_STRIKETHROUGH;`
if framed == no or encircled == no then C_code `attr |= BTUI_NO_FRAMED_OR_ENCIRCLED;`
if overlined == no then C_code `attr |= BTUI_NO_OVERLINED;`
# Turn on attributes
if normal == yes then C_code `attr |= BTUI_NORMAL;`
if bold == yes then C_code `attr |= BTUI_BOLD;`
if faint == yes then C_code `attr |= BTUI_FAINT;`
if italic == yes then C_code `attr |= BTUI_ITALIC;`
if underline == yes then C_code `attr |= BTUI_UNDERLINE;`
if blink_slow == yes then C_code `attr |= BTUI_BLINK_SLOW;`
if blink_fast == yes then C_code `attr |= BTUI_BLINK_FAST;`
if reverse == yes then C_code `attr |= BTUI_REVERSE;`
if conceal == yes then C_code `attr |= BTUI_CONCEAL;`
if strikethrough == yes then C_code `attr |= BTUI_STRIKETHROUGH;`
if fraktur == yes then C_code `attr |= BTUI_FRAKTUR;`
if double_underline == yes then C_code `attr |= BTUI_DOUBLE_UNDERLINE;`
if framed == yes then C_code `attr |= BTUI_FRAMED;`
if encircled == yes then C_code `attr |= BTUI_ENCIRCLED;`
if overlined == yes then C_code `attr |= BTUI_OVERLINED;`
C_code `if (attr) btui_set_attributes(attr);`
func get_bg(->Color?)
no_color : Color? = none
response := C_code:Text`
const char *query_bg = "\\x1b]11;?\\x07";
write(fileno(bt.out), query_bg, strlen(query_bg));
char buf[256] = {};
ssize_t n = read(fileno(bt.in), buf, sizeof(buf)-1);
if (n < 0) {
return @(no_color);
}
const char *response_prefix = "\\x1b]11;";
size_t prefix_len = strlen(response_prefix);
if ((size_t)n < prefix_len || strncmp(buf, response_prefix, prefix_len) != 0) {
return @(no_color);
}
Text$from_strn(buf + prefix_len, n - prefix_len)
`
value : Text
if response.starts_with("rgb:", &value) or response.starts_with("rgba:", &value)
rgb := value.split("/")
r := Byte.parse("0x"++rgb[1]!.to(2)) or return none
g := Byte.parse("0x"++rgb[2]!.to(2)) or return none
b := Byte.parse("0x"++rgb[3]!.to(2)) or return none
return Color.RGB(r,g,b)
else if response.starts_with("#", &value)
r := Byte.parse("0x"++value.from(1).to(2)) or return none
g := Byte.parse("0x"++value.from(3).to(4)) or return none
b := Byte.parse("0x"++value.from(5).to(6)) or return none
return Color.RGB(r, g, b)
else if response.starts_with("rgb(", &value) or response.starts_with("rgba(", &value)
value = value.without_suffix(")")
rgb := value.split(",")
r := Byte.parse(rgb[1] or return none) or return none
g := Byte.parse(rgb[2] or return none) or return none
b := Byte.parse(rgb[3] or return none) or return none
return Color.RGB(r,g,b)
else
return none
func suspend()
C_code `btui_suspend();`
func main()
set_mode(TUI)
size := get_size()
style(bold=yes)
write("Welcome to BTUI", size/2, Center)
style(bold=no, faint=yes)
write("Press 'q' or 'Ctrl-c' to quit", size/2 + ScreenVec2(0,1), Center)
bg := get_bg()
write("BG: $bg (dark=$((bg or Color.Normal).is_dark()))", size/2 + ScreenVec2(0,3), Center)
style(bold=no)
repeat
key := get_key()
pos := size/2 + ScreenVec2(0,2)
clear(Line, pos=pos)
style(Magenta)
write("Your input: $key", pos, Center)
when key is "q"
stop
is "Ctrl-c"
# This error message will print correctly after the cleanup code has run
fail("User interrupt")
disable()
|