Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
@@ -0,0 +1,7 @@
|
||||
import term.ui
|
||||
import term
|
||||
|
||||
fn test_term_and_term_ui_can_compile_together() {
|
||||
println('${term.bold('hello')} world ${ui.color_table[0]}')
|
||||
assert true
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import term
|
||||
import term.ui
|
||||
|
||||
fn test_term_and_term_ui_can_compile_together() {
|
||||
println('${term.bold('hello')} world ${ui.color_table[0]}')
|
||||
assert true
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
## `term.ui`
|
||||
|
||||
A V module for designing terminal UI apps
|
||||
|
||||
#### Quickstart
|
||||
|
||||
```v
|
||||
import term.ui as tui
|
||||
|
||||
struct App {
|
||||
mut:
|
||||
tui &tui.Context = unsafe { nil }
|
||||
}
|
||||
|
||||
fn event(e &tui.Event, x voidptr) {
|
||||
if e.typ == .key_down && e.code == .escape {
|
||||
exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
fn frame(x voidptr) {
|
||||
mut app := unsafe { &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()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
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.
|
||||
- `mouse_enabled bool` - enables terminal mouse tracking for `mouse_*` events.
|
||||
Leave this off unless your app actually handles mouse input, so the terminal can still use the
|
||||
mouse for regular text selection and copy/paste.
|
||||
- `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: `term.ui` emits `key_up` on Windows and on terminals that support the kitty keyboard
|
||||
protocol. Legacy terminal input still only provides `key_down`, so `key_up` is not
|
||||
available everywhere.
|
||||
@@ -0,0 +1,89 @@
|
||||
// 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]!
|
||||
|
||||
pub const color_table = init_color_table()
|
||||
|
||||
@[direct_array_access]
|
||||
fn init_color_table() []u32 {
|
||||
mut color_table_ := []u32{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 := value_range[(i / 36) % 6]
|
||||
g := value_range[(i / 6) % 6]
|
||||
b := value_range[i % 6]
|
||||
color_table_[i + 16] = ((u32(r) << 16) & 0xffffff) + ((u32(g) << 8) & 0xffff) +
|
||||
(u32(b) & 0xff)
|
||||
}
|
||||
// grayscale
|
||||
for i in 0 .. 24 {
|
||||
r := 8 + (i * 10)
|
||||
color_table_[i + 232] = ((u32(r) << 16) & 0xffffff) + ((u32(r) << 8) & 0xffff) +
|
||||
(u32(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 := (u32(r) << 16) + (u32(g) << 8) + u32(b)
|
||||
// lookup extended colors only, coz non-extended can be changed by users.
|
||||
for i in 16 .. 256 {
|
||||
if 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
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
module ui
|
||||
|
||||
pub 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]
|
||||
pub struct C.INPUT_RECORD {
|
||||
EventType u16
|
||||
Event C.Event
|
||||
}
|
||||
|
||||
pub union C.uChar {
|
||||
mut:
|
||||
UnicodeChar rune
|
||||
AsciiChar u8
|
||||
}
|
||||
|
||||
@[typedef]
|
||||
pub struct C.KEY_EVENT_RECORD {
|
||||
bKeyDown int
|
||||
wRepeatCount u16
|
||||
wVirtualKeyCode u16
|
||||
wVirtualScanCode u16
|
||||
uChar C.uChar
|
||||
dwControlKeyState u32
|
||||
}
|
||||
|
||||
@[typedef]
|
||||
pub struct C.MOUSE_EVENT_RECORD {
|
||||
dwMousePosition C.COORD
|
||||
dwButtonState u32
|
||||
dwControlKeyState u32
|
||||
dwEventFlags u32
|
||||
}
|
||||
|
||||
@[typedef]
|
||||
pub struct C.WINDOW_BUFFER_SIZE_RECORD {
|
||||
dwSize C.COORD
|
||||
}
|
||||
|
||||
@[typedef]
|
||||
pub struct C.MENU_EVENT_RECORD {
|
||||
dwCommandId u32
|
||||
}
|
||||
|
||||
@[typedef]
|
||||
pub struct C.FOCUS_EVENT_RECORD {
|
||||
bSetFocus int
|
||||
}
|
||||
|
||||
@[typedef]
|
||||
pub struct C.COORD {
|
||||
mut:
|
||||
X i16
|
||||
Y i16
|
||||
}
|
||||
|
||||
@[typedef]
|
||||
pub struct C.SMALL_RECT {
|
||||
mut:
|
||||
Left u16
|
||||
Top u16
|
||||
Right u16
|
||||
Bottom u16
|
||||
}
|
||||
|
||||
@[typedef]
|
||||
pub struct C.CONSOLE_SCREEN_BUFFER_INFO {
|
||||
mut:
|
||||
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
|
||||
@@ -0,0 +1,268 @@
|
||||
// Copyright (c) 2020-2024 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
|
||||
key_up
|
||||
resized
|
||||
}
|
||||
|
||||
@[flag]
|
||||
pub enum Modifiers {
|
||||
ctrl
|
||||
shift
|
||||
alt
|
||||
}
|
||||
|
||||
struct TerminalCapabilities {
|
||||
enable_ansi256 bool = true
|
||||
supports_alternate_buffer bool = true
|
||||
supports_sgr_mouse bool = true
|
||||
supports_sync_updates bool = true
|
||||
supports_window_title bool = true
|
||||
}
|
||||
|
||||
fn terminal_capabilities_for(term_name string) TerminalCapabilities {
|
||||
if term_name == 'linux' {
|
||||
return TerminalCapabilities{
|
||||
enable_ansi256: false
|
||||
supports_alternate_buffer: false
|
||||
supports_sgr_mouse: false
|
||||
supports_sync_updates: false
|
||||
supports_window_title: false
|
||||
}
|
||||
}
|
||||
return TerminalCapabilities{}
|
||||
}
|
||||
|
||||
fn current_terminal_capabilities() TerminalCapabilities {
|
||||
return terminal_capabilities_for(os.getenv('TERM'))
|
||||
}
|
||||
|
||||
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 u8
|
||||
utf8 string
|
||||
// Resized event info
|
||||
width int
|
||||
height int
|
||||
}
|
||||
|
||||
pub struct Context {
|
||||
ExtraContext // contains fields specific to an implementation
|
||||
pub:
|
||||
cfg Config // the initial configuration, passed to ui.init()
|
||||
mut:
|
||||
print_buf []u8
|
||||
paused bool
|
||||
enable_su bool
|
||||
enable_rgb bool
|
||||
enable_ansi256 bool = true
|
||||
supports_alternate_buffer bool = true
|
||||
supports_sgr_mouse bool = true
|
||||
supports_sync_updates bool = true
|
||||
supports_window_title bool = true
|
||||
pub mut:
|
||||
frame_count u64
|
||||
window_width int
|
||||
window_height int
|
||||
}
|
||||
|
||||
pub struct Config {
|
||||
pub:
|
||||
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
|
||||
mouse_enabled 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() {
|
||||
f := ctx.cfg.init_fn or { return }
|
||||
f(ctx.cfg.user_data)
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn (ctx &Context) frame() {
|
||||
f := ctx.cfg.frame_fn or { return }
|
||||
f(ctx.cfg.user_data)
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn (ctx &Context) cleanup() {
|
||||
f := ctx.cfg.cleanup_fn or { return }
|
||||
f(ctx.cfg.user_data)
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn (ctx &Context) fail(error string) {
|
||||
f := ctx.cfg.fail_fn or { return }
|
||||
f(error)
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn (ctx &Context) event(event &Event) {
|
||||
f := ctx.cfg.event_fn or { return }
|
||||
f(event, ctx.cfg.user_data)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) 2020-2024 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.
|
||||
@[has_globals]
|
||||
module ui
|
||||
|
||||
struct ExtraContext {
|
||||
mut:
|
||||
read_buf []u8
|
||||
// read_all_bytes causes all the raw bytes to be read as one event unit.
|
||||
// This is cruicial for UTF-8 support since Unicode codepoints can span several bytes.
|
||||
read_all_bytes bool = true
|
||||
}
|
||||
|
||||
__global ctx_ptr = &Context(unsafe { nil })
|
||||
|
||||
// init initializes the terminal console with Config `cfg`.
|
||||
pub fn init(cfg Config) &Context {
|
||||
caps := current_terminal_capabilities()
|
||||
mut ctx := &Context{
|
||||
cfg: cfg
|
||||
enable_ansi256: caps.enable_ansi256
|
||||
supports_alternate_buffer: caps.supports_alternate_buffer
|
||||
supports_sgr_mouse: caps.supports_sgr_mouse
|
||||
supports_sync_updates: caps.supports_sync_updates
|
||||
supports_window_title: caps.supports_window_title
|
||||
}
|
||||
ctx.read_buf = []u8{cap: cfg.buffer_size}
|
||||
|
||||
ctx_ptr = ctx
|
||||
return ctx
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn save_title() {
|
||||
mut ctx := ctx_ptr
|
||||
if unsafe { ctx == 0 } || !ctx.supports_window_title {
|
||||
return
|
||||
}
|
||||
// restore the previously saved terminal title
|
||||
print('\x1b[22;0t')
|
||||
flush_stdout()
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn load_title() {
|
||||
mut ctx := ctx_ptr
|
||||
if unsafe { ctx == 0 } || !ctx.supports_window_title {
|
||||
return
|
||||
}
|
||||
// restore the previously saved terminal title
|
||||
print('\x1b[23;0t')
|
||||
flush_stdout()
|
||||
}
|
||||
|
||||
// run sets up and starts the terminal.
|
||||
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() or { panic(err) }
|
||||
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, &u8(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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
module ui
|
||||
|
||||
fn test_escape_sequence_parses_csi_u_plain_key() {
|
||||
seq := '\x1b[97;1u'
|
||||
event, len := escape_sequence(seq)
|
||||
assert len == seq.len
|
||||
assert event.typ == .key_down
|
||||
assert event.code == .a
|
||||
assert event.modifiers.is_empty()
|
||||
assert event.utf8 == seq
|
||||
}
|
||||
|
||||
fn test_escape_sequence_parses_csi_u_plain_key_without_modifier_param() {
|
||||
seq := '\x1b[27u'
|
||||
event, len := escape_sequence(seq)
|
||||
assert len == seq.len
|
||||
assert event.typ == .key_down
|
||||
assert event.code == .escape
|
||||
assert event.modifiers.is_empty()
|
||||
assert event.utf8 == seq
|
||||
}
|
||||
|
||||
fn test_escape_sequence_parses_csi_u_modified_key() {
|
||||
seq := '\x1b[97;5u'
|
||||
event, len := escape_sequence(seq)
|
||||
assert len == seq.len
|
||||
assert event.typ == .key_down
|
||||
assert event.code == .a
|
||||
assert event.modifiers.has(.ctrl)
|
||||
assert !event.modifiers.has(.shift)
|
||||
assert !event.modifiers.has(.alt)
|
||||
assert event.utf8 == seq
|
||||
}
|
||||
|
||||
fn test_escape_sequence_parses_csi_u_key_release() {
|
||||
seq := '\x1b[97;1:3u'
|
||||
event, len := escape_sequence(seq)
|
||||
assert len == seq.len
|
||||
assert event.typ == .key_up
|
||||
assert event.code == .a
|
||||
assert event.modifiers.is_empty()
|
||||
assert event.utf8 == seq
|
||||
}
|
||||
|
||||
fn test_escape_sequence_parses_csi_u_key_repeat_with_text() {
|
||||
seq := '\x1b[97;1:2;97u'
|
||||
event, len := escape_sequence(seq)
|
||||
assert len == seq.len
|
||||
assert event.typ == .key_down
|
||||
assert event.code == .a
|
||||
assert event.ascii == `a`
|
||||
assert event.utf8 == 'a'
|
||||
}
|
||||
|
||||
fn test_escape_sequence_parses_csi_u_shifted_text_with_associated_text() {
|
||||
seq := '\x1b[97;2;65u'
|
||||
event, len := escape_sequence(seq)
|
||||
assert len == seq.len
|
||||
assert event.typ == .key_down
|
||||
assert event.code == .a
|
||||
assert event.ascii == `A`
|
||||
assert event.modifiers == .shift
|
||||
assert event.utf8 == 'A'
|
||||
}
|
||||
|
||||
fn test_escape_sequence_parses_csi_u_enter_key() {
|
||||
seq := '\x1b[13;1u'
|
||||
event, len := escape_sequence(seq)
|
||||
assert len == seq.len
|
||||
assert event.typ == .key_down
|
||||
assert event.code == .enter
|
||||
assert event.utf8 == seq
|
||||
}
|
||||
|
||||
fn test_escape_sequence_parses_modify_other_keys_sequence() {
|
||||
seq := '\x1b[27;3;97~'
|
||||
event, len := escape_sequence(seq)
|
||||
assert len == seq.len
|
||||
assert event.typ == .key_down
|
||||
assert event.code == .a
|
||||
assert event.modifiers == .alt
|
||||
assert event.utf8 == seq
|
||||
}
|
||||
|
||||
fn test_escape_sequence_parses_modified_special_key_sequence() {
|
||||
seq := '\x1b[3;5~'
|
||||
event, len := escape_sequence(seq)
|
||||
assert len == seq.len
|
||||
assert event.typ == .key_down
|
||||
assert event.code == .delete
|
||||
assert event.modifiers == .ctrl
|
||||
assert event.utf8 == seq
|
||||
}
|
||||
|
||||
fn test_escape_sequence_parses_modified_special_key_release_sequence() {
|
||||
seq := '\x1b[1;5:3A'
|
||||
event, len := escape_sequence(seq)
|
||||
assert len == seq.len
|
||||
assert event.typ == .key_up
|
||||
assert event.code == .up
|
||||
assert event.modifiers == .ctrl
|
||||
assert event.utf8 == seq
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
// Copyright (c) 2020-2024 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.
|
||||
@[has_globals]
|
||||
module ui
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
const buf_size = 64
|
||||
|
||||
__global ctx_ptr = &Context(unsafe { nil })
|
||||
|
||||
__global 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 unsafe { ctx_ptr != 0 } {
|
||||
if ctx_ptr.cfg.use_alternate_buffer {
|
||||
// clear the terminal and set the cursor to the origin
|
||||
print('\x1b[2J\x1b[3J')
|
||||
print('\x1b[?1049l')
|
||||
flush_stdout()
|
||||
}
|
||||
C.SetConsoleMode(ctx_ptr.stdin_handle, stdin_at_startup)
|
||||
}
|
||||
load_title()
|
||||
os.flush()
|
||||
}
|
||||
|
||||
// init initializes the context of a windows console given the `cfg`.
|
||||
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, &stdin_at_startup) {
|
||||
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) {
|
||||
panic('could not set raw input mode')
|
||||
}
|
||||
mut input_mode := u32(C.ENABLE_WINDOW_INPUT)
|
||||
if ctx.cfg.mouse_enabled {
|
||||
input_mode |= u32(C.ENABLE_MOUSE_INPUT)
|
||||
}
|
||||
// enable window input and optionally mouse input events.
|
||||
if !C.SetConsoleMode(stdin_handle, input_mode) {
|
||||
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')
|
||||
flush_stdout()
|
||||
}
|
||||
|
||||
if ctx.cfg.hide_cursor {
|
||||
ctx.hide_cursor()
|
||||
ctx.flush()
|
||||
}
|
||||
|
||||
if ctx.cfg.window_title != '' {
|
||||
print('\x1b]0;${ctx.cfg.window_title}\x07')
|
||||
flush_stdout()
|
||||
}
|
||||
|
||||
ctx_ptr = ctx
|
||||
at_exit(restore_terminal_state) or {}
|
||||
for code in ctx.cfg.reset {
|
||||
os.signal_opt(code, fn (_ os.Signal) {
|
||||
mut c := ctx_ptr
|
||||
if unsafe { c != 0 } {
|
||||
c.cleanup()
|
||||
}
|
||||
exit(0)
|
||||
}) or {}
|
||||
}
|
||||
|
||||
ctx.stdin_handle = stdin_handle
|
||||
ctx.stdout_handle = stdout_handle
|
||||
return ctx
|
||||
}
|
||||
|
||||
// run starts the windows console or restarts if it was paused.
|
||||
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 != none {
|
||||
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], 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 }
|
||||
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 { unsafe { KeyCode(ch + 32) } } // letters
|
||||
91...93 { KeyCode.null } // special keys
|
||||
96...105 { unsafe { KeyCode(ch - 48) } } // numpad numbers
|
||||
112...135 { unsafe { KeyCode(ch + 178) } } // f1 - f24
|
||||
else { unsafe { KeyCode(ascii) } }
|
||||
}
|
||||
|
||||
mut modifiers := unsafe { Modifiers(0) }
|
||||
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)
|
||||
}
|
||||
|
||||
event_type := if e.bKeyDown == 0 {
|
||||
EventType.key_up
|
||||
} else {
|
||||
EventType.key_down
|
||||
}
|
||||
repeat_count := if event_type == .key_down && e.wRepeatCount > 0 {
|
||||
int(e.wRepeatCount)
|
||||
} else {
|
||||
1
|
||||
}
|
||||
for _ in 0 .. repeat_count {
|
||||
mut event := &Event{
|
||||
typ: event_type
|
||||
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 := unsafe { Modifiers(0) }
|
||||
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, 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')
|
||||
flush_stdout()
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn load_title() {
|
||||
// restore the previously saved terminal title
|
||||
print('\x1b[23;0t')
|
||||
flush_stdout()
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
module ui
|
||||
|
||||
fn test_mouse_tracking_is_disabled_by_default() {
|
||||
cfg := Config{}
|
||||
assert !cfg.mouse_enabled
|
||||
}
|
||||
|
||||
fn test_mouse_tracking_can_be_enabled_explicitly() {
|
||||
cfg := Config{
|
||||
mouse_enabled: true
|
||||
}
|
||||
assert cfg.mouse_enabled
|
||||
}
|
||||
@@ -0,0 +1,765 @@
|
||||
// Copyright (c) 2020-2024 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 strings
|
||||
import time
|
||||
import term.termios
|
||||
|
||||
#include <signal.h>
|
||||
|
||||
pub struct C.winsize {
|
||||
ws_row u16
|
||||
ws_col u16
|
||||
}
|
||||
|
||||
const termios_at_startup = get_termios()
|
||||
|
||||
const kitty_keyboard_flags = 0b10 | 0b1000 | 0b10000
|
||||
|
||||
@[inline]
|
||||
fn get_termios() termios.Termios {
|
||||
mut t := termios.Termios{}
|
||||
termios.tcgetattr(C.STDIN_FILENO, mut t)
|
||||
return t
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn get_terminal_size() (u16, u16) {
|
||||
winsz := C.winsize{}
|
||||
termios.ioctl(0, u64(termios.flag(C.TIOCGWINSZ)), voidptr(&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 unsafe { 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 tios := 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
|
||||
tios.c_iflag &=
|
||||
termios.invert(termios.flag(int(C.IGNBRK) | int(C.BRKINT) | int(C.PARMRK) | int(C.IXON)))
|
||||
tios.c_lflag &=
|
||||
termios.invert(termios.flag(int(C.ICANON) | int(C.ISIG) | int(C.ECHO) | int(C.IEXTEN) | int(C.TOSTOP)))
|
||||
} else {
|
||||
// Set raw input mode by unsetting ICANON and ECHO
|
||||
tios.c_lflag &= termios.invert(termios.flag(int(C.ICANON) | int(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')
|
||||
flush_stdout()
|
||||
}
|
||||
|
||||
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
|
||||
tios.c_cc[C.VTIME] = 1
|
||||
tios.c_cc[C.VMIN] = 0
|
||||
termios.tcsetattr(C.STDIN_FILENO, C.TCSAFLUSH, mut tios)
|
||||
// feature-test the SU spec
|
||||
sx, sy := get_cursor_position()
|
||||
print('${bsu}${esu}')
|
||||
flush_stdout()
|
||||
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
|
||||
tios.c_cc[C.VTIME] = 0
|
||||
tios.c_cc[C.VMIN] = 0
|
||||
termios.tcsetattr(C.STDIN_FILENO, C.TCSAFLUSH, mut tios)
|
||||
if ctx.cfg.mouse_enabled {
|
||||
// enable mouse input
|
||||
print('\x1b[?1003h\x1b[?1006h')
|
||||
flush_stdout()
|
||||
}
|
||||
if ctx.cfg.use_alternate_buffer {
|
||||
// switch to the alternate buffer
|
||||
print('\x1b[?1049h')
|
||||
flush_stdout()
|
||||
// clear the terminal and set the cursor to the origin
|
||||
print('\x1b[2J\x1b[3J\x1b[1;1H')
|
||||
flush_stdout()
|
||||
}
|
||||
if ctx.cfg.capture_events {
|
||||
// Ask supporting terminals to report press/repeat/release in CSI-u form.
|
||||
print('\x1b[>${kitty_keyboard_flags}u')
|
||||
flush_stdout()
|
||||
}
|
||||
ctx.window_height, ctx.window_width = get_terminal_size()
|
||||
|
||||
// Reset console on exit
|
||||
at_exit(restore_terminal_state) or {}
|
||||
os.signal_opt(.tstp, restore_terminal_state_signal) or {}
|
||||
os.signal_opt(.cont, fn (_ os.Signal) {
|
||||
mut c := ctx_ptr
|
||||
if unsafe { 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 unsafe { c != 0 } {
|
||||
c.cleanup()
|
||||
}
|
||||
exit(0)
|
||||
}) or {}
|
||||
}
|
||||
|
||||
os.signal_opt(.winch, fn (_ os.Signal) {
|
||||
mut c := ctx_ptr
|
||||
if unsafe { 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')
|
||||
flush_stdout()
|
||||
mut s := ''
|
||||
unsafe {
|
||||
buf := malloc_noscan(25)
|
||||
len := C.read(C.STDIN_FILENO, buf, 24)
|
||||
buf[len] = 0
|
||||
s = tos(buf, len)
|
||||
}
|
||||
if s.len == 0 {
|
||||
return -1, -1
|
||||
}
|
||||
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 arbitrary value (#010203), assumed not to be the default
|
||||
print('\x1b[48:2:1:2:3m')
|
||||
flush_stdout()
|
||||
// andquery the current color
|
||||
print('\x1bP\$qm\x1b\\')
|
||||
flush_stdout()
|
||||
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 ??
|
||||
mut startup := termios_at_startup
|
||||
termios.tcsetattr(C.STDIN_FILENO, C.TCSAFLUSH, mut startup)
|
||||
c := ctx_ptr
|
||||
if unsafe { c != 0 } && c.cfg.capture_events {
|
||||
// Pop the keyboard mode stack before leaving the current screen.
|
||||
print('\x1b[<u')
|
||||
}
|
||||
print('\x1b[?1003l\x1b[?1006l\x1b[?25h')
|
||||
flush_stdout()
|
||||
if unsafe { 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 != none {
|
||||
unsafe {
|
||||
len := C.read(C.STDIN_FILENO, &u8(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(unsafe { nil })
|
||||
if ctx.read_buf[0] == 0x1b {
|
||||
e, len := escape_sequence(ctx.read_buf.bytestr())
|
||||
event = unsafe { e }
|
||||
ctx.shift(len)
|
||||
} else {
|
||||
if ctx.read_all_bytes {
|
||||
e, len := multi_char(ctx.read_buf.bytestr())
|
||||
event = unsafe { e }
|
||||
ctx.shift(len)
|
||||
} else {
|
||||
event = single_char(ctx.read_buf.bytestr())
|
||||
ctx.shift(1)
|
||||
}
|
||||
}
|
||||
if unsafe { 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: unsafe { KeyCode(ch) }
|
||||
utf8: ch.ascii_str()
|
||||
}
|
||||
|
||||
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: unsafe { KeyCode(96 | ch) }
|
||||
modifiers: .ctrl
|
||||
}
|
||||
}
|
||||
65...90 {
|
||||
event = &Event{
|
||||
typ: event.typ
|
||||
ascii: event.ascii
|
||||
utf8: event.utf8
|
||||
code: unsafe { KeyCode(32 | ch) }
|
||||
modifiers: .shift
|
||||
}
|
||||
}
|
||||
else {}
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
fn multi_char(buf string) (&Event, int) {
|
||||
ch := buf[0]
|
||||
|
||||
mut event := &Event{
|
||||
typ: .key_down
|
||||
ascii: ch
|
||||
code: unsafe { 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: unsafe { KeyCode(96 | ch) }
|
||||
modifiers: .ctrl
|
||||
}
|
||||
}
|
||||
65...90 {
|
||||
event = &Event{
|
||||
typ: event.typ
|
||||
ascii: event.ascii
|
||||
utf8: event.utf8
|
||||
code: unsafe { KeyCode(32 | ch) }
|
||||
modifiers: .shift
|
||||
}
|
||||
}
|
||||
else {}
|
||||
}
|
||||
|
||||
return event, buf.len
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn modifiers_from_report_param(param int) Modifiers {
|
||||
flags := if param > 0 { param - 1 } else { 0 }
|
||||
mut modifiers := unsafe { Modifiers(0) }
|
||||
if flags & 0b001 != 0 {
|
||||
modifiers.set(.shift)
|
||||
}
|
||||
if flags & 0b010 != 0 {
|
||||
modifiers.set(.alt)
|
||||
}
|
||||
if flags & 0b100 != 0 {
|
||||
modifiers.set(.ctrl)
|
||||
}
|
||||
return modifiers
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn key_event_type_from_report_param(param int) EventType {
|
||||
return match param {
|
||||
3 { .key_up }
|
||||
else { .key_down }
|
||||
}
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn parse_key_report_param(param string) (Modifiers, EventType) {
|
||||
if param.len == 0 {
|
||||
return unsafe { Modifiers(0) }, EventType.key_down
|
||||
}
|
||||
parts := param.split(':')
|
||||
modifiers := modifiers_from_report_param(parts[0].int())
|
||||
event_type := if parts.len > 1 {
|
||||
key_event_type_from_report_param(parts[1].int())
|
||||
} else {
|
||||
EventType.key_down
|
||||
}
|
||||
return modifiers, event_type
|
||||
}
|
||||
|
||||
fn utf8_from_reported_text(param string) string {
|
||||
if param.len == 0 {
|
||||
return ''
|
||||
}
|
||||
mut builder := strings.new_builder(param.len)
|
||||
for part in param.split(':') {
|
||||
codepoint := part.int()
|
||||
if codepoint <= 0 || codepoint > 0x10ffff {
|
||||
continue
|
||||
}
|
||||
builder.write_string(utf32_to_str(u32(codepoint)))
|
||||
}
|
||||
return builder.str()
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn event_from_reported_key(codepoint int, raw string, modifiers Modifiers, event_type EventType, text string) &Event {
|
||||
mut utf8 := raw
|
||||
if text.len > 0 {
|
||||
utf8 = text
|
||||
}
|
||||
mut ascii := u8(0)
|
||||
if text.len == 1 {
|
||||
ascii = text[0]
|
||||
}
|
||||
if codepoint <= 0 || codepoint > 0x10ffff {
|
||||
return &Event{
|
||||
typ: event_type
|
||||
ascii: ascii
|
||||
utf8: utf8
|
||||
modifiers: modifiers
|
||||
}
|
||||
}
|
||||
if codepoint <= 255 {
|
||||
ch := u8(codepoint)
|
||||
if ch == `\r` {
|
||||
return &Event{
|
||||
typ: event_type
|
||||
ascii: ch
|
||||
code: .enter
|
||||
utf8: utf8
|
||||
modifiers: modifiers
|
||||
}
|
||||
}
|
||||
base := single_char(ch.ascii_str())
|
||||
event_ascii := if ascii != 0 { ascii } else { base.ascii }
|
||||
return &Event{
|
||||
typ: event_type
|
||||
ascii: event_ascii
|
||||
code: base.code
|
||||
utf8: utf8
|
||||
modifiers: base.modifiers | modifiers
|
||||
}
|
||||
}
|
||||
return &Event{
|
||||
typ: event_type
|
||||
ascii: ascii
|
||||
utf8: utf8
|
||||
modifiers: modifiers
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_csi_u_key_sequence(single string, buf string) &Event {
|
||||
if buf.len < 4 || buf[0] != `[` || buf[buf.len - 1] != `u` {
|
||||
return unsafe { nil }
|
||||
}
|
||||
parts := buf[1..buf.len - 1].split(';')
|
||||
if parts.len < 1 || parts.len > 3 {
|
||||
return unsafe { nil }
|
||||
}
|
||||
codepoint := parts[0].split(':')[0].int()
|
||||
mut modifiers := unsafe { Modifiers(0) }
|
||||
mut event_type := EventType.key_down
|
||||
if parts.len > 1 {
|
||||
modifiers, event_type = parse_key_report_param(parts[1])
|
||||
}
|
||||
text := if parts.len > 2 { utf8_from_reported_text(parts[2]) } else { '' }
|
||||
return event_from_reported_key(codepoint, single, modifiers, event_type, text)
|
||||
}
|
||||
|
||||
fn parse_modify_other_keys_sequence(single string, buf string) &Event {
|
||||
if buf.len < 7 || buf[0] != `[` || buf[buf.len - 1] != `~` {
|
||||
return unsafe { nil }
|
||||
}
|
||||
parts := buf[1..buf.len - 1].split(';')
|
||||
if parts.len != 3 || parts[0] != '27' {
|
||||
return unsafe { nil }
|
||||
}
|
||||
return event_from_reported_key(parts[2].int(), single,
|
||||
modifiers_from_report_param(parts[1].int()), .key_down, '')
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn key_code_from_csi_final(final u8) KeyCode {
|
||||
return match final {
|
||||
`A` { .up }
|
||||
`B` { .down }
|
||||
`C` { .right }
|
||||
`D` { .left }
|
||||
`F` { .end }
|
||||
`H` { .home }
|
||||
`P` { .f1 }
|
||||
`Q` { .f2 }
|
||||
`R` { .f3 }
|
||||
`S` { .f4 }
|
||||
else { .null }
|
||||
}
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn key_code_from_tilde_param(param string) KeyCode {
|
||||
return match param {
|
||||
'2' { .insert }
|
||||
'3' { .delete }
|
||||
'5' { .page_up }
|
||||
'6' { .page_down }
|
||||
'11' { .f1 }
|
||||
'12' { .f2 }
|
||||
'13' { .f3 }
|
||||
'14' { .f4 }
|
||||
'15' { .f5 }
|
||||
'17' { .f6 }
|
||||
'18' { .f7 }
|
||||
'19' { .f8 }
|
||||
'20' { .f9 }
|
||||
'21' { .f10 }
|
||||
'23' { .f11 }
|
||||
'24' { .f12 }
|
||||
else { .null }
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_csi_modified_key_sequence(buf string) (KeyCode, Modifiers, EventType, bool) {
|
||||
if buf.len < 5 || buf[0] != `[` {
|
||||
return KeyCode.null, unsafe { Modifiers(0) }, EventType.key_down, false
|
||||
}
|
||||
final := buf[buf.len - 1]
|
||||
params := buf[1..buf.len - 1].split(';')
|
||||
if params.len != 2 {
|
||||
return KeyCode.null, unsafe { Modifiers(0) }, EventType.key_down, false
|
||||
}
|
||||
modifiers, event_type := parse_key_report_param(params[1])
|
||||
if final == `~` {
|
||||
code := key_code_from_tilde_param(params[0])
|
||||
return code, modifiers, event_type, code != .null
|
||||
}
|
||||
code := key_code_from_csi_final(final)
|
||||
return code, modifiers, event_type, code != .null
|
||||
}
|
||||
|
||||
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(unsafe { nil }), 0
|
||||
}
|
||||
|
||||
typ, x, y := split[0].int(), split[1].int(), split[2].int()
|
||||
lo := typ & 0b00011
|
||||
hi := typ & 0b11100
|
||||
|
||||
mut modifiers := unsafe { Modifiers(0) }
|
||||
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 { unsafe { 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 {
|
||||
unsafe { 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
|
||||
// ----------------------------
|
||||
|
||||
e := parse_csi_u_key_sequence(single, buf)
|
||||
if unsafe { e != nil } {
|
||||
return e, end
|
||||
}
|
||||
e2 := parse_modify_other_keys_sequence(single, buf)
|
||||
if unsafe { e2 != nil } {
|
||||
return e2, end
|
||||
}
|
||||
|
||||
mut code := KeyCode.null
|
||||
mut modifiers := unsafe { Modifiers(0) }
|
||||
mut event_type := EventType.key_down
|
||||
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 code == .null {
|
||||
parsed_code, parsed_modifiers, parsed_event_type, ok := parse_csi_modified_key_sequence(buf)
|
||||
if ok {
|
||||
code = parsed_code
|
||||
modifiers = parsed_modifiers
|
||||
event_type = parsed_event_type
|
||||
}
|
||||
}
|
||||
|
||||
return &Event{
|
||||
typ: event_type
|
||||
code: code
|
||||
utf8: single
|
||||
modifiers: modifiers
|
||||
}, end
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
module ui
|
||||
|
||||
import os
|
||||
|
||||
fn test_terminal_capabilities_disable_xterm_features_for_linux_console() {
|
||||
caps := terminal_capabilities_for('linux')
|
||||
assert !caps.enable_ansi256
|
||||
assert !caps.supports_alternate_buffer
|
||||
assert !caps.supports_sgr_mouse
|
||||
assert !caps.supports_sync_updates
|
||||
assert !caps.supports_window_title
|
||||
}
|
||||
|
||||
fn test_terminal_capabilities_keep_xterm_defaults() {
|
||||
caps := terminal_capabilities_for('xterm-256color')
|
||||
assert caps.enable_ansi256
|
||||
assert caps.supports_alternate_buffer
|
||||
assert caps.supports_sgr_mouse
|
||||
assert caps.supports_sync_updates
|
||||
assert caps.supports_window_title
|
||||
}
|
||||
|
||||
fn test_get_cursor_position_reads_valid_row_column_data() ! {
|
||||
mut original_stdin_fd := -1
|
||||
unsafe {
|
||||
mut pipe := os.pipe()!
|
||||
|
||||
fake_cursor_pos_data := '\033[45;70R'
|
||||
written_bytes := pipe.write_string(fake_cursor_pos_data)!
|
||||
if written_bytes == -1 {
|
||||
pipe.close()
|
||||
return error('error writing into pipe: ${C.strerror(C.errno)}')
|
||||
}
|
||||
|
||||
C.close(pipe.write_fd)
|
||||
|
||||
if C.dup2(pipe.read_fd, C.STDIN_FILENO) == -1 {
|
||||
C.close(pipe.read_fd)
|
||||
return error('error redirecting stdin with dup2: ${C.strerror(C.errno)}')
|
||||
}
|
||||
|
||||
C.close(pipe.read_fd)
|
||||
|
||||
cursor_pos_x, cursor_pos_y := get_cursor_position()
|
||||
assert cursor_pos_x == 45
|
||||
assert cursor_pos_y == 70
|
||||
}
|
||||
}
|
||||
|
||||
fn test_get_cursor_position_reads_empty_position_data() ! {
|
||||
mut original_stdin_fd := -1
|
||||
unsafe {
|
||||
mut pipe := os.pipe()!
|
||||
|
||||
fake_cursor_pos_data := ''
|
||||
written_bytes := pipe.write_string(fake_cursor_pos_data)!
|
||||
if written_bytes == -1 {
|
||||
pipe.close()
|
||||
return error('error writing into pipe: ${C.strerror(C.errno)}')
|
||||
}
|
||||
|
||||
C.close(pipe.write_fd)
|
||||
|
||||
if C.dup2(pipe.read_fd, C.STDIN_FILENO) == -1 {
|
||||
C.close(pipe.read_fd)
|
||||
return error('error redirecting stdin with dup2: ${C.strerror(C.errno)}')
|
||||
}
|
||||
|
||||
C.close(pipe.read_fd)
|
||||
|
||||
cursor_pos_x, cursor_pos_y := get_cursor_position()
|
||||
assert cursor_pos_x == -1
|
||||
assert cursor_pos_y == -1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
// Copyright (c) 2020-2024 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 u8
|
||||
g u8
|
||||
b u8
|
||||
}
|
||||
|
||||
// hex returns `c`'s RGB color in hex format.
|
||||
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
|
||||
// The alternate `CSI ? 2026 h/l` sequences are preferred when no parameters
|
||||
// are passed, since they are correctly interpreted by terminals like screen.
|
||||
const bsu = '\x1b[?2026h'
|
||||
|
||||
const esu = '\x1b[?2026l'
|
||||
|
||||
// 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, bsu.str, bsu.len)
|
||||
C.write(1, ctx.print_buf.data, ctx.print_buf.len)
|
||||
C.write(1, esu.str, 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 if ctx.enable_ansi256 {
|
||||
ctx.write('\x1b[38;5;${rgb2ansi(c.r, c.g, c.b)}m')
|
||||
} else {
|
||||
ctx.write('\x1b[${30 + rgb2basic_ansi(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 if ctx.enable_ansi256 {
|
||||
ctx.write('\x1b[48;5;${rgb2ansi(c.r, c.g, c.b)}m')
|
||||
} else {
|
||||
ctx.write('\x1b[${40 + rgb2basic_ansi(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')
|
||||
}
|
||||
|
||||
// clear erases the entire terminal window and any saved lines.
|
||||
@[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) {
|
||||
if !ctx.supports_window_title {
|
||||
return
|
||||
}
|
||||
print('\x1b]0;${s}\x07')
|
||||
flush_stdout()
|
||||
}
|
||||
|
||||
fn rgb2basic_ansi(r int, g int, b int) int {
|
||||
mut best_index := 0
|
||||
mut best_distance := -1
|
||||
for i in 0 .. 8 {
|
||||
ref := color_table[i]
|
||||
dr := r - int((ref >> 16) & 0xff)
|
||||
dg := g - int((ref >> 8) & 0xff)
|
||||
db := b - int(ref & 0xff)
|
||||
distance := dr * dr + dg * dg + db * db
|
||||
if best_distance == -1 || distance < best_distance {
|
||||
best_distance = distance
|
||||
best_index = i
|
||||
}
|
||||
}
|
||||
return best_index
|
||||
}
|
||||
|
||||
// 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)) // /* `⎽` */
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import term.ui
|
||||
|
||||
// This test just ensures that programs importing term.ui can compile
|
||||
|
||||
fn test_a_simple_term_ui_program_can_be_compiled() {
|
||||
println(ui.color_table)
|
||||
assert true
|
||||
}
|
||||
Reference in new issue
Block a user