aboutsummaryrefslogtreecommitdiff
path: root/v_windows/v/vlib/term/ui
diff options
context:
space:
mode:
authorIndrajith K L2022-12-03 17:00:20 +0530
committerIndrajith K L2022-12-03 17:00:20 +0530
commitf5c4671bfbad96bf346bd7e9a21fc4317b4959df (patch)
tree2764fc62da58f2ba8da7ed341643fc359873142f /v_windows/v/vlib/term/ui
downloadcli-tools-windows-master.tar.gz
cli-tools-windows-master.tar.bz2
cli-tools-windows-master.zip
Adds most of the toolsHEADmaster
Diffstat (limited to 'v_windows/v/vlib/term/ui')
-rw-r--r--v_windows/v/vlib/term/ui/README.md99
-rw-r--r--v_windows/v/vlib/term/ui/color.v88
-rw-r--r--v_windows/v/vlib/term/ui/consoleapi_windows.c.v82
-rw-r--r--v_windows/v/vlib/term/ui/input.v241
-rw-r--r--v_windows/v/vlib/term/ui/input_nix.c.v70
-rw-r--r--v_windows/v/vlib/term/ui/input_windows.c.v326
-rw-r--r--v_windows/v/vlib/term/ui/termios_nix.c.v530
-rw-r--r--v_windows/v/vlib/term/ui/ui.v256
8 files changed, 1692 insertions, 0 deletions
diff --git a/v_windows/v/vlib/term/ui/README.md b/v_windows/v/vlib/term/ui/README.md
new file mode 100644
index 0000000..6bce054
--- /dev/null
+++ b/v_windows/v/vlib/term/ui/README.md
@@ -0,0 +1,99 @@
+## `term.ui`
+
+A V module for designing terminal UI apps
+
+#### Quickstart
+
+```v
+import term.ui as tui
+
+struct App {
+mut:
+ tui &tui.Context = 0
+}
+
+fn event(e &tui.Event, x voidptr) {
+ mut app := &App(x)
+ println(e)
+ if e.typ == .key_down && e.code == .escape {
+ exit(0)
+ }
+}
+
+fn frame(x voidptr) {
+ mut app := &App(x)
+
+ app.tui.clear()
+ app.tui.set_bg_color(r: 63, g: 81, b: 181)
+ app.tui.draw_rect(20, 6, 41, 10)
+ app.tui.draw_text(24, 8, 'Hello from V!')
+ app.tui.set_cursor_position(0, 0)
+
+ app.tui.reset()
+ app.tui.flush()
+}
+
+mut app := &App{}
+app.tui = tui.init(
+ user_data: app
+ event_fn: event
+ frame_fn: frame
+ hide_cursor: true
+)
+app.tui.run() ?
+```
+
+See the `/examples/term.ui/` folder for more usage examples.
+
+#### Configuration
+
+- `user_data voidptr` - a pointer to any `user_data`, it will be passed as the last argument to
+ each callback. Used for accessing your app context from the different callbacks.
+- `init_fn fn(voidptr)` - a callback that will be called after initialization
+ and before the first event / frame. Useful for initializing any user data.
+- `frame_fn fn(voidptr)` - a callback that will be fired on each frame,
+ at a rate of `frame_rate` frames per second.
+`event_fn fn(&Event, voidptr)` - a callback that will be fired for every event received.
+- `cleanup_fn fn(voidptr)` - a callback that will be fired once, before the application exits.
+- `fail_fn fn(string)` - a callback that will be fired
+ if a fatal error occurs during app initialization.
+- `buffer_size int = 256` - the internal size of the read buffer.
+ Increasing it may help in case you're missing events, but you probably shouldn't lower
+ this value unless you make sure you're still receiving all events. In general,
+ higher frame rates work better with lower buffer sizes, and vice versa.
+- `frame_rate int = 30` - the number of times per second that the `frame` callback will be fired.
+ 30fps is a nice balance between smoothness and performance,
+ but you can increase or lower it as you wish.
+- `hide_cursor bool` - whether to hide the mouse cursor. Useful if you want to use your own.
+- `capture_events bool` - sets the terminal into raw mode, which makes it intercept some
+ escape codes such as `ctrl + c` and `ctrl + z`.
+ Useful if you want to use those key combinations in your app.
+- `window_title string` - sets the title of the terminal window.
+ This may be changed later, by calling the `set_window_title()` method.
+- `reset []int = [1, 2, 3, 4, 6, 7, 8, 9, 11, 13, 14, 15, 19]` - a list of reset signals,
+ to setup handlers to cleanup the terminal state when they're received.
+ You should not need to change this, unless you know what you're doing.
+
+All of these fields may be omitted, in which case, the default value will be used.
+In the case of the various callbacks, they will not be fired if a handler has not been specified.
+
+
+#### FAQ
+
+Q: My terminal (doesn't receive events / doesn't print anything / prints gibberish characters),
+what's up with that?
+A: Please check if your terminal. The module has been tested with `xterm`-based terminals on Linux
+(like `gnome-terminal` and `konsole`), and `Terminal.app` and `iterm2` on macOS.
+If your terminal does not work, open an issue with the output of `echo $TERM`.
+
+Q: There are screen tearing issues when doing large prints
+A: This is an issue with how terminals render frames,
+as they may decide to do so in the middle of receiving a frame,
+and cannot be fully fixed unless your console implements the [synchronized updates spec](https://gitlab.com/gnachman/iterm2/-/wikis/synchronized-updates-spec).
+It can be reduced *drastically*, though, by using the rendering methods built in to the module,
+and by only painting frames when your app's content has actually changed.
+
+Q: Why does the module only emit `keydown` events, and not `keyup` like `sokol`/`gg`?
+A: It's because of the way terminals emit events. Every key event is received as a keypress,
+and there isn't a way of telling terminals to send keyboard events differently,
+nor a reliable way of converting these into `keydown` / `keyup` events.
diff --git a/v_windows/v/vlib/term/ui/color.v b/v_windows/v/vlib/term/ui/color.v
new file mode 100644
index 0000000..3e0a0bb
--- /dev/null
+++ b/v_windows/v/vlib/term/ui/color.v
@@ -0,0 +1,88 @@
+// radare - LGPL - Copyright 2013-2020 - pancake, xarkes
+// ansi 256 color extension for r_cons
+// https://en.wikipedia.org/wiki/ANSI_color
+
+module ui
+
+const (
+ value_range = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]!
+ color_table = init_color_table()
+)
+
+[direct_array_access]
+fn init_color_table() []int {
+ mut color_table_ := []int{len: 256}
+ // ansi colors
+ color_table_[0] = 0x000000
+ color_table_[1] = 0x800000
+ color_table_[2] = 0x008000
+ color_table_[3] = 0x808000
+ color_table_[4] = 0x000080
+ color_table_[5] = 0x800080
+ color_table_[6] = 0x008080
+ color_table_[7] = 0xc0c0c0
+ color_table_[8] = 0x808080
+ color_table_[9] = 0xff0000
+ color_table_[10] = 0x00ff00
+ color_table_[11] = 0xffff00
+ color_table_[12] = 0x0000ff
+ color_table_[13] = 0xff00ff
+ color_table_[14] = 0x00ffff
+ color_table_[15] = 0xffffff
+ // color palette
+ for i in 0 .. 216 {
+ r := ui.value_range[(i / 36) % 6]
+ g := ui.value_range[(i / 6) % 6]
+ b := ui.value_range[i % 6]
+ color_table_[i + 16] = ((r << 16) & 0xffffff) + ((g << 8) & 0xffff) + (b & 0xff)
+ }
+ // grayscale
+ for i in 0 .. 24 {
+ r := 8 + (i * 10)
+ color_table_[i + 232] = ((r << 16) & 0xffffff) + ((r << 8) & 0xffff) + (r & 0xff)
+ }
+ return color_table_
+}
+
+fn clamp(x int, y int, z int) int {
+ if x < y {
+ return y
+ }
+ if x > z {
+ return z
+ }
+ return x
+}
+
+fn approximate_rgb(r int, g int, b int) int {
+ grey := r > 0 && r < 255 && r == g && r == b
+ if grey {
+ return 232 + int(f64(r) / (255 / 24.1))
+ }
+ k := int(256.0 / 6)
+ r2 := clamp(r / k, 0, 5)
+ g2 := clamp(g / k, 0, 5)
+ b2 := clamp(b / k, 0, 5)
+ return 16 + (r2 * 36) + (g2 * 6) + b2
+}
+
+fn lookup_rgb(r int, g int, b int) int {
+ color := (r << 16) + (g << 8) + b
+ // lookup extended colors only, coz non-extended can be changed by users.
+ for i in 16 .. 256 {
+ if ui.color_table[i] == color {
+ return i
+ }
+ }
+ return -1
+}
+
+// converts an RGB color to an ANSI 256-color, approximating it to the nearest available color
+// if an exact match is not found
+fn rgb2ansi(r int, g int, b int) int {
+ c := lookup_rgb(r, g, b)
+ if c == -1 {
+ return approximate_rgb(r, g, b)
+ }
+ return c
+}
diff --git a/v_windows/v/vlib/term/ui/consoleapi_windows.c.v b/v_windows/v/vlib/term/ui/consoleapi_windows.c.v
new file mode 100644
index 0000000..a6002a6
--- /dev/null
+++ b/v_windows/v/vlib/term/ui/consoleapi_windows.c.v
@@ -0,0 +1,82 @@
+module ui
+
+union C.Event {
+ KeyEvent C.KEY_EVENT_RECORD
+ MouseEvent C.MOUSE_EVENT_RECORD
+ WindowBufferSizeEvent C.WINDOW_BUFFER_SIZE_RECORD
+ MenuEvent C.MENU_EVENT_RECORD
+ FocusEvent C.FOCUS_EVENT_RECORD
+}
+
+[typedef]
+struct C.INPUT_RECORD {
+ EventType u16
+ Event C.Event
+}
+
+union C.uChar {
+ UnicodeChar rune
+ AsciiChar byte
+}
+
+[typedef]
+struct C.KEY_EVENT_RECORD {
+ bKeyDown int
+ wRepeatCount u16
+ wVirtualKeyCode u16
+ wVirtualScanCode u16
+ uChar C.uChar
+ dwControlKeyState u32
+}
+
+[typedef]
+struct C.MOUSE_EVENT_RECORD {
+ dwMousePosition C.COORD
+ dwButtonState u32
+ dwControlKeyState u32
+ dwEventFlags u32
+}
+
+[typedef]
+struct C.WINDOW_BUFFER_SIZE_RECORD {
+ dwSize C.COORD
+}
+
+[typedef]
+struct C.MENU_EVENT_RECORD {
+ dwCommandId u32
+}
+
+[typedef]
+struct C.FOCUS_EVENT_RECORD {
+ bSetFocus int
+}
+
+[typedef]
+struct C.COORD {
+ X i16
+ Y i16
+}
+
+[typedef]
+struct C.SMALL_RECT {
+ Left u16
+ Top u16
+ Right u16
+ Bottom u16
+}
+
+[typedef]
+struct C.CONSOLE_SCREEN_BUFFER_INFO {
+ dwSize C.COORD
+ dwCursorPosition C.COORD
+ wAttributes u16
+ srWindow C.SMALL_RECT
+ dwMaximumWindowSize C.COORD
+}
+
+fn C.ReadConsoleInput(hConsoleInput C.HANDLE, lpBuffer &C.INPUT_RECORD, nLength u32, lpNumberOfEventsRead &u32) bool
+
+fn C.GetNumberOfConsoleInputEvents(hConsoleInput C.HANDLE, lpcNumberOfEvents &u32) bool
+
+fn C.GetConsoleScreenBufferInfo(handle C.HANDLE, info &C.CONSOLE_SCREEN_BUFFER_INFO) bool
diff --git a/v_windows/v/vlib/term/ui/input.v b/v_windows/v/vlib/term/ui/input.v
new file mode 100644
index 0000000..0532b39
--- /dev/null
+++ b/v_windows/v/vlib/term/ui/input.v
@@ -0,0 +1,241 @@
+// Copyright (c) 2020-2021 Raúl Hernández. All rights reserved.
+// Use of this source code is governed by an MIT license
+// that can be found in the LICENSE file.
+module ui
+
+import os
+
+pub enum KeyCode {
+ null = 0
+ tab = 9
+ enter = 10
+ escape = 27
+ space = 32
+ backspace = 127
+ exclamation = 33
+ double_quote = 34
+ hashtag = 35
+ dollar = 36
+ percent = 37
+ ampersand = 38
+ single_quote = 39
+ left_paren = 40
+ right_paren = 41
+ asterisk = 42
+ plus = 43
+ comma = 44
+ minus = 45
+ period = 46
+ slash = 47
+ _0 = 48
+ _1 = 49
+ _2 = 50
+ _3 = 51
+ _4 = 52
+ _5 = 53
+ _6 = 54
+ _7 = 55
+ _8 = 56
+ _9 = 57
+ colon = 58
+ semicolon = 59
+ less_than = 60
+ equal = 61
+ greater_than = 62
+ question_mark = 63
+ at = 64
+ a = 97
+ b = 98
+ c = 99
+ d = 100
+ e = 101
+ f = 102
+ g = 103
+ h = 104
+ i = 105
+ j = 106
+ k = 107
+ l = 108
+ m = 109
+ n = 110
+ o = 111
+ p = 112
+ q = 113
+ r = 114
+ s = 115
+ t = 116
+ u = 117
+ v = 118
+ w = 119
+ x = 120
+ y = 121
+ z = 122
+ left_square_bracket = 91
+ backslash = 92
+ right_square_bracket = 93
+ caret = 94
+ underscore = 95
+ backtick = 96
+ left_curly_bracket = 123
+ vertical_bar = 124
+ right_curly_bracket = 125
+ tilde = 126
+ insert = 260
+ delete = 261
+ up = 262
+ down = 263
+ right = 264
+ left = 265
+ page_up = 266
+ page_down = 267
+ home = 268
+ end = 269
+ f1 = 290
+ f2 = 291
+ f3 = 292
+ f4 = 293
+ f5 = 294
+ f6 = 295
+ f7 = 296
+ f8 = 297
+ f9 = 298
+ f10 = 299
+ f11 = 300
+ f12 = 301
+ f13 = 302
+ f14 = 303
+ f15 = 304
+ f16 = 305
+ f17 = 306
+ f18 = 307
+ f19 = 308
+ f20 = 309
+ f21 = 310
+ f22 = 311
+ f23 = 312
+ f24 = 313
+}
+
+pub enum Direction {
+ unknown
+ up
+ down
+ left
+ right
+}
+
+pub enum MouseButton {
+ unknown
+ left
+ middle
+ right
+}
+
+pub enum EventType {
+ unknown
+ mouse_down
+ mouse_up
+ mouse_move
+ mouse_drag
+ mouse_scroll
+ key_down
+ resized
+}
+
+[flag]
+pub enum Modifiers {
+ ctrl
+ shift
+ alt
+}
+
+pub struct Event {
+pub:
+ typ EventType
+ // Mouse event info
+ x int
+ y int
+ button MouseButton
+ direction Direction
+ // Keyboard event info
+ code KeyCode
+ modifiers Modifiers
+ ascii byte
+ utf8 string
+ // Resized event info
+ width int
+ height int
+}
+
+pub struct Context {
+ ExtraContext // contains fields specific to an implementation
+pub:
+ cfg Config // adsasdas
+mut:
+ print_buf []byte
+ paused bool
+ enable_su bool
+ enable_rgb bool
+pub mut:
+ frame_count u64
+ window_width int
+ window_height int
+}
+
+pub struct Config {
+ user_data voidptr
+ init_fn fn (voidptr)
+ frame_fn fn (voidptr)
+ cleanup_fn fn (voidptr)
+ event_fn fn (&Event, voidptr)
+ fail_fn fn (string)
+
+ buffer_size int = 256
+ frame_rate int = 30
+ use_x11 bool
+
+ window_title string
+ hide_cursor bool
+ capture_events bool
+ use_alternate_buffer bool = true
+ skip_init_checks bool
+ // All kill signals to set up exit listeners on:
+ reset []os.Signal = [.hup, .int, .quit, .ill, .abrt, .bus, .fpe, .kill, .segv, .pipe, .alrm, .term,
+ .stop,
+]
+}
+
+[inline]
+fn (ctx &Context) init() {
+ if ctx.cfg.init_fn != voidptr(0) {
+ ctx.cfg.init_fn(ctx.cfg.user_data)
+ }
+}
+
+[inline]
+fn (ctx &Context) frame() {
+ if ctx.cfg.frame_fn != voidptr(0) {
+ ctx.cfg.frame_fn(ctx.cfg.user_data)
+ }
+}
+
+[inline]
+fn (ctx &Context) cleanup() {
+ if ctx.cfg.cleanup_fn != voidptr(0) {
+ ctx.cfg.cleanup_fn(ctx.cfg.user_data)
+ }
+}
+
+[inline]
+fn (ctx &Context) fail(error string) {
+ if ctx.cfg.fail_fn != voidptr(0) {
+ ctx.cfg.fail_fn(error)
+ }
+}
+
+[inline]
+fn (ctx &Context) event(event &Event) {
+ if ctx.cfg.event_fn != voidptr(0) {
+ ctx.cfg.event_fn(event, ctx.cfg.user_data)
+ }
+}
diff --git a/v_windows/v/vlib/term/ui/input_nix.c.v b/v_windows/v/vlib/term/ui/input_nix.c.v
new file mode 100644
index 0000000..e806fb8
--- /dev/null
+++ b/v_windows/v/vlib/term/ui/input_nix.c.v
@@ -0,0 +1,70 @@
+// Copyright (c) 2020-2021 Raúl Hernández. All rights reserved.
+// Use of this source code is governed by an MIT license
+// that can be found in the LICENSE file.
+module ui
+
+struct ExtraContext {
+mut:
+ read_buf []byte
+}
+
+const (
+ ctx_ptr = &Context(0)
+)
+
+pub fn init(cfg Config) &Context {
+ mut ctx := &Context{
+ cfg: cfg
+ }
+ ctx.read_buf = []byte{cap: cfg.buffer_size}
+
+ // lmao
+ unsafe {
+ x := &ui.ctx_ptr
+ *x = ctx
+ _ = x
+ }
+ return ctx
+}
+
+[inline]
+fn save_title() {
+ // restore the previously saved terminal title
+ print('\x1b[22;0t')
+}
+
+[inline]
+fn load_title() {
+ // restore the previously saved terminal title
+ print('\x1b[23;0t')
+}
+
+pub fn (mut ctx Context) run() ? {
+ if ctx.cfg.use_x11 {
+ ctx.fail('error: x11 backend not implemented yet')
+ exit(1)
+ } else {
+ ctx.termios_setup() ?
+ ctx.termios_loop()
+ }
+}
+
+// shifts the array left, to remove any data that was just read, and updates its len
+// TODO: remove
+[inline]
+fn (mut ctx Context) shift(len int) {
+ unsafe {
+ C.memmove(ctx.read_buf.data, &byte(ctx.read_buf.data) + len, ctx.read_buf.cap - len)
+ ctx.resize_arr(ctx.read_buf.len - len)
+ }
+}
+
+// TODO: don't actually do this, lmao
+[inline]
+fn (mut ctx Context) resize_arr(size int) {
+ mut l := unsafe { &ctx.read_buf.len }
+ unsafe {
+ *l = size
+ _ = l
+ }
+}
diff --git a/v_windows/v/vlib/term/ui/input_windows.c.v b/v_windows/v/vlib/term/ui/input_windows.c.v
new file mode 100644
index 0000000..bd9782d
--- /dev/null
+++ b/v_windows/v/vlib/term/ui/input_windows.c.v
@@ -0,0 +1,326 @@
+// Copyright (c) 2020-2021 Raúl Hernández. All rights reserved.
+// Use of this source code is governed by an MIT license
+// that can be found in the LICENSE file.
+module ui
+
+import os
+import time
+
+const (
+ buf_size = 64
+ ctx_ptr = &Context(0)
+ stdin_at_startup = u32(0)
+)
+
+struct ExtraContext {
+mut:
+ stdin_handle C.HANDLE
+ stdout_handle C.HANDLE
+ read_buf [buf_size]C.INPUT_RECORD
+ mouse_down MouseButton
+}
+
+fn restore_terminal_state() {
+ if ui.ctx_ptr != 0 {
+ if ui.ctx_ptr.cfg.use_alternate_buffer {
+ // clear the terminal and set the cursor to the origin
+ print('\x1b[2J\x1b[3J')
+ print('\x1b[?1049l')
+ }
+ C.SetConsoleMode(ui.ctx_ptr.stdin_handle, ui.stdin_at_startup)
+ }
+ load_title()
+ os.flush()
+}
+
+pub fn init(cfg Config) &Context {
+ mut ctx := &Context{
+ cfg: cfg
+ }
+ // get the standard input handle
+ stdin_handle := C.GetStdHandle(C.STD_INPUT_HANDLE)
+ stdout_handle := C.GetStdHandle(C.STD_OUTPUT_HANDLE)
+ if stdin_handle == C.INVALID_HANDLE_VALUE {
+ panic('could not get stdin handle')
+ }
+ // save the current input mode, to be restored on exit
+ if C.GetConsoleMode(stdin_handle, &ui.stdin_at_startup) == 0 {
+ panic('could not get stdin console mode')
+ }
+
+ // enable extended input flags (see https://stackoverflow.com/a/46802726)
+ // 0x80 == C.ENABLE_EXTENDED_FLAGS
+ if C.SetConsoleMode(stdin_handle, 0x80) == 0 {
+ panic('could not set raw input mode')
+ }
+ // enable window and mouse input events.
+ if C.SetConsoleMode(stdin_handle, C.ENABLE_WINDOW_INPUT | C.ENABLE_MOUSE_INPUT) == 0 {
+ panic('could not set raw input mode')
+ }
+ // store the current title, so restore_terminal_state can get it back
+ save_title()
+
+ if ctx.cfg.use_alternate_buffer {
+ // switch to the alternate buffer
+ print('\x1b[?1049h')
+ // clear the terminal and set the cursor to the origin
+ print('\x1b[2J\x1b[3J\x1b[1;1H')
+ }
+
+ if ctx.cfg.hide_cursor {
+ ctx.hide_cursor()
+ ctx.flush()
+ }
+
+ if ctx.cfg.window_title != '' {
+ print('\x1b]0;$ctx.cfg.window_title\x07')
+ }
+
+ unsafe {
+ x := &ui.ctx_ptr
+ *x = ctx
+ }
+ C.atexit(restore_terminal_state)
+ for code in ctx.cfg.reset {
+ os.signal_opt(code, fn (_ os.Signal) {
+ mut c := ui.ctx_ptr
+ if c != 0 {
+ c.cleanup()
+ }
+ exit(0)
+ }) or {}
+ }
+
+ ctx.stdin_handle = stdin_handle
+ ctx.stdout_handle = stdout_handle
+ return ctx
+}
+
+pub fn (mut ctx Context) run() ? {
+ frame_time := 1_000_000 / ctx.cfg.frame_rate
+ mut init_called := false
+ mut sw := time.new_stopwatch(auto_start: false)
+ mut sleep_len := 0
+ for {
+ if !init_called {
+ ctx.init()
+ init_called = true
+ }
+ if sleep_len > 0 {
+ time.sleep(sleep_len * time.microsecond)
+ }
+ if !ctx.paused {
+ sw.restart()
+ if ctx.cfg.event_fn != voidptr(0) {
+ ctx.parse_events()
+ }
+ ctx.frame()
+ sw.pause()
+ e := sw.elapsed().microseconds()
+ sleep_len = frame_time - int(e)
+ ctx.frame_count++
+ }
+ }
+}
+
+fn (mut ctx Context) parse_events() {
+ nr_events := u32(0)
+ if !C.GetNumberOfConsoleInputEvents(ctx.stdin_handle, &nr_events) {
+ panic('could not get number of events in stdin')
+ }
+ if nr_events < 1 {
+ return
+ }
+
+ // print('$nr_events | ')
+ if !C.ReadConsoleInput(ctx.stdin_handle, &ctx.read_buf[0], ui.buf_size, &nr_events) {
+ panic('could not read from stdin')
+ }
+ for i in 0 .. nr_events {
+ // print('E ')
+ match int(ctx.read_buf[i].EventType) {
+ C.KEY_EVENT {
+ e := unsafe { ctx.read_buf[i].Event.KeyEvent }
+ ch := e.wVirtualKeyCode
+ ascii := unsafe { e.uChar.AsciiChar }
+ if e.bKeyDown == 0 {
+ continue
+ }
+ // we don't handle key_up events because they don't exist on linux...
+ // see: https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
+ code := match int(ch) {
+ C.VK_BACK { KeyCode.backspace }
+ C.VK_RETURN { KeyCode.enter }
+ C.VK_PRIOR { KeyCode.page_up }
+ 14...20 { KeyCode.null }
+ C.VK_NEXT { KeyCode.page_down }
+ C.VK_END { KeyCode.end }
+ C.VK_HOME { KeyCode.home }
+ C.VK_LEFT { KeyCode.left }
+ C.VK_UP { KeyCode.up }
+ C.VK_RIGHT { KeyCode.right }
+ C.VK_DOWN { KeyCode.down }
+ C.VK_INSERT { KeyCode.insert }
+ C.VK_DELETE { KeyCode.delete }
+ 65...90 { KeyCode(ch + 32) } // letters
+ 91...93 { KeyCode.null } // special keys
+ 96...105 { KeyCode(ch - 48) } // numpad numbers
+ 112...135 { KeyCode(ch + 178) } // f1 - f24
+ else { KeyCode(ascii) }
+ }
+
+ mut modifiers := Modifiers{}
+ if e.dwControlKeyState & (0x1 | 0x2) != 0 {
+ modifiers.set(.alt)
+ }
+ if e.dwControlKeyState & (0x4 | 0x8) != 0 {
+ modifiers.set(.ctrl)
+ }
+ if e.dwControlKeyState & 0x10 != 0 {
+ modifiers.set(.shift)
+ }
+
+ mut event := &Event{
+ typ: .key_down
+ modifiers: modifiers
+ code: code
+ ascii: ascii
+ width: int(e.dwControlKeyState)
+ height: int(e.wVirtualKeyCode)
+ utf8: unsafe { e.uChar.UnicodeChar.str() }
+ }
+ ctx.event(event)
+ }
+ C.MOUSE_EVENT {
+ e := unsafe { ctx.read_buf[i].Event.MouseEvent }
+ sb_info := C.CONSOLE_SCREEN_BUFFER_INFO{}
+ if !C.GetConsoleScreenBufferInfo(ctx.stdout_handle, &sb_info) {
+ panic('could not get screenbuffer info')
+ }
+ x := e.dwMousePosition.X + 1
+ y := int(e.dwMousePosition.Y) - sb_info.srWindow.Top + 1
+ mut modifiers := Modifiers{}
+ if e.dwControlKeyState & (0x1 | 0x2) != 0 {
+ modifiers.set(.alt)
+ }
+ if e.dwControlKeyState & (0x4 | 0x8) != 0 {
+ modifiers.set(.ctrl)
+ }
+ if e.dwControlKeyState & 0x10 != 0 {
+ modifiers.set(.shift)
+ }
+ // TODO: handle capslock/numlock/etc?? events exist for those keys
+ match int(e.dwEventFlags) {
+ C.MOUSE_MOVED {
+ mut button := match int(e.dwButtonState) {
+ 0 { MouseButton.unknown }
+ 1 { MouseButton.left }
+ 2 { MouseButton.right }
+ else { MouseButton.middle }
+ }
+ typ := if e.dwButtonState == 0 {
+ if ctx.mouse_down != .unknown {
+ button = ctx.mouse_down
+ ctx.mouse_down = .unknown
+ EventType.mouse_up
+ } else {
+ EventType.mouse_move
+ }
+ } else {
+ EventType.mouse_drag
+ }
+ ctx.event(&Event{
+ typ: typ
+ x: x
+ y: y
+ button: button
+ modifiers: modifiers
+ })
+ }
+ C.MOUSE_WHEELED {
+ ctx.event(&Event{
+ typ: .mouse_scroll
+ direction: if i16(e.dwButtonState >> 16) < 0 {
+ Direction.up
+ } else {
+ Direction.down
+ }
+ x: x
+ y: y
+ modifiers: modifiers
+ })
+ }
+ 0x0008 /* C.MOUSE_HWHEELED */ {
+ ctx.event(&Event{
+ typ: .mouse_scroll
+ direction: if i16(e.dwButtonState >> 16) < 0 {
+ Direction.right
+ } else {
+ Direction.left
+ }
+ x: x
+ y: y
+ modifiers: modifiers
+ })
+ }
+ 0 /* CLICK */, C.DOUBLE_CLICK {
+ button := match int(e.dwButtonState) {
+ 0 { ctx.mouse_down }
+ 1 { MouseButton.left }
+ 2 { MouseButton.right }
+ else { MouseButton.middle }
+ }
+ ctx.mouse_down = button
+ ctx.event(&Event{
+ typ: .mouse_down
+ x: x
+ y: y
+ button: button
+ modifiers: modifiers
+ })
+ }
+ else {}
+ }
+ }
+ C.WINDOW_BUFFER_SIZE_EVENT {
+ // e := unsafe { ctx.read_buf[i].Event.WindowBufferSizeEvent }
+ sb := C.CONSOLE_SCREEN_BUFFER_INFO{}
+ if !C.GetConsoleScreenBufferInfo(ctx.stdout_handle, &sb) {
+ panic('could not get screenbuffer info')
+ }
+ w := sb.srWindow.Right - sb.srWindow.Left + 1
+ h := sb.srWindow.Bottom - sb.srWindow.Top + 1
+ utf8 := '($ctx.window_width, $ctx.window_height) -> ($w, $h)'
+ if w != ctx.window_width || h != ctx.window_height {
+ ctx.window_width, ctx.window_height = w, h
+ mut event := &Event{
+ typ: .resized
+ width: ctx.window_width
+ height: ctx.window_height
+ utf8: utf8
+ }
+ ctx.event(event)
+ }
+ }
+ // C.MENU_EVENT {
+ // e := unsafe { ctx.read_buf[i].Event.MenuEvent }
+ // }
+ // C.FOCUS_EVENT {
+ // e := unsafe { ctx.read_buf[i].Event.FocusEvent }
+ // }
+ else {}
+ }
+ }
+}
+
+[inline]
+fn save_title() {
+ // restore the previously saved terminal title
+ print('\x1b[22;0t')
+}
+
+[inline]
+fn load_title() {
+ // restore the previously saved terminal title
+ print('\x1b[23;0t')
+}
diff --git a/v_windows/v/vlib/term/ui/termios_nix.c.v b/v_windows/v/vlib/term/ui/termios_nix.c.v
new file mode 100644
index 0000000..fb5ff76
--- /dev/null
+++ b/v_windows/v/vlib/term/ui/termios_nix.c.v
@@ -0,0 +1,530 @@
+// Copyright (c) 2020-2021 Raúl Hernández. All rights reserved.
+// Use of this source code is governed by an MIT license
+// that can be found in the LICENSE file.
+module ui
+
+import os
+import time
+
+#include <termios.h>
+#include <sys/ioctl.h>
+#include <signal.h>
+
+fn C.tcgetattr(fd int, termios_p &C.termios) int
+
+fn C.tcsetattr(fd int, optional_actions int, termios_p &C.termios) int
+
+fn C.ioctl(fd int, request u64, arg voidptr) int
+
+struct C.termios {
+mut:
+ c_iflag u32
+ c_lflag u32
+ c_cc [32]byte
+}
+
+struct C.winsize {
+ ws_row u16
+ ws_col u16
+}
+
+const (
+ termios_at_startup = get_termios()
+)
+
+[inline]
+fn get_termios() C.termios {
+ mut t := C.termios{}
+ C.tcgetattr(C.STDIN_FILENO, &t)
+ return t
+}
+
+[inline]
+fn get_terminal_size() (u16, u16) {
+ winsz := C.winsize{}
+ C.ioctl(0, C.TIOCGWINSZ, &winsz)
+ return winsz.ws_row, winsz.ws_col
+}
+
+fn restore_terminal_state_signal(_ os.Signal) {
+ restore_terminal_state()
+}
+
+fn restore_terminal_state() {
+ termios_reset()
+ mut c := ctx_ptr
+ if c != 0 {
+ c.paused = true
+ load_title()
+ }
+ os.flush()
+}
+
+fn (mut ctx Context) termios_setup() ? {
+ // store the current title, so restore_terminal_state can get it back
+ save_title()
+
+ if !ctx.cfg.skip_init_checks && !(os.is_atty(C.STDIN_FILENO) != 0
+ && os.is_atty(C.STDOUT_FILENO) != 0) {
+ return error('not running under a TTY')
+ }
+
+ mut termios := get_termios()
+
+ if ctx.cfg.capture_events {
+ // Set raw input mode by unsetting ICANON and ECHO,
+ // as well as disable e.g. ctrl+c and ctrl.z
+ termios.c_iflag &= ~u32(C.IGNBRK | C.BRKINT | C.PARMRK | C.IXON)
+ termios.c_lflag &= ~u32(C.ICANON | C.ISIG | C.ECHO | C.IEXTEN | C.TOSTOP)
+ } else {
+ // Set raw input mode by unsetting ICANON and ECHO
+ termios.c_lflag &= ~u32(C.ICANON | C.ECHO)
+ }
+
+ if ctx.cfg.hide_cursor {
+ ctx.hide_cursor()
+ ctx.flush()
+ }
+
+ if ctx.cfg.window_title != '' {
+ print('\x1b]0;$ctx.cfg.window_title\x07')
+ }
+
+ if !ctx.cfg.skip_init_checks {
+ // prevent blocking during the feature detections, but allow enough time for the terminal
+ // to send back the relevant input data
+ termios.c_cc[C.VTIME] = 1
+ termios.c_cc[C.VMIN] = 0
+ C.tcsetattr(C.STDIN_FILENO, C.TCSAFLUSH, &termios)
+ // feature-test the SU spec
+ sx, sy := get_cursor_position()
+ print('$bsu$esu')
+ ex, ey := get_cursor_position()
+ if sx == ex && sy == ey {
+ // the terminal either ignored or handled the sequence properly, enable SU
+ ctx.enable_su = true
+ } else {
+ ctx.draw_line(sx, sy, ex, ey)
+ ctx.set_cursor_position(sx, sy)
+ ctx.flush()
+ }
+ // feature-test rgb (truecolor) support
+ ctx.enable_rgb = supports_truecolor()
+ }
+ // Prevent stdin from blocking by making its read time 0
+ termios.c_cc[C.VTIME] = 0
+ termios.c_cc[C.VMIN] = 0
+ C.tcsetattr(C.STDIN_FILENO, C.TCSAFLUSH, &termios)
+ // enable mouse input
+ print('\x1b[?1003h\x1b[?1006h')
+ if ctx.cfg.use_alternate_buffer {
+ // switch to the alternate buffer
+ print('\x1b[?1049h')
+ // clear the terminal and set the cursor to the origin
+ print('\x1b[2J\x1b[3J\x1b[1;1H')
+ }
+ ctx.window_height, ctx.window_width = get_terminal_size()
+
+ // Reset console on exit
+ C.atexit(restore_terminal_state)
+ os.signal_opt(.tstp, restore_terminal_state_signal) or {}
+ os.signal_opt(.cont, fn (_ os.Signal) {
+ mut c := ctx_ptr
+ if c != 0 {
+ c.termios_setup() or { panic(err) }
+ c.window_height, c.window_width = get_terminal_size()
+ mut event := &Event{
+ typ: .resized
+ width: c.window_width
+ height: c.window_height
+ }
+ c.paused = false
+ c.event(event)
+ }
+ }) or {}
+ for code in ctx.cfg.reset {
+ os.signal_opt(code, fn (_ os.Signal) {
+ mut c := ctx_ptr
+ if c != 0 {
+ c.cleanup()
+ }
+ exit(0)
+ }) or {}
+ }
+
+ os.signal_opt(.winch, fn (_ os.Signal) {
+ mut c := ctx_ptr
+ if c != 0 {
+ c.window_height, c.window_width = get_terminal_size()
+
+ mut event := &Event{
+ typ: .resized
+ width: c.window_width
+ height: c.window_height
+ }
+ c.event(event)
+ }
+ }) or {}
+
+ os.flush()
+}
+
+fn get_cursor_position() (int, int) {
+ print('\033[6n')
+ mut s := ''
+ unsafe {
+ buf := malloc_noscan(25)
+ len := C.read(C.STDIN_FILENO, buf, 24)
+ buf[len] = 0
+ s = tos(buf, len)
+ }
+ a := s[2..].split(';')
+ if a.len != 2 {
+ return -1, -1
+ }
+ return a[0].int(), a[1].int()
+}
+
+fn supports_truecolor() bool {
+ // faster/simpler, but less reliable, check
+ if os.getenv('COLORTERM') in ['truecolor', '24bit'] {
+ return true
+ }
+ // set the bg color to some arbirtrary value (#010203), assumed not to be the default
+ print('\x1b[48:2:1:2:3m')
+ // andquery the current color
+ print('\x1bP\$qm\x1b\\')
+ mut s := ''
+ unsafe {
+ buf := malloc_noscan(25)
+ len := C.read(C.STDIN_FILENO, buf, 24)
+ buf[len] = 0
+ s = tos(buf, len)
+ }
+ return s.contains('1:2:3')
+}
+
+fn termios_reset() {
+ // C.TCSANOW ??
+ C.tcsetattr(C.STDIN_FILENO, C.TCSAFLUSH, &ui.termios_at_startup)
+ print('\x1b[?1003l\x1b[?1006l\x1b[?25h')
+ c := ctx_ptr
+ if c != 0 && c.cfg.use_alternate_buffer {
+ print('\x1b[?1049l')
+ }
+ os.flush()
+}
+
+///////////////////////////////////////////
+// TODO: do multiple sleep/read cycles, rather than one big one
+fn (mut ctx Context) termios_loop() {
+ frame_time := 1_000_000 / ctx.cfg.frame_rate
+ mut init_called := false
+ mut sw := time.new_stopwatch(auto_start: false)
+ mut sleep_len := 0
+ for {
+ if !init_called {
+ ctx.init()
+ init_called = true
+ }
+ // println('SLEEPING: $sleep_len')
+ if sleep_len > 0 {
+ time.sleep(sleep_len * time.microsecond)
+ }
+ if !ctx.paused {
+ sw.restart()
+ if ctx.cfg.event_fn != voidptr(0) {
+ unsafe {
+ len := C.read(C.STDIN_FILENO, &byte(ctx.read_buf.data) + ctx.read_buf.len,
+ ctx.read_buf.cap - ctx.read_buf.len)
+ ctx.resize_arr(ctx.read_buf.len + len)
+ }
+ if ctx.read_buf.len > 0 {
+ ctx.parse_events()
+ }
+ }
+ ctx.frame()
+ sw.pause()
+ e := sw.elapsed().microseconds()
+ sleep_len = frame_time - int(e)
+
+ ctx.frame_count++
+ }
+ }
+}
+
+fn (mut ctx Context) parse_events() {
+ // Stop this from getting stuck in rare cases where something isn't parsed correctly
+ mut nr_iters := 0
+ for ctx.read_buf.len > 0 {
+ nr_iters++
+ if nr_iters > 100 {
+ ctx.shift(1)
+ }
+ mut event := &Event(0)
+ if ctx.read_buf[0] == 0x1b {
+ e, len := escape_sequence(ctx.read_buf.bytestr())
+ event = e
+ ctx.shift(len)
+ } else {
+ event = single_char(ctx.read_buf.bytestr())
+ ctx.shift(1)
+ }
+ if event != 0 {
+ ctx.event(event)
+ nr_iters = 0
+ }
+ }
+}
+
+fn single_char(buf string) &Event {
+ ch := buf[0]
+
+ mut event := &Event{
+ typ: .key_down
+ ascii: ch
+ code: KeyCode(ch)
+ utf8: buf
+ }
+
+ match ch {
+ // special handling for `ctrl + letter`
+ // TODO: Fix assoc in V and remove this workaround :/
+ // 1 ... 26 { event = Event{ ...event, code: KeyCode(96 | ch), modifiers: .ctrl } }
+ // 65 ... 90 { event = Event{ ...event, code: KeyCode(32 | ch), modifiers: .shift } }
+ // The bit `or`s here are really just `+`'s, just written in this way for a tiny performance improvement
+ // don't treat tab, enter as ctrl+i, ctrl+j
+ 1...8, 11...26 {
+ event = &Event{
+ typ: event.typ
+ ascii: event.ascii
+ utf8: event.utf8
+ code: KeyCode(96 | ch)
+ modifiers: .ctrl
+ }
+ }
+ 65...90 {
+ event = &Event{
+ typ: event.typ
+ ascii: event.ascii
+ utf8: event.utf8
+ code: KeyCode(32 | ch)
+ modifiers: .shift
+ }
+ }
+ else {}
+ }
+
+ return event
+}
+
+// Gets an entire, independent escape sequence from the buffer
+// Normally, this just means reading until the first letter, but there are some exceptions...
+fn escape_end(buf string) int {
+ mut i := 0
+ for {
+ if i + 1 == buf.len {
+ return buf.len
+ }
+
+ if buf[i].is_letter() || buf[i] == `~` {
+ if buf[i] == `O` && i + 2 <= buf.len {
+ n := buf[i + 1]
+ if (n >= `A` && n <= `D`) || (n >= `P` && n <= `S`) || n == `F` || n == `H` {
+ return i + 2
+ }
+ }
+ return i + 1
+ // escape hatch to avoid potential issues/crashes, although ideally this should never eval to true
+ } else if buf[i + 1] == 0x1b {
+ return i + 1
+ }
+ i++
+ }
+ // this point should be unreachable
+ assert false
+ return 0
+}
+
+fn escape_sequence(buf_ string) (&Event, int) {
+ end := escape_end(buf_)
+ single := buf_[..end] // read until the end of the sequence
+ buf := single[1..] // skip the escape character
+
+ if buf.len == 0 {
+ return &Event{
+ typ: .key_down
+ ascii: 27
+ code: .escape
+ utf8: single
+ }, 1
+ }
+
+ if buf.len == 1 {
+ c := single_char(buf)
+ mut modifiers := c.modifiers
+ modifiers.set(.alt)
+ return &Event{
+ typ: c.typ
+ ascii: c.ascii
+ code: c.code
+ utf8: single
+ modifiers: modifiers
+ }, 2
+ }
+ // ----------------
+ // Mouse events
+ // ----------------
+ // Documentation: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
+ if buf.len > 2 && buf[1] == `<` {
+ split := buf[2..].split(';')
+ if split.len < 3 {
+ return &Event(0), 0
+ }
+
+ typ, x, y := split[0].int(), split[1].int(), split[2].int()
+ lo := typ & 0b00011
+ hi := typ & 0b11100
+
+ mut modifiers := Modifiers{}
+ if hi & 4 != 0 {
+ modifiers.set(.shift)
+ }
+ if hi & 8 != 0 {
+ modifiers.set(.alt)
+ }
+ if hi & 16 != 0 {
+ modifiers.set(.ctrl)
+ }
+
+ match typ {
+ 0...31 {
+ last := buf[buf.len - 1]
+ button := if lo < 3 { MouseButton(lo + 1) } else { MouseButton.unknown }
+ event := if last == `m` || lo == 3 {
+ EventType.mouse_up
+ } else {
+ EventType.mouse_down
+ }
+
+ return &Event{
+ typ: event
+ x: x
+ y: y
+ button: button
+ modifiers: modifiers
+ utf8: single
+ }, end
+ }
+ 32...63 {
+ button, event := if lo < 3 {
+ MouseButton(lo + 1), EventType.mouse_drag
+ } else {
+ MouseButton.unknown, EventType.mouse_move
+ }
+
+ return &Event{
+ typ: event
+ x: x
+ y: y
+ button: button
+ modifiers: modifiers
+ utf8: single
+ }, end
+ }
+ 64...95 {
+ direction := if typ & 1 == 0 { Direction.down } else { Direction.up }
+ return &Event{
+ typ: .mouse_scroll
+ x: x
+ y: y
+ direction: direction
+ modifiers: modifiers
+ utf8: single
+ }, end
+ }
+ else {
+ return &Event{
+ typ: .unknown
+ utf8: single
+ }, end
+ }
+ }
+ }
+ // ----------------------------
+ // Special key combinations
+ // ----------------------------
+
+ mut code := KeyCode.null
+ mut modifiers := Modifiers{}
+ match buf {
+ '[A', 'OA' { code = .up }
+ '[B', 'OB' { code = .down }
+ '[C', 'OC' { code = .right }
+ '[D', 'OD' { code = .left }
+ '[5~', '[[5~' { code = .page_up }
+ '[6~', '[[6~' { code = .page_down }
+ '[F', 'OF', '[4~', '[[8~' { code = .end }
+ '[H', 'OH', '[1~', '[[7~' { code = .home }
+ '[2~' { code = .insert }
+ '[3~' { code = .delete }
+ 'OP', '[11~' { code = .f1 }
+ 'OQ', '[12~' { code = .f2 }
+ 'OR', '[13~' { code = .f3 }
+ 'OS', '[14~' { code = .f4 }
+ '[15~' { code = .f5 }
+ '[17~' { code = .f6 }
+ '[18~' { code = .f7 }
+ '[19~' { code = .f8 }
+ '[20~' { code = .f9 }
+ '[21~' { code = .f10 }
+ '[23~' { code = .f11 }
+ '[24~' { code = .f12 }
+ else {}
+ }
+
+ if buf == '[Z' {
+ code = .tab
+ modifiers.set(.shift)
+ }
+
+ if buf.len == 5 && buf[0] == `[` && buf[1].is_digit() && buf[2] == `;` {
+ match buf[3] {
+ `2` { modifiers = .shift }
+ `3` { modifiers = .alt }
+ `4` { modifiers = .shift | .alt }
+ `5` { modifiers = .ctrl }
+ `6` { modifiers = .ctrl | .shift }
+ `7` { modifiers = .ctrl | .alt }
+ `8` { modifiers = .ctrl | .alt | .shift }
+ else {}
+ }
+
+ if buf[1] == `1` {
+ match buf[4] {
+ `A` { code = KeyCode.up }
+ `B` { code = KeyCode.down }
+ `C` { code = KeyCode.right }
+ `D` { code = KeyCode.left }
+ `F` { code = KeyCode.end }
+ `H` { code = KeyCode.home }
+ `P` { code = KeyCode.f1 }
+ `Q` { code = KeyCode.f2 }
+ `R` { code = KeyCode.f3 }
+ `S` { code = KeyCode.f4 }
+ else {}
+ }
+ } else if buf[1] == `5` {
+ code = KeyCode.page_up
+ } else if buf[1] == `6` {
+ code = KeyCode.page_down
+ }
+ }
+
+ return &Event{
+ typ: .key_down
+ code: code
+ utf8: single
+ modifiers: modifiers
+ }, end
+}
diff --git a/v_windows/v/vlib/term/ui/ui.v b/v_windows/v/vlib/term/ui/ui.v
new file mode 100644
index 0000000..6ba3d7c
--- /dev/null
+++ b/v_windows/v/vlib/term/ui/ui.v
@@ -0,0 +1,256 @@
+// Copyright (c) 2020-2021 Raúl Hernández. All rights reserved.
+// Use of this source code is governed by an MIT license
+// that can be found in the LICENSE file.
+module ui
+
+import strings
+
+pub struct Color {
+pub:
+ r byte
+ g byte
+ b byte
+}
+
+pub fn (c Color) hex() string {
+ return '#$c.r.hex()$c.g.hex()$c.b.hex()'
+}
+
+// Synchronized Updates spec, designed to avoid tearing during renders
+// https://gitlab.com/gnachman/iterm2/-/wikis/synchronized-updates-spec
+const (
+ bsu = '\x1bP=1s\x1b\\'
+ esu = '\x1bP=2s\x1b\\'
+)
+
+// write puts the string `s` into the print buffer.
+[inline]
+pub fn (mut ctx Context) write(s string) {
+ if s == '' {
+ return
+ }
+ unsafe { ctx.print_buf.push_many(s.str, s.len) }
+}
+
+// flush displays the accumulated print buffer to the screen.
+[inline]
+pub fn (mut ctx Context) flush() {
+ // TODO: Diff the previous frame against this one, and only render things that changed?
+ if !ctx.enable_su {
+ C.write(1, ctx.print_buf.data, ctx.print_buf.len)
+ } else {
+ C.write(1, ui.bsu.str, ui.bsu.len)
+ C.write(1, ctx.print_buf.data, ctx.print_buf.len)
+ C.write(1, ui.esu.str, ui.esu.len)
+ }
+ ctx.print_buf.clear()
+}
+
+// bold sets the character state to bold.
+[inline]
+pub fn (mut ctx Context) bold() {
+ ctx.write('\x1b[1m')
+}
+
+// set_cursor_position positions the cusor at the given coordinates `x`,`y`.
+[inline]
+pub fn (mut ctx Context) set_cursor_position(x int, y int) {
+ ctx.write('\x1b[$y;${x}H')
+}
+
+// show_cursor will make the cursor appear if it is not already visible
+[inline]
+pub fn (mut ctx Context) show_cursor() {
+ ctx.write('\x1b[?25h')
+}
+
+// hide_cursor will make the cursor invisible
+[inline]
+pub fn (mut ctx Context) hide_cursor() {
+ ctx.write('\x1b[?25l')
+}
+
+// set_color sets the current foreground color used by any succeeding `draw_*` calls.
+[inline]
+pub fn (mut ctx Context) set_color(c Color) {
+ if ctx.enable_rgb {
+ ctx.write('\x1b[38;2;${int(c.r)};${int(c.g)};${int(c.b)}m')
+ } else {
+ ctx.write('\x1b[38;5;${rgb2ansi(c.r, c.g, c.b)}m')
+ }
+}
+
+// set_color sets the current background color used by any succeeding `draw_*` calls.
+[inline]
+pub fn (mut ctx Context) set_bg_color(c Color) {
+ if ctx.enable_rgb {
+ ctx.write('\x1b[48;2;${int(c.r)};${int(c.g)};${int(c.b)}m')
+ } else {
+ ctx.write('\x1b[48;5;${rgb2ansi(c.r, c.g, c.b)}m')
+ }
+}
+
+// reset_color sets the current foreground color back to it's default value.
+[inline]
+pub fn (mut ctx Context) reset_color() {
+ ctx.write('\x1b[39m')
+}
+
+// reset_bg_color sets the current background color back to it's default value.
+[inline]
+pub fn (mut ctx Context) reset_bg_color() {
+ ctx.write('\x1b[49m')
+}
+
+// reset restores the state of all colors and text formats back to their default values.
+[inline]
+pub fn (mut ctx Context) reset() {
+ ctx.write('\x1b[0m')
+}
+
+[inline]
+pub fn (mut ctx Context) clear() {
+ ctx.write('\x1b[2J\x1b[3J')
+}
+
+// set_window_title sets the string `s` as the window title.
+[inline]
+pub fn (mut ctx Context) set_window_title(s string) {
+ print('\x1b]0;$s\x07')
+}
+
+// draw_point draws a point at position `x`,`y`.
+[inline]
+pub fn (mut ctx Context) draw_point(x int, y int) {
+ ctx.set_cursor_position(x, y)
+ ctx.write(' ')
+}
+
+// draw_text draws the string `s`, starting from position `x`,`y`.
+[inline]
+pub fn (mut ctx Context) draw_text(x int, y int, s string) {
+ ctx.set_cursor_position(x, y)
+ ctx.write(s)
+}
+
+// draw_line draws a line segment, starting at point `x`,`y`, and ending at point `x2`,`y2`.
+pub fn (mut ctx Context) draw_line(x int, y int, x2 int, y2 int) {
+ min_x, min_y := if x < x2 { x } else { x2 }, if y < y2 { y } else { y2 }
+ max_x, _ := if x > x2 { x } else { x2 }, if y > y2 { y } else { y2 }
+ if y == y2 {
+ // Horizontal line, performance improvement
+ ctx.set_cursor_position(min_x, min_y)
+ ctx.write(strings.repeat(` `, max_x + 1 - min_x))
+ return
+ }
+ // Draw the various points with Bresenham's line algorithm:
+ mut x0, x1 := x, x2
+ mut y0, y1 := y, y2
+ sx := if x0 < x1 { 1 } else { -1 }
+ sy := if y0 < y1 { 1 } else { -1 }
+ dx := if x0 < x1 { x1 - x0 } else { x0 - x1 }
+ dy := if y0 < y1 { y0 - y1 } else { y1 - y0 } // reversed
+ mut err := dx + dy
+ for {
+ // res << Segment{ x0, y0 }
+ ctx.draw_point(x0, y0)
+ if x0 == x1 && y0 == y1 {
+ break
+ }
+ e2 := 2 * err
+ if e2 >= dy {
+ err += dy
+ x0 += sx
+ }
+ if e2 <= dx {
+ err += dx
+ y0 += sy
+ }
+ }
+}
+
+// draw_dashed_line draws a dashed line segment, starting at point `x`,`y`, and ending at point `x2`,`y2`.
+pub fn (mut ctx Context) draw_dashed_line(x int, y int, x2 int, y2 int) {
+ // Draw the various points with Bresenham's line algorithm:
+ mut x0, x1 := x, x2
+ mut y0, y1 := y, y2
+ sx := if x0 < x1 { 1 } else { -1 }
+ sy := if y0 < y1 { 1 } else { -1 }
+ dx := if x0 < x1 { x1 - x0 } else { x0 - x1 }
+ dy := if y0 < y1 { y0 - y1 } else { y1 - y0 } // reversed
+ mut err := dx + dy
+ mut i := 0
+ for {
+ if i % 2 == 0 {
+ ctx.draw_point(x0, y0)
+ }
+ if x0 == x1 && y0 == y1 {
+ break
+ }
+ e2 := 2 * err
+ if e2 >= dy {
+ err += dy
+ x0 += sx
+ }
+ if e2 <= dx {
+ err += dx
+ y0 += sy
+ }
+ i++
+ }
+}
+
+// draw_rect draws a rectangle, starting at top left `x`,`y`, and ending at bottom right `x2`,`y2`.
+pub fn (mut ctx Context) draw_rect(x int, y int, x2 int, y2 int) {
+ if y == y2 || x == x2 {
+ ctx.draw_line(x, y, x2, y2)
+ return
+ }
+ min_y, max_y := if y < y2 { y, y2 } else { y2, y }
+ for y_pos in min_y .. max_y + 1 {
+ ctx.draw_line(x, y_pos, x2, y_pos)
+ }
+}
+
+// draw_empty_dashed_rect draws a rectangle with dashed lines, starting at top left `x`,`y`, and ending at bottom right `x2`,`y2`.
+pub fn (mut ctx Context) draw_empty_dashed_rect(x int, y int, x2 int, y2 int) {
+ if y == y2 || x == x2 {
+ ctx.draw_dashed_line(x, y, x2, y2)
+ return
+ }
+
+ min_x, max_x := if x < x2 { x, x2 } else { x2, x }
+ min_y, max_y := if y < y2 { y, y2 } else { y2, y }
+
+ ctx.draw_dashed_line(min_x, min_y, max_x, min_y)
+ ctx.draw_dashed_line(min_x, min_y, min_x, max_y)
+ if (max_y - min_y) & 1 == 0 {
+ ctx.draw_dashed_line(min_x, max_y, max_x, max_y)
+ } else {
+ ctx.draw_dashed_line(min_x + 1, max_y, max_x, max_y)
+ }
+ if (max_x - min_x) & 1 == 0 {
+ ctx.draw_dashed_line(max_x, min_y, max_x, max_y)
+ } else {
+ ctx.draw_dashed_line(max_x, min_y + 1, max_x, max_y)
+ }
+}
+
+// draw_empty_rect draws a rectangle with no fill, starting at top left `x`,`y`, and ending at bottom right `x2`,`y2`.
+pub fn (mut ctx Context) draw_empty_rect(x int, y int, x2 int, y2 int) {
+ if y == y2 || x == x2 {
+ ctx.draw_line(x, y, x2, y2)
+ return
+ }
+ ctx.draw_line(x, y, x2, y)
+ ctx.draw_line(x, y2, x2, y2)
+ ctx.draw_line(x, y, x, y2)
+ ctx.draw_line(x2, y, x2, y2)
+}
+
+// horizontal_separator draws a horizontal separator, spanning the width of the screen.
+[inline]
+pub fn (mut ctx Context) horizontal_separator(y int) {
+ ctx.set_cursor_position(0, y)
+ ctx.write(strings.repeat(`-`, ctx.window_width)) // /* `⎽` */
+}