├── .gitignore ├── widgets ├── box.odin ├── panel.odin ├── selector.odin ├── progress.odin └── button.odin ├── term ├── platform_darwin.odin ├── platform_linux.odin ├── platform_posix.odin ├── term.odin ├── platform_windows.odin └── input.odin ├── examples ├── user_input │ └── main.odin ├── window │ └── main.odin ├── stress │ └── main.odin ├── paint │ └── main.odin ├── fire │ └── main.odin └── snake │ └── main.odin ├── LICENSE ├── input.odin ├── README.md ├── kitty_keyboard └── kitty_keyboard.odin ├── sdl3 ├── backend.odin └── input.odin ├── raw └── raw.odin └── termcl.odin /.gitignore: -------------------------------------------------------------------------------- 1 | TermCL 2 | *.bin 3 | -------------------------------------------------------------------------------- /widgets/box.odin: -------------------------------------------------------------------------------- 1 | package widgets 2 | 3 | import t ".." 4 | 5 | Box_Style :: struct { 6 | bg, fg: Maybe(t.Any_Color), 7 | y, x: uint, 8 | height, width: uint, 9 | vertical: rune, 10 | horizontal: rune, 11 | top_left: rune, 12 | top_right: rune, 13 | bottom_left: rune, 14 | bottom_right: rune, 15 | } 16 | 17 | Box :: struct { 18 | _screen: ^t.Screen, 19 | _window: t.Window, 20 | } 21 | 22 | box_init :: proc(screen: ^t.Screen) -> Box { 23 | return Box{_screen = screen, _window = t.init_window(0, 0, 0, 0)} 24 | } 25 | 26 | box_destroy :: proc(b: ^Box) { 27 | t.destroy_window(&b._window) 28 | } 29 | 30 | Box_Border_Style :: enum {} 31 | // allow to choose from premade border styles 32 | box_set_style :: proc() {} 33 | 34 | _box_set_layout :: proc() {} 35 | box_blit :: proc() {} 36 | 37 | -------------------------------------------------------------------------------- /term/platform_darwin.odin: -------------------------------------------------------------------------------- 1 | package term 2 | 3 | import t ".." 4 | import "core:c" 5 | import "core:sys/darwin" 6 | 7 | /* 8 | Get terminal size 9 | 10 | NOTE: this functional does syscalls to figure out the size of the terminal. 11 | For most use cases, passing `Screen` to `get_window_size` achieves the same result 12 | and doesn't need to do any system calls. 13 | 14 | You should only use this function if you don't have access to `Screen` and still somehow 15 | need to figure out the terminal size. Otherwise this function might or might not cause 16 | your program to slow down a bit due to OS context switching. 17 | */ 18 | get_term_size :: proc() -> t.Window_Size { 19 | winsize :: struct { 20 | ws_row, ws_col: c.ushort, 21 | ws_xpixel, ws_ypixel: c.ushort, 22 | } 23 | 24 | w: winsize 25 | if darwin.syscall_ioctl(1, darwin.TIOCGWINSZ, &w) != 0 { 26 | panic("Failed to get terminal size") 27 | } 28 | 29 | win := t.Window_Size { 30 | h = uint(w.ws_row), 31 | w = uint(w.ws_col), 32 | } 33 | 34 | return win 35 | } 36 | 37 | -------------------------------------------------------------------------------- /term/platform_linux.odin: -------------------------------------------------------------------------------- 1 | package term 2 | 3 | import t ".." 4 | import "core:c" 5 | import "core:sys/linux" 6 | 7 | /* 8 | Get terminal size 9 | 10 | NOTE: this functional does syscalls to figure out the size of the terminal. 11 | For most use cases, passing `Screen` to `get_window_size` achieves the same result 12 | and doesn't need to do any system calls. 13 | 14 | You should only use this function if you don't have access to `Screen` and still somehow 15 | need to figure out the terminal size. Otherwise this function might or might not cause 16 | your program to slow down a bit due to OS context switching. 17 | */ 18 | get_term_size :: proc() -> t.Window_Size { 19 | winsize :: struct { 20 | ws_row, ws_col: c.ushort, 21 | ws_xpixel, ws_ypixel: c.ushort, 22 | } 23 | 24 | w: winsize 25 | if linux.ioctl(linux.STDOUT_FILENO, linux.TIOCGWINSZ, cast(uintptr)&w) != 0 { 26 | panic("Failed to get terminal size") 27 | } 28 | 29 | win := t.Window_Size { 30 | h = uint(w.ws_row), 31 | w = uint(w.ws_col), 32 | } 33 | 34 | return win 35 | } 36 | 37 | -------------------------------------------------------------------------------- /examples/user_input/main.odin: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import t "../.." 4 | import tb "../../term" 5 | 6 | main :: proc() { 7 | s := t.init_screen(tb.VTABLE) 8 | defer t.destroy_screen(&s) 9 | t.set_term_mode(&s, .Raw) 10 | 11 | t.clear(&s, .Everything) 12 | t.move_cursor(&s, 0, 0) 13 | t.write(&s, "Please type something or move your mouse to start") 14 | t.blit(&s) 15 | 16 | for { 17 | t.clear(&s, .Everything) 18 | defer t.blit(&s) 19 | 20 | t.move_cursor(&s, 0, 0) 21 | t.write(&s, "Press `Esc` to exit") 22 | 23 | raw_input := tb.read_raw_blocking(&s) or_continue 24 | t.move_cursor(&s, 2, 0) 25 | t.writef(&s, "Raw: %v", raw_input) 26 | 27 | kb_input, kb_has_input := tb.parse_keyboard_input(raw_input) 28 | if kb_has_input { 29 | t.move_cursor(&s, 4, 0) 30 | t.write(&s, "Keyboard: ") 31 | t.writef(&s, "%v", kb_input) 32 | if kb_input.key == .Escape do return 33 | } 34 | 35 | mouse_input, mouse_has_input := tb.parse_mouse_input(raw_input) 36 | if mouse_has_input { 37 | t.move_cursor(&s, 6, 0) 38 | t.write(&s, "Mouse: ") 39 | t.writef(&s, "%v", mouse_input) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2025, Raph 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /examples/window/main.odin: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import t "../.." 4 | import tb "../../term" 5 | 6 | main :: proc() { 7 | s := t.init_screen(tb.VTABLE) 8 | defer t.destroy_screen(&s) 9 | t.set_term_mode(&s, .Raw) 10 | t.hide_cursor(true) 11 | 12 | 13 | msg := "As you can see here, if the text continues, it will eventually wrap around instead of going outside the bounds of the window" 14 | termsize := t.get_term_size() 15 | 16 | window := t.init_window(termsize.h / 2 - 3, termsize.w / 2 - 26 / 2, 6, 26) 17 | defer t.destroy_window(&window) 18 | 19 | main_loop: for { 20 | t.clear(&s, .Everything) 21 | defer t.blit(&window) 22 | defer t.blit(&s) 23 | 24 | t.set_text_style(&s, {.Bold}) 25 | t.set_color_style(&s, .Red, nil) 26 | t.move_cursor(&s, 0, 0) 27 | t.write(&s, "Press 'Q' to exit") 28 | t.move_cursor(&s, 1, 0) 29 | 30 | t.reset_styles(&s) 31 | t.set_color_style(&s, nil, nil) 32 | t.write(&s, msg) 33 | 34 | t.set_color_style(&window, .Black, .White) 35 | t.clear(&window, .Everything) 36 | // this proves that no matter what the window will never be overflowed by moving the cursor 37 | t.move_cursor(&window, 2, 0) 38 | t.write_string(&window, msg) 39 | 40 | kb_input, kb_ok := t.read(&s).(t.Keyboard_Input) 41 | 42 | if kb_ok do #partial switch kb_input.key { 43 | case .Arrow_Left, .A: 44 | window.x_offset -= 1 45 | case .Arrow_Right, .D: 46 | window.x_offset += 1 47 | case .Arrow_Up, .W: 48 | window.y_offset -= 1 49 | case .Arrow_Down, .S: 50 | window.y_offset += 1 51 | 52 | case .Q: 53 | break main_loop 54 | } 55 | } 56 | } 57 | 58 | -------------------------------------------------------------------------------- /term/platform_posix.odin: -------------------------------------------------------------------------------- 1 | #+build linux, darwin, netbsd, freebsd, openbsd 2 | package term 3 | 4 | import t ".." 5 | import os "core:os/os2" 6 | import "core:sys/posix" 7 | 8 | Terminal_State :: struct { 9 | state: posix.termios, 10 | } 11 | 12 | get_terminal_state :: proc() -> (Terminal_State, bool) { 13 | termstate: posix.termios 14 | ok := posix.tcgetattr(posix.STDIN_FILENO, &termstate) == .OK 15 | return Terminal_State{state = termstate}, ok 16 | } 17 | 18 | change_terminal_mode :: proc(screen: ^t.Screen, mode: t.Term_Mode) { 19 | termstate, ok := get_terminal_state() 20 | if !ok { 21 | panic("failed to get terminal state") 22 | } 23 | 24 | raw := termstate.state 25 | 26 | switch mode { 27 | case .Raw: 28 | raw.c_lflag -= {.ECHO, .ICANON, .ISIG, .IEXTEN} 29 | raw.c_iflag -= {.ICRNL, .IXON} 30 | raw.c_oflag -= {.OPOST} 31 | 32 | // probably meaningless on modern terminals but apparently it's good practice 33 | raw.c_iflag -= {.BRKINT, .INPCK, .ISTRIP} 34 | raw.c_cflag |= {.CS8} 35 | 36 | case .Cbreak: 37 | raw.c_lflag -= {.ECHO, .ICANON} 38 | 39 | case .Restored: 40 | raw = orig_termstate.state 41 | } 42 | 43 | if posix.tcsetattr(posix.STDIN_FILENO, .TCSAFLUSH, &raw) != .OK { 44 | panic("failed to set new terminal state") 45 | } 46 | } 47 | 48 | raw_read :: proc(buf: []byte) -> (user_input: []byte, has_input: bool) { 49 | stdin_pollfd := posix.pollfd { 50 | fd = posix.STDIN_FILENO, 51 | events = {.IN}, 52 | } 53 | 54 | if posix.poll(&stdin_pollfd, 1, 8) > 0 { 55 | bytes_read, err := os.read_ptr(os.stdin, raw_data(buf), len(buf)) 56 | if err != nil { 57 | panic("failing to get user input") 58 | } 59 | return buf[:bytes_read], true 60 | } 61 | 62 | return {}, false 63 | } 64 | -------------------------------------------------------------------------------- /input.odin: -------------------------------------------------------------------------------- 1 | package termcl 2 | 3 | Input :: union { 4 | Keyboard_Input, 5 | Mouse_Input, 6 | } 7 | 8 | Key :: enum { 9 | None, 10 | Arrow_Left, 11 | Arrow_Right, 12 | Arrow_Up, 13 | Arrow_Down, 14 | Page_Up, 15 | Page_Down, 16 | Home, 17 | End, 18 | Insert, 19 | Delete, 20 | F1, 21 | F2, 22 | F3, 23 | F4, 24 | F5, 25 | F6, 26 | F7, 27 | F8, 28 | F9, 29 | F10, 30 | F11, 31 | F12, 32 | Escape, 33 | Num_0, 34 | Num_1, 35 | Num_2, 36 | Num_3, 37 | Num_4, 38 | Num_5, 39 | Num_6, 40 | Num_7, 41 | Num_8, 42 | Num_9, 43 | Enter, 44 | Tab, 45 | Backspace, 46 | A, 47 | B, 48 | C, 49 | D, 50 | E, 51 | F, 52 | G, 53 | H, 54 | I, 55 | J, 56 | K, 57 | L, 58 | M, 59 | N, 60 | O, 61 | P, 62 | Q, 63 | R, 64 | S, 65 | T, 66 | U, 67 | V, 68 | W, 69 | X, 70 | Y, 71 | Z, 72 | Minus, 73 | Plus, 74 | Equal, 75 | Open_Paren, 76 | Close_Paren, 77 | Open_Curly_Bracket, 78 | Close_Curly_Bracket, 79 | Open_Square_Bracket, 80 | Close_Square_Bracket, 81 | Colon, 82 | Semicolon, 83 | Slash, 84 | Backslash, 85 | Single_Quote, 86 | Double_Quote, 87 | Period, 88 | Asterisk, 89 | Backtick, 90 | Space, 91 | Dollar, 92 | Exclamation, 93 | Hash, 94 | Percent, 95 | Ampersand, 96 | Tick, 97 | Underscore, 98 | Caret, 99 | Comma, 100 | Pipe, 101 | At, 102 | Tilde, 103 | Less_Than, 104 | Greater_Than, 105 | Question_Mark, 106 | } 107 | 108 | Mod :: enum { 109 | None, 110 | Alt, 111 | Ctrl, 112 | Shift, 113 | } 114 | 115 | Keyboard_Input :: struct { 116 | mod: Mod, 117 | key: Key, 118 | } 119 | 120 | Mouse_Event :: enum { 121 | Pressed, 122 | Released, 123 | } 124 | 125 | Mouse_Key :: enum { 126 | None, 127 | Left, 128 | Middle, 129 | Right, 130 | Scroll_Up, 131 | Scroll_Down, 132 | } 133 | 134 | Mouse_Input :: struct { 135 | event: bit_set[Mouse_Event], 136 | mod: bit_set[Mod], 137 | key: Mouse_Key, 138 | coord: Cursor_Position, 139 | } 140 | 141 | -------------------------------------------------------------------------------- /widgets/panel.odin: -------------------------------------------------------------------------------- 1 | package widgets 2 | 3 | import t ".." 4 | 5 | Panel_Item_Position :: enum { 6 | None, 7 | Left, 8 | Right, 9 | Center, 10 | } 11 | 12 | Panel_Item :: struct { 13 | position: Panel_Item_Position, 14 | content: string, 15 | } 16 | 17 | Panel_Style :: struct { 18 | fg, bg: t.Any_Color, 19 | space_between: uint, 20 | } 21 | 22 | Panel :: struct { 23 | _window: t.Window, 24 | items: []Panel_Item, 25 | style: Panel_Style, 26 | } 27 | 28 | panel_init :: proc(screen: ^t.Screen) -> Panel { 29 | termsize := t.get_term_size() 30 | return Panel { 31 | _window = t.init_window(0, 0, 1, 0), 32 | style = {space_between = 2, fg = .Black, bg = .White}, 33 | } 34 | } 35 | 36 | _panel_set_layout :: proc(panel: ^Panel) { 37 | termsize := t.get_term_size() 38 | panel._window.y_offset = termsize.h 39 | panel._window.width = termsize.w 40 | } 41 | 42 | panel_destroy :: proc(panel: ^Panel) { 43 | t.destroy_window(&panel._window) 44 | } 45 | 46 | panel_blit :: proc(panel: ^Panel) { 47 | _panel_set_layout(panel) 48 | 49 | defer t.reset_styles(&panel._window) 50 | t.set_color_style(&panel._window, panel.style.fg, panel.style.bg) 51 | t.clear(&panel._window, .Everything) 52 | 53 | cursor_on_left := t.Cursor_Position { 54 | x = 1, 55 | y = 0, 56 | } 57 | cursor_on_right := t.Cursor_Position { 58 | x = panel._window.width.? - 1, 59 | y = 0, 60 | } 61 | 62 | t.move_cursor(&panel._window, 0, 1) 63 | center_items := make([dynamic]Panel_Item) 64 | defer delete(center_items) 65 | 66 | drawing_panel: for item in panel.items { 67 | switch item.position { 68 | case .None: 69 | continue drawing_panel 70 | 71 | case .Left: 72 | t.move_cursor(&panel._window, cursor_on_left.y, cursor_on_left.x) 73 | t.write(&panel._window, item.content) 74 | cursor_pos := t.get_cursor_position(&panel._window) 75 | cursor_pos.x += panel.style.space_between 76 | cursor_on_left = cursor_pos 77 | 78 | case .Right: 79 | t.move_cursor(&panel._window, cursor_on_right.y, cursor_on_right.x - len(item.content)) 80 | cursor_on_right.x -= len(item.content) + panel.style.space_between 81 | t.write(&panel._window, item.content) 82 | 83 | case .Center: 84 | append(¢er_items, item) 85 | } 86 | } 87 | 88 | // the space in between each item will always be num_of_items - 1 89 | center_items_width: uint = (len(center_items) - 1) * panel.style.space_between 90 | for item in center_items { 91 | center_items_width += len(item.content) 92 | } 93 | 94 | panel_width := panel._window.width.? 95 | t.move_cursor(&panel._window, 0, panel_width / 2 - center_items_width / 2) 96 | for item in center_items { 97 | t.write(&panel._window, item.content) 98 | cursor_pos := t.get_cursor_position(&panel._window) 99 | cursor_pos.x += panel.style.space_between 100 | t.move_cursor(&panel._window, cursor_pos.y, cursor_pos.x) 101 | } 102 | 103 | t.blit(&panel._window) 104 | } 105 | 106 | -------------------------------------------------------------------------------- /widgets/selector.odin: -------------------------------------------------------------------------------- 1 | package widgets 2 | 3 | import t ".." 4 | 5 | Selector_Item :: struct($T: typeid) { 6 | content: string, 7 | value: T, 8 | } 9 | 10 | Selector_Style :: struct { 11 | bg, fg: t.Any_Color, 12 | active: t.Any_Color, 13 | width: Maybe(uint), 14 | x, y: uint, 15 | } 16 | 17 | Selector :: struct($T: typeid) { 18 | _screen: ^t.Screen, 19 | _window: t.Window, 20 | _curr: uint, 21 | items: []Selector_Item(T), 22 | style: Selector_Style, 23 | } 24 | 25 | selector_init :: proc(screen: ^t.Screen, $Value: typeid) -> Selector(Value) { 26 | sel := Selector(Value) { 27 | _screen = screen, 28 | _window = t.init_window(0, 0, nil, nil), 29 | style = {fg = .White, active = .Green}, 30 | } 31 | 32 | return sel 33 | } 34 | 35 | selector_destroy :: proc(selector: ^Selector($T)) { 36 | t.destroy_window(&selector._window) 37 | } 38 | 39 | Select_Action :: enum { 40 | Next, 41 | Prev, 42 | } 43 | 44 | selector_do :: proc(selector: ^Selector($T), action: Select_Action) { 45 | switch action { 46 | case .Next: 47 | if len(selector.items) != 0 && selector._curr >= len(selector.items) - 1 { 48 | selector._curr = 0 49 | } else { 50 | selector._curr += 1 51 | } 52 | case .Prev: 53 | if selector._curr == 0 { 54 | selector._curr = len(selector.items) - 1 55 | } else { 56 | selector._curr -= 1 57 | } 58 | } 59 | } 60 | 61 | selector_curr :: proc(selector: ^Selector($T)) -> T { 62 | return selector.items[selector._curr].value 63 | } 64 | 65 | _selector_set_layout :: proc(selector: ^Selector($T)) { 66 | selector._window.x_offset = selector.style.x 67 | selector._window.y_offset = selector.style.y 68 | 69 | width, has_width := selector.style.width.? 70 | if has_width { 71 | selector._window.width = width 72 | } else { 73 | largest: uint 74 | for item in selector.items { 75 | if largest < len(item.content) do largest = len(item.content) 76 | } 77 | selector._window.width = largest + 1 78 | } 79 | 80 | // TODO set height based on selector.items 81 | // knowing how many lines a string will span starting from a cursor position 82 | // seems to be a common occurrence in this widget library so we should probably 83 | // create something to do that for us in common.odin 84 | // PS: just using _get_cursor_pos_from_string might be enough 85 | height: uint = 50 86 | for item in selector.items { 87 | 88 | } 89 | 90 | selector._window.height = height 91 | } 92 | 93 | selector_blit :: proc(selector: ^Selector($T)) { 94 | _selector_set_layout(selector) 95 | 96 | for item, idx in selector.items { 97 | is_curr := selector._curr == uint(idx) 98 | 99 | set_any_color_style( 100 | &selector._window, 101 | selector.style.active if is_curr else selector.style.fg, 102 | selector.style.bg, 103 | ) 104 | 105 | t.move_cursor(&selector._window, uint(idx), 0) 106 | t.write(&selector._window, '>' if is_curr else ' ') 107 | t.write(&selector._window, item.content) 108 | } 109 | 110 | t.reset_styles(&selector._window) 111 | t.blit(&selector._window) 112 | } 113 | 114 | -------------------------------------------------------------------------------- /widgets/progress.odin: -------------------------------------------------------------------------------- 1 | package widgets 2 | 3 | import t ".." 4 | import "core:bytes" 5 | import "core:strconv" 6 | 7 | // TODO: implement color gradient in the future 8 | 9 | @(rodata) 10 | progression_bar := [?]rune{'▏', '▎', '▍', '▌', '▋', '▊', '▉'} 11 | 12 | Progress_Style :: struct { 13 | description_color: t.Any_Color, 14 | bg, fg: t.Any_Color, 15 | text: bit_set[t.Text_Style], 16 | width: Maybe(uint), 17 | y, x: uint, 18 | } 19 | 20 | Progress :: struct { 21 | _screen: ^t.Screen, 22 | _window: t.Window, 23 | max, curr: uint, 24 | description: string, 25 | style: Progress_Style, 26 | } 27 | 28 | progress_init :: proc(s: ^t.Screen) -> Progress { 29 | return Progress{_screen = s, _window = t.init_window(0, 0, 0, 0)} 30 | } 31 | 32 | progress_destroy :: proc(prog: ^Progress) { 33 | t.destroy_window(&prog._window) 34 | } 35 | 36 | progress_add :: proc(prog: ^Progress, done: uint) { 37 | if prog.curr < prog.max do prog.curr += done 38 | } 39 | 40 | progress_done :: proc(prog: ^Progress) -> bool { 41 | return prog.curr >= prog.max 42 | } 43 | 44 | // size of ` 100.0%` 45 | @(private) 46 | PROGRESS_PERCENT_SIZE :: 7 47 | 48 | // TODO: set height to 1 if no description and 2 if has desc 49 | // set width dependent on len(description) and prog.max 50 | _progress_set_layout :: proc(prog: ^Progress) { 51 | prog._window.height = 1 if len(prog.description) == 0 else 2 52 | 53 | progress_width: uint 54 | if prog.style.width != nil { 55 | progress_width = prog.style.width.? 56 | } else { 57 | DEFAULT_WIDTH :: 15 58 | progress_width = 59 | len(prog.description) if len(prog.description) > DEFAULT_WIDTH else DEFAULT_WIDTH 60 | } 61 | 62 | progress_width += PROGRESS_PERCENT_SIZE 63 | prog._window.width = progress_width 64 | 65 | prog._window.y_offset = prog.style.y 66 | prog._window.x_offset = prog.style.x 67 | } 68 | 69 | progress_blit :: proc(prog: ^Progress) { 70 | if prog.max == 0 do return 71 | _progress_set_layout(prog) 72 | t.clear(&prog._window, .Everything) 73 | t.move_cursor(&prog._window, 0, 0) 74 | 75 | percentage := cast(f64)prog.curr / cast(f64)prog.max 76 | curr_progress_width := uint(cast(f64)(prog._window.width.? - 8) * percentage) 77 | 78 | t.set_color_style(&prog._window, prog.style.fg, prog.style.fg) 79 | for i in 0 ..< curr_progress_width { 80 | t.write(&prog._window, progression_bar[len(progression_bar) - 1]) 81 | } 82 | 83 | total_progress_width := prog._window.width.? - PROGRESS_PERCENT_SIZE 84 | 85 | t.set_color_style(&prog._window, prog.style.bg, prog.style.bg) 86 | for i in 0 ..< total_progress_width - curr_progress_width - 1 { 87 | t.write(&prog._window, progression_bar[len(progression_bar) - 1]) 88 | } 89 | 90 | 91 | t.set_color_style(&prog._window, prog.style.fg, nil) 92 | t.move_cursor(&prog._window, 0, total_progress_width) 93 | t.writef(&prog._window, " %1.1f%%", percentage * 100) 94 | 95 | if len(prog.description) != 0 { 96 | t.move_cursor(&prog._window, 1, 0) 97 | t.write(&prog._window, prog.description) 98 | } 99 | 100 | t.reset_styles(&prog._window) 101 | t.blit(&prog._window) 102 | } 103 | 104 | -------------------------------------------------------------------------------- /examples/stress/main.odin: -------------------------------------------------------------------------------- 1 | // stress is a port of the tcell stress test 2 | 3 | // Copyright 2022 The TCell Authors 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use file except in compliance with the License. 7 | // You may obtain a copy of the license at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | // stress will fill the whole screen with random characters, colors and 18 | // formatting. The frames are pre-generated to draw as fast as possible. 19 | // ESC and Ctrl-C will end the program. Note that resizing isn't supported. 20 | package stress 21 | 22 | import t "../.." 23 | import tb "../../term" 24 | import "core:fmt" 25 | import "core:math/rand" 26 | import "core:time" 27 | 28 | 29 | main :: proc() { 30 | screen := t.init_screen(tb.VTABLE) 31 | frames: int 32 | 33 | Style :: struct { 34 | text_style: t.Text_Style, 35 | fg: t.Color_RGB, 36 | bg: t.Color_RGB, 37 | } 38 | 39 | cell :: struct { 40 | c: rune, 41 | style: Style, 42 | } 43 | 44 | size := t.get_window_size(&screen) 45 | height, width := int(size.h), int(size.w) 46 | glyphs := []rune{'@', '#', '&', '*', '=', '%', 'Z', 'A'} 47 | attrs := []t.Text_Style{.Bold, .Dim, .Italic, .Crossed} 48 | 49 | // Pre-Generate 100 different frame patterns, so we stress the terminal as 50 | // much as possible :D 51 | patterns := make([][][]cell, 100) 52 | for i := 0; i < 100; i += 1 { 53 | pattern := make([][]cell, height) 54 | for h := 0; h < height; h += 1 { 55 | row := make([]cell, width) 56 | for w := 0; w < width; w += 1 { 57 | rF := u8(rand.int_max(256)) 58 | gF := u8(rand.int_max(256)) 59 | bF := u8(rand.int_max(256)) 60 | rB := u8(rand.int_max(256)) 61 | gB := u8(rand.int_max(256)) 62 | bB := u8(rand.int_max(256)) 63 | 64 | row[w] = cell { 65 | c = rand.choice(glyphs), 66 | style = { 67 | text_style = rand.choice(attrs), 68 | bg = {rB, gB, bB}, 69 | fg = {rF, gF, bF}, 70 | }, 71 | } 72 | } 73 | pattern[h] = row 74 | } 75 | patterns[i] = pattern 76 | } 77 | 78 | t.clear(&screen, .Everything) 79 | start_time := time.now() 80 | for time.since(start_time) < 30 * time.Second { 81 | pattern := patterns[frames % len(patterns)] 82 | for h in 0 ..< height { 83 | for w in 0 ..< width { 84 | cell := &pattern[h][w] 85 | t.move_cursor(&screen, uint(h), uint(w)) 86 | t.set_color_style(&screen, cell.style.fg, cell.style.bg) 87 | t.write(&screen, cell.c) 88 | } 89 | } 90 | t.blit(&screen) 91 | frames += 1 92 | } 93 | delete(patterns) 94 | t.reset_styles(&screen) 95 | t.clear(&screen, .Everything) 96 | t.blit(&screen) 97 | t.destroy_screen(&screen) 98 | duration := time.since(start_time) 99 | fps := int(f64(frames) / time.duration_seconds(duration)) 100 | fmt.println("------RESULTS------") 101 | fmt.println("FPS:", fps) 102 | } 103 | 104 | -------------------------------------------------------------------------------- /examples/paint/main.odin: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import t "../../" 4 | import tb "../../term" 5 | 6 | Brush :: struct { 7 | color: t.Color_8, 8 | size: uint, 9 | } 10 | 11 | PAINT_BUFFER_WIDTH :: 80 12 | PAINT_BUFFER_HEIGHT :: 30 13 | 14 | Paint_Buffer :: struct { 15 | buffer: [PAINT_BUFFER_HEIGHT][PAINT_BUFFER_WIDTH]Maybe(t.Color_8), 16 | screen: ^t.Screen, 17 | window: t.Window, 18 | } 19 | 20 | paint_buffer_init :: proc(s: ^t.Screen) -> Paint_Buffer { 21 | return Paint_Buffer { 22 | screen = s, 23 | window = t.init_window(0, 0, PAINT_BUFFER_HEIGHT, PAINT_BUFFER_WIDTH), 24 | } 25 | } 26 | 27 | paint_buffer_destroy :: proc(pbuf: ^Paint_Buffer) { 28 | t.clear(&pbuf.window, .Everything) 29 | t.blit(&pbuf.window) 30 | t.destroy_window(&pbuf.window) 31 | } 32 | 33 | paint_buffer_to_screen :: proc(pbuf: ^Paint_Buffer) { 34 | termsize := t.get_window_size(pbuf.screen) 35 | pbuf.window.x_offset = termsize.w / 2 - PAINT_BUFFER_WIDTH / 2 36 | pbuf.window.y_offset = termsize.h / 2 - PAINT_BUFFER_HEIGHT / 2 37 | 38 | t.set_color_style(&pbuf.window, .White, .White) 39 | t.clear(&pbuf.window, .Everything) 40 | 41 | defer { 42 | t.reset_styles(&pbuf.window) 43 | t.blit(&pbuf.window) 44 | } 45 | 46 | for y in 0 ..< PAINT_BUFFER_HEIGHT { 47 | for x in 0 ..< PAINT_BUFFER_WIDTH { 48 | t.move_cursor(&pbuf.window, uint(y), uint(x)) 49 | color := pbuf.buffer[y][x] 50 | if color == nil do continue 51 | t.set_color_style(&pbuf.window, color.?, color.?) 52 | t.write(&pbuf.window, ' ') 53 | } 54 | } 55 | } 56 | 57 | paint_buffer_set_cell :: proc(pbuf: ^Paint_Buffer, y, x: uint, color: Maybe(t.Color_8)) { 58 | pbuf.buffer[y][x] = color 59 | } 60 | 61 | main :: proc() { 62 | s := t.init_screen(tb.VTABLE) 63 | defer t.destroy_screen(&s) 64 | t.set_term_mode(&s, .Raw) 65 | t.hide_cursor(true) 66 | 67 | t.clear(&s, .Everything) 68 | t.blit(&s) 69 | 70 | pbuf := paint_buffer_init(&s) 71 | defer paint_buffer_destroy(&pbuf) 72 | 73 | for { 74 | defer { 75 | t.blit(&s) 76 | paint_buffer_to_screen(&pbuf) 77 | } 78 | 79 | termsize := t.get_window_size(&s) 80 | 81 | help_msg := "Draw (Right Click) / Delete (Left Click) / Quit (Q or CTRL + C)" 82 | t.move_cursor(&s, termsize.h - 2, termsize.w / 2 - len(help_msg) / 2) 83 | t.write(&s, help_msg) 84 | 85 | if termsize.w <= PAINT_BUFFER_WIDTH || termsize.w <= PAINT_BUFFER_WIDTH { 86 | t.clear(&s, .Everything) 87 | size_small_msg := "Size is too small, increase size to continue or press 'q' to exit" 88 | t.move_cursor(&s, termsize.h / 2, termsize.w / 2 - len(size_small_msg) / 2) 89 | t.write(&s, size_small_msg) 90 | continue 91 | } 92 | 93 | 94 | input := t.read(&s) 95 | switch i in input { 96 | case t.Keyboard_Input: 97 | if (i.mod == .Ctrl && i.key == .C) || i.key == .Q { 98 | return 99 | } 100 | case t.Mouse_Input: 101 | win_cursor := t.window_coord_from_global( 102 | &pbuf.window, 103 | i.coord.y, 104 | i.coord.x, 105 | ) or_continue 106 | 107 | #partial switch i.key { 108 | case .Left: 109 | if i.mod == nil && .Pressed in i.event { 110 | paint_buffer_set_cell(&pbuf, win_cursor.y, win_cursor.x, .Black) 111 | } 112 | case .Right: 113 | if i.mod == nil && .Pressed in i.event { 114 | paint_buffer_set_cell(&pbuf, win_cursor.y, win_cursor.x, nil) 115 | } 116 | } 117 | } 118 | } 119 | } 120 | 121 | -------------------------------------------------------------------------------- /widgets/button.odin: -------------------------------------------------------------------------------- 1 | package widgets 2 | 3 | import t ".." 4 | 5 | Button_Style :: struct { 6 | bg, fg: t.Any_Color, 7 | text: bit_set[t.Text_Style], 8 | padding: uint, 9 | width, height: Maybe(uint), 10 | y, x: uint, 11 | } 12 | 13 | Button :: struct { 14 | _screen: ^t.Screen, 15 | _window: t.Window, 16 | _content_box: t.Window, 17 | content: string, 18 | style: Button_Style, 19 | } 20 | 21 | button_init :: proc(screen: ^t.Screen) -> Button { 22 | return Button { 23 | _screen = screen, 24 | _window = t.init_window(0, 0, 0, 0), 25 | _content_box = t.init_window(0, 0, 0, 0), 26 | } 27 | } 28 | 29 | button_destroy :: proc(btn: ^Button) { 30 | t.destroy_window(&btn._window) 31 | t.destroy_window(&btn._content_box) 32 | } 33 | 34 | _button_set_layout :: proc(btn: ^Button) { 35 | btn._window.width = btn.style.width 36 | btn._window.height = btn.style.height 37 | btn._window.x_offset = btn.style.x 38 | btn._window.y_offset = btn.style.y 39 | 40 | y_padding := btn.style.padding * 2 41 | x_padding := btn.style.padding * 4 42 | 43 | if btn.style.height == nil { 44 | btn._window.height = y_padding + 1 45 | btn._content_box.height = 1 46 | } else { 47 | btn._content_box.height = btn._window.height.? - y_padding 48 | } 49 | box_y_padding_to_center := (btn._window.height.? - btn._content_box.height.?) / 2 50 | btn._content_box.y_offset = btn._window.y_offset + box_y_padding_to_center 51 | 52 | if btn.style.width == nil { 53 | // for width padding is doubled because cursor bar is taller than it is wider 54 | btn._window.width = x_padding + len(btn.content) 55 | btn._content_box.width = cast(uint)len(btn.content) 56 | } else { 57 | btn._content_box.width = btn._window.width.? - x_padding 58 | } 59 | box_x_padding_to_center := (btn._window.width.? - btn._content_box.width.?) / 2 60 | btn._content_box.x_offset = btn._window.x_offset + box_x_padding_to_center 61 | } 62 | 63 | button_blit :: proc(btn: ^Button) { 64 | if len(btn.content) == 0 do return 65 | 66 | _button_set_layout(btn) 67 | if btn._window.width == nil || btn._window.width == 0 { 68 | return 69 | } 70 | if btn._window.height == nil || btn._window.height == 0 { 71 | return 72 | } 73 | 74 | defer { 75 | t.reset_styles(&btn._window) 76 | t.blit(&btn._window) 77 | t.reset_styles(&btn._content_box) 78 | t.blit(&btn._content_box) 79 | } 80 | 81 | t.set_color_style(&btn._window, btn.style.fg, btn.style.bg) 82 | t.set_text_style(&btn._window, btn.style.text) 83 | 84 | t.set_color_style(&btn._content_box, btn.style.fg, btn.style.bg) 85 | t.set_text_style(&btn._content_box, btn.style.text) 86 | 87 | t.clear(&btn._window, .Everything) 88 | t.move_cursor(&btn._content_box, 0, 0) 89 | t.write(&btn._content_box, btn.content) 90 | } 91 | 92 | button_hovered :: proc(btn: ^Button) -> bool { 93 | mouse_input := t.parse_mouse_input(t.Input(btn._screen.input_buf[:])) or_return 94 | _, in_window := t.window_coord_from_global( 95 | &btn._window, 96 | mouse_input.coord.y, 97 | mouse_input.coord.x, 98 | ) 99 | 100 | return in_window && mouse_input.key == .None 101 | } 102 | 103 | button_clicked :: proc(btn: ^Button) -> bool { 104 | // we avoid using button_hovered here so we don't have to parse input twice 105 | mouse_input := t.parse_mouse_input(t.Input(btn._screen.input_buf[:])) or_return 106 | _, in_window := t.window_coord_from_global( 107 | &btn._window, 108 | mouse_input.coord.y, 109 | mouse_input.coord.x, 110 | ) 111 | 112 | return in_window && .Pressed in mouse_input.event && mouse_input.key == .Left 113 | } 114 | 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 |

