├── .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 | -
24 | About The Project
25 |
26 | - How it works
27 | - Usage
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 |
--------------------------------------------------------------------------------