Download
271 lines 10.4 KiBTomoModified 2025-11-23 raw
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()