4 |

TermCL

5 |

Terminal control and ANSI escape code library for Odin

6 |

7 |
8 | 9 | 10 |
11 |
12 | · 13 | Report Bug 14 | · 15 | Request Feature 16 |

17 |

18 | 19 | 20 |
21 | Table of Contents 22 |
    23 |
  1. 24 | About The Project 25 |
  2. 26 |
  3. How it works
  4. 27 |
  5. Usage
  6. 28 |
29 |
30 | 31 | 32 | 33 | TermCL is an Odin library for writing TUIs and CLIs with. 34 | The library is compatible with any ANSI escape code compatible terminal, which is to say, almost every single modern terminal worth using :) 35 | 36 | The library should also work on windows and any posix compatible operating system. 37 | 38 | ## How it works 39 | The library uses a multiple backend architecture, so to start you need to pick your backend. 40 | 41 | Currently there's: 42 | - term: outputs terminal escape codes for modern terminals. This is probably what you want most of the time. 43 | - sdl3: uses SDL3 to render to a window. This is good for tools that need the extra performance, or more flexibility with how you draw pixels, or you just want 44 | to have a terminal-like UI 45 | 46 | Once you've picked your backend you should set it and then initialze the screen. 47 | 48 | > [!NOTE] 49 | > you should call destroy_screen before you exit to restore the terminal state otherwise you might end up with a weird behaving terminal 50 | 51 | After that you should just set the terminal to whatever mode you want with the `set_term_mode` function, there are 3 modes you can use: 52 | - Raw mode (`.Raw`) - prevents the terminal from processing the user input so that you can handle them yourself 53 | - Cooked mode (`.Cbreak`) - prevents user input but unlike raw, it still processed for signals like Ctrl + C and others 54 | - Restored mode (`.Restored`) - restores the terminal to the state it was in before the program started messing with it, this is also called when the screen is destroyed 55 | 56 | After doing this, you should be good to go to do whatever you want. 57 | 58 | Here's a few minor things to take into consideration: 59 | - To handle input you can use the `read` function or the `read_blocking` function, as the default read is nonblocking. 60 | - There's convenience functions that allow you to more easily process input, they're called `parse_keyboard_input` and `parse_mouse_input` 61 | - Whatever you do won't show up on screen until you `blit`, since everything is cached first, windows also have their own cached escapes, so make sure you blit them as well 62 | 63 | ## Usage 64 | 65 | ```odin 66 | package main 67 | 68 | import t "termcl" 69 | import "termcl/term" 70 | 71 | main :: proc() { 72 | scr := t.init_screen(term.VTABLE) 73 | defer t.destroy_screen(&scr) 74 | t.clear(&scr, .Everything) 75 | 76 | t.set_text_style(&scr, {.Bold, .Italic}) 77 | t.write(&scr, "Hello ") 78 | t.reset_styles(&scr) 79 | 80 | t.set_text_style(&scr, {.Dim}) 81 | t.set_color_style(&scr, .Green, nil) 82 | t.write(&scr, "from ANSI escapes") 83 | t.reset_styles(&scr) 84 | 85 | t.move_cursor(&scr, 10, 10) 86 | t.write(&scr, "Alles Ordnung") 87 | 88 | t.blit(&scr) 89 | } 90 | ``` 91 | 92 | Check the `examples` directory to see more on how to use it. 93 | -------------------------------------------------------------------------------- /term/term.odin: -------------------------------------------------------------------------------- 1 | package term 2 | 3 | import t ".." 4 | import "../raw" 5 | import "core:fmt" 6 | import os "core:os/os2" 7 | import "core:strings" 8 | 9 | @(private) 10 | orig_termstate: Terminal_State 11 | 12 | VTABLE :: t.Backend_VTable { 13 | init_screen = init_screen, 14 | destroy_screen = destroy_screen, 15 | get_term_size = get_term_size, 16 | set_term_mode = set_term_mode, 17 | blit = blit, 18 | read = read, 19 | read_blocking = read_blocking, 20 | } 21 | 22 | init_screen :: proc(allocator := context.allocator) -> t.Screen { 23 | context.allocator = allocator 24 | 25 | termstate, ok := get_terminal_state() 26 | if !ok { 27 | panic("failed to get terminal state") 28 | } 29 | orig_termstate = termstate 30 | 31 | // TODO: get cursor position from terminal on init 32 | return t.Screen{winbuf = t.init_window(0, 0, nil, nil, allocator = allocator)} 33 | } 34 | 35 | destroy_screen :: proc(screen: ^t.Screen) { 36 | set_term_mode(screen, .Restored) 37 | t.destroy_window(&screen.winbuf) 38 | t.enable_alt_buffer(false) 39 | } 40 | 41 | set_term_mode :: proc(screen: ^t.Screen, mode: t.Term_Mode) { 42 | change_terminal_mode(screen, mode) 43 | 44 | #partial switch mode { 45 | case .Restored: 46 | t.enable_alt_buffer(false) 47 | t.enable_mouse(false) 48 | 49 | case .Raw: 50 | t.enable_alt_buffer(true) 51 | raw.enable_mouse(true) 52 | } 53 | 54 | t.hide_cursor(false) 55 | // when changing modes some OSes (like windows) might put garbage that we don't care about 56 | // in stdin potentially causing nonblocking reads to block on the first read, so to avoid this, 57 | // stdin is always flushed when the mode is changed 58 | os.flush(os.stdin) 59 | } 60 | 61 | blit :: proc(win: ^t.Window) { 62 | if win.height == 0 || win.width == 0 { 63 | return 64 | } 65 | 66 | // this is needed to prevent the window from sharing the same style as the terminal 67 | // this avoids messing up users' styles from one window to another 68 | raw.set_fg_color_style(&win.seq_builder, win.curr_styles.fg) 69 | raw.set_bg_color_style(&win.seq_builder, win.curr_styles.bg) 70 | raw.set_text_style(&win.seq_builder, win.curr_styles.text) 71 | 72 | curr_styles := win.curr_styles 73 | // always zero valued 74 | reset_styles: t.Styles 75 | 76 | for y in 0 ..< win.cell_buffer.height { 77 | global_pos := t.global_coord_from_window(win, y, 0) 78 | raw.move_cursor(&win.seq_builder, global_pos.y, global_pos.x) 79 | 80 | for x in 0 ..< win.cell_buffer.width { 81 | curr_cell := t.cellbuf_get(win.cell_buffer, y, x) 82 | defer { 83 | curr_styles = curr_cell.styles 84 | strings.write_rune(&win.seq_builder, curr_cell.r) 85 | } 86 | 87 | /* OPTIMIZATION: don't change styles unless they change between cells */{ 88 | if curr_styles != reset_styles && curr_cell.styles == reset_styles { 89 | raw.reset_styles(&win.seq_builder) 90 | continue 91 | } 92 | 93 | if curr_styles.fg != curr_cell.styles.fg { 94 | raw.set_fg_color_style(&win.seq_builder, curr_cell.styles.fg) 95 | } 96 | 97 | if curr_styles.bg != curr_cell.styles.bg { 98 | raw.set_bg_color_style(&win.seq_builder, curr_cell.styles.bg) 99 | } 100 | 101 | if curr_styles.text != curr_cell.styles.text { 102 | if curr_cell.styles.text == nil { 103 | raw.reset_styles(&win.seq_builder) 104 | raw.set_fg_color_style(&win.seq_builder, curr_cell.styles.fg) 105 | raw.set_bg_color_style(&win.seq_builder, curr_cell.styles.bg) 106 | } 107 | raw.set_text_style(&win.seq_builder, curr_cell.styles.text) 108 | } 109 | } 110 | } 111 | } 112 | // we move the cursor back to where the window left it 113 | // just in case if the user is relying on the terminal drawing the cursor 114 | raw.move_cursor(&win.seq_builder, win.cursor.y, win.cursor.x) 115 | 116 | fmt.print(strings.to_string(win.seq_builder), flush = true) 117 | strings.builder_reset(&win.seq_builder) 118 | } 119 | -------------------------------------------------------------------------------- /kitty_keyboard/kitty_keyboard.odin: -------------------------------------------------------------------------------- 1 | package kitty_keyboard 2 | 3 | import "../term" 4 | import "core:fmt" 5 | import os "core:os/os2" 6 | import "core:strconv" 7 | import "core:strings" 8 | import "core:terminal/ansi" 9 | import "core:unicode" 10 | 11 | Modifier :: enum { 12 | Shift = 1, 13 | Alt = 2, 14 | Ctrl = 3, 15 | Super = 4, 16 | Hyper = 5, 17 | Meta = 6, 18 | Caps_Lock = 7, 19 | Num_Lock = 8, 20 | } 21 | 22 | Event :: enum { 23 | Press = 1, 24 | Repeat = 2, 25 | Release = 3, 26 | } 27 | 28 | enable :: proc(enable: bool) { 29 | if enable { 30 | fmt.print(ansi.CSI + ">1u") 31 | } else { 32 | fmt.print(ansi.CSI + " bool { 41 | _, ok := query_enhancements() 42 | return ok 43 | } 44 | 45 | Enhancement :: enum { 46 | // https://sw.kovidgoyal.net/kitty/keyboard-protocol/#disambiguate-escape-codes 47 | Disambiguate_Escape_Codes = 0, 48 | // https://sw.kovidgoyal.net/kitty/keyboard-protocol/#report-event-types 49 | Report_Event_types = 1, 50 | // https://sw.kovidgoyal.net/kitty/keyboard-protocol/#report-alternate-keys 51 | Report_Alternate_Keys = 2, 52 | // https://sw.kovidgoyal.net/kitty/keyboard-protocol/#report-all-keys-as-escape-codes 53 | Report_All_Keys_As_Escape_Codes = 3, 54 | // https://sw.kovidgoyal.net/kitty/keyboard-protocol/#report-associated-text 55 | Report_Associated_Text = 4, 56 | } 57 | 58 | query_enhancements :: proc() -> (enhancements: bit_set[Enhancement], ok: bool) { 59 | sb: strings.Builder 60 | strings.builder_init(&sb) 61 | defer strings.builder_destroy(&sb) 62 | 63 | strings.write_string(&sb, ansi.CSI) 64 | strings.write_rune(&sb, '?') 65 | strings.write_rune(&sb, 'u') 66 | 67 | query := strings.to_string(sb) 68 | os.write_string(os.stdout, query) 69 | 70 | response: [10]byte 71 | term.raw_read(response[:]) 72 | 73 | if cast(string)response[:2] != ansi.CSI do return 74 | if response[2] != '?' do return 75 | 76 | enhancement_end := 3 77 | for b, i in response[3:] { 78 | if !unicode.is_digit(cast(rune)b) { 79 | enhancement_end += i 80 | break 81 | } 82 | } 83 | 84 | enhancement_int := strconv.parse_int(cast(string)response[3:enhancement_end]) or_return 85 | enhancements = transmute(bit_set[Enhancement])cast(u8)enhancement_int 86 | ok = true 87 | return 88 | } 89 | 90 | Enhancement_Request_Mode :: enum { 91 | // only the bits that have beeen set are enabled 92 | Enable_Set_Bits_Only = 1, 93 | // add set bits and leave unset bits unchanged 94 | Add_Set_Bits = 2, 95 | // remove set bits and leave unset bits unchanged 96 | Remote_Set_Bits = 3, 97 | } 98 | 99 | request_enhancements :: proc(enhancements: bit_set[Enhancement], mode: Enhancement_Request_Mode) { 100 | sb: strings.Builder 101 | strings.builder_init(&sb) 102 | defer strings.builder_destroy(&sb) 103 | 104 | strings.write_string(&sb, ansi.CSI) 105 | strings.write_rune(&sb, '=') 106 | strings.write_int(&sb, cast(int)transmute(u8)enhancements) 107 | strings.write_rune(&sb, ';') 108 | strings.write_int(&sb, cast(int)mode) 109 | strings.write_rune(&sb, 'u') 110 | os.write_string(os.stdout, strings.to_string(sb)) 111 | } 112 | 113 | push_enhancements :: proc(enhancements: bit_set[Enhancement]) { 114 | sb: strings.Builder 115 | strings.builder_init(&sb) 116 | defer strings.builder_destroy(&sb) 117 | 118 | enhancements_buf: [2]byte 119 | strings.write_string(&sb, ansi.CSI) 120 | strings.write_rune(&sb, '>') 121 | strings.write_int(&sb, cast(int)transmute(u8)enhancements) 122 | strings.write_rune(&sb, 'u') 123 | os.write_string(os.stdout, strings.to_string(sb)) 124 | } 125 | 126 | pop_enhancements :: proc(num_of_entries: int) { 127 | if num_of_entries < 0 do return 128 | 129 | sb: strings.Builder 130 | strings.builder_init(&sb) 131 | defer strings.builder_destroy(&sb) 132 | 133 | strings.write_string(&sb, ansi.CSI) 134 | strings.write_rune(&sb, '<') 135 | strings.write_int(&sb, num_of_entries) 136 | strings.write_rune(&sb, 'u') 137 | os.write_string(os.stdout, strings.to_string(sb)) 138 | } 139 | -------------------------------------------------------------------------------- /term/platform_windows.odin: -------------------------------------------------------------------------------- 1 | package term 2 | 3 | import t ".." 4 | import os "core:os" 5 | import "core:sys/windows" 6 | 7 | Terminal_State :: struct { 8 | mode: windows.DWORD, 9 | input_codepage: windows.CODEPAGE, 10 | input_mode: windows.DWORD, 11 | output_codepage: windows.CODEPAGE, 12 | output_mode: windows.DWORD, 13 | } 14 | 15 | get_terminal_state :: proc() -> (Terminal_State, bool) { 16 | termstate: Terminal_State 17 | windows.GetConsoleMode(windows.HANDLE(os.stdout), &termstate.output_mode) 18 | termstate.output_codepage = windows.GetConsoleOutputCP() 19 | 20 | windows.GetConsoleMode(windows.HANDLE(os.stdin), &termstate.input_mode) 21 | termstate.input_codepage = windows.GetConsoleCP() 22 | 23 | return termstate, true 24 | } 25 | 26 | change_terminal_mode :: proc(screen: ^t.Screen, mode: t.Term_Mode) { 27 | termstate, ok := get_terminal_state() 28 | if !ok { 29 | panic("failed to get terminal state") 30 | } 31 | 32 | switch mode { 33 | case .Raw: 34 | termstate.output_mode |= windows.DISABLE_NEWLINE_AUTO_RETURN 35 | termstate.output_mode |= windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING 36 | termstate.output_mode |= windows.ENABLE_PROCESSED_OUTPUT 37 | 38 | termstate.input_mode &= ~windows.ENABLE_PROCESSED_INPUT 39 | termstate.input_mode &= ~windows.ENABLE_ECHO_INPUT 40 | termstate.input_mode &= ~windows.ENABLE_LINE_INPUT 41 | termstate.input_mode |= windows.ENABLE_VIRTUAL_TERMINAL_INPUT 42 | 43 | case .Cbreak: 44 | termstate.output_mode |= windows.ENABLE_PROCESSED_OUTPUT 45 | termstate.output_mode |= windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING 46 | 47 | termstate.input_mode |= windows.ENABLE_VIRTUAL_TERMINAL_INPUT 48 | termstate.input_mode &= ~windows.ENABLE_LINE_INPUT 49 | termstate.input_mode &= ~windows.ENABLE_ECHO_INPUT 50 | 51 | case .Restored: 52 | termstate = orig_termstate 53 | } 54 | 55 | if !windows.SetConsoleMode(windows.HANDLE(os.stdout), termstate.output_mode) || 56 | !windows.SetConsoleMode(windows.HANDLE(os.stdin), termstate.input_mode) { 57 | panic("failed to set new terminal state") 58 | } 59 | 60 | if mode != .Restored { 61 | windows.SetConsoleOutputCP(.UTF8) 62 | windows.SetConsoleCP(.UTF8) 63 | } else { 64 | windows.SetConsoleOutputCP(termstate.output_codepage) 65 | windows.SetConsoleCP(termstate.input_codepage) 66 | } 67 | } 68 | 69 | /* 70 | Get terminal size 71 | 72 | NOTE: this functional does syscalls to figure out the size of the terminal. 73 | For most use cases, passing `t.Screen` to `get_window_size` achieves the same result 74 | and doesn't need to do any system calls. 75 | 76 | You should only use this function if you don't have access to `t.Screen` and still somehow 77 | need to figure out the terminal size. Otherwise this function might or might not cause 78 | your program to slow down a bit due to OS context switching. 79 | */ 80 | get_term_size :: proc() -> t.Window_Size { 81 | sbi: windows.CONSOLE_SCREEN_BUFFER_INFO 82 | 83 | if !windows.GetConsoleScreenBufferInfo(windows.HANDLE(os.stdout), &sbi) { 84 | panic("Failed to get terminal size") 85 | } 86 | 87 | screen_size := t.Window_Size { 88 | w = uint(sbi.srWindow.Right - sbi.srWindow.Left) + 1, 89 | h = uint(sbi.srWindow.Bottom - sbi.srWindow.Top) + 1, 90 | } 91 | 92 | return screen_size 93 | } 94 | 95 | 96 | raw_read :: proc(buf: []byte) -> (user_input: []byte, has_input: bool) { 97 | num_events: u32 98 | if !windows.GetNumberOfConsoleInputEvents(windows.HANDLE(os.stdin), &num_events) { 99 | error_id := windows.GetLastError() 100 | error_msg: ^u16 101 | 102 | strsize := windows.FormatMessageW( 103 | windows.FORMAT_MESSAGE_ALLOCATE_BUFFER | 104 | windows.FORMAT_MESSAGE_FROM_SYSTEM | 105 | windows.FORMAT_MESSAGE_IGNORE_INSERTS, 106 | nil, 107 | error_id, 108 | windows.MAKELANGID(windows.LANG_NEUTRAL, windows.SUBLANG_DEFAULT), 109 | cast(^u16)&error_msg, 110 | 0, 111 | nil, 112 | ) 113 | windows.WriteConsoleW(windows.HANDLE(os.stdout), error_msg, strsize, nil, nil) 114 | windows.LocalFree(error_msg) 115 | panic("Failed to get console input events") 116 | } 117 | 118 | if num_events > 0 { 119 | bytes_read, err := os.read(os.stdin, buf[:]) 120 | if err != nil { 121 | panic("failing to get user input") 122 | } 123 | return buf[:bytes_read], true 124 | } 125 | 126 | return 127 | } 128 | -------------------------------------------------------------------------------- /sdl3/backend.odin: -------------------------------------------------------------------------------- 1 | package termcl_sdl3 2 | 3 | import t ".." 4 | import "core:c" 5 | import "core:fmt" 6 | import "core:unicode/utf8" 7 | import "vendor:sdl3" 8 | import "vendor:sdl3/ttf" 9 | 10 | Context :: struct { 11 | window: ^sdl3.Window, 12 | renderer: ^sdl3.Renderer, 13 | font: ^ttf.Font, 14 | text_engine: ^ttf.TextEngine, 15 | font_cache: map[rune]^ttf.Text, 16 | } 17 | 18 | render_ctx: Context 19 | 20 | VTABLE :: t.Backend_VTable { 21 | init_screen = init_screen, 22 | destroy_screen = destroy_screen, 23 | get_term_size = get_term_size, 24 | set_term_mode = set_term_mode, 25 | blit = blit, 26 | read = read, 27 | read_blocking = read_blocking, 28 | } 29 | 30 | // NOTE: Cooked vs Raw modes are pretty much useless on a GUI 31 | // so we do an no-op just here just so that programs don't crash attempting 32 | // to call a non existent function 33 | set_term_mode :: proc(screen: ^t.Screen, mode: t.Term_Mode) {} 34 | 35 | init_screen :: proc(allocator := context.allocator) -> t.Screen { 36 | if !sdl3.InitSubSystem({.VIDEO, .EVENTS}) { 37 | fmt.eprintln(sdl3.GetError()) 38 | panic("failed to initialize virtual terminal") 39 | } 40 | 41 | if !ttf.Init() { 42 | fmt.eprintln(sdl3.GetError()) 43 | panic("failed to load font") 44 | } 45 | 46 | screen: t.Screen 47 | screen.allocator = allocator 48 | 49 | if !sdl3.CreateWindowAndRenderer( 50 | "", 51 | 1000, 52 | 800, 53 | {.RESIZABLE}, 54 | &render_ctx.window, 55 | &render_ctx.renderer, 56 | ) { 57 | fmt.eprintln(sdl3.GetError()) 58 | panic("failed to initialize virtual terminal") 59 | } 60 | 61 | // TODO: dont hardcode font 62 | render_ctx.font = ttf.OpenFont("/usr/share/fonts/TTF/JetBrainsMono-Regular.ttf", 15) 63 | render_ctx.text_engine = ttf.CreateRendererTextEngine(render_ctx.renderer) 64 | if render_ctx.text_engine == nil { 65 | fmt.eprintln(sdl3.GetError()) 66 | panic("failed to initialize text renderer engine") 67 | } 68 | render_ctx.font_cache = make(map[rune]^ttf.Text) 69 | 70 | screen.winbuf = t.init_window(0, 0, nil, nil) 71 | return screen 72 | } 73 | 74 | destroy_screen :: proc(screen: ^t.Screen) { 75 | for _, text in render_ctx.font_cache { 76 | ttf.DestroyText(text) 77 | } 78 | ttf.CloseFont(render_ctx.font) 79 | ttf.DestroyRendererTextEngine(render_ctx.text_engine) 80 | t.destroy_window(&screen.winbuf) 81 | sdl3.DestroyWindow(render_ctx.window) 82 | sdl3.DestroyRenderer(render_ctx.renderer) 83 | sdl3.Quit() 84 | } 85 | 86 | get_cell_size :: proc() -> (cell_h, cell_w: uint) { 87 | cell_width, cell_height: c.int 88 | ttf.GetStringSize(render_ctx.font, " ", len(" "), &cell_width, &cell_height) 89 | return cast(uint)cell_height, cast(uint)cell_width 90 | } 91 | 92 | get_term_size :: proc() -> t.Window_Size { 93 | win_w, win_h: c.int 94 | sdl3.GetWindowSize(render_ctx.window, &win_w, &win_h) 95 | cell_h, cell_w := get_cell_size() 96 | 97 | return t.Window_Size{h = uint(f32(win_h) / f32(cell_h)), w = uint(f32(win_w) / f32(cell_w))} 98 | } 99 | 100 | blit :: proc(win: ^t.Window) { 101 | get_sdl_color :: proc(color: t.Any_Color) -> sdl3.Color { 102 | sdl_color: sdl3.Color 103 | switch c in color { 104 | case t.Color_RGB: 105 | sdl_color.rgb = c.rgb 106 | sdl_color.a = 0xFF 107 | 108 | case t.Color_8: 109 | switch c { 110 | case .Black: 111 | sdl_color = {0x28, 0x2A, 0x36, 0xFF} 112 | case .Blue: 113 | sdl_color = {0x62, 0x72, 0xA4, 0xFF} 114 | case .Cyan: 115 | sdl_color = {0x8B, 0xE9, 0xFD, 0xFF} 116 | case .Green: 117 | sdl_color = {0x50, 0xFA, 0x7B, 0xFF} 118 | case .Magenta: 119 | sdl_color = {0xFF, 0x79, 0xC6, 0xFF} 120 | case .Red: 121 | sdl_color = {0xFF, 0x55, 0x55, 0xFF} 122 | case .White: 123 | sdl_color = {0xF8, 0xF8, 0xF2, 0xFF} 124 | case .Yellow: 125 | sdl_color = {0xF1, 0xFA, 0x8C, 0xFF} 126 | } 127 | } 128 | return sdl_color 129 | } 130 | 131 | sdl3.SetRenderDrawColor(render_ctx.renderer, 0, 0, 0, 0xFF) 132 | sdl3.RenderClear(render_ctx.renderer) 133 | defer sdl3.RenderPresent(render_ctx.renderer) 134 | 135 | cell_h, cell_w := get_cell_size() 136 | x_coord, y_coord: uint 137 | for y in 0 ..< win.cell_buffer.height { 138 | y_coord = cell_h * y + cell_h * win.y_offset 139 | for x in 0 ..< win.cell_buffer.width { 140 | x_coord = cell_w * x + cell_w * win.x_offset 141 | curr_cell := t.cellbuf_get(win.cell_buffer, y, x) 142 | if curr_cell.r == {} { 143 | curr_cell.r = ' ' 144 | } 145 | 146 | if curr_cell.r not_in render_ctx.font_cache { 147 | r, r_len := utf8.encode_rune(curr_cell.r) 148 | text := ttf.CreateText( 149 | render_ctx.text_engine, 150 | render_ctx.font, 151 | cast(cstring)raw_data(&r), 152 | cast(uint)r_len, 153 | ) 154 | render_ctx.font_cache[curr_cell.r] = text 155 | } 156 | 157 | text := render_ctx.font_cache[curr_cell.r] 158 | rect := sdl3.FRect { 159 | x = cast(f32)x_coord, 160 | y = cast(f32)y_coord, 161 | w = cast(f32)cell_w, 162 | h = cast(f32)cell_h, 163 | } 164 | fg_color := get_sdl_color( 165 | curr_cell.styles.fg if curr_cell.styles.fg != nil else .White, 166 | ) 167 | bg_color := get_sdl_color( 168 | curr_cell.styles.bg if curr_cell.styles.bg != nil else .Black, 169 | ) 170 | 171 | sdl3.SetRenderDrawColor( 172 | render_ctx.renderer, 173 | bg_color.r, 174 | bg_color.g, 175 | bg_color.b, 176 | bg_color.a, 177 | ) 178 | sdl3.RenderFillRect(render_ctx.renderer, &rect) 179 | ttf.SetTextColor(text, fg_color.r, fg_color.g, fg_color.b, fg_color.a) 180 | ttf.DrawRendererText(text, cast(f32)x_coord, cast(f32)y_coord) 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /examples/fire/main.odin: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * This file is a partial odin port of Let's Build a Roguelike Chapter 6 3 | * by Richard D. Clark, originally licensed under the Wide Open License. 4 | * 5 | * The original copyright notice is preserved below. 6 | * 7 | * Copyright 2010, Richard D. Clark 8 | * 9 | * The Wide Open License (WOL) 10 | * 11 | * Permission to use, copy, modify, distribute and sell this software and its 12 | * documentation for any purpose is hereby granted without fee, provided that 13 | * the above copyright notice and this license appear in all source copies. 14 | * THIS SOFTWARE IS PROVIDED "AS IS" WITHOUT EXPRESS OR IMPLIED WARRANTY OF 15 | * ANY KIND. See http://www.dspguru.com/wol.htm for more information. 16 | * 17 | *****************************************************************************/ 18 | 19 | package main 20 | 21 | import t "../.." 22 | import tb "../../term" 23 | import "core:math/rand" 24 | 25 | // Set the dimensions 26 | txcols :: 80 // W 27 | txrows :: 40 // H 28 | 29 | fire: [txrows][txcols]int 30 | coolmap: [txrows][txcols]int 31 | 32 | // Executes the game intro. 33 | main :: proc() { 34 | screen := t.init_screen(tb.VTABLE) 35 | defer t.destroy_screen(&screen) 36 | t.clear(&screen, .Everything) 37 | t.blit(&screen) 38 | t.hide_cursor(true) 39 | 40 | CreateCoolMap() 41 | 42 | for { 43 | DrawScreen(&screen, false) 44 | t.blit(&screen) 45 | } 46 | } 47 | 48 | MAXAGE :: 80 49 | 50 | pal := [MAXAGE]t.Color_RGB { 51 | {0xF9, 0xF7, 0xD4}, 52 | {0xF9, 0xF7, 0xD4}, 53 | {0xF9, 0xF7, 0xD4}, 54 | {0xF9, 0xF7, 0xD4}, 55 | {0xF9, 0xF7, 0xD4}, 56 | {0xF9, 0xF7, 0xD4}, 57 | {0xF9, 0xF7, 0xD4}, 58 | {0xF9, 0xF7, 0xD4}, 59 | {0xF9, 0xF7, 0xD4}, 60 | {0xF9, 0xF7, 0xD4}, 61 | {0xF9, 0xF7, 0xD4}, 62 | {0xF9, 0xF7, 0xD4}, 63 | {0xF9, 0xF7, 0xD4}, 64 | {0xF9, 0xF7, 0xD4}, 65 | {0xF9, 0xF7, 0xD4}, 66 | {0xF9, 0xF7, 0xD4}, 67 | {0xF9, 0xF7, 0xD4}, 68 | {0xF9, 0xF7, 0xD4}, 69 | {0xF9, 0xF7, 0xD4}, 70 | {0xF9, 0xF7, 0xD4}, 71 | {0xF9, 0xF7, 0xD4}, 72 | {0xF9, 0xF7, 0xD4}, 73 | {0xF9, 0xF7, 0xD4}, 74 | {0xF9, 0xF7, 0xD4}, 75 | {0xF9, 0xF7, 0xD4}, 76 | {0xF9, 0xF6, 0xB6}, 77 | {0xF8, 0xF4, 0x8E}, 78 | {0xF8, 0xF3, 0x64}, 79 | {0xF8, 0xF1, 0x39}, 80 | {0xF9, 0xEC, 0x14}, 81 | {0xFA, 0xD5, 0x1B}, 82 | {0xFC, 0xBE, 0x22}, 83 | {0xFD, 0xA5, 0x2A}, 84 | {0xFF, 0x8C, 0x31}, 85 | {0xFA, 0x80, 0x2E}, 86 | {0xF4, 0x75, 0x2A}, 87 | {0xEE, 0x6A, 0x26}, 88 | {0xE9, 0x5E, 0x22}, 89 | {0xE3, 0x53, 0x1D}, 90 | {0xDE, 0x47, 0x1A}, 91 | {0xD5, 0x36, 0x13}, 92 | {0xCC, 0x25, 0x0D}, 93 | {0xC3, 0x12, 0x07}, 94 | {0xBB, 0x01, 0x00}, 95 | {0xAC, 0x00, 0x00}, 96 | {0x9D, 0x00, 0x00}, 97 | {0x8E, 0x00, 0x00}, 98 | {0x7F, 0x00, 0x00}, 99 | {0x70, 0x00, 0x00}, 100 | {0x61, 0x00, 0x00}, 101 | {0x5A, 0x00, 0x00}, 102 | {0x55, 0x00, 0x00}, 103 | {0x51, 0x00, 0x00}, 104 | {0x4D, 0x00, 0x00}, 105 | {0x48, 0x00, 0x00}, 106 | {0x44, 0x00, 0x00}, 107 | {0x3F, 0x00, 0x00}, 108 | {0x3B, 0x00, 0x00}, 109 | {0x37, 0x00, 0x00}, 110 | {0x32, 0x00, 0x00}, 111 | {0x2E, 0x00, 0x00}, 112 | {0x2A, 0x00, 0x00}, 113 | {0x25, 0x00, 0x00}, 114 | {0x21, 0x00, 0x00}, 115 | {0x1C, 0x00, 0x00}, 116 | {0x18, 0x00, 0x00}, 117 | {0x13, 0x00, 0x00}, 118 | {0x10, 0x00, 0x00}, 119 | {0x0B, 0x00, 0x00}, 120 | {0x07, 0x00, 0x00}, 121 | {0x02, 0x00, 0x00}, 122 | {0x00, 0x00, 0x00}, 123 | {0x00, 0x00, 0x00}, 124 | {0x00, 0x00, 0x00}, 125 | {0x00, 0x00, 0x00}, 126 | {0x00, 0x00, 0x00}, 127 | {0x00, 0x00, 0x00}, 128 | {0x00, 0x00, 0x00}, 129 | {0x00, 0x00, 0x00}, 130 | {0x00, 0x00, 0x00}, 131 | } 132 | 133 | // This smooths the fire by averaging the values. 134 | Smooth :: proc(arr: [txrows][txcols]int, x, y: int) -> int { 135 | xx, yy, cnt, v: int 136 | 137 | cnt = 0 138 | 139 | v = arr[y][x] 140 | cnt += 1 141 | 142 | if x < txcols - 1 { 143 | xx = x + 1 144 | yy = y 145 | v += arr[yy][xx] 146 | cnt += 1 147 | } 148 | 149 | if x > 0 { 150 | xx = x - 1 151 | yy = y 152 | v += arr[yy][xx] 153 | cnt += 1 154 | } 155 | 156 | if y < txrows - 1 { 157 | xx = x 158 | yy = (y + 1) 159 | v += arr[y + 1][x] 160 | cnt += 1 161 | } 162 | 163 | if y > 0 { 164 | xx = x 165 | yy = (y - 1) 166 | v += arr[y - 1][x] 167 | cnt += 1 168 | } 169 | 170 | v = v / cnt 171 | 172 | return v 173 | } 174 | 175 | //Creates a cool map that will combined with the fire value to give a nice effect. 176 | CreateCoolMap :: proc() { 177 | for y in 0 ..< txrows { 178 | for x in 0 ..< txcols { 179 | coolmap[y][x] = rand.int_max(21) - 10 180 | } 181 | } 182 | 183 | 184 | for _ in 0 ..= 9 { 185 | for y in 1 ..< txrows - 1 { 186 | for x in 1 ..< txcols - 1 { 187 | coolmap[y][x] = Smooth(coolmap, x, y) 188 | } 189 | } 190 | } 191 | } 192 | 193 | // Moves each particle up on the screen, with a chance of moving side to side. 194 | MoveParticles :: proc() { 195 | for y in 1 ..< txrows { 196 | for x in 0 ..< txcols { 197 | // Get the current age of the particle. 198 | tage := fire[y][x] 199 | //Moves particle left (-1) or right (1) or keeps it in current column (0). 200 | xx := rand.int_max(3) - 1 + x 201 | // Wrap around the screen. 202 | if xx < 0 do xx = txcols - 1 203 | if xx > txcols - 1 do xx = 0 204 | // Set the particle age. 205 | tage += coolmap[y - 1][xx] + 1 206 | // Make sure the age is in range. 207 | if tage < 0 do tage = 0 208 | if tage > (MAXAGE - 1) do tage = MAXAGE - 1 209 | fire[y - 1][xx] = tage 210 | } 211 | } 212 | 213 | } 214 | 215 | // Adds particles to the fire along bottom of screen. 216 | AddParticles :: proc() { 217 | for x in 0 ..< txcols { 218 | fire[txrows - 1][x] = rand.int_max(21) 219 | } 220 | 221 | } 222 | 223 | // Draws the fire or parchment on the screen. 224 | DrawScreen :: proc(screen: ^t.Screen, egg: bool) { 225 | MoveParticles() 226 | AddParticles() 227 | for y in 0 ..< txrows { 228 | for x in 0 ..< txcols { 229 | if fire[y][x] < MAXAGE { 230 | cage := Smooth(fire, x, y) 231 | cage += 10 232 | if cage >= MAXAGE do cage = MAXAGE - 1 233 | clr := pal[cage] 234 | t.move_cursor(screen, uint(y), uint(x)) 235 | t.set_color_style(screen, nil, clr) 236 | t.write_rune(screen, ' ') 237 | t.reset_styles(screen) 238 | } 239 | } 240 | } 241 | } 242 | 243 | -------------------------------------------------------------------------------- /examples/snake/main.odin: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import t "../.." 4 | import tb "../../term" 5 | import "core:math/rand" 6 | import "core:time" 7 | 8 | Direction :: enum { 9 | Up, 10 | Down, 11 | Left, 12 | Right, 13 | } 14 | 15 | Snake :: struct { 16 | head: [2]uint, 17 | body: [dynamic][2]uint, 18 | dir: Direction, 19 | } 20 | 21 | get_term_center :: proc(s: ^t.Screen) -> [2]uint { 22 | termsize := t.get_window_size(s) 23 | pos := [2]uint{termsize.w / 2, termsize.h / 2} 24 | return pos 25 | } 26 | 27 | snake_init :: proc(s: ^t.Screen) -> Snake { 28 | DEFAULT_SNAKE_SIZE :: 4 29 | pos := get_term_center(s) 30 | pos.x -= DEFAULT_SNAKE_SIZE 31 | 32 | body := make([dynamic][2]uint) 33 | 34 | for i in 0 ..< DEFAULT_SNAKE_SIZE { 35 | curr_pos := pos 36 | curr_pos.x += uint(i) 37 | append(&body, curr_pos) 38 | } 39 | 40 | pos.x += DEFAULT_SNAKE_SIZE 41 | 42 | return Snake{head = pos, body = body, dir = .Right} 43 | } 44 | 45 | snake_destroy :: proc(snake: Snake) { 46 | delete(snake.body) 47 | } 48 | 49 | snake_draw :: proc(snake: Snake, s: ^t.Screen) { 50 | t.set_color_style(s, nil, .Green) 51 | defer t.reset_styles(s) 52 | 53 | for part in snake.body { 54 | t.move_cursor(s, part.y, part.x) 55 | t.write(s, ' ') 56 | } 57 | 58 | t.set_color_style(s, nil, .White) 59 | t.move_cursor(s, snake.head.y, snake.head.x) 60 | t.write(s, ' ') 61 | } 62 | 63 | Game_Box :: struct { 64 | x, y, w, h: uint, 65 | } 66 | 67 | box_init :: proc(s: ^t.Screen) -> Game_Box { 68 | termcenter := get_term_center(s) 69 | 70 | BOX_HEIGHT :: 20 71 | BOX_WIDTH :: 60 72 | 73 | return Game_Box { 74 | x = termcenter.x - BOX_WIDTH / 2, 75 | y = termcenter.y - BOX_HEIGHT / 2, 76 | w = BOX_WIDTH, 77 | h = BOX_HEIGHT, 78 | } 79 | } 80 | 81 | box_draw :: proc(game: Game, s: ^t.Screen) { 82 | box := game.box 83 | t.set_color_style(s, .Black, .White) 84 | defer t.reset_styles(s) 85 | 86 | draw_row :: proc(box: Game_Box, s: ^t.Screen, y: uint) { 87 | for i in 0 ..= box.w { 88 | t.move_cursor(s, y, box.x + i) 89 | t.write(s, ' ') 90 | } 91 | } 92 | 93 | draw_row(box, s, box.y) 94 | draw_row(box, s, box.y + box.h) 95 | 96 | draw_col :: proc(box: Game_Box, s: ^t.Screen, x: uint) { 97 | for i in 0 ..= box.h { 98 | t.move_cursor(s, box.y + i, x) 99 | t.write(s, ' ') 100 | } 101 | } 102 | 103 | draw_col(box, s, box.x) 104 | draw_col(box, s, box.x + box.w) 105 | 106 | t.set_text_style(s, {.Bold}) 107 | msg := "Press 'q' to exit. Player Score: %d" 108 | t.move_cursor(s, box.y + box.h, box.x + (box.w / 2 - len(msg) / 2)) 109 | t.writef(s, msg, game.score) 110 | t.reset_styles(s) 111 | } 112 | 113 | snake_handle_input :: proc(s: ^t.Screen, game: ^Game, input: t.Keyboard_Input) { 114 | snake := &game.snake 115 | box := &game.box 116 | 117 | #partial switch input.key { 118 | case .Arrow_Left, .A: 119 | if snake.dir != .Right do snake.dir = .Left 120 | case .Arrow_Right, .D: 121 | if snake.dir != .Left do snake.dir = .Right 122 | case .Arrow_Up, .W: 123 | if snake.dir != .Down do snake.dir = .Up 124 | case .Arrow_Down, .S: 125 | if snake.dir != .Up do snake.dir = .Down 126 | } 127 | 128 | ordered_remove(&snake.body, 0) 129 | append(&snake.body, snake.head) 130 | 131 | switch snake.dir { 132 | case .Up: 133 | snake.head.y -= 1 134 | case .Down: 135 | snake.head.y += 1 136 | case .Left: 137 | snake.head.x -= 1 138 | case .Right: 139 | snake.head.x += 1 140 | } 141 | 142 | if snake.head.y <= box.y { 143 | snake.head.y = box.y + box.h - 1 144 | } 145 | 146 | if snake.head.y >= box.y + box.h { 147 | snake.head.y = box.y 148 | } 149 | 150 | if snake.head.x >= box.x + box.w { 151 | snake.head.x = box.x + 1 152 | } 153 | 154 | if snake.head.x <= box.x { 155 | snake.head.x = box.x + box.w - 1 156 | } 157 | } 158 | 159 | Food :: struct { 160 | x, y: uint, 161 | } 162 | 163 | food_generate :: proc(game: Game) -> Food { 164 | box := game.box 165 | x, y: uint 166 | 167 | for { 168 | x = (cast(uint)rand.uint32() % (box.w - 1)) + box.x + 1 169 | y = (cast(uint)rand.uint32() % (box.h - 1)) + box.y + 1 170 | 171 | no_collision := true 172 | for part in game.snake.body { 173 | if part.x == x && part.y == y { 174 | no_collision = false 175 | } 176 | } 177 | 178 | if no_collision do break 179 | } 180 | 181 | return Food{x = x, y = y} 182 | } 183 | 184 | Game :: struct { 185 | snake: Snake, 186 | box: Game_Box, 187 | food: Food, 188 | score: uint, 189 | } 190 | 191 | game_init :: proc(s: ^t.Screen) -> Game { 192 | game := Game { 193 | snake = snake_init(s), 194 | box = box_init(s), 195 | } 196 | game.food = food_generate(game) 197 | return game 198 | } 199 | 200 | game_destroy :: proc(game: Game) { 201 | snake_destroy(game.snake) 202 | } 203 | 204 | game_is_over :: proc(game: Game, s: ^t.Screen) -> bool { 205 | snake := game.snake 206 | for part in snake.body { 207 | if part.x == snake.head.x && part.y == snake.head.y { 208 | return true 209 | } 210 | } 211 | 212 | return false 213 | } 214 | 215 | game_tick :: proc(game: ^Game, s: ^t.Screen) { 216 | if game.snake.head.x == game.food.x && game.snake.head.y == game.food.y { 217 | game.score += 1 218 | game.food = food_generate(game^) 219 | append(&game.snake.body, game.snake.body[len(game.snake.body) - 1]) 220 | } 221 | 222 | t.move_cursor(s, game.food.y, game.food.x) 223 | t.set_color_style(s, nil, .Yellow) 224 | t.write(s, ' ') 225 | t.reset_styles(s) 226 | 227 | snake_draw(game.snake, s) 228 | box_draw(game^, s) 229 | } 230 | 231 | 232 | main :: proc() { 233 | s := t.init_screen(tb.VTABLE) 234 | defer t.destroy_screen(&s) 235 | t.set_term_mode(&s, .Cbreak) 236 | t.hide_cursor(true) 237 | t.clear(&s, .Everything) 238 | t.blit(&s) 239 | 240 | game := game_init(&s) 241 | stopwatch: time.Stopwatch 242 | 243 | for { 244 | defer t.blit(&s) 245 | 246 | if game_is_over(game, &s) { 247 | t.move_cursor(&s, game.box.y + game.box.h + 5, 0) 248 | t.write(&s, "=== Game Over ===\n") 249 | break 250 | } 251 | 252 | time.stopwatch_start(&stopwatch) 253 | defer time.stopwatch_reset(&stopwatch) 254 | t.clear(&s, .Everything) 255 | 256 | input := t.read(&s) 257 | kb_input, kb_ok := input.(t.Keyboard_Input) 258 | 259 | if kb_input.key == .Q { 260 | return 261 | } 262 | 263 | snake_handle_input(&s, &game, kb_input) 264 | 265 | for { 266 | duration := time.stopwatch_duration(stopwatch) 267 | millisecs := time.duration_milliseconds(duration) 268 | if millisecs > 80 { 269 | break 270 | } 271 | } 272 | 273 | game_tick(&game, &s) 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /sdl3/input.odin: -------------------------------------------------------------------------------- 1 | package termcl_sdl3 2 | 3 | import t ".." 4 | import os "core:os/os2" 5 | import "vendor:sdl3" 6 | 7 | read :: proc(screen: ^t.Screen) -> t.Input { 8 | e: sdl3.Event 9 | for sdl3.PollEvent(&e) { 10 | // TODO: consider other approach for quitting? idk 11 | if e.type == .QUIT { 12 | os.exit(0) 13 | } 14 | 15 | #partial switch e.type { 16 | case .KEY_DOWN: 17 | kb: t.Keyboard_Input 18 | /* MODIFIERS */{ 19 | if (e.key.mod & {.LCTRL, .RCTRL}) != {} { 20 | kb.mod = .Ctrl 21 | } 22 | if (e.key.mod & {.LALT, .RALT}) != {} { 23 | kb.mod = .Alt 24 | } 25 | if (e.key.mod & {.LSHIFT, .RSHIFT}) != {} { 26 | kb.mod = .Shift 27 | } 28 | } 29 | 30 | switch e.key.key { 31 | case sdl3.K_LEFT: 32 | kb.key = .Arrow_Left 33 | case sdl3.K_RIGHT: 34 | kb.key = .Arrow_Right 35 | case sdl3.K_UP: 36 | kb.key = .Arrow_Up 37 | case sdl3.K_DOWN: 38 | kb.key = .Arrow_Down 39 | case sdl3.K_PAGEUP: 40 | kb.key = .Page_Up 41 | case sdl3.K_PAGEDOWN: 42 | kb.key = .Page_Down 43 | case sdl3.K_HOME: 44 | kb.key = .Home 45 | case sdl3.K_END: 46 | kb.key = .End 47 | case sdl3.K_INSERT: 48 | kb.key = .Insert 49 | case sdl3.K_DELETE: 50 | kb.key = .Delete 51 | case sdl3.K_F1: 52 | kb.key = .F1 53 | case sdl3.K_F2: 54 | kb.key = .F2 55 | case sdl3.K_F3: 56 | kb.key = .F3 57 | case sdl3.K_F4: 58 | kb.key = .F4 59 | case sdl3.K_F5: 60 | kb.key = .F5 61 | case sdl3.K_F6: 62 | kb.key = .F6 63 | case sdl3.K_F7: 64 | kb.key = .F7 65 | case sdl3.K_F8: 66 | kb.key = .F8 67 | case sdl3.K_F9: 68 | kb.key = .F9 69 | case sdl3.K_F10: 70 | kb.key = .F10 71 | case sdl3.K_F11: 72 | kb.key = .F11 73 | case sdl3.K_F12: 74 | kb.key = .F12 75 | case sdl3.K_ESCAPE: 76 | kb.key = .Escape 77 | case sdl3.K_0: 78 | kb.key = .Num_0 79 | case sdl3.K_1: 80 | kb.key = .Num_1 81 | case sdl3.K_2: 82 | kb.key = .Num_2 83 | case sdl3.K_3: 84 | kb.key = .Num_3 85 | case sdl3.K_4: 86 | kb.key = .Num_4 87 | case sdl3.K_5: 88 | kb.key = .Num_5 89 | case sdl3.K_6: 90 | kb.key = .Num_6 91 | case sdl3.K_7: 92 | kb.key = .Num_7 93 | case sdl3.K_8: 94 | kb.key = .Num_8 95 | case sdl3.K_9: 96 | kb.key = .Num_9 97 | case sdl3.K_RETURN: 98 | kb.key = .Enter 99 | case sdl3.K_TAB: 100 | kb.key = .Tab 101 | case sdl3.K_BACKSPACE: 102 | kb.key = .Backspace 103 | case sdl3.K_A: 104 | kb.key = .A 105 | case sdl3.K_B: 106 | kb.key = .B 107 | case sdl3.K_C: 108 | kb.key = .C 109 | case sdl3.K_D: 110 | kb.key = .D 111 | case sdl3.K_E: 112 | kb.key = .E 113 | case sdl3.K_F: 114 | kb.key = .F 115 | case sdl3.K_G: 116 | kb.key = .G 117 | case sdl3.K_H: 118 | kb.key = .H 119 | case sdl3.K_I: 120 | kb.key = .I 121 | case sdl3.K_J: 122 | kb.key = .J 123 | case sdl3.K_K: 124 | kb.key = .K 125 | case sdl3.K_L: 126 | kb.key = .L 127 | case sdl3.K_M: 128 | kb.key = .M 129 | case sdl3.K_N: 130 | kb.key = .N 131 | case sdl3.K_O: 132 | kb.key = .O 133 | case sdl3.K_P: 134 | kb.key = .P 135 | case sdl3.K_Q: 136 | kb.key = .Q 137 | case sdl3.K_R: 138 | kb.key = .R 139 | case sdl3.K_S: 140 | kb.key = .S 141 | case sdl3.K_T: 142 | kb.key = .T 143 | case sdl3.K_U: 144 | kb.key = .U 145 | case sdl3.K_V: 146 | kb.key = .V 147 | case sdl3.K_W: 148 | kb.key = .W 149 | case sdl3.K_X: 150 | kb.key = .X 151 | case sdl3.K_Y: 152 | kb.key = .Y 153 | case sdl3.K_Z: 154 | kb.key = .Z 155 | case sdl3.K_MINUS: 156 | kb.key = .Minus 157 | case sdl3.K_PLUS: 158 | kb.key = .Plus 159 | case sdl3.K_EQUALS: 160 | kb.key = .Equal 161 | case sdl3.K_LEFTPAREN: 162 | kb.key = .Open_Paren 163 | case sdl3.K_RIGHTPAREN: 164 | kb.key = .Close_Paren 165 | case sdl3.K_LEFTBRACE: 166 | kb.key = .Open_Curly_Bracket 167 | case sdl3.K_RIGHTBRACE: 168 | kb.key = .Close_Curly_Bracket 169 | case sdl3.K_LEFTBRACKET: 170 | kb.key = .Open_Square_Bracket 171 | case sdl3.K_RIGHTBRACKET: 172 | kb.key = .Close_Square_Bracket 173 | case sdl3.K_COLON: 174 | kb.key = .Colon 175 | case sdl3.K_SEMICOLON: 176 | kb.key = .Semicolon 177 | case sdl3.K_SLASH: 178 | kb.key = .Slash 179 | case sdl3.K_BACKSLASH: 180 | kb.key = .Backslash 181 | // TODO 182 | // case: 183 | // kb.key = .Single_Quote 184 | // TODO 185 | // case: 186 | // kb.key = .Double_Quote 187 | case sdl3.K_PERIOD: 188 | kb.key = .Period 189 | case sdl3.K_ASTERISK: 190 | kb.key = .Asterisk 191 | // TODO 192 | // case: 193 | // kb.key = .Backtick 194 | case sdl3.K_SPACE: 195 | kb.key = .Space 196 | case sdl3.K_DOLLAR: 197 | kb.key = .Dollar 198 | case sdl3.K_EXCLAIM: 199 | kb.key = .Exclamation 200 | case sdl3.K_HASH: 201 | kb.key = .Hash 202 | case sdl3.K_PERCENT: 203 | kb.key = .Percent 204 | case sdl3.K_AMPERSAND: 205 | kb.key = .Ampersand 206 | // // TODO 207 | // case: 208 | // kb.key = .Tick 209 | case sdl3.K_UNDERSCORE: 210 | kb.key = .Underscore 211 | case sdl3.K_CARET: 212 | kb.key = .Caret 213 | case sdl3.K_COMMA: 214 | kb.key = .Comma 215 | case sdl3.K_PIPE: 216 | kb.key = .Pipe 217 | case sdl3.K_AT: 218 | kb.key = .At 219 | case sdl3.K_TILDE: 220 | kb.key = .Tilde 221 | case sdl3.K_LESS: 222 | kb.key = .Less_Than 223 | case sdl3.K_GREATER: 224 | kb.key = .Greater_Than 225 | case sdl3.K_QUESTION: 226 | kb.key = .Question_Mark 227 | case: 228 | kb.key = .None 229 | } 230 | 231 | return kb 232 | 233 | case .MOUSE_BUTTON_DOWN, .MOUSE_BUTTON_UP, .MOUSE_WHEEL, .MOUSE_MOTION: 234 | mouse: t.Mouse_Input 235 | // TODO: convert to cell based coordinates 236 | mouse.coord.x = cast(uint)e.motion.x 237 | mouse.coord.y = cast(uint)e.motion.y 238 | 239 | if e.wheel.y > 0 { 240 | mouse.key = .Scroll_Up 241 | } else if e.wheel.y < 0 { 242 | mouse.key = .Scroll_Down 243 | } 244 | 245 | #partial switch e.type { 246 | case .MOUSE_BUTTON_UP: 247 | mouse.event = {.Released} 248 | case .MOUSE_BUTTON_DOWN: 249 | mouse.event = {.Pressed} 250 | } 251 | 252 | switch e.button.button { 253 | case sdl3.BUTTON_RIGHT: 254 | mouse.key = .Right 255 | case sdl3.BUTTON_LEFT: 256 | mouse.key = .Left 257 | case sdl3.BUTTON_MIDDLE: 258 | mouse.key = .Middle 259 | } 260 | 261 | /* MODIFIERS */{ 262 | if (e.key.mod & {.LCTRL, .RCTRL}) != {} { 263 | mouse.mod += {.Ctrl} 264 | } 265 | if (e.key.mod & {.LALT, .RALT}) != {} { 266 | mouse.mod += {.Alt} 267 | } 268 | if (e.key.mod & {.LSHIFT, .RSHIFT}) != {} { 269 | mouse.mod += {.Shift} 270 | } 271 | } 272 | 273 | return mouse 274 | } 275 | } 276 | 277 | return nil 278 | } 279 | 280 | read_blocking :: proc(screen: ^t.Screen) -> t.Input { 281 | for { 282 | i := read(screen) 283 | if i != nil { 284 | return i 285 | } 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /raw/raw.odin: -------------------------------------------------------------------------------- 1 | package raw 2 | 3 | import "core:fmt" 4 | import "core:strings" 5 | import "core:terminal/ansi" 6 | 7 | Text_Style :: enum { 8 | None, 9 | Bold, 10 | Italic, 11 | Underline, 12 | Crossed, 13 | Inverted, 14 | Dim, 15 | } 16 | 17 | /* 18 | Colors from the original 8-color palette. 19 | These should be supported everywhere this library is supported. 20 | */ 21 | Color_8 :: enum { 22 | Black, 23 | Red, 24 | Green, 25 | Yellow, 26 | Blue, 27 | Magenta, 28 | Cyan, 29 | White, 30 | } 31 | 32 | /* 33 | RGB color. This is should be supported by every modern terminal. 34 | In case you need to support an older terminals, use `Color_8` instead 35 | */ 36 | Color_RGB :: [3]u8 37 | 38 | Any_Color :: union { 39 | Color_8, 40 | Color_RGB, 41 | } 42 | 43 | /* 44 | Indicates how to clear the window. 45 | */ 46 | Clear_Mode :: enum { 47 | // Clear everything before the cursor 48 | Before_Cursor, 49 | // Clear everything after the cursor 50 | After_Cursor, 51 | // Clear the whole screen/window 52 | Everything, 53 | } 54 | 55 | /* 56 | Move the cursor with 0-based index 57 | */ 58 | move_cursor :: proc(sb: ^strings.Builder, y, x: uint) { 59 | CURSOR_POSITION :: ansi.CSI + "%d;%dH" 60 | strings.write_string(sb, ansi.CSI) 61 | // x and y are shifted by one position so that programmers can keep using 0 based indexing 62 | strings.write_uint(sb, y + 1) 63 | strings.write_rune(sb, ';') 64 | strings.write_uint(sb, x + 1) 65 | strings.write_rune(sb, 'H') 66 | } 67 | 68 | /* 69 | Hides the cursor so that it's not showed in the terminal 70 | */ 71 | hide_cursor :: proc(hide: bool) { 72 | SHOW_CURSOR :: ansi.CSI + "?25h" 73 | HIDE_CURSOR :: ansi.CSI + "?25l" 74 | fmt.print(HIDE_CURSOR if hide else SHOW_CURSOR) 75 | } 76 | 77 | /* 78 | Sets the style used by the window. 79 | 80 | **Inputs** 81 | - `win`: the window whose text style will be changed 82 | - `styles`: the styles that will be applied 83 | 84 | Note: It is good practice to `reset_styles` when the styles are not needed anymore. 85 | */ 86 | set_text_style :: proc(sb: ^strings.Builder, styles: bit_set[Text_Style]) { 87 | SGR_BOLD :: ansi.CSI + ansi.BOLD + "m" 88 | SGR_DIM :: ansi.CSI + ansi.FAINT + "m" 89 | SGR_ITALIC :: ansi.CSI + ansi.ITALIC + "m" 90 | SGR_UNDERLINE :: ansi.CSI + ansi.UNDERLINE + "m" 91 | SGR_INVERTED :: ansi.CSI + ansi.INVERT + "m" 92 | SGR_CROSSED :: ansi.CSI + ansi.STRIKE + "m" 93 | 94 | if .Bold in styles do strings.write_string(sb, SGR_BOLD) 95 | if .Dim in styles do strings.write_string(sb, SGR_DIM) 96 | if .Italic in styles do strings.write_string(sb, SGR_ITALIC) 97 | if .Underline in styles do strings.write_string(sb, SGR_UNDERLINE) 98 | if .Inverted in styles do strings.write_string(sb, SGR_INVERTED) 99 | if .Crossed in styles do strings.write_string(sb, SGR_CROSSED) 100 | } 101 | 102 | @(private) 103 | _set_color_8 :: proc(builder: ^strings.Builder, color: uint) { 104 | SGR_COLOR :: ansi.CSI + "%dm" 105 | strings.write_string(builder, ansi.CSI) 106 | strings.write_uint(builder, color) 107 | strings.write_rune(builder, 'm') 108 | } 109 | 110 | @(private) 111 | _get_color_8_code :: proc(c: Color_8, is_bg: bool) -> uint { 112 | code: uint 113 | switch c { 114 | case .Black: 115 | code = 30 116 | case .Red: 117 | code = 31 118 | case .Green: 119 | code = 32 120 | case .Yellow: 121 | code = 33 122 | case .Blue: 123 | code = 34 124 | case .Magenta: 125 | code = 35 126 | case .Cyan: 127 | code = 36 128 | case .White: 129 | code = 37 130 | } 131 | 132 | if is_bg do code += 10 133 | return code 134 | } 135 | 136 | @(private) 137 | _set_color_rgb :: proc(builder: ^strings.Builder, color: Color_RGB, is_bg: bool) { 138 | strings.write_string(builder, ansi.CSI) 139 | strings.write_uint(builder, 48 if is_bg else 38) 140 | strings.write_string(builder, ";2;") 141 | strings.write_uint(builder, cast(uint)color.r) 142 | strings.write_rune(builder, ';') 143 | strings.write_uint(builder, cast(uint)color.g) 144 | strings.write_rune(builder, ';') 145 | strings.write_uint(builder, cast(uint)color.b) 146 | strings.write_rune(builder, 'm') 147 | } 148 | 149 | /* 150 | Sets background and foreground colors based on the original 8-color palette 151 | 152 | **Inputs** 153 | - `win`: the window that will use the colors set 154 | - `fg`: the foreground color, if the color is nil the default foreground color will be used 155 | */ 156 | set_fg_color_style :: proc(sb: ^strings.Builder, fg: Any_Color) { 157 | DEFAULT_FG :: 39 158 | switch fg_color in fg { 159 | case Color_8: 160 | _set_color_8(sb, _get_color_8_code(fg_color, false)) 161 | case Color_RGB: 162 | _set_color_rgb(sb, fg_color, false) 163 | case: 164 | _set_color_8(sb, DEFAULT_FG) 165 | } 166 | } 167 | 168 | /* 169 | Sets background and foreground colors based on the original 8-color palette 170 | 171 | **Inputs** 172 | - `win`: the window that will use the colors set 173 | - `bg`: the foreground color, if the color is nil the default foreground color will be used 174 | */ 175 | set_bg_color_style :: proc(sb: ^strings.Builder, bg: Any_Color) { 176 | DEFAULT_BG :: 49 177 | switch bg_color in bg { 178 | case Color_8: 179 | _set_color_8(sb, _get_color_8_code(bg_color, true)) 180 | case Color_RGB: 181 | _set_color_rgb(sb, bg_color, true) 182 | case: 183 | _set_color_8(sb, DEFAULT_BG) 184 | } 185 | } 186 | 187 | reset_styles :: proc(bg: ^strings.Builder) { 188 | strings.write_string(bg, ansi.CSI + "0m") 189 | } 190 | 191 | clear :: proc(sb: ^strings.Builder, mode: Clear_Mode) { 192 | switch mode { 193 | case .After_Cursor: 194 | strings.write_string(sb, ansi.CSI + "0J") 195 | case .Before_Cursor: 196 | strings.write_string(sb, ansi.CSI + "1J") 197 | case .Everything: 198 | strings.write_string(sb, ansi.CSI + "H" + ansi.CSI + "2J") 199 | } 200 | } 201 | 202 | /* 203 | Clear the current line the cursor is in. 204 | 205 | **Inputs** 206 | - `win`: the window whose current line will be cleared 207 | - `mode`: how the window will be cleared 208 | */ 209 | clear_line :: proc(sb: ^strings.Builder, mode: Clear_Mode) { 210 | switch mode { 211 | case .After_Cursor: 212 | strings.write_string(sb, ansi.CSI + "0K") 213 | case .Before_Cursor: 214 | strings.write_string(sb, ansi.CSI + "1K") 215 | case .Everything: 216 | strings.write_string(sb, ansi.CSI + "2K") 217 | } 218 | } 219 | 220 | /* 221 | Ring the terminal bell. (potentially annoying to users :P) 222 | 223 | Note: this rings the bell as soon as this procedure is called. 224 | */ 225 | ring_bell :: proc() { 226 | fmt.print("\a") 227 | } 228 | 229 | /* 230 | Enable mouse to be able to respond to mouse inputs. 231 | 232 | Note: Mouse is enabled by default if you're in raw mode. 233 | */ 234 | enable_mouse :: proc(enable: bool) { 235 | ANY_EVENT :: "\x1b[?1003" 236 | SGR_MOUSE :: "\x1b[?1006" 237 | 238 | if enable { 239 | fmt.print(ANY_EVENT + "h", SGR_MOUSE + "h") 240 | } else { 241 | fmt.print(ANY_EVENT + "l", SGR_MOUSE + "l") 242 | } 243 | } 244 | 245 | /* 246 | Enables alternative screen buffer 247 | 248 | This allows one to draw to the screen and then recover the screen the user had 249 | before the program started. Useful for TUI programs. 250 | */ 251 | enable_alt_buffer :: proc(enable: bool) { 252 | if enable { 253 | fmt.print("\x1b[?1049h") 254 | } else { 255 | fmt.print("\x1b[?1049l") 256 | } 257 | } 258 | 259 | -------------------------------------------------------------------------------- /term/input.odin: -------------------------------------------------------------------------------- 1 | package term 2 | 3 | import t ".." 4 | import "core:fmt" 5 | import os "core:os" 6 | import "core:strconv" 7 | import "core:unicode" 8 | 9 | read :: proc(screen: ^t.Screen) -> t.Input { 10 | input, has_input := raw_read(screen.input_buf[:]) 11 | if !has_input do return nil 12 | 13 | mouse_input, mouse_ok := parse_mouse_input(input[:]) 14 | if mouse_ok { 15 | return mouse_input 16 | } 17 | 18 | kb_input, kb_ok := parse_keyboard_input(input[:]) 19 | if kb_ok { 20 | return kb_input 21 | } 22 | 23 | return nil 24 | } 25 | 26 | read_raw :: proc(screen: ^t.Screen) -> (input: []byte, ok: bool) { 27 | return raw_read(screen.input_buf[:]) 28 | } 29 | 30 | read_raw_blocking :: proc(screen: ^t.Screen) -> (input: []byte, ok: bool) { 31 | bytes_read, err := os.read(os.stdin, screen.input_buf[:]) 32 | if err != nil { 33 | panic("failing to get user input") 34 | } 35 | 36 | return screen.input_buf[:bytes_read], bytes_read > 0 37 | } 38 | 39 | read_blocking :: proc(screen: ^t.Screen) -> t.Input { 40 | input, input_ok := read_raw_blocking(screen) 41 | if !input_ok do return nil 42 | 43 | mouse_input, mouse_ok := parse_mouse_input(input) 44 | if mouse_ok { 45 | return mouse_input 46 | } 47 | 48 | kb_input, kb_ok := parse_keyboard_input(input) 49 | if kb_ok { 50 | return kb_input 51 | } 52 | 53 | return nil 54 | } 55 | 56 | 57 | /* 58 | Parses the raw bytes sent by the terminal in `Input`. 59 | 60 | **Inputs** 61 | - `input`: bytes received from terminal stdin 62 | 63 | **Returns** 64 | - `keyboard_input`: parsed keyboard input 65 | - `has_input`: true if has input 66 | 67 | Note: the terminal processes some inputs making them be treated the same 68 | so if you're checking for a certain input and it's never true, 69 | check what value it is processed into. 70 | 71 | For example, `.Escape` is either Esc, Ctrl + [ and Ctrl + 3 to the terminal. 72 | */ 73 | parse_keyboard_input :: proc( 74 | input: []byte, 75 | ) -> ( 76 | keyboard_input: t.Keyboard_Input, 77 | has_input: bool, 78 | ) { 79 | _alnum_to_key :: proc(b: byte) -> (key: t.Key, ok: bool) { 80 | switch b { 81 | case '\x1b': 82 | key = .Escape 83 | case '1': 84 | key = .Num_1 85 | case '2': 86 | key = .Num_2 87 | case '3': 88 | key = .Num_3 89 | case '4': 90 | key = .Num_4 91 | case '5': 92 | key = .Num_5 93 | case '6': 94 | key = .Num_6 95 | case '7': 96 | key = .Num_7 97 | case '8': 98 | key = .Num_8 99 | case '9': 100 | key = .Num_9 101 | case '0': 102 | key = .Num_0 103 | case '\r', '\n': 104 | key = .Enter 105 | case '\t': 106 | key = .Tab 107 | case 8, 127: 108 | key = .Backspace 109 | case 'a', 'A': 110 | key = .A 111 | case 'b', 'B': 112 | key = .B 113 | case 'c', 'C': 114 | key = .C 115 | case 'd', 'D': 116 | key = .D 117 | case 'e', 'E': 118 | key = .E 119 | case 'f', 'F': 120 | key = .F 121 | case 'g', 'G': 122 | key = .G 123 | case 'h', 'H': 124 | key = .H 125 | case 'i', 'I': 126 | key = .I 127 | case 'j', 'J': 128 | key = .J 129 | case 'k', 'K': 130 | key = .K 131 | case 'l', 'L': 132 | key = .L 133 | case 'm', 'M': 134 | key = .M 135 | case 'n', 'N': 136 | key = .N 137 | case 'o', 'O': 138 | key = .O 139 | case 'p', 'P': 140 | key = .P 141 | case 'q', 'Q': 142 | key = .Q 143 | case 'r', 'R': 144 | key = .R 145 | case 's', 'S': 146 | key = .S 147 | case 't', 'T': 148 | key = .T 149 | case 'u', 'U': 150 | key = .U 151 | case 'v', 'V': 152 | key = .V 153 | case 'w', 'W': 154 | key = .W 155 | case 'x', 'X': 156 | key = .X 157 | case 'y', 'Y': 158 | key = .Y 159 | case 'z', 'Z': 160 | key = .Z 161 | case ',': 162 | key = .Comma 163 | case ':': 164 | key = .Colon 165 | case ';': 166 | key = .Semicolon 167 | case '-': 168 | key = .Minus 169 | case '+': 170 | key = .Plus 171 | case '=': 172 | key = .Equal 173 | case '{': 174 | key = .Open_Curly_Bracket 175 | case '}': 176 | key = .Close_Curly_Bracket 177 | case '(': 178 | key = .Open_Paren 179 | case ')': 180 | key = .Close_Paren 181 | case '[': 182 | key = .Open_Square_Bracket 183 | case ']': 184 | key = .Close_Square_Bracket 185 | case '/': 186 | key = .Slash 187 | case '\'': 188 | key = .Single_Quote 189 | case '"': 190 | key = .Double_Quote 191 | case '.': 192 | key = .Period 193 | case '*': 194 | key = .Asterisk 195 | case '`': 196 | key = .Backtick 197 | case '\\': 198 | key = .Backslash 199 | case ' ': 200 | key = .Space 201 | case '$': 202 | key = .Dollar 203 | case '!': 204 | key = .Exclamation 205 | case '#': 206 | key = .Hash 207 | case '%': 208 | key = .Percent 209 | case '&': 210 | key = .Ampersand 211 | case '´': 212 | key = .Tick 213 | case '_': 214 | key = .Underscore 215 | case '^': 216 | key = .Caret 217 | case '|': 218 | key = .Pipe 219 | case '@': 220 | key = .At 221 | case '~': 222 | key = .Tilde 223 | case '<': 224 | key = .Less_Than 225 | case '>': 226 | key = .Greater_Than 227 | case '?': 228 | key = .Question_Mark 229 | case: 230 | ok = false 231 | return 232 | } 233 | 234 | ok = true 235 | return 236 | } 237 | 238 | input := input 239 | seq: t.Keyboard_Input 240 | 241 | if len(input) == 0 do return 242 | 243 | if len(input) == 1 { 244 | input_rune := cast(rune)input[0] 245 | if unicode.is_upper(input_rune) { 246 | seq.mod = .Shift 247 | } 248 | 249 | if unicode.is_control(input_rune) { 250 | switch input_rune { 251 | case: 252 | seq.mod = .Ctrl 253 | input[0] += 64 254 | case '\b', '\n': 255 | seq.mod = .Ctrl 256 | case '\r', 257 | 127, /* backspace */ 258 | '\t', 259 | '\x1b': 260 | } 261 | } 262 | 263 | key, ok := _alnum_to_key(input[0]) 264 | if !ok do return {}, false 265 | seq.key = key 266 | return seq, true 267 | } 268 | 269 | if input[0] != '\x1b' do return 270 | 271 | if len(input) > 3 { 272 | input_len := len(input) 273 | 274 | if input[input_len - 3] == ';' { 275 | switch input[input_len - 2] { 276 | case '2': 277 | seq.mod = .Shift 278 | case '3': 279 | seq.mod = .Alt 280 | case '5': 281 | seq.mod = .Ctrl 282 | 283 | } 284 | } 285 | } 286 | 287 | if len(input) >= 2 { 288 | switch input[len(input) - 1] { 289 | case 'P': 290 | seq.key = .F1 291 | case 'Q': 292 | seq.key = .F2 293 | case 'R': 294 | seq.key = .F3 295 | case 'S': 296 | seq.key = .F4 297 | 298 | } 299 | 300 | if input[1] == 'O' do return seq, true 301 | } 302 | 303 | if input[1] == '[' { 304 | input = input[2:] 305 | 306 | if len(input) > 2 && input[0] == '1' && input[1] == ';' { 307 | switch input[2] { 308 | case '2': 309 | seq.mod = .Shift 310 | case '3': 311 | seq.mod = .Alt 312 | case '5': 313 | seq.mod = .Ctrl 314 | } 315 | 316 | input = input[3:] 317 | } 318 | 319 | 320 | if len(input) == 1 { 321 | switch input[0] { 322 | case 'H': 323 | seq.key = .Home 324 | case 'F': 325 | seq.key = .End 326 | case 'A': 327 | seq.key = .Arrow_Up 328 | case 'B': 329 | seq.key = .Arrow_Down 330 | case 'C': 331 | seq.key = .Arrow_Right 332 | case 'D': 333 | seq.key = .Arrow_Left 334 | case 'Z': 335 | seq.key = .Tab 336 | seq.mod = .Shift 337 | } 338 | } 339 | 340 | 341 | if len(input) >= 2 { 342 | switch input[0] { 343 | case 'O': 344 | switch input[1] { 345 | case 'H': 346 | seq.key = .Home 347 | case 'F': 348 | seq.key = .End 349 | } 350 | case '1': 351 | switch input[1] { 352 | case 'P': 353 | seq.key = .F1 354 | case 'Q': 355 | seq.key = .F2 356 | case 'R': 357 | seq.key = .F3 358 | case 'S': 359 | seq.key = .F4 360 | } 361 | } 362 | } 363 | 364 | 365 | if input[len(input) - 1] == '~' { 366 | switch input[0] { 367 | case '1', '7': 368 | seq.key = .Home 369 | case '2': 370 | seq.key = .Insert 371 | case '3': 372 | seq.key = .Delete 373 | case '4', '8': 374 | seq.key = .End 375 | case '5': 376 | seq.key = .Page_Up 377 | case '6': 378 | seq.key = .Page_Down 379 | } 380 | 381 | switch input[0] { 382 | case '1': 383 | switch input[1] { 384 | case '1': 385 | seq.key = .F1 386 | case '2': 387 | seq.key = .F2 388 | case '3': 389 | seq.key = .F3 390 | case '4': 391 | seq.key = .F4 392 | case '5': 393 | seq.key = .F5 394 | case '7': 395 | seq.key = .F6 396 | case '8': 397 | seq.key = .F7 398 | case '9': 399 | seq.key = .F8 400 | } 401 | 402 | case '2': 403 | switch input[1] { 404 | case '0': 405 | seq.key = .F9 406 | case '1': 407 | seq.key = .F10 408 | case '3': 409 | seq.key = .F11 410 | case '4': 411 | seq.key = .F12 412 | } 413 | } 414 | } 415 | 416 | 417 | if seq != {} do return seq, true 418 | } 419 | 420 | // alt is ESC + 421 | if len(input) == 2 { 422 | key, ok := _alnum_to_key(input[1]) 423 | if ok { 424 | seq.mod = .Alt 425 | seq.key = key 426 | return seq, true 427 | } 428 | } 429 | 430 | return 431 | } 432 | 433 | /* 434 | Parses the raw bytes sent by the terminal in `Input` 435 | 436 | **Returns** 437 | - `mouse_input`: parsed mouse input 438 | - `has_input`: true there was valid mouse input 439 | 440 | Note: mouse input is not always guaranteed. The user might be running the program from 441 | a tty or the terminal emulator might just not support mouse input. 442 | 443 | */ 444 | parse_mouse_input :: proc(input: []byte) -> (mouse_input: t.Mouse_Input, has_input: bool) { 445 | // the mouse input we support is SGR escape code based 446 | if len(input) < 6 do return 447 | 448 | if input[0] != '\x1b' && input[1] != '[' && input[2] != '<' do return 449 | 450 | consume_semicolon :: proc(input: ^string) -> bool { 451 | is_semicolon := len(input) >= 1 && input[0] == ';' 452 | if is_semicolon do input^ = input[1:] 453 | return is_semicolon 454 | } 455 | 456 | consumed: int 457 | input := cast(string)input[3:] 458 | 459 | mod, _ := strconv.parse_uint(input, n = &consumed) 460 | input = input[consumed:] 461 | consume_semicolon(&input) or_return 462 | 463 | x_coord, _ := strconv.parse_uint(input, n = &consumed) 464 | input = input[consumed:] 465 | consume_semicolon(&input) or_return 466 | 467 | y_coord, _ := strconv.parse_uint(input, n = &consumed) 468 | input = input[consumed:] 469 | 470 | mouse_key: t.Mouse_Key 471 | low_two_bits := mod & 0b11 472 | switch low_two_bits { 473 | case 0: 474 | mouse_key = .Left 475 | case 1: 476 | mouse_key = .Middle 477 | case 2: 478 | mouse_key = .Right 479 | } 480 | 481 | mouse_event: bit_set[t.Mouse_Event] 482 | if mouse_key != .None { 483 | if input[0] == 'm' do mouse_event |= {.Released} 484 | if input[0] == 'M' do mouse_event |= {.Pressed} 485 | } 486 | 487 | next_three_bits := mod & 0b11100 488 | mouse_mod: bit_set[t.Mod] 489 | if next_three_bits & 4 == 4 do mouse_mod |= {.Shift} 490 | if next_three_bits & 8 == 8 do mouse_mod |= {.Alt} 491 | if next_three_bits & 16 == 16 do mouse_mod |= {.Ctrl} 492 | 493 | if mod & 64 == 64 do mouse_key = .Scroll_Up 494 | if mod & 65 == 65 do mouse_key = .Scroll_Down 495 | 496 | return t.Mouse_Input { 497 | event = mouse_event, 498 | mod = mouse_mod, 499 | key = mouse_key, 500 | // coords are converted so it's 0 based index 501 | coord = {x = x_coord - 1, y = y_coord - 1}, 502 | }, true 503 | } 504 | -------------------------------------------------------------------------------- /termcl.odin: -------------------------------------------------------------------------------- 1 | package termcl 2 | 3 | import "base:runtime" 4 | import "core:fmt" 5 | import "core:strings" 6 | import "raw" 7 | 8 | Text_Style :: raw.Text_Style 9 | Color_8 :: raw.Color_8 10 | Color_RGB :: raw.Color_RGB 11 | Any_Color :: raw.Any_Color 12 | Clear_Mode :: raw.Clear_Mode 13 | 14 | ring_bell :: raw.ring_bell 15 | enable_mouse :: raw.enable_mouse 16 | hide_cursor :: raw.hide_cursor 17 | enable_alt_buffer :: raw.enable_alt_buffer 18 | 19 | Backend_VTable :: struct { 20 | init_screen: proc(allocator: runtime.Allocator) -> Screen, 21 | destroy_screen: proc(screen: ^Screen), 22 | get_term_size: proc() -> Window_Size, 23 | set_term_mode: proc(screen: ^Screen, mode: Term_Mode), 24 | blit: proc(win: ^Window), 25 | read: proc(screen: ^Screen) -> Input, 26 | read_blocking: proc(screen: ^Screen) -> Input, 27 | } 28 | 29 | @(private) 30 | backend_vtable: Backend_VTable 31 | 32 | /* 33 | Initializes the terminal screen and creates a backup of the state the terminal 34 | was in when this function was called. 35 | 36 | Note: A screen **OUGHT** to be destroyed before exitting the program. 37 | Destroying the screen causes the terminal to be restored to its previous state. 38 | If the state is not restored your terminal might start misbehaving. 39 | */ 40 | init_screen :: proc(backend: Backend_VTable, allocator := context.allocator) -> Screen { 41 | set_backend(backend) 42 | return backend_vtable.init_screen(allocator) 43 | } 44 | 45 | /* 46 | Restores the terminal to its original state and frees all memory allocated by the `t.Screen` 47 | */ 48 | destroy_screen :: proc(screen: ^Screen) { 49 | backend_vtable.destroy_screen(screen) 50 | } 51 | 52 | /* 53 | Change terminal mode. 54 | 55 | This changes how the terminal behaves. 56 | By default the terminal will preprocess inputs and handle handle signals, 57 | preventing you to have full access to user input. 58 | 59 | **Inputs** 60 | - `screen`: the terminal screen 61 | - `mode`: how terminal should behave from now on 62 | */ 63 | set_term_mode :: proc(screen: ^Screen, mode: Term_Mode) { 64 | backend_vtable.set_term_mode(screen, mode) 65 | } 66 | 67 | get_term_size :: proc() -> Window_Size { 68 | return backend_vtable.get_term_size() 69 | } 70 | 71 | /* 72 | Sends instructions to terminal 73 | 74 | **Inputs** 75 | - `win`: A pointer to a window 76 | 77 | */ 78 | blit :: proc(win: ^Window) { 79 | backend_vtable.blit(win) 80 | 81 | // we need to keep the internal buffers in sync with the terminal size 82 | // so that we can render things correctly 83 | termsize := get_term_size() 84 | win_h, win_h_ok := win.height.? 85 | win_w, win_w_ok := win.width.? 86 | 87 | if !win_h_ok || !win_w_ok { 88 | if !win_h_ok do win_h = termsize.h 89 | if !win_w_ok do win_w = termsize.w 90 | 91 | if win.cell_buffer.height != win_h && win.cell_buffer.width != win_w { 92 | cellbuf_resize(&win.cell_buffer, win_h, win_w) 93 | } 94 | } 95 | } 96 | 97 | read :: proc(screen: ^Screen) -> Input { 98 | return backend_vtable.read(screen) 99 | } 100 | 101 | /* 102 | Reads input from the terminal. 103 | The read blocks execution until a value is read. 104 | If you want it to not block, use `read` instead. 105 | */ 106 | read_blocking :: proc(screen: ^Screen) -> Input { 107 | return backend_vtable.read_blocking(screen) 108 | } 109 | 110 | /* 111 | Set the current rendering backend 112 | 113 | This sets the VTable for the functions that are in charge of dealing 114 | with anything that is related with displaying the TUI to the screen. 115 | */ 116 | set_backend :: proc(backend: Backend_VTable) { 117 | if backend.set_term_mode == nil do panic("missing `set_term_mode` implementation") 118 | if backend.read_blocking == nil do panic("missing `read_blocking` implementation") 119 | if backend.read == nil do panic("missing `read` implementation") 120 | if backend.init_screen == nil do panic("missing `init_screen` implementation") 121 | if backend.get_term_size == nil do panic("missing `get_term_size` implementation") 122 | if backend.destroy_screen == nil do panic("missing `destroy_screen` implementation") 123 | if backend.blit == nil do panic("missing `blit` implementation") 124 | backend_vtable = backend 125 | } 126 | 127 | Cell :: struct { 128 | r: rune, 129 | styles: Styles, 130 | } 131 | 132 | /* 133 | used internally to store cells in before they're blitted to the screen 134 | doing this allows us to reduce the amount of escape codes sent to the terminal 135 | by inspecting state and only sending the states that changed 136 | */ 137 | Cell_Buffer :: struct { 138 | cells: [dynamic]Cell, 139 | width, height: uint, 140 | } 141 | 142 | cellbuf_init :: proc(height, width: uint, allocator := context.allocator) -> Cell_Buffer { 143 | cb := Cell_Buffer { 144 | height = height, 145 | width = width, 146 | cells = make([dynamic]Cell, allocator), 147 | } 148 | 149 | cb_len := height * width 150 | resize(&cb.cells, cb_len) 151 | return cb 152 | } 153 | 154 | cellbuf_destroy :: proc(cb: ^Cell_Buffer) { 155 | delete(cb.cells) 156 | cb.height = 0 157 | cb.width = 0 158 | } 159 | 160 | cellbuf_resize :: proc(cb: ^Cell_Buffer, height, width: uint) { 161 | if cb.height == height && cb.width == width do return 162 | cb_len := height * width 163 | cb.height = height 164 | cb.width = width 165 | resize(&cb.cells, cb_len) 166 | } 167 | 168 | cellbuf_get :: proc(cb: Cell_Buffer, y, x: uint) -> Cell { 169 | constrained_x := x % cb.width 170 | constrained_y := y % cb.height 171 | return cb.cells[constrained_x + constrained_y * cb.width] 172 | } 173 | 174 | cellbuf_set :: proc(cb: ^Cell_Buffer, y, x: uint, cell: Cell) { 175 | constrained_x := x % cb.width 176 | constrained_y := y % cb.height 177 | cb.cells[constrained_x + constrained_y * cb.width] = cell 178 | } 179 | 180 | Styles :: struct { 181 | text: bit_set[Text_Style], 182 | fg: Any_Color, 183 | bg: Any_Color, 184 | } 185 | 186 | /* 187 | A bounded "drawing" box in the terminal. 188 | 189 | **Fields** 190 | - `allocator`: the allocator used by the window 191 | - `seq_builder`: where the escape sequences are stored 192 | - `x_offset`, `y_offset`: offsets from (0, 0) coordinates of the terminal 193 | - `width`, `height`: sizes for the window 194 | - `cursor`: where the cursor was last when this window was interacted with 195 | */ 196 | Window :: struct { 197 | allocator: runtime.Allocator, 198 | // where the ascii escape sequence is stored 199 | seq_builder: strings.Builder, 200 | y_offset, x_offset: uint, 201 | width, height: Maybe(uint), 202 | cursor: Cursor_Position, 203 | 204 | /* 205 | these styles are guaranteed because they're always the first thing 206 | pushed to the `seq_builder` after a `blit` 207 | */ 208 | curr_styles: Styles, 209 | /* 210 | Double buffer used to store the cells in the terminal. 211 | The double buffer allows diffing between current and previous frames to reduce work required by the terminal. 212 | */ 213 | cell_buffer: Cell_Buffer, 214 | } 215 | 216 | /* 217 | Initialize a window. 218 | 219 | **Inputs** 220 | - `x`, `y`: offsets from (0, 0) coordinates of the terminal 221 | - `height`, `width`: size in cells of the window 222 | 223 | **Returns** 224 | Initialized window. Window is freed with `destroy_window` 225 | 226 | Note: 227 | - A height or width with size zero makes so that nothing happens when the window is blitted 228 | - A height or width of size nil makes it stretch to terminal length on that axis 229 | */ 230 | init_window :: proc( 231 | y, x: uint, 232 | height, width: Maybe(uint), 233 | allocator := context.allocator, 234 | ) -> Window { 235 | h, h_ok := height.? 236 | w, w_ok := width.? 237 | termsize := backend_vtable.get_term_size() 238 | cell_buffer := cellbuf_init(h if h_ok else termsize.h, w if w_ok else termsize.w, allocator) 239 | 240 | return Window { 241 | seq_builder = strings.builder_make(allocator = allocator), 242 | y_offset = y, 243 | x_offset = x, 244 | height = height, 245 | width = width, 246 | cell_buffer = cell_buffer, 247 | } 248 | } 249 | 250 | /* 251 | Destroys all memory allocated by the window 252 | */ 253 | destroy_window :: proc(win: ^Window) { 254 | strings.builder_destroy(&win.seq_builder) 255 | cellbuf_destroy(&win.cell_buffer) 256 | } 257 | 258 | /* 259 | Changes the size of the window. 260 | 261 | **Inputs** 262 | - `height`, `width`: size in cells of the window 263 | 264 | NOTE: 265 | - A height or width with size zero makes so that nothing happens when the window is blitted 266 | - A height or width of size nil makes it stretch to terminal length on that axis 267 | */ 268 | resize_window :: proc(win: ^Window, height, width: Maybe(uint)) { 269 | if type_of(win) == ^Screen { 270 | win.height = nil 271 | win.width = nil 272 | } else { 273 | win.height = height 274 | win.width = width 275 | } 276 | 277 | h, h_ok := height.? 278 | w, w_ok := width.? 279 | 280 | termsize := backend_vtable.get_term_size() 281 | cellbuf_resize(&win.cell_buffer, h if h_ok else termsize.h, w if w_ok else termsize.w) 282 | } 283 | 284 | Window_Size :: struct { 285 | h, w: uint, 286 | } 287 | 288 | /* 289 | Get the window size. 290 | 291 | **Inputs** 292 | - `screen`: the terminal screen 293 | 294 | **Returns** 295 | The screen size, where both the width and height are measured 296 | by the number of terminal cells. 297 | */ 298 | get_window_size :: proc(win: ^Window) -> Window_Size { 299 | return Window_Size{h = win.cell_buffer.height, w = win.cell_buffer.width} 300 | } 301 | 302 | /* 303 | Screen is a window for the entire terminal screen. It is a superset of `Window` and can be used anywhere a window can. 304 | */ 305 | Screen :: struct { 306 | using winbuf: Window, 307 | input_buf: [512]byte, 308 | size: Window_Size, 309 | } 310 | 311 | /* 312 | Converts window coordinates to the global terminal coordinates 313 | */ 314 | global_coord_from_window :: proc(win: ^Window, y, x: uint) -> Cursor_Position { 315 | cursor_pos := Cursor_Position { 316 | x = x, 317 | y = y, 318 | } 319 | 320 | cursor_pos.y = (y % win.cell_buffer.height) + win.y_offset 321 | cursor_pos.x = (x % win.cell_buffer.width) + win.x_offset 322 | return cursor_pos 323 | } 324 | 325 | /* 326 | Converts from global coordinates to window coordinates 327 | */ 328 | window_coord_from_global :: proc( 329 | win: ^Window, 330 | y, x: uint, 331 | ) -> ( 332 | cursor_pos: Cursor_Position, 333 | in_window: bool, 334 | ) { 335 | height := win.cell_buffer.height 336 | width := win.cell_buffer.width 337 | 338 | if height == 0 || width == 0 { 339 | return 340 | } 341 | 342 | if y < win.y_offset || y >= win.y_offset + height { 343 | return 344 | } 345 | 346 | if x < win.x_offset || x >= win.x_offset + width { 347 | return 348 | } 349 | 350 | cursor_pos.y = (y - win.y_offset) % height 351 | cursor_pos.x = (x - win.x_offset) % width 352 | in_window = true 353 | return 354 | } 355 | 356 | /* 357 | Changes the position of the window cursor 358 | */ 359 | move_cursor :: proc(win: ^Window, y, x: uint) { 360 | win.cursor = { 361 | x = x, 362 | y = y, 363 | } 364 | } 365 | 366 | /* 367 | Clear the screen. 368 | 369 | **Inputs** 370 | - `win`: the window whose contents will be cleared 371 | - `mode`: how the clearing will be done 372 | */ 373 | clear :: proc(win: ^Window, mode: Clear_Mode) { 374 | height := win.cell_buffer.height 375 | width := win.cell_buffer.width 376 | 377 | // we compute the number of spaces required to clear a window and then 378 | // let the write_rune function take care of properly moving the cursor 379 | // through its own window isolation logic 380 | space_num: uint 381 | curr_pos := get_cursor_position(win) 382 | 383 | switch mode { 384 | case .After_Cursor: 385 | space_in_same_line := width - (win.cursor.x + 1) 386 | space_after_same_line := width * (height - ((win.cursor.y + 1) % height)) 387 | space_num = space_in_same_line + space_after_same_line 388 | move_cursor(win, curr_pos.y, curr_pos.x + 1) 389 | case .Before_Cursor: 390 | space_num = win.cursor.x + 1 + win.cursor.y * width 391 | move_cursor(win, 0, 0) 392 | case .Everything: 393 | space_num = (width + 1) * height 394 | move_cursor(win, 0, 0) 395 | } 396 | 397 | for _ in 0 ..< space_num { 398 | write_rune(win, ' ') 399 | } 400 | 401 | move_cursor(win, curr_pos.y, curr_pos.x) 402 | } 403 | 404 | clear_line :: proc(win: ^Window, mode: Clear_Mode) { 405 | from, to: uint 406 | switch mode { 407 | case .After_Cursor: 408 | from = win.cursor.x + 1 409 | to = win.cell_buffer.width 410 | case .Before_Cursor: 411 | from = 0 412 | to = win.cursor.x - 1 413 | case .Everything: 414 | from = 0 415 | to = win.cell_buffer.width 416 | } 417 | 418 | for x in from ..< to { 419 | move_cursor(win, win.cursor.y, x) 420 | write_rune(win, ' ') 421 | } 422 | } 423 | 424 | // This is used internally to figure out and update where the cursor will be after a rune is written to the terminal 425 | _get_cursor_pos_from_rune :: proc(win: ^Window, r: rune) -> [2]uint { 426 | height := win.cell_buffer.height 427 | width := win.cell_buffer.width 428 | 429 | new_pos := [2]uint{win.cursor.x + 1, win.cursor.y} 430 | if new_pos.x >= width { 431 | new_pos.x = 0 432 | new_pos.y += 1 433 | } 434 | 435 | if new_pos.y >= height { 436 | new_pos.y = 0 437 | new_pos.x = 0 438 | } 439 | return new_pos 440 | } 441 | 442 | /* 443 | Writes a rune to the terminal 444 | */ 445 | write_rune :: proc(win: ^Window, r: rune) { 446 | curr_cell := Cell { 447 | r = r, 448 | styles = win.curr_styles, 449 | } 450 | cellbuf_set(&win.cell_buffer, win.cursor.y, win.cursor.x, curr_cell) 451 | // the new cursor position has to be calculated after writing the rune 452 | // otherwise the rune will be misplaced when blitted to terminal 453 | new_pos := _get_cursor_pos_from_rune(win, r) 454 | move_cursor(win, new_pos.y, new_pos.x) 455 | } 456 | 457 | /* 458 | Writes a string to the terminal 459 | */ 460 | write_string :: proc(win: ^Window, str: string) { 461 | // the string is written in chunks so that it doesn't overflow the 462 | // window in which it is contained 463 | for r in str { 464 | write_rune(win, r) 465 | } 466 | } 467 | 468 | /* 469 | Write a formatted string to the window. 470 | */ 471 | writef :: proc(win: ^Window, format: string, args: ..any) { 472 | str_builder: strings.Builder 473 | _, err := strings.builder_init(&str_builder, allocator = win.allocator) 474 | if err != nil { 475 | panic("Failed to get more memory for format string") 476 | } 477 | defer strings.builder_destroy(&str_builder) 478 | str := fmt.sbprintf(&str_builder, format, ..args) 479 | write_string(win, str) 480 | } 481 | 482 | /* 483 | Write to the window. 484 | */ 485 | write :: proc { 486 | write_string, 487 | write_rune, 488 | } 489 | 490 | 491 | // A terminal mode. This changes how the terminal will preprocess inputs and handle signals. 492 | Term_Mode :: enum { 493 | // Raw mode, prevents the terminal from preprocessing inputs and handling signals 494 | Raw, 495 | // Restores the terminal to the state it was in before program started 496 | Restored, 497 | // A sort of "soft" raw mode that still allows the terminal to handle signals 498 | Cbreak, 499 | } 500 | 501 | set_text_style :: proc(win: ^Window, styles: bit_set[Text_Style]) { 502 | win.curr_styles.text = styles 503 | } 504 | 505 | set_color_style :: proc(win: ^Window, fg: Any_Color, bg: Any_Color) { 506 | win.curr_styles.fg = fg 507 | win.curr_styles.bg = bg 508 | } 509 | 510 | reset_styles :: proc(win: ^Window) { 511 | win.curr_styles = {} 512 | } 513 | 514 | Cursor_Position :: struct { 515 | y, x: uint, 516 | } 517 | 518 | /* 519 | Get the current cursor position. 520 | */ 521 | get_cursor_position :: #force_inline proc(win: ^Window) -> Cursor_Position { 522 | return win.cursor 523 | } 524 | --------------------------------------------------------------------------------