├── .gitignore
├── LICENSE
├── README.md
├── examples
├── paint
│ └── main.odin
├── snake
│ └── main.odin
├── user_input
│ └── main.odin
└── window
│ └── main.odin
├── input.odin
├── platform_dependent.odin
├── termcl.odin
└── widgets
├── button.odin
├── common.odin
├── panel.odin
└── progress.odin
/.gitignore:
--------------------------------------------------------------------------------
1 | TermCL
2 | *.bin
3 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 Screen struct to represent the terminal. To start a CLI/TUI you need to call `init_screen`,
40 | this function calls the operating system to get information on the terminal state.
41 |
42 | > [!NOTE]
43 | > you should call destroy_screen before you exit to restore the terminal state otherwise you might end up with a weird behaving terminal
44 |
45 | 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:
46 | - Raw mode (`.Raw`) - prevents the terminal from processing the user input so that you can handle them yourself
47 | - Cooked mode (`.Cbreak`) - prevents user input but unlike raw, it still processed for signals like Ctrl + C and others
48 | - 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
49 |
50 | After doing this, you should be good to go to do whatever you want.
51 |
52 | Here's a few minor things to take into consideration:
53 | - To handle input you can use the `read` function or the `read_blocking` function, as the default read is nonblocking.
54 | - There's convenience functions that allow you to more easily process input, they're called `parse_keyboard_input` and `parse_mouse_input`
55 | - 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
56 |
57 | ## Usage
58 |
59 | ```odin
60 | package main
61 |
62 | import "termcl"
63 |
64 | main :: proc() {
65 | scr := termcl.init_screen()
66 | defer termcl.destroy_screen(&scr)
67 |
68 | termcl.set_text_style(&scr, {.Bold, .Italic})
69 | termcl.write(&scr, "Hello ")
70 | termcl.reset_styles(&scr)
71 |
72 | termcl.set_text_style(&scr, {.Dim})
73 | termcl.set_color_style_8(&scr, .Green, nil)
74 | termcl.write(&scr, "from ANSI escapes")
75 | termcl.reset_styles(&scr)
76 |
77 | termcl.move_cursor(&scr, 10, 10)
78 | termcl.write(&scr, "Alles Ordnung")
79 |
80 | termcl.blit(&scr)
81 | }
82 | ```
83 |
84 | Check the `examples` directory to see more on how to use it.
85 |
--------------------------------------------------------------------------------
/examples/paint/main.odin:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import t "../../"
4 |
5 | Brush :: struct {
6 | color: t.Color_8,
7 | size: uint,
8 | }
9 |
10 | PAINT_BUFFER_WIDTH :: 80
11 | PAINT_BUFFER_HEIGHT :: 30
12 |
13 | Paint_Buffer :: struct {
14 | buffer: [PAINT_BUFFER_HEIGHT][PAINT_BUFFER_WIDTH]Maybe(t.Color_8),
15 | screen: ^t.Screen,
16 | window: t.Window,
17 | }
18 |
19 | paint_buffer_init :: proc(s: ^t.Screen) -> Paint_Buffer {
20 | return Paint_Buffer {
21 | window = t.init_window(0, 0, PAINT_BUFFER_HEIGHT, PAINT_BUFFER_WIDTH),
22 | screen = s,
23 | }
24 | }
25 |
26 | paint_buffer_destroy :: proc(pbuf: ^Paint_Buffer) {
27 | t.destroy_window(&pbuf.window)
28 | }
29 |
30 | paint_buffer_to_screen :: proc(pbuf: ^Paint_Buffer) {
31 | termsize := t.get_term_size(pbuf.screen)
32 | pbuf.window.x_offset = termsize.w / 2 - PAINT_BUFFER_WIDTH / 2
33 | pbuf.window.y_offset = termsize.h / 2 - PAINT_BUFFER_HEIGHT / 2
34 |
35 | t.set_color_style_8(&pbuf.window, .White, .White)
36 | t.clear(&pbuf.window, .Everything)
37 |
38 | defer {
39 | t.reset_styles(&pbuf.window)
40 | t.blit(&pbuf.window)
41 | }
42 |
43 | for y in 0 ..< PAINT_BUFFER_HEIGHT {
44 | for x in 0 ..< PAINT_BUFFER_WIDTH {
45 | t.move_cursor(&pbuf.window, uint(y), uint(x))
46 | color := pbuf.buffer[y][x]
47 | if color == nil do continue
48 | t.set_color_style_8(&pbuf.window, color, color)
49 | t.write(&pbuf.window, ' ')
50 | }
51 | }
52 | }
53 |
54 | paint_buffer_set_cell :: proc(pbuf: ^Paint_Buffer, y, x: uint, color: Maybe(t.Color_8)) {
55 | pbuf.buffer[y][x] = color
56 | }
57 |
58 | main :: proc() {
59 | s := t.init_screen()
60 | defer t.destroy_screen(&s)
61 | t.set_term_mode(&s, .Raw)
62 | t.hide_cursor(true)
63 |
64 | t.clear(&s, .Everything)
65 | t.blit(&s)
66 |
67 | pbuf := paint_buffer_init(&s)
68 | defer paint_buffer_destroy(&pbuf)
69 |
70 | for {
71 | defer {
72 | t.blit(&s)
73 | paint_buffer_to_screen(&pbuf)
74 | }
75 |
76 | termsize := t.get_term_size(&s)
77 |
78 | help_msg := "Draw (Right Click) / Delete (Left Click) / Quit (Q or CTRL + C)"
79 | t.move_cursor(&s, termsize.h - 2, termsize.w / 2 - len(help_msg) / 2)
80 | t.write(&s, help_msg)
81 |
82 | if termsize.w <= PAINT_BUFFER_WIDTH || termsize.w <= PAINT_BUFFER_WIDTH {
83 | t.clear(&s, .Everything)
84 | size_small_msg := "Size is too small, increase size to continue or press 'q' to exit"
85 | t.move_cursor(&s, termsize.h / 2, termsize.w / 2 - len(size_small_msg) / 2)
86 | t.write(&s, size_small_msg)
87 | continue
88 | }
89 |
90 |
91 | input := t.read(&s) or_continue
92 |
93 | keyboard, kb_has_input := t.parse_keyboard_input(input)
94 | if kb_has_input && (keyboard.mod == .Ctrl && keyboard.key == .C) || keyboard.key == .Q {
95 | break
96 | }
97 |
98 | mouse := t.parse_mouse_input(input) or_continue
99 | win_cursor := t.window_coord_from_global(
100 | &pbuf.window,
101 | mouse.coord.y,
102 | mouse.coord.x,
103 | ) or_continue
104 |
105 | #partial switch mouse.key {
106 | case .Left:
107 | if mouse.mod == nil && .Pressed in mouse.event {
108 | paint_buffer_set_cell(&pbuf, win_cursor.y, win_cursor.x, .Black)
109 | }
110 | case .Right:
111 | if mouse.mod == nil && .Pressed in mouse.event {
112 | paint_buffer_set_cell(&pbuf, win_cursor.y, win_cursor.x, nil)
113 | }
114 | }
115 |
116 | }
117 | }
118 |
119 |
--------------------------------------------------------------------------------
/examples/snake/main.odin:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import t "../.."
4 | import "core:math/rand"
5 | import "core:time"
6 |
7 | Direction :: enum {
8 | Up,
9 | Down,
10 | Left,
11 | Right,
12 | }
13 |
14 | Snake :: struct {
15 | head: [2]uint,
16 | body: [dynamic][2]uint,
17 | dir: Direction,
18 | }
19 |
20 | get_term_center :: proc(s: ^t.Screen) -> [2]uint {
21 | termsize := t.get_term_size(s)
22 | pos := [2]uint{termsize.w / 2, termsize.h / 2}
23 | return pos
24 | }
25 |
26 | snake_init :: proc(s: ^t.Screen) -> Snake {
27 | DEFAULT_SNAKE_SIZE :: 4
28 | pos := get_term_center(s)
29 | pos.x -= DEFAULT_SNAKE_SIZE
30 |
31 | body := make([dynamic][2]uint)
32 |
33 | for i in 0 ..< DEFAULT_SNAKE_SIZE {
34 | curr_pos := pos
35 | curr_pos.x += uint(i)
36 | append(&body, curr_pos)
37 | }
38 |
39 | pos.x += DEFAULT_SNAKE_SIZE
40 |
41 | return Snake{head = pos, body = body, dir = .Right}
42 | }
43 |
44 | snake_destroy :: proc(snake: Snake) {
45 | delete(snake.body)
46 | }
47 |
48 | snake_draw :: proc(snake: Snake, s: ^t.Screen) {
49 | t.set_color_style_8(s, nil, .Green)
50 | defer t.reset_styles(s)
51 |
52 | for part in snake.body {
53 | t.move_cursor(s, part.y, part.x)
54 | t.write(s, ' ')
55 | }
56 |
57 | t.set_color_style_8(s, nil, .White)
58 | t.move_cursor(s, snake.head.y, snake.head.x)
59 | t.write(s, ' ')
60 | }
61 |
62 | Game_Box :: struct {
63 | x, y, w, h: uint,
64 | }
65 |
66 | box_init :: proc(s: ^t.Screen) -> Game_Box {
67 | termcenter := get_term_center(s)
68 |
69 | BOX_HEIGHT :: 20
70 | BOX_WIDTH :: 60
71 |
72 | return Game_Box {
73 | x = termcenter.x - BOX_WIDTH / 2,
74 | y = termcenter.y - BOX_HEIGHT / 2,
75 | w = BOX_WIDTH,
76 | h = BOX_HEIGHT,
77 | }
78 | }
79 |
80 | box_draw :: proc(game: Game, s: ^t.Screen) {
81 | box := game.box
82 | t.set_color_style_8(s, .Black, .White)
83 | defer t.reset_styles(s)
84 |
85 | draw_row :: proc(box: Game_Box, s: ^t.Screen, y: uint) {
86 | for i in 0 ..= box.w {
87 | t.move_cursor(s, y, box.x + i)
88 | t.write(s, ' ')
89 | }
90 | }
91 |
92 | draw_row(box, s, box.y)
93 | draw_row(box, s, box.y + box.h)
94 |
95 | draw_col :: proc(box: Game_Box, s: ^t.Screen, x: uint) {
96 | for i in 0 ..= box.h {
97 | t.move_cursor(s, box.y + i, x)
98 | t.write(s, ' ')
99 | }
100 | }
101 |
102 | draw_col(box, s, box.x)
103 | draw_col(box, s, box.x + box.w)
104 |
105 | t.set_text_style(s, {.Bold})
106 | msg := "Press 'q' to exit. Player Score: %d"
107 | t.move_cursor(s, box.y + box.h, box.x + (box.w / 2 - len(msg) / 2))
108 | t.writef(s, msg, game.score)
109 | t.reset_styles(s)
110 | }
111 |
112 | snake_handle_input :: proc(s: ^t.Screen, game: ^Game, input: t.Input_Seq) {
113 | snake := &game.snake
114 | box := &game.box
115 |
116 | #partial switch input.key {
117 | case .Arrow_Left, .A:
118 | if snake.dir != .Right do snake.dir = .Left
119 | case .Arrow_Right, .D:
120 | if snake.dir != .Left do snake.dir = .Right
121 | case .Arrow_Up, .W:
122 | if snake.dir != .Down do snake.dir = .Up
123 | case .Arrow_Down, .S:
124 | if snake.dir != .Up do snake.dir = .Down
125 | }
126 |
127 | ordered_remove(&snake.body, 0)
128 | append(&snake.body, snake.head)
129 |
130 | switch snake.dir {
131 | case .Up:
132 | snake.head.y -= 1
133 | case .Down:
134 | snake.head.y += 1
135 | case .Left:
136 | snake.head.x -= 1
137 | case .Right:
138 | snake.head.x += 1
139 | }
140 |
141 | if snake.head.y <= box.y {
142 | snake.head.y = box.y + box.h - 1
143 | }
144 |
145 | if snake.head.y >= box.y + box.h {
146 | snake.head.y = box.y
147 | }
148 |
149 | if snake.head.x >= box.x + box.w {
150 | snake.head.x = box.x + 1
151 | }
152 |
153 | if snake.head.x <= box.x {
154 | snake.head.x = box.x + box.w - 1
155 | }
156 | }
157 |
158 | Food :: struct {
159 | x, y: uint,
160 | }
161 |
162 | food_generate :: proc(game: Game) -> Food {
163 | box := game.box
164 | x, y: uint
165 |
166 | for {
167 | x = (cast(uint)rand.uint32() % (box.w - 1)) + box.x + 1
168 | y = (cast(uint)rand.uint32() % (box.h - 1)) + box.y + 1
169 |
170 | no_collision := true
171 | for part in game.snake.body {
172 | if part.x == x && part.y == y {
173 | no_collision = false
174 | }
175 | }
176 |
177 | if no_collision do break
178 | }
179 |
180 | return Food{x = x, y = y}
181 | }
182 |
183 | Game :: struct {
184 | snake: Snake,
185 | box: Game_Box,
186 | food: Food,
187 | score: uint,
188 | }
189 |
190 | game_init :: proc(s: ^t.Screen) -> Game {
191 | game := Game {
192 | snake = snake_init(s),
193 | box = box_init(s),
194 | }
195 | game.food = food_generate(game)
196 | return game
197 | }
198 |
199 | game_destroy :: proc(game: Game) {
200 | snake_destroy(game.snake)
201 | }
202 |
203 | game_is_over :: proc(game: Game, s: ^t.Screen) -> bool {
204 | snake := game.snake
205 | for part in snake.body {
206 | if part.x == snake.head.x && part.y == snake.head.y {
207 | return true
208 | }
209 | }
210 |
211 | return false
212 | }
213 |
214 | game_tick :: proc(game: ^Game, s: ^t.Screen) {
215 | if game.snake.head.x == game.food.x && game.snake.head.y == game.food.y {
216 | game.score += 1
217 | game.food = food_generate(game^)
218 | append(&game.snake.body, game.snake.body[len(game.snake.body) - 1])
219 | }
220 |
221 | t.move_cursor(s, game.food.y, game.food.x)
222 | t.set_color_style_8(s, nil, .Yellow)
223 | t.write(s, ' ')
224 | t.reset_styles(s)
225 |
226 | snake_draw(game.snake, s)
227 | box_draw(game^, s)
228 | }
229 |
230 |
231 | main :: proc() {
232 | s := t.init_screen()
233 | defer t.destroy_screen(&s)
234 | t.set_term_mode(&s, .Cbreak)
235 | t.hide_cursor(true)
236 | t.clear(&s, .Everything)
237 | t.blit(&s)
238 |
239 | game := game_init(&s)
240 | stopwatch: time.Stopwatch
241 |
242 | for {
243 | defer t.blit(&s)
244 |
245 | if game_is_over(game, &s) {
246 | t.move_cursor(&s, game.box.y + game.box.h + 5, 0)
247 | t.write(&s, "=== Game Over ===\n")
248 | break
249 | }
250 |
251 | time.stopwatch_start(&stopwatch)
252 | defer time.stopwatch_reset(&stopwatch)
253 | t.clear(&s, .Everything)
254 |
255 | input, _ := t.read(&s)
256 | keys, kb_has_input := t.parse_keyboard_input(input)
257 |
258 | if kb_has_input && (keys.key == .Q) {
259 | break
260 | }
261 |
262 |
263 | snake_handle_input(&s, &game, keys)
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 |
277 |
--------------------------------------------------------------------------------
/examples/user_input/main.odin:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import t "../.."
4 |
5 | main :: proc() {
6 | s := t.init_screen()
7 | defer t.destroy_screen(&s)
8 | t.set_term_mode(&s, .Raw)
9 |
10 | t.clear(&s, .Everything)
11 | t.move_cursor(&s, 0, 0)
12 | t.write(&s, "Please type something or move your mouse to start")
13 | t.blit(&s)
14 |
15 | for {
16 | t.clear(&s, .Everything)
17 | defer t.blit(&s)
18 |
19 | input := t.read_blocking(&s) or_continue
20 |
21 | t.move_cursor(&s, 0, 0)
22 | t.write(&s, "Press `Esc` to exit")
23 |
24 | kb_input, has_kb_input := t.parse_keyboard_input(input)
25 |
26 | if has_kb_input && kb_input.key != .None {
27 | t.move_cursor(&s, 2, 0)
28 | t.writef(&s, "%v", kb_input)
29 | }
30 |
31 | mouse_input, has_mouse_input := t.parse_mouse_input(input)
32 | if has_mouse_input {
33 | t.move_cursor(&s, 4, 0)
34 | t.writef(&s, "%v", mouse_input)
35 | }
36 |
37 | if kb_input.key == .Escape do break
38 | }
39 | }
40 |
41 |
--------------------------------------------------------------------------------
/examples/window/main.odin:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import t "../.."
4 |
5 | main :: proc() {
6 | s := t.init_screen()
7 | defer t.destroy_screen(&s)
8 | t.set_term_mode(&s, .Raw)
9 | t.hide_cursor(true)
10 |
11 | termsize := t.get_term_size(&s)
12 |
13 | msg := "Use WASD or arrow keys to move window and 'q' to quit."
14 | window := t.init_window(termsize.h / 2, termsize.w / 2 - len(msg) / 2, 3, (uint)(len(msg) + 2))
15 | defer t.destroy_window(&window)
16 |
17 | main_loop: for {
18 | t.clear(&window, .Everything)
19 | defer t.blit(&window)
20 |
21 | t.clear(&s, .Everything)
22 | defer t.blit(&s)
23 |
24 | t.set_color_style_8(&window, .Black, .White)
25 | // this proves that no matter what the window will never be overflowed by moving the cursor
26 | for i in 0 ..= 10 {
27 | t.move_cursor(&window, cast(uint)i, 1)
28 | t.write_string(&window, msg)
29 | }
30 | t.reset_styles(&window)
31 |
32 | input := t.read(&s) or_continue
33 | kb_input := t.parse_keyboard_input(input) or_continue
34 |
35 | #partial switch kb_input.key {
36 | case .Arrow_Left, .A:
37 | window.x_offset -= 1
38 | case .Arrow_Right, .D:
39 | window.x_offset += 1
40 | case .Arrow_Up, .W:
41 | window.y_offset -= 1
42 | case .Arrow_Down, .S:
43 | window.y_offset += 1
44 |
45 | case .Q:
46 | break main_loop
47 | }
48 |
49 | }
50 | }
51 |
52 |
--------------------------------------------------------------------------------
/input.odin:
--------------------------------------------------------------------------------
1 | package termcl
2 |
3 | import "core:fmt"
4 | import "core:os"
5 | import "core:strconv"
6 | import "core:unicode"
7 |
8 | Input :: distinct []byte
9 |
10 | Key :: enum {
11 | None,
12 | Arrow_Left,
13 | Arrow_Right,
14 | Arrow_Up,
15 | Arrow_Down,
16 | Page_Up,
17 | Page_Down,
18 | Home,
19 | End,
20 | Insert,
21 | Delete,
22 | F1,
23 | F2,
24 | F3,
25 | F4,
26 | F5,
27 | F6,
28 | F7,
29 | F8,
30 | F9,
31 | F10,
32 | F11,
33 | F12,
34 | Escape,
35 | Num_0,
36 | Num_1,
37 | Num_2,
38 | Num_3,
39 | Num_4,
40 | Num_5,
41 | Num_6,
42 | Num_7,
43 | Num_8,
44 | Num_9,
45 | Enter,
46 | Tab,
47 | Backspace,
48 | A,
49 | B,
50 | C,
51 | D,
52 | E,
53 | F,
54 | G,
55 | H,
56 | I,
57 | J,
58 | K,
59 | L,
60 | M,
61 | N,
62 | O,
63 | P,
64 | Q,
65 | R,
66 | S,
67 | T,
68 | U,
69 | V,
70 | W,
71 | X,
72 | Y,
73 | Z,
74 | Minus,
75 | Plus,
76 | Equal,
77 | Open_Paren,
78 | Close_Paren,
79 | Open_Curly_Bracket,
80 | Close_Curly_Bracket,
81 | Open_Square_Bracket,
82 | Close_Square_Bracket,
83 | Colon,
84 | Semicolon,
85 | Slash,
86 | Backslash,
87 | Single_Quote,
88 | Double_Quote,
89 | Period,
90 | Asterisk,
91 | Backtick,
92 | Space,
93 | Dollar,
94 | Exclamation,
95 | Hash,
96 | Percent,
97 | Ampersand,
98 | Tick,
99 | Underscore,
100 | Caret,
101 | Comma,
102 | Pipe,
103 | At,
104 | Tilde,
105 | }
106 |
107 | Mod :: enum {
108 | None,
109 | Alt,
110 | Ctrl,
111 | Shift,
112 | }
113 |
114 | Input_Seq :: struct {
115 | mod: Mod,
116 | key: Key,
117 | }
118 |
119 | // Parses the raw bytes sent by the terminal in `Input` and returns an input sequence
120 | // If there's no valid keyboard input, `has_input` is false
121 | //
122 | // Note: the terminal processes some inputs making them be treated the same so if you try to
123 | // parse an input and find that it's not being detected, check what value it is processed into.
124 | // Example: Escape might be Esc, Ctrl + [ and Ctrl + 3
125 | parse_keyboard_input :: proc(input: Input) -> (keyboard_input: Input_Seq, has_input: bool) {
126 | input := input
127 | seq: Input_Seq
128 |
129 | if len(input) == 0 do return
130 |
131 | if len(input) == 1 {
132 | input_rune := cast(rune)input[0]
133 | if unicode.is_upper(input_rune) {
134 | seq.mod = .Shift
135 | }
136 |
137 | if unicode.is_control(input_rune) {
138 | switch input_rune {
139 | case '\r',
140 | '\n',
141 | '\b',
142 | 127, /* backspace */
143 | '\t',
144 | '\x1b':
145 | case:
146 | seq.mod = .Ctrl
147 | input[0] += 64
148 | }
149 | }
150 |
151 | switch input[0] {
152 | case '\x1b':
153 | seq.key = .Escape
154 | case '1':
155 | seq.key = .Num_1
156 | case '2':
157 | seq.key = .Num_2
158 | case '3':
159 | seq.key = .Num_3
160 | case '4':
161 | seq.key = .Num_4
162 | case '5':
163 | seq.key = .Num_5
164 | case '6':
165 | seq.key = .Num_6
166 | case '7':
167 | seq.key = .Num_7
168 | case '8':
169 | seq.key = .Num_8
170 | case '9':
171 | seq.key = .Num_9
172 | case '0':
173 | seq.key = .Num_0
174 | case '\r', '\n':
175 | seq.key = .Enter
176 | case '\t':
177 | seq.key = .Tab
178 | case 8, 127:
179 | seq.key = .Backspace
180 | case 'a', 'A':
181 | seq.key = .A
182 | case 'b', 'B':
183 | seq.key = .B
184 | case 'c', 'C':
185 | seq.key = .C
186 | case 'd', 'D':
187 | seq.key = .D
188 | case 'e', 'E':
189 | seq.key = .E
190 | case 'f', 'F':
191 | seq.key = .F
192 | case 'g', 'G':
193 | seq.key = .G
194 | case 'h', 'H':
195 | seq.key = .H
196 | case 'i', 'I':
197 | seq.key = .I
198 | case 'j', 'J':
199 | seq.key = .J
200 | case 'k', 'K':
201 | seq.key = .K
202 | case 'l', 'L':
203 | seq.key = .L
204 | case 'm', 'M':
205 | seq.key = .M
206 | case 'n', 'N':
207 | seq.key = .N
208 | case 'o', 'O':
209 | seq.key = .O
210 | case 'p', 'P':
211 | seq.key = .P
212 | case 'q', 'Q':
213 | seq.key = .Q
214 | case 'r', 'R':
215 | seq.key = .R
216 | case 's', 'S':
217 | seq.key = .S
218 | case 't', 'T':
219 | seq.key = .T
220 | case 'u', 'U':
221 | seq.key = .U
222 | case 'v', 'V':
223 | seq.key = .V
224 | case 'w', 'W':
225 | seq.key = .W
226 | case 'x', 'X':
227 | seq.key = .X
228 | case 'y', 'Y':
229 | seq.key = .Y
230 | case 'z', 'Z':
231 | seq.key = .Z
232 | case ',':
233 | seq.key = .Comma
234 | case ':':
235 | seq.key = .Colon
236 | case ';':
237 | seq.key = .Semicolon
238 | case '-':
239 | seq.key = .Minus
240 | case '+':
241 | seq.key = .Plus
242 | case '=':
243 | seq.key = .Equal
244 | case '{':
245 | seq.key = .Open_Curly_Bracket
246 | case '}':
247 | seq.key = .Close_Curly_Bracket
248 | case '(':
249 | seq.key = .Open_Paren
250 | case ')':
251 | seq.key = .Close_Paren
252 | case '[':
253 | seq.key = .Open_Square_Bracket
254 | case ']':
255 | seq.key = .Close_Square_Bracket
256 | case '/':
257 | seq.key = .Slash
258 | case '\'':
259 | seq.key = .Single_Quote
260 | case '"':
261 | seq.key = .Double_Quote
262 | case '.':
263 | seq.key = .Period
264 | case '*':
265 | seq.key = .Asterisk
266 | case '`':
267 | seq.key = .Backtick
268 | case '\\':
269 | seq.key = .Backslash
270 | case ' ':
271 | seq.key = .Space
272 | case '$':
273 | seq.key = .Dollar
274 | case '!':
275 | seq.key = .Exclamation
276 | case '#':
277 | seq.key = .Hash
278 | case '%':
279 | seq.key = .Percent
280 | case '&':
281 | seq.key = .Ampersand
282 | case '´':
283 | seq.key = .Tick
284 | case '_':
285 | seq.key = .Underscore
286 | case '^':
287 | seq.key = .Caret
288 | case '|':
289 | seq.key = .Pipe
290 | case '@':
291 | seq.key = .At
292 | case '~':
293 | seq.key = .Tilde
294 | case:
295 | return
296 | }
297 |
298 | return seq, true
299 | }
300 |
301 | if input[0] != '\x1b' do return
302 |
303 | if input[1] == 10 {
304 | seq.mod = .Alt
305 | seq.key = .Enter
306 | return seq, true
307 | }
308 |
309 | if len(input) > 3 {
310 | input_len := len(input)
311 |
312 | if input[input_len - 3] == ';' {
313 | switch input[input_len - 2] {
314 | case '2':
315 | seq.mod = .Shift
316 | case '3':
317 | seq.mod = .Alt
318 | case '5':
319 | seq.mod = .Ctrl
320 |
321 | }
322 | }
323 | }
324 |
325 | if len(input) >= 2 {
326 | switch input[len(input) - 1] {
327 | case 'P':
328 | seq.key = .F1
329 | case 'Q':
330 | seq.key = .F2
331 | case 'R':
332 | seq.key = .F3
333 | case 'S':
334 | seq.key = .F4
335 |
336 | }
337 |
338 | if input[1] == 'O' do return seq, true
339 | }
340 |
341 | if input[1] == '[' {
342 | input = input[2:]
343 |
344 | if len(input) > 2 && input[0] == '1' && input[1] == ';' {
345 | switch input[2] {
346 | case '2':
347 | seq.mod = .Shift
348 | case '3':
349 | seq.mod = .Alt
350 | case '5':
351 | seq.mod = .Ctrl
352 | }
353 |
354 | input = input[3:]
355 | }
356 |
357 |
358 | if len(input) == 1 {
359 | switch input[0] {
360 | case 'H':
361 | seq.key = .Home
362 | case 'F':
363 | seq.key = .End
364 | case 'A':
365 | seq.key = .Arrow_Up
366 | case 'B':
367 | seq.key = .Arrow_Down
368 | case 'C':
369 | seq.key = .Arrow_Right
370 | case 'D':
371 | seq.key = .Arrow_Left
372 | case 'Z':
373 | seq.key = .Tab
374 | seq.mod = .Shift
375 | }
376 | }
377 |
378 |
379 | if len(input) >= 2 {
380 | switch input[0] {
381 | case 'O':
382 | switch input[1] {
383 | case 'H':
384 | seq.key = .Home
385 | case 'F':
386 | seq.key = .End
387 | }
388 | case '1':
389 | switch input[1] {
390 | case 'P':
391 | seq.key = .F1
392 | case 'Q':
393 | seq.key = .F2
394 | case 'R':
395 | seq.key = .F3
396 | case 'S':
397 | seq.key = .F4
398 | }
399 | }
400 | }
401 |
402 |
403 | if input[len(input) - 1] == '~' {
404 | switch input[0] {
405 | case '1', '7':
406 | seq.key = .Home
407 | case '2':
408 | seq.key = .Insert
409 | case '3':
410 | seq.key = .Delete
411 | case '4', '8':
412 | seq.key = .End
413 | case '5':
414 | seq.key = .Page_Up
415 | case '6':
416 | seq.key = .Page_Down
417 | }
418 |
419 | switch input[0] {
420 | case '1':
421 | switch input[1] {
422 | case '1':
423 | seq.key = .F1
424 | case '2':
425 | seq.key = .F2
426 | case '3':
427 | seq.key = .F3
428 | case '4':
429 | seq.key = .F4
430 | case '5':
431 | seq.key = .F5
432 | case '7':
433 | seq.key = .F6
434 | case '8':
435 | seq.key = .F7
436 | case '9':
437 | seq.key = .F8
438 | }
439 |
440 | case '2':
441 | switch input[1] {
442 | case '0':
443 | seq.key = .F9
444 | case '1':
445 | seq.key = .F10
446 | case '3':
447 | seq.key = .F11
448 | case '4':
449 | seq.key = .F12
450 | }
451 | }
452 | }
453 |
454 | return seq, true
455 | }
456 |
457 | return
458 | }
459 |
460 | Mouse_Event :: enum {
461 | Pressed,
462 | Released,
463 | }
464 |
465 | Mouse_Key :: enum {
466 | None,
467 | Left,
468 | Middle,
469 | Right,
470 | Scroll_Up,
471 | Scroll_Down,
472 | }
473 |
474 | Mouse_Input :: struct {
475 | event: bit_set[Mouse_Event],
476 | mod: bit_set[Mod],
477 | key: Mouse_Key,
478 | coord: Cursor_Position,
479 | }
480 |
481 | // Parses the raw bytes sent by the terminal in `Input` and returns an input sequence
482 | // If there's no valid mouse input, `has_input` is false
483 | parse_mouse_input :: proc(input: Input) -> (mouse_input: Mouse_Input, has_input: bool) {
484 | if len(input) < 6 do return
485 |
486 | if input[0] != '\x1b' && input[1] != '[' && input[2] != '<' do return
487 |
488 | consume_semicolon :: proc(input: ^string) -> bool {
489 | is_semicolon := len(input) >= 1 && input[0] == ';'
490 | if is_semicolon do input^ = input[1:]
491 | return is_semicolon
492 | }
493 |
494 | consumed: int
495 | input := cast(string)input[3:]
496 |
497 | mod, _ := strconv.parse_uint(input, n = &consumed)
498 | input = input[consumed:]
499 | consume_semicolon(&input) or_return
500 |
501 | x_coord, _ := strconv.parse_uint(input, n = &consumed)
502 | input = input[consumed:]
503 | consume_semicolon(&input) or_return
504 |
505 | y_coord, _ := strconv.parse_uint(input, n = &consumed)
506 | input = input[consumed:]
507 |
508 | mouse_event: bit_set[Mouse_Event]
509 | if input[0] == 'm' do mouse_event |= {.Released}
510 | if input[0] == 'M' do mouse_event |= {.Pressed}
511 |
512 | mouse_key: Mouse_Key
513 | low_two_bits := mod & 0b11
514 | switch low_two_bits {
515 | case 0:
516 | mouse_key = .Left
517 | case 1:
518 | mouse_key = .Middle
519 | case 2:
520 | mouse_key = .Right
521 | }
522 |
523 | next_three_bits := mod & 0b11100
524 | mouse_mod: bit_set[Mod]
525 | if next_three_bits & 4 == 4 do mouse_mod |= {.Shift}
526 | if next_three_bits & 8 == 8 do mouse_mod |= {.Alt}
527 | if next_three_bits & 16 == 16 do mouse_mod |= {.Ctrl}
528 |
529 | if mod & 64 == 64 do mouse_key = .Scroll_Up
530 | if mod & 65 == 65 do mouse_key = .Scroll_Down
531 |
532 | return Mouse_Input {
533 | event = mouse_event,
534 | mod = mouse_mod,
535 | key = mouse_key,
536 | // coords are converted so it's 0 based index
537 | coord = {x = x_coord - 1, y = y_coord - 1},
538 | }, true
539 | }
540 |
541 |
--------------------------------------------------------------------------------
/platform_dependent.odin:
--------------------------------------------------------------------------------
1 | package termcl
2 |
3 | import "base:runtime"
4 | import "core:c"
5 | import "core:fmt"
6 | import "core:os"
7 | import "core:strings"
8 | import "core:sys/linux"
9 | import "core:sys/posix"
10 | import "core:sys/windows"
11 |
12 | VALID_POSIX_OSES :: bit_set[runtime.Odin_OS_Type] {
13 | .Linux,
14 | .Haiku,
15 | .Darwin,
16 | .FreeBSD,
17 | .NetBSD,
18 | .OpenBSD,
19 | }
20 |
21 | when ODIN_OS in VALID_POSIX_OSES {
22 | Terminal_State :: struct {
23 | state: posix.termios,
24 | }
25 | } else when ODIN_OS == .Windows {
26 | Terminal_State :: struct {
27 | mode: windows.DWORD,
28 | input_codepage: windows.CODEPAGE,
29 | input_mode: windows.DWORD,
30 | output_codepage: windows.CODEPAGE,
31 | output_mode: windows.DWORD,
32 | }
33 | } else {
34 | Terminal_State :: struct {}
35 | }
36 |
37 | @(private)
38 | get_terminal_state :: proc() -> (Terminal_State, bool) {
39 | when ODIN_OS in VALID_POSIX_OSES {
40 | termstate: posix.termios
41 | ok := posix.tcgetattr(posix.STDIN_FILENO, &termstate) == .OK
42 | return Terminal_State{state = termstate}, ok
43 | } else when ODIN_OS == .Windows {
44 | termstate: Terminal_State
45 | windows.GetConsoleMode(windows.HANDLE(os.stdout), &termstate.output_mode)
46 | termstate.output_codepage = windows.GetConsoleOutputCP()
47 |
48 | windows.GetConsoleMode(windows.HANDLE(os.stdin), &termstate.input_mode)
49 | termstate.input_codepage = windows.GetConsoleCP()
50 |
51 | return termstate, true
52 | } else {
53 | return {}, false
54 | }
55 | }
56 |
57 | @(private)
58 | change_terminal_mode :: proc(screen: ^Screen, mode: Term_Mode) {
59 | termstate, ok := get_terminal_state()
60 | if !ok {
61 | panic("failed to get terminal state")
62 | }
63 |
64 | when ODIN_OS in VALID_POSIX_OSES {
65 | raw := termstate.state
66 |
67 | switch mode {
68 | case .Raw:
69 | raw.c_lflag -= {.ECHO, .ICANON, .ISIG, .IEXTEN}
70 | raw.c_iflag -= {.ICRNL, .IXON}
71 | raw.c_oflag -= {.OPOST}
72 |
73 | // probably meaningless on modern terminals but apparently it's good practice
74 | raw.c_iflag -= {.BRKINT, .INPCK, .ISTRIP}
75 | raw.c_cflag |= {.CS8}
76 |
77 | case .Cbreak:
78 | raw.c_lflag -= {.ECHO, .ICANON}
79 |
80 | case .Restored:
81 | raw = screen.original_termstate.state
82 | }
83 |
84 | if posix.tcsetattr(posix.STDIN_FILENO, .TCSAFLUSH, &raw) != .OK {
85 | panic("failed to set new terminal state")
86 | }
87 | } else when ODIN_OS == .Windows {
88 | switch mode {
89 | case .Raw:
90 | termstate.output_mode |= windows.DISABLE_NEWLINE_AUTO_RETURN
91 | termstate.output_mode |= windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING
92 | termstate.output_mode |= windows.ENABLE_PROCESSED_OUTPUT
93 |
94 | termstate.input_mode &= ~windows.ENABLE_PROCESSED_INPUT
95 | termstate.input_mode &= ~windows.ENABLE_ECHO_INPUT
96 | termstate.input_mode &= ~windows.ENABLE_LINE_INPUT
97 | termstate.input_mode |= windows.ENABLE_VIRTUAL_TERMINAL_INPUT
98 |
99 | case .Cbreak:
100 | termstate.output_mode |= windows.ENABLE_PROCESSED_OUTPUT
101 | termstate.output_mode |= windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING
102 |
103 | termstate.input_mode |= windows.ENABLE_VIRTUAL_TERMINAL_INPUT
104 | termstate.input_mode &= ~windows.ENABLE_LINE_INPUT
105 | termstate.input_mode &= ~windows.ENABLE_ECHO_INPUT
106 |
107 | case .Restored:
108 | termstate = screen.original_termstate
109 | }
110 |
111 | if !windows.SetConsoleMode(windows.HANDLE(os.stdout), termstate.output_mode) ||
112 | !windows.SetConsoleMode(windows.HANDLE(os.stdin), termstate.input_mode) {
113 | panic("failed to set new terminal state")
114 | }
115 |
116 | if mode != .Restored {
117 | windows.SetConsoleOutputCP(.UTF8)
118 | windows.SetConsoleCP(.UTF8)
119 | } else {
120 | windows.SetConsoleOutputCP(termstate.output_codepage)
121 | windows.SetConsoleCP(termstate.input_codepage)
122 | }
123 |
124 | }
125 | }
126 |
127 | get_term_size_via_syscall :: proc() -> (Screen_Size, bool) {
128 | when ODIN_OS == .Linux {
129 | winsize :: struct {
130 | ws_row, ws_col: c.ushort,
131 | ws_xpixel, ws_ypixel: c.ushort,
132 | }
133 |
134 | // right now this is supported by all odin platforms
135 | // but there's a few platforms that have a different value
136 | // check: https://github.com/search?q=repo%3Atorvalds%2Flinux%20TIOCGWINSZ&type=code
137 | TIOCGWINSZ :: 0x5413
138 |
139 | w: winsize
140 | if linux.ioctl(linux.STDOUT_FILENO, TIOCGWINSZ, cast(uintptr)&w) != 0 do return {}, false
141 |
142 | win := Screen_Size {
143 | h = uint(w.ws_row),
144 | w = uint(w.ws_col),
145 | }
146 |
147 | return win, true
148 | } else when ODIN_OS == .Windows {
149 | sbi: windows.CONSOLE_SCREEN_BUFFER_INFO
150 |
151 | if !windows.GetConsoleScreenBufferInfo(windows.HANDLE(os.stdout), &sbi) {
152 | return {}, false
153 | }
154 |
155 | screen_size := Screen_Size {
156 | w = uint(sbi.srWindow.Right - sbi.srWindow.Left) + 1,
157 | h = uint(sbi.srWindow.Bottom - sbi.srWindow.Top) + 1,
158 | }
159 |
160 | return screen_size, true
161 | } else {
162 | return {}, false
163 | }
164 | }
165 |
166 | // Reads input from the terminal.
167 | // The read blocks execution until a value is read.
168 | // If you want it to not block, use `read` instead.
169 | read_blocking :: proc(screen: ^Screen) -> (user_input: Input, has_input: bool) {
170 | bytes_read, err := os.read_ptr(os.stdin, &screen.input_buf, len(screen.input_buf))
171 | if err != nil {
172 | fmt.eprintln("failing to get user input")
173 | os.exit(1)
174 | }
175 |
176 | return Input(screen.input_buf[:bytes_read]), bytes_read > 0
177 | }
178 |
179 |
180 | // Reads input from the terminal
181 | // Reading is nonblocking, if you want it to block, use `read_blocking`
182 | //
183 | // The Input returned is a slice of bytes returned from the terminal.
184 | // If you want to read a single character, you could just handle it directly without
185 | // having to parse the input.
186 | //
187 | // example:
188 | // ```odin
189 | // input := read(&screen)
190 | // if len(input) == 1 do switch input[0] {
191 | // case 'a':
192 | // case 'b':
193 | // }
194 | // ```
195 | read :: proc(screen: ^Screen) -> (user_input: Input, has_input: bool) {
196 | when ODIN_OS in VALID_POSIX_OSES {
197 | stdin_pollfd := posix.pollfd {
198 | fd = posix.STDIN_FILENO,
199 | events = {.IN},
200 | }
201 |
202 | if posix.poll(&stdin_pollfd, 1, 8) > 0 {
203 | return read_blocking(screen)
204 | }
205 | } else when ODIN_OS == .Windows {
206 | num_events: u32
207 | if !windows.GetNumberOfConsoleInputEvents(windows.HANDLE(os.stdin), &num_events) {
208 | error_id := windows.GetLastError()
209 | error_msg: ^u16
210 |
211 | strsize := windows.FormatMessageW(
212 | windows.FORMAT_MESSAGE_ALLOCATE_BUFFER |
213 | windows.FORMAT_MESSAGE_FROM_SYSTEM |
214 | windows.FORMAT_MESSAGE_IGNORE_INSERTS,
215 | nil,
216 | error_id,
217 | windows.MAKELANGID(windows.LANG_NEUTRAL, windows.SUBLANG_DEFAULT),
218 | cast(^u16)&error_msg,
219 | 0,
220 | nil,
221 | )
222 | windows.WriteConsoleW(windows.HANDLE(os.stdout), error_msg, strsize, nil, nil)
223 | windows.LocalFree(error_msg)
224 | panic("Failed to get console input events")
225 | }
226 |
227 | if num_events > 0 {
228 | return read_blocking(screen)
229 | }
230 | } else {
231 | #panic("nonblocking read is not supported in the target platform")
232 | }
233 |
234 | return
235 | }
236 |
237 |
--------------------------------------------------------------------------------
/termcl.odin:
--------------------------------------------------------------------------------
1 | package termcl
2 |
3 | import "base:runtime"
4 | import "core:encoding/ansi"
5 | import "core:fmt"
6 | import "core:mem"
7 | import "core:mem/virtual"
8 | import "core:os"
9 | import "core:strconv"
10 | import "core:strings"
11 | import "core:unicode/utf8"
12 |
13 | // Sends instructions to terminal
14 | blit :: proc(win: $T/^Window) {
15 | fmt.print(strings.to_string(win.seq_builder))
16 | strings.builder_reset(&win.seq_builder)
17 | os.flush(os.stdout)
18 | }
19 |
20 | Window :: struct {
21 | allocator: runtime.Allocator,
22 | seq_builder: strings.Builder,
23 | y_offset, x_offset: uint,
24 | width, height: Maybe(uint),
25 | cursor: Cursor_Position,
26 | }
27 |
28 | // you should never init a window with size zero unless you're going to assign the sizes later.
29 | // using a window with width and height of zero will result in a division by zero
30 | init_window :: proc(
31 | y, x: uint,
32 | height, width: Maybe(uint),
33 | allocator := context.allocator,
34 | ) -> Window {
35 | return Window {
36 | seq_builder = strings.builder_make(allocator = allocator),
37 | y_offset = y,
38 | x_offset = x,
39 | height = height,
40 | width = width,
41 | }
42 | }
43 |
44 | destroy_window :: proc(win: ^Window) {
45 | strings.builder_destroy(&win.seq_builder)
46 | }
47 |
48 | // Screen is a special derivate of Window, so it can mostly be used anywhere a Window can be used
49 | Screen :: struct {
50 | using winbuf: Window,
51 | original_termstate: Terminal_State,
52 | input_buf: [512]byte,
53 | }
54 |
55 | // Initializes screen and saves terminal state
56 | init_screen :: proc(allocator := context.allocator) -> Screen {
57 | context.allocator = allocator
58 |
59 | termstate, ok := get_terminal_state()
60 | if !ok {
61 | panic("failed to get terminal state")
62 | }
63 |
64 | return Screen {
65 | original_termstate = termstate,
66 | winbuf = init_window(0, 0, nil, nil, allocator = allocator),
67 | }
68 | }
69 |
70 | // Restores terminal settings and does necessary memory cleanup
71 | destroy_screen :: proc(screen: ^Screen) {
72 | set_term_mode(screen, .Restored)
73 | destroy_window(&screen.winbuf)
74 | fmt.print("\x1b[?1049l")
75 | }
76 |
77 | // converts coordinates from window coordinates to the global terminal coordinate
78 | global_coord_from_window :: proc(win: $T/^Window, y, x: uint) -> Cursor_Position {
79 | cursor_pos := Cursor_Position {
80 | x = x,
81 | y = y,
82 | }
83 |
84 | when type_of(win) == ^Screen {
85 | term_size := get_term_size(win)
86 | height := term_size.h
87 | width := term_size.w
88 | } else {
89 | height, h_ok := win.height.?
90 | width, w_ok := win.width.?
91 |
92 | if !w_ok && !h_ok {
93 | return cursor_pos
94 | }
95 | }
96 |
97 | cursor_pos.y = (y % height) + win.y_offset
98 | cursor_pos.x = (x % width) + win.x_offset
99 | return cursor_pos
100 | }
101 |
102 | // converts coordinates from global coordinates to window coordinates
103 | window_coord_from_global :: proc(
104 | win: ^Window,
105 | y, x: uint,
106 | ) -> (
107 | cursor_pos: Cursor_Position,
108 | in_window: bool,
109 | ) {
110 | height, h_ok := win.height.?
111 | width, w_ok := win.width.?
112 |
113 | if !w_ok && !h_ok {
114 | return
115 | }
116 |
117 | if y < win.y_offset || y >= win.y_offset + height {
118 | return
119 | }
120 |
121 | if x < win.x_offset || x >= win.x_offset + width {
122 | return
123 | }
124 |
125 | cursor_pos.y = (y - win.y_offset) % height
126 | cursor_pos.x = (x - win.x_offset) % width
127 | in_window = true
128 | return
129 | }
130 |
131 | // Changes the cursor's absolute position
132 | move_cursor :: proc(win: $T/^Window, y, x: uint) {
133 | win.cursor = {
134 | x = x,
135 | y = y,
136 | }
137 |
138 | global_cursor_pos := global_coord_from_window(win, y, x)
139 | CURSOR_POSITION :: ansi.CSI + "%d;%dH"
140 | strings.write_string(&win.seq_builder, ansi.CSI)
141 | // x and y are shifted by one position so that programmers can keep using 0 based indexing
142 | strings.write_uint(&win.seq_builder, global_cursor_pos.y + 1)
143 | strings.write_rune(&win.seq_builder, ';')
144 | strings.write_uint(&win.seq_builder, global_cursor_pos.x + 1)
145 | strings.write_rune(&win.seq_builder, 'H')
146 | }
147 |
148 | Text_Style :: enum {
149 | Bold,
150 | Italic,
151 | Underline,
152 | Crossed,
153 | Inverted,
154 | Dim,
155 | }
156 |
157 | // Hides the terminal cursor
158 | hide_cursor :: proc(hide: bool) {
159 | SHOW_CURSOR :: ansi.CSI + "?25h"
160 | HIDE_CURSOR :: ansi.CSI + "?25l"
161 | fmt.print(HIDE_CURSOR if hide else SHOW_CURSOR)
162 | }
163 |
164 | set_text_style :: proc(win: $T/^Window, styles: bit_set[Text_Style]) {
165 | SGR_BOLD :: ansi.CSI + ansi.BOLD + "m"
166 | SGR_DIM :: ansi.CSI + ansi.FAINT + "m"
167 | SGR_ITALIC :: ansi.CSI + ansi.ITALIC + "m"
168 | SGR_UNDERLINE :: ansi.CSI + ansi.UNDERLINE + "m"
169 | SGR_INVERTED :: ansi.CSI + ansi.INVERT + "m"
170 | SGR_CROSSED :: ansi.CSI + ansi.STRIKE + "m"
171 |
172 | if .Bold in styles do strings.write_string(&win.seq_builder, SGR_BOLD)
173 | if .Dim in styles do strings.write_string(&win.seq_builder, SGR_DIM)
174 | if .Italic in styles do strings.write_string(&win.seq_builder, SGR_ITALIC)
175 | if .Underline in styles do strings.write_string(&win.seq_builder, SGR_UNDERLINE)
176 | if .Inverted in styles do strings.write_string(&win.seq_builder, SGR_INVERTED)
177 | if .Crossed in styles do strings.write_string(&win.seq_builder, SGR_CROSSED)
178 | }
179 |
180 | Color_8 :: enum {
181 | Black,
182 | Red,
183 | Green,
184 | Yellow,
185 | Blue,
186 | Magenta,
187 | Cyan,
188 | White,
189 | }
190 |
191 | // Sets background and foreground colors based on the original 8 color palette
192 | set_color_style_8 :: proc(win: $T/^Window, fg: Maybe(Color_8), bg: Maybe(Color_8)) {
193 | get_color_code :: proc(c: Color_8, is_bg: bool) -> uint {
194 | code: uint
195 | switch c {
196 | case .Black:
197 | code = 30
198 | case .Red:
199 | code = 31
200 | case .Green:
201 | code = 32
202 | case .Yellow:
203 | code = 33
204 | case .Blue:
205 | code = 34
206 | case .Magenta:
207 | code = 35
208 | case .Cyan:
209 | code = 36
210 | case .White:
211 | code = 37
212 | }
213 |
214 | if is_bg do code += 10
215 | return code
216 | }
217 |
218 | SGR_COLOR :: ansi.CSI + "%dm"
219 | set_color :: proc(builder: ^strings.Builder, color: uint) {
220 | strings.write_string(builder, ansi.CSI)
221 | strings.write_uint(builder, color)
222 | strings.write_rune(builder, 'm')
223 | }
224 |
225 | DEFAULT_FG :: 39
226 | DEFAULT_BG :: 49
227 | set_color(&win.seq_builder, get_color_code(fg.?, false) if fg != nil else DEFAULT_FG)
228 | set_color(&win.seq_builder, get_color_code(bg.?, true) if bg != nil else DEFAULT_BG)
229 | }
230 |
231 | RGB_Color :: struct {
232 | r, g, b: u8,
233 | }
234 |
235 | // Sets background and foreground colors based on the RGB values.
236 | // The terminal has to support true colors for it to work.
237 | set_color_style_rgb :: proc(win: $T/^Window, fg: Maybe(RGB_Color), bg: Maybe(RGB_Color)) {
238 | RGB_FG_COLOR :: ansi.CSI + "38;2;%d;%d;%dm"
239 | RGB_BG_COLOR :: ansi.CSI + "48;2;%d;%d;%dm"
240 |
241 | set_color :: proc(builder: ^strings.Builder, is_fg: bool, color: RGB_Color) {
242 | strings.write_string(builder, ansi.CSI)
243 | strings.write_uint(builder, 38 if is_fg else 48)
244 | strings.write_string(builder, ";2;")
245 | strings.write_uint(builder, cast(uint)color.r)
246 | strings.write_rune(builder, ';')
247 | strings.write_uint(builder, cast(uint)color.g)
248 | strings.write_rune(builder, ';')
249 | strings.write_uint(builder, cast(uint)color.b)
250 | strings.write_rune(builder, 'm')
251 | }
252 |
253 | fg_color, has_fg := fg.?
254 | bg_color, has_bg := bg.?
255 |
256 | if !has_fg || !has_bg {
257 | set_color_style_8(win, nil, nil)
258 | }
259 |
260 | if has_fg do set_color(&win.seq_builder, true, fg_color)
261 | if has_bg do set_color(&win.seq_builder, false, bg_color)
262 |
263 | }
264 |
265 | // Sets foreground and background colors
266 | set_color_style :: proc {
267 | set_color_style_8,
268 | set_color_style_rgb,
269 | }
270 |
271 | // Resets all styles previously set.
272 | // It is good practice to reset after being done with a style as to prevent styles to be applied erroneously.
273 | reset_styles :: proc(win: $T/^Window) {
274 | strings.write_string(&win.seq_builder, ansi.CSI + "0m")
275 | }
276 |
277 | Clear_Mode :: enum {
278 | Before_Cursor,
279 | After_Cursor,
280 | Everything,
281 | }
282 |
283 | // Clears screen starting from current line.
284 | clear :: proc(win: $T/^Window, mode: Clear_Mode) {
285 | height, h_ok := win.height.?
286 | width, w_ok := win.width.?
287 |
288 | if !h_ok && !w_ok do switch mode {
289 | case .After_Cursor:
290 | strings.write_string(&win.seq_builder, ansi.CSI + "0J")
291 | case .Before_Cursor:
292 | strings.write_string(&win.seq_builder, ansi.CSI + "1J")
293 | case .Everything:
294 | strings.write_string(&win.seq_builder, ansi.CSI + "H" + ansi.CSI + "2J")
295 | win.cursor = {0, 0}
296 | }
297 | else {
298 | // we compute the number of spaces required to clear a window and then
299 | // let the write_rune function take care of properly moving the cursor
300 | // through its own window isolation logic
301 | space_num: uint
302 | curr_pos := get_cursor_position(win)
303 |
304 | switch mode {
305 | case .After_Cursor:
306 | space_in_same_line := width - (win.cursor.x + 1)
307 | space_after_same_line := width * (height - ((win.cursor.y + 1) % height))
308 | space_num = space_in_same_line + space_after_same_line
309 | move_cursor(win, curr_pos.y, curr_pos.x + 1)
310 | case .Before_Cursor:
311 | space_num = win.cursor.x + 1 + win.cursor.y * width
312 | move_cursor(win, 0, 0)
313 | case .Everything:
314 | space_num = (width + 1) * height
315 | move_cursor(win, 0, 0)
316 | }
317 |
318 | for i in 0 ..< space_num {
319 | write_rune(win, ' ')
320 | }
321 |
322 | move_cursor(win, curr_pos.y, curr_pos.x)
323 |
324 | }
325 | }
326 |
327 | // Only clears the line the cursor is in
328 | clear_line :: proc(win: $T/^Window, mode: Clear_Mode) {
329 | switch mode {
330 | case .After_Cursor:
331 | strings.write_string(&win.seq_builder, ansi.CSI + "0K")
332 | case .Before_Cursor:
333 | strings.write_string(&win.seq_builder, ansi.CSI + "1K")
334 | case .Everything:
335 | strings.write_string(&win.seq_builder, ansi.CSI + "2K")
336 | }
337 | }
338 |
339 | // Ring terminal bell. (potentially annoying to users :P)
340 | ring_bell :: proc() {
341 | fmt.print("\a")
342 | }
343 |
344 | // This is used internally to figure out and update where the cursor will be after a string is written to the terminal
345 | _get_cursor_pos_from_string :: proc(win: $T/^Window, str: string) -> [2]uint {
346 | calculate_cursor_pos :: proc(
347 | cursor: ^Cursor_Position,
348 | height, width: uint,
349 | str: string,
350 | ) -> [2]uint {
351 | new_pos := [2]uint{cursor.x, cursor.y}
352 | for r in str {
353 | if new_pos.y >= height && r == '\n' {
354 | new_pos.x = 0
355 | continue
356 | }
357 |
358 | if new_pos.x >= width || r == '\n' {
359 | new_pos.y += 1
360 | new_pos.x = 0
361 | } else {
362 | new_pos.x += 1
363 | }
364 | }
365 | return new_pos
366 | }
367 |
368 | when type_of(win) == ^Screen {
369 | term_size := get_term_size(win)
370 | height := term_size.h
371 | width := term_size.w
372 | return calculate_cursor_pos(&win.cursor, height, width, str)
373 | } else {
374 | height, h_ok := win.height.?
375 | width, w_ok := win.width.?
376 |
377 | if h_ok && w_ok {
378 | return calculate_cursor_pos(&win.cursor, height, width, str)
379 | } else {
380 | return [2]uint{win.cursor.x, win.cursor.y}
381 | }
382 | }
383 | }
384 |
385 | // Writes a rune to the terminal
386 | write_rune :: proc(win: $T/^Window, r: rune) {
387 | strings.write_rune(&win.seq_builder, r)
388 | // the new cursor position has to be calculated after writing the rune
389 | // otherwise the rune will be misplaced when blitted to terminal
390 | r_bytes, r_len := utf8.encode_rune(r)
391 | r_str := string(r_bytes[:r_len])
392 | new_pos := _get_cursor_pos_from_string(win, r_str)
393 | move_cursor(win, new_pos.y, new_pos.x)
394 | }
395 |
396 | // Writes a string to the terminal
397 | write_string :: proc(win: $T/^Window, str: string) {
398 | when type_of(win) == ^Screen {
399 | term_size := get_term_size(win)
400 | win_width := term_size.w
401 | } else {
402 | win_width, w_ok := win.width.?
403 | if !w_ok {
404 | strings.write_string(&win.seq_builder, str)
405 | return
406 | }
407 | }
408 |
409 | // the string is written in chunks so that it doesn't overflow the
410 | // window in which it is contained
411 | str_slice_start: uint
412 | for str_slice_start < len(str) {
413 | chunk_len := win_width - win.cursor.x
414 | str_slice_end := str_slice_start + chunk_len
415 | if str_slice_end > cast(uint)len(str) {
416 | str_slice_end = len(str)
417 | }
418 |
419 | str_slice := str[str_slice_start:str_slice_end]
420 | strings.write_string(&win.seq_builder, str_slice)
421 |
422 | str_slice_start = str_slice_end
423 |
424 | new_pos := _get_cursor_pos_from_string(win, str_slice)
425 | win.cursor.x = new_pos.x
426 | win.cursor.y = new_pos.y
427 | // we try an empty string so that we can compute starting from the next character
428 | // that's going to be inserted
429 | new_pos = _get_cursor_pos_from_string(win, " ")
430 | move_cursor(win, new_pos.y, new_pos.x)
431 | }
432 | }
433 |
434 | // Write a formatted string to the terminal
435 | writef :: proc(win: $T/^Window, format: string, args: ..any) {
436 | str_builder: strings.Builder
437 | _, err := strings.builder_init(&str_builder, allocator = win.allocator)
438 | if err != nil {
439 | panic("Failed to get more memory for format string")
440 | }
441 | defer strings.builder_destroy(&str_builder)
442 | str := fmt.sbprintf(&str_builder, format, ..args)
443 | write_string(win, str)
444 | }
445 |
446 | // Write to the terminal
447 | write :: proc {
448 | write_string,
449 | write_rune,
450 | }
451 |
452 |
453 | Term_Mode :: enum {
454 | // Raw mode, prevents the terminal from preprocessing inputs
455 | Raw,
456 | // Restores the mode the terminal was in before program started
457 | Restored,
458 | // A sort of "soft" raw mode that allows interrupts to still work
459 | Cbreak,
460 | }
461 |
462 | // Change terminal mode.
463 | //
464 | // This changes how the terminal processes inputs.
465 | // By default the terminal will process inputs, preventing you to have full access to user input.
466 | // Changing the terminal mode will your program to process every input.
467 | set_term_mode :: proc(screen: ^Screen, mode: Term_Mode) {
468 | change_terminal_mode(screen, mode)
469 |
470 | #partial switch mode {
471 | case .Restored:
472 | // enables main screen buffer
473 | fmt.print("\x1b[?1049l")
474 | enable_mouse(false)
475 |
476 | case:
477 | // enables alternate screen buffer
478 | fmt.print("\x1b[?1049h")
479 | enable_mouse(true)
480 | }
481 |
482 | hide_cursor(false)
483 | // when changing modes some OSes (like windows) might put garbage that we don't care about
484 | // in stdin potentially causing nonblocking reads to block on the first read, so to avoid this,
485 | // stdin is always flushed when the mode is changed
486 | os.flush(os.stdin)
487 | }
488 |
489 | Screen_Size :: struct {
490 | h, w: uint,
491 | }
492 |
493 | // Get the terminal screen size.
494 | //
495 | // The width and height are measured in number of cells not pixels.
496 | // Aka the same value you use with `move_mouse` and other functions.
497 | get_term_size :: proc(screen: ^Screen) -> Screen_Size {
498 | win, ok := get_term_size_via_syscall()
499 | if ok do return win
500 |
501 | curr_pos := get_cursor_position(screen)
502 |
503 | MAX_CURSOR_POSITION :: ansi.CSI + "9999;9999H"
504 | fmt.print(MAX_CURSOR_POSITION)
505 | pos := get_cursor_position(screen)
506 |
507 | // restore cursor position
508 | fmt.printf(ansi.CSI + "%d;%dH", curr_pos.y, curr_pos.x)
509 |
510 | return Screen_Size{w = pos.x, h = pos.y}
511 | }
512 |
513 | // Enable mouse to be able to respond to mouse inputs.
514 | //
515 | // It's enabled by default. This is here so you can opt-out of it or control when to enable or disable it.
516 | enable_mouse :: proc(enable: bool) {
517 | ANY_EVENT :: "\x1b[?1003"
518 | SGR_MOUSE :: "\x1b[?1006"
519 |
520 | if enable {
521 | fmt.print(ANY_EVENT + "h", SGR_MOUSE + "h")
522 | } else {
523 | fmt.print(ANY_EVENT + "l", SGR_MOUSE + "l")
524 | }
525 | }
526 |
527 | Cursor_Position :: struct {
528 | y, x: uint,
529 | }
530 |
531 | // Get the current cursor position.
532 | get_cursor_position :: #force_inline proc(win: $T/^Window) -> Cursor_Position {
533 | return win.cursor
534 | }
535 |
536 |
--------------------------------------------------------------------------------
/widgets/button.odin:
--------------------------------------------------------------------------------
1 | package widgets
2 |
3 | import t ".."
4 |
5 | Button_Style :: struct {
6 | bg, fg: 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 | set_any_color_style(&btn._window, btn.style.fg, btn.style.bg)
82 | t.set_text_style(&btn._window, btn.style.text)
83 |
84 | set_any_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 |
--------------------------------------------------------------------------------
/widgets/common.odin:
--------------------------------------------------------------------------------
1 | package widgets
2 |
3 | import t ".."
4 |
5 | Any_Color :: union {
6 | t.RGB_Color,
7 | t.Color_8,
8 | }
9 |
10 | set_any_color_style :: proc(win: $T/^t.Window, fg: Any_Color, bg: Any_Color) {
11 | // sadly this is the only way I found to allow either RGB or 8 color palette colors
12 | // without having to make the user cast into union types
13 | fg_rgb, has_fg_rgb := fg.(t.RGB_Color)
14 | bg_rgb, has_bg_rgb := bg.(t.RGB_Color)
15 |
16 | fg_8, has_fg_8 := fg.(t.Color_8)
17 | bg_8, has_bg_8 := bg.(t.Color_8)
18 |
19 | if (has_fg_8 || has_bg_8) && (has_fg_rgb || has_bg_rgb) {
20 | panic("both fg and bg have to be the same color type or nil")
21 | }
22 |
23 | if has_fg_8 || has_bg_8 {
24 | t.set_color_style_8(win, fg_8 if fg != nil else nil, bg_8 if bg != nil else nil)
25 | }
26 |
27 | if has_fg_rgb || has_bg_rgb {
28 | t.set_color_style_rgb(win, fg_rgb if fg != nil else nil, bg_rgb if bg != nil else nil)
29 | }
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/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: Any_Color,
19 | space_between: uint,
20 | }
21 |
22 | Panel :: struct {
23 | _screen: ^t.Screen,
24 | _window: t.Window,
25 | items: []Panel_Item,
26 | style: Panel_Style,
27 | }
28 |
29 | panel_init :: proc(screen: ^t.Screen) -> Panel {
30 | termsize := t.get_term_size(screen)
31 | return Panel {
32 | _screen = screen,
33 | _window = t.init_window(0, 0, 1, 0),
34 | style = {space_between = 2, fg = .Black, bg = .White},
35 | }
36 | }
37 |
38 | _panel_set_layout :: proc(panel: ^Panel) {
39 | termsize := t.get_term_size(panel._screen)
40 | panel._window.y_offset = termsize.h
41 | panel._window.width = termsize.w
42 | }
43 |
44 | panel_destroy :: proc(panel: ^Panel) {
45 | t.destroy_window(&panel._window)
46 | }
47 |
48 | panel_blit :: proc(panel: ^Panel) {
49 | _panel_set_layout(panel)
50 |
51 | defer t.reset_styles(&panel._window)
52 | set_any_color_style(&panel._window, panel.style.fg, panel.style.bg)
53 | t.clear(&panel._window, .Everything)
54 |
55 | cursor_on_left := t.Cursor_Position {
56 | x = 1,
57 | y = 0,
58 | }
59 | cursor_on_right := t.Cursor_Position {
60 | x = panel._window.width.? - 1,
61 | y = 0,
62 | }
63 |
64 | t.move_cursor(&panel._window, 0, 1)
65 | center_items := make([dynamic]Panel_Item)
66 | defer delete(center_items)
67 |
68 | drawing_panel: for item in panel.items {
69 | switch item.position {
70 | case .None:
71 | continue drawing_panel
72 |
73 | case .Left:
74 | t.move_cursor(&panel._window, cursor_on_left.y, cursor_on_left.x)
75 | t.write(&panel._window, item.content)
76 | cursor_pos := t.get_cursor_position(&panel._window)
77 | cursor_pos.x += panel.style.space_between
78 | cursor_on_left = cursor_pos
79 |
80 | case .Right:
81 | t.move_cursor(&panel._window, cursor_on_right.y, cursor_on_right.x - len(item.content))
82 | cursor_on_right.x -= len(item.content) + panel.style.space_between
83 | t.write(&panel._window, item.content)
84 |
85 | case .Center:
86 | append(¢er_items, item)
87 | }
88 | }
89 |
90 | // the space in between each item will always be num_of_items - 1
91 | center_items_width: uint = (len(center_items) - 1) * panel.style.space_between
92 | for item in center_items {
93 | center_items_width += len(item.content)
94 | }
95 |
96 | panel_width := panel._window.width.?
97 | t.move_cursor(&panel._window, 0, panel_width / 2 - center_items_width / 2)
98 | for item in center_items {
99 | t.write(&panel._window, item.content)
100 | cursor_pos := t.get_cursor_position(&panel._window)
101 | cursor_pos.x += panel.style.space_between
102 | t.move_cursor(&panel._window, cursor_pos.y, cursor_pos.x)
103 | }
104 |
105 | t.blit(&panel._window)
106 | }
107 |
108 |
--------------------------------------------------------------------------------
/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: Any_Color,
14 | bg, fg: 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 | set_any_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 | set_any_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 | set_any_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 |
--------------------------------------------------------------------------------