term: add `term.ui` module (part 2) (#6798)

pull/6804/head
spaceface777 2020-11-12 12:12:51 +01:00 committed by GitHub
parent 4ddfff287c
commit 24f743ee12
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 1434 additions and 1 deletions

View File

@ -0,0 +1,94 @@
import term.ui as tui
struct Point {
x int
y int
}
const (
colors = [
tui.Color{33, 150, 243}
tui.Color{0, 150, 136}
tui.Color{205, 220, 57}
tui.Color{255, 152, 0}
tui.Color{244, 67, 54}
tui.Color{156, 39, 176}
]
)
struct App {
mut:
tui &tui.Context = 0
points []Point
color tui.Color = colors[0]
color_idx int
cut_rate f64 = 5
}
fn frame(x voidptr) {
mut app := &App(x)
app.tui.clear()
if app.points.len > 0 {
app.tui.set_bg_color(app.color)
mut last := app.points[0]
for segment in app.points {
// if the cursor moveds quickly enough, different events are not
// necessarily touching, so we need to draw a line between them
app.tui.draw_line(last.x, last.y, segment.x, segment.y)
last = segment
}
app.tui.reset()
l := int(app.points.len / app.cut_rate) + 1
app.points = app.points[l..].clone()
}
ww := app.tui.window_width
app.tui.bold()
app.tui.draw_text(ww / 6, 2, 'V term.input: cursor chaser demo')
app.tui.draw_text((ww - ww / 6) - 14, 2, 'cut rate: ${(100 / app.cut_rate):3.0f}%')
app.tui.horizontal_separator(3)
app.tui.reset()
app.tui.flush()
}
fn event(e &tui.Event, x voidptr) {
mut app := &App(x)
match e.typ {
.key_down {
match e.code {
.escape {
app.tui.set_cursor_position(0, 0)
app.tui.flush()
exit(0)
}
.space, .enter {
app.color_idx++
if app.color_idx == colors.len { app.color_idx = 0 }
app.color = colors[app.color_idx]
} else {}
}
} .mouse_move, .mouse_drag, .mouse_down {
app.points << Point{ e.x, e.y }
} .mouse_scroll {
d := if e.direction == .up { 0.1 } else { -0.1 }
app.cut_rate += d
if app.cut_rate < 1 { app.cut_rate = 1 }
} else {}
}
}
mut app := &App{}
app.tui = tui.init(
user_data: app,
frame_fn: frame,
event_fn: event,
hide_cursor: true
)
app.tui.run()

View File

@ -0,0 +1,33 @@
import term.ui as tui
struct App {
mut:
tui &tui.Context = 0
}
fn event(e &tui.Event, x voidptr) {
print('\x1b[0;0H\x1b[2J\x1b[3J') // Clear everything
println('V term.input event viewer (press `esc` to exit)\n\n')
println(e)
println('Raw event bytes: "${e.utf8.bytes().hex()}" = ${e.utf8.bytes()}')
if e.modifiers == tui.ctrl | tui.alt {
println('CTRL + ALT')
}
if e.typ == .key_down && e.code == .escape { exit(0) }
}
mut app := &App{}
app.tui = tui.init(
user_data: app,
event_fn: event
hide_cursor: true
capture_events: true
frame_rate: 60
)
println('V term.input event viewer (press `esc` to exit)\n\n')
app.tui.run()

View File

@ -0,0 +1,90 @@
import term.ui as tui
import rand
struct Rect {
mut:
c tui.Color
x int
y int
x2 int
y2 int
}
struct App {
mut:
tui &tui.Context = 0
rects []Rect
cur_rect Rect
is_drag bool
redraw bool
}
fn random_color() tui.Color {
return {
r: byte(rand.intn(256))
g: byte(rand.intn(256))
b: byte(rand.intn(256))
}
}
fn event(e &tui.Event, x voidptr) {
mut app := &App(x)
match e.typ {
.mouse_down {
app.is_drag = true
app.cur_rect = {
c: random_color()
x: e.x
y: e.y
x2: e.x
y2: e.y
}
}
.mouse_drag {
app.cur_rect.x2 = e.x
app.cur_rect.y2 = e.y
} .mouse_up {
app.rects << app.cur_rect
app.is_drag = false
} .key_down {
if e.code == .c { app.rects.clear() }
else if e.code == .escape { exit(0) }
} else {}
}
app.redraw = true
}
fn frame(x voidptr) {
mut app := &App(x)
if !app.redraw { return }
app.tui.clear()
for rect in app.rects {
app.tui.set_bg_color(rect.c)
app.tui.draw_rect(rect.x, rect.y, rect.x2, rect.y2)
}
if app.is_drag {
r := app.cur_rect
app.tui.set_bg_color(r.c)
app.tui.draw_empty_rect(r.x, r.y, r.x2, r.y2)
}
app.tui.reset_bg_color()
app.tui.flush()
app.redraw = false
}
mut app := &App{}
app.tui = tui.init(
user_data: app,
event_fn: event,
frame_fn: frame
hide_cursor: true
frame_rate: 60
)
app.tui.run()

View File

@ -0,0 +1,335 @@
module main
import term.ui as tui
// The color palette, taken from Google's Material design
const (
colors = [
[
tui.Color{239, 154, 154}
tui.Color{244, 143, 177}
tui.Color{206, 147, 216}
tui.Color{179, 157, 219}
tui.Color{159, 168, 218}
tui.Color{144, 202, 249}
tui.Color{129, 212, 250}
tui.Color{128, 222, 234}
tui.Color{128, 203, 196}
tui.Color{165, 214, 167}
tui.Color{197, 225, 165}
tui.Color{230, 238, 156}
tui.Color{255, 245, 157}
tui.Color{255, 224, 130}
tui.Color{255, 204, 128}
tui.Color{255, 171, 145}
tui.Color{188, 170, 164}
tui.Color{238, 238, 238}
tui.Color{176, 190, 197}
], [
tui.Color{244, 67, 54}
tui.Color{233, 30, 99}
tui.Color{156, 39, 176}
tui.Color{103, 58, 183}
tui.Color{63, 81, 181}
tui.Color{33, 150, 243}
tui.Color{3, 169, 244}
tui.Color{0, 188, 212}
tui.Color{0, 150, 136}
tui.Color{76, 175, 80}
tui.Color{139, 195, 74}
tui.Color{205, 220, 57}
tui.Color{255, 235, 59}
tui.Color{255, 193, 7}
tui.Color{255, 152, 0}
tui.Color{255, 87, 34}
tui.Color{121, 85, 72}
tui.Color{120, 120, 120}
tui.Color{96, 125, 139}
], [
tui.Color{198, 40, 40}
tui.Color{173, 20, 87}
tui.Color{106, 27, 154}
tui.Color{69, 39, 160}
tui.Color{40, 53, 147}
tui.Color{21, 101, 192}
tui.Color{2, 119, 189}
tui.Color{0, 131, 143}
tui.Color{0, 105, 92}
tui.Color{46, 125, 50}
tui.Color{85, 139, 47}
tui.Color{158, 157, 36}
tui.Color{249, 168, 37}
tui.Color{255, 143, 0}
tui.Color{239, 108, 0}
tui.Color{216, 67, 21}
tui.Color{78, 52, 46}
tui.Color{33, 33, 33}
tui.Color{55, 71, 79}
]
]
)
const (
frame_rate = 30 // fps
msg_display_time = 5 * frame_rate
w = 200
h = 100
space = ' '
spaces = ' '
select_color = 'Select color:'
select_size = 'Size: '
help_1 = ''
help_2 = ' HELP '
help_3 = ''
)
struct App {
mut:
tui &tui.Context = 0
header_text []string
mouse_pos Point
msg string
msg_hide_tick int
primary_color tui.Color = colors[1][6]
secondary_color tui.Color = colors[1][9]
drawing [][]tui.Color = [][]tui.Color{ len: h, init: []tui.Color{ len: w } }
size int = 1
should_redraw bool = true
is_dragging bool
}
struct Point {
x int
y int
}
fn main() {
mut app := &App{}
app.tui = tui.init(
user_data: app
frame_fn: frame
event_fn: event
frame_rate: frame_rate
hide_cursor: true
)
app.tui.run()
}
fn frame(x voidptr) {
mut app := &App(x)
mut redraw := app.should_redraw
if app.msg != '' && app.tui.frame_count >= app.msg_hide_tick {
app.msg = ''
redraw = true
}
if redraw {
app.render(false)
app.should_redraw = false
}
}
fn event(event &tui.Event, x voidptr) {
mut app := &App(x)
match event.typ {
.mouse_down {
app.is_dragging = true
if app.tui.window_height - event.y < 5 {
app.footer_click(event)
} else {
app.paint(event)
}
} .mouse_up {
app.is_dragging = false
} .mouse_drag {
app.mouse_pos = { x: event.x, y: event.y }
app.paint(event)
} .mouse_move {
app.mouse_pos = { x: event.x, y: event.y }
} .mouse_scroll {
if event.direction == .down { app.inc_size() } else { app.dec_size() }
} .key_down {
match event.code {
.c {
app.drawing = [][]tui.Color{ len: h, init: []tui.Color{ len: w } }
} .escape {
app.render(true)
exit(0)
} else {}
}
} else {}
}
app.should_redraw = true
}
fn (mut app App) render(paint_only bool) {
app.tui.clear()
app.draw_header()
app.draw_content()
if !paint_only {
app.draw_footer()
app.draw_cursor()
}
app.tui.flush()
}
fn (mut app App) set_pixel(x_ int, y_ int, c tui.Color) {
// Term coords start at 1, and adjust for the header
x, y := x_ - 1, y_ - 4
if y < 0 || app.tui.window_height - y < 3 { return }
if y >= app.drawing.len || x < 0 || x >= app.drawing[0].len { return }
app.drawing[y][x] = c
}
fn (mut app App) paint(event &tui.Event) {
x_start, y_start := int(f32((event.x - 1) / 2) - app.size / 2 + 1), event.y - app.size / 2
color := if event.button == .primary { app.primary_color } else { app.secondary_color }
for x in x_start .. x_start + app.size {
for y in y_start .. y_start + app.size {
app.set_pixel(x, y, color)
}
}
}
fn (mut app App) draw_content() {
w, mut h := app.tui.window_width / 2, app.tui.window_height - 8
if h > app.drawing.len {
h = app.drawing.len
}
for row_idx, row in app.drawing[..h] {
app.tui.set_cursor_position(0, row_idx + 4)
mut last := tui.Color{ 0, 0, 0 }
for cell in row[..w] {
if cell.r == 0 && cell.g == 0 && cell.b == 0 {
if !(cell.r == last.r && cell.g == last.g && cell.b == last.b) {
app.tui.reset()
}
} else {
if !(cell.r == last.r && cell.g == last.g && cell.b == last.b) {
app.tui.set_bg_color(cell)
}
}
app.tui.write(spaces)
last = cell
}
app.tui.reset()
}
}
fn (mut app App) draw_cursor() {
if app.mouse_pos.y in [3, app.tui.window_height - 5] {
// inside the horizontal separators
return
}
cursor_color := if app.is_dragging { tui.Color{ 220, 220, 220 } } else { tui.Color{ 160, 160, 160 } }
app.tui.set_bg_color(cursor_color)
if app.mouse_pos.y >= 3 && app.mouse_pos.y <= app.tui.window_height - 4 {
// inside the main content
mut x_start := int(f32((app.mouse_pos.x - 1) / 2) - app.size / 2 + 1) * 2 - 1
mut y_start := app.mouse_pos.y - app.size / 2
mut x_end := x_start + app.size * 2 - 1
mut y_end := y_start + app.size - 1
if x_start < 1 { x_start = 1 }
if y_start < 4 { y_start = 4 }
if x_end > app.tui.window_width { x_end = app.tui.window_width }
if y_end > app.tui.window_height - 5 { y_end = app.tui.window_height - 5 }
app.tui.draw_rect(x_start, y_start, x_end, y_end)
} else {
app.tui.draw_text(app.mouse_pos.x, app.mouse_pos.y, space)
}
app.tui.reset()
}
fn (mut app App) draw_header() {
if app.msg != '' {
app.tui.set_color(r: 0, g: 0, b: 0)
app.tui.set_bg_color(r: 220, g: 220, b: 220)
app.tui.draw_text(0, 0, ' $app.msg ')
app.tui.reset()
}
app.tui.draw_text(3, 2, /* 'tick: $app.tui.frame_count | ' + */ 'terminal size: ($app.tui.window_width, $app.tui.window_height) | primary color: $app.primary_color.hex() | secondary color: $app.secondary_color.hex()')
app.tui.horizontal_separator(3)
}
fn (mut app App) draw_footer() {
ww, wh := app.tui.window_width, app.tui.window_height
app.tui.horizontal_separator(wh - 4)
for i, color_row in colors {
for j, color in color_row {
x := j * 3 + 19
y := wh - 3 + i
app.tui.set_bg_color(color)
app.tui.draw_rect(x, y, x+1, y)
}
}
app.tui.reset_bg_color()
app.tui.draw_text(3, wh - 3, select_color)
app.tui.bold()
app.tui.draw_text(3, wh - 1, select_size)
app.tui.reset()
if ww >= 90 {
app.tui.draw_text(80, wh - 3, help_1)
app.tui.draw_text(80, wh - 2, help_2)
app.tui.draw_text(80, wh - 1, help_3)
}
}
[inline]
fn (mut app App) inc_size() {
if app.size < 20 { app.size++ }
app.show_msg('inc. size: $app.size', 1)
}
[inline]
fn (mut app App) dec_size() {
if app.size > 1 { app.size-- }
app.show_msg('dec. size: $app.size', 1)
}
fn (mut app App) footer_click(event &tui.Event) {
footer_y := 3 - (app.tui.window_height - event.y)
match event.x {
8...11 {
app.inc_size()
} 12...15 {
app.dec_size()
} 18...75 {
if (event.x % 3) == 0 { return } // Inside the gap between tiles
idx := footer_y * 19 - 6 + event.x / 3
color := colors[idx / 19][idx % 19]
if event.button == .primary { app.primary_color = color } else { app.secondary_color = color }
app.show_msg('set ${event.button.str().to_lower()} color idx: $idx', 1)
} else {}
}
}
fn (mut app App) show_msg(text string, time int) {
frames := time * frame_rate
app.msg_hide_tick = if time > 0 { int(app.tui.frame_count) + frames } else { -1 }
app.msg = text
}

View File

@ -1,6 +1,8 @@
# Quickstart
The V `term` module is a module which is made to provide an interactive api that helps building TUI apps.
The V `term` module is a module designed to provide the building blocks for building very simple TUI apps.
For more complex apps, you should really look at the `term.input` module, as it includes terminal events, is easier to use,
and is much more performant for large draws.
# Use

View File

@ -0,0 +1,42 @@
## `term.ui`
A V module for designing terminal UI apps
#### Quickstart
```v
// todo
```
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.
- `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: Why does this module not work on Windows?
A: As with many other things, Windows has a completely different and incompatible way of handling input parsing and drawing primitives, and support has not been implemented yet. Contributions are definitely welcome though.
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.

View File

@ -0,0 +1,193 @@
module ui
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
}
pub const (
shift = u32(1 << 0)
ctrl = u32(1 << 1)
alt = u32(1 << 2)
)
pub enum Direction {
unknown
up
down
left
right
}
pub enum MouseButton {
unknown
primary
secondary
}
pub enum EventType {
unknown
mouse_down
mouse_up
mouse_move
mouse_drag
mouse_scroll
key_down
resized
}
pub struct Event {
pub:
typ EventType
// Mouse event info
x int
y int
button MouseButton
direction Direction
// Keyboard event info
code KeyCode
modifiers u32
ascii byte
utf8 string
// Resized event info
width int
height int
}
pub struct Context {
pub:
cfg Config
mut:
termios C.termios
read_buf []byte
print_buf []byte
// init_called bool
// quit_ordered 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
hide_cursor bool
capture_events bool
// All kill signals
reset []int = [1, 2, 3, 4, 6, 7, 8, 9, 11, 13, 14, 15, 19]
}

View File

@ -0,0 +1,86 @@
module ui
const (
ctx_ptr = &Context(0)
)
pub fn init(cfg Config) &Context {
mut ctx := &Context{
cfg: cfg,
read_buf: []byte{ cap: cfg.buffer_size }
}
if cfg.hide_cursor {
s := '\x1b[?25l'
C.write(C.STDOUT_FILENO, s.str, s.len)
}
unsafe {
x := &ctx_ptr
*x = ctx
}
return ctx
}
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()
}
}
[inline]
// shifts the array left, to remove any data that was just read, and updates its len
// TODO: remove
fn (mut ctx Context) shift(len int) {
unsafe {
C.memmove(ctx.read_buf.data, 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 := &ctx.read_buf.len
unsafe { *l = size }
}
[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)
}
}

View File

@ -0,0 +1,10 @@
module ui
pub fn init(cfg Config) &Context {
panic("term.input: error: Windows support isn't implemented yet")
return &Context{}
}
pub fn (mut ctx Context) run() {
panic("term.input: error: Windows support isn't implemented yet")
}

View File

@ -0,0 +1,377 @@
module ui
import os
import time
#include <termios.h>
#include <sys/ioctl.h>
#include <signal.h>
fn C.tcgetattr()
fn C.tcsetattr()
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 (mut ctx Context) termios_setup() {
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)
}
// 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)
print('\x1b[?1003h\x1b[?1015h\x1b[?1006h')
ctx.termios = termios
ctx.window_height, ctx.window_width = get_terminal_size()
// Reset console on exit
C.atexit(termios_reset)
for code in ctx.cfg.reset {
os.signal(code, fn() {
mut c := ctx_ptr
if c != 0 {
c.cleanup()
}
exit(0)
})
}
os.signal(C.SIGWINCH, fn() {
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)
}
})
}
fn termios_reset() {
C.tcsetattr(C.STDIN_FILENO, C.TCSAFLUSH /* C.TCSANOW ?? */, &termios_at_startup)
print('\x1b[?1003l\x1b[?1015l\x1b[?1006l\x1b[0J\x1b[?25h')
}
///////////////////////////////////////////
/*
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 last_frame_time := 0
mut sleep_len := 0
for {
sw.restart()
if !init_called {
ctx.init()
init_called = true
}
for _ in 0 .. 7 {
// println('SLEEPING: $sleep_len')
if sleep_len > 0 {
time.usleep(sleep_len)
}
if ctx.cfg.event_fn != voidptr(0) {
len := C.read(C.STDIN_FILENO, ctx.read_buf.data, ctx.read_buf.cap - ctx.read_buf.len)
if len > 0 {
ctx.resize_arr(len)
ctx.parse_events()
}
}
}
ctx.frame()
sw.pause()
last_frame_time = int(sw.elapsed().microseconds())
if
println('Sleeping for $frame_time - $last_frame_time = ${frame_time - last_frame_time}')
// time.usleep(frame_time - last_frame_time - sleep_len * 7)
last_frame_time = 0
sw.start()
sw.pause()
last_frame_time += int(sw.elapsed().microseconds())
sleep_len = (frame_time - last_frame_time) / 8
ctx.frame_count++
}
}
*/
// 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.usleep(sleep_len)
}
sw.restart()
if ctx.cfg.event_fn != voidptr(0) {
len := C.read(C.STDIN_FILENO, ctx.read_buf.data, ctx.read_buf.cap - ctx.read_buf.len)
if len > 0 {
ctx.resize_arr(len)
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 | code: KeyCode(96 | ch), modifiers: ctrl } }
// 65 ... 90 { 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
// 10 == `\n` == enter, don't treat it as ctrl + j
1 ... 9, 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
}
[inline]
// 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() {
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
}
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)
// return { c | modifiers: c.modifiers | alt }, 2
return &Event{
typ: c.typ
ascii: c.ascii
code: c.code
utf8: single
modifiers: c.modifiers | alt
}, 2
}
// ----------------
// Mouse events
// ----------------
if buf.len > 2 && buf[1] == `<` { // Mouse control
split := buf[2..].split(';')
if split.len < 3 { return &Event(0), 0 }
typ, x, y := split[0].int(), split[1].int(), split[2].int()
match typ {
0, 2 {
last := buf[buf.len - 1]
button := if typ == 0 { MouseButton.primary } else { MouseButton.secondary }
event := if last == `M` { EventType.mouse_down } else { EventType.mouse_up }
return &Event{ typ: event, x: x, y: y, button: button, utf8: single }, end
}
32, 34 {
button := if typ == 32 { MouseButton.primary } else { MouseButton.secondary }
return &Event{ typ: .mouse_drag, x: x, y: y, button: button, utf8: single }, end
}
35 {
return &Event{ typ: .mouse_move, x: x, y: y, utf8: single }, end
}
64, 65 {
direction := if typ == 64 { Direction.down } else { Direction.up }
return &Event{ typ: .mouse_scroll, x: x, y: y, direction: direction, utf8: single }, end
} else {}
}
}
// ----------------------------
// Special key combinations
// ----------------------------
mut code := KeyCode.null
mut modifiers := u32(0)
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.len == 5 && buf[0] == `[` && buf[1] == `1` && buf[2] == `;` {
// code = KeyCode(buf[4] + 197)
modifiers = match buf[3] {
`2` { shift }
`3` { alt }
`4` { shift | alt }
`5` { ctrl }
`6` { ctrl | shift }
`7` { ctrl | alt }
`8` { ctrl | alt | shift }
else { modifiers } // probably unreachable? idk, terminal events are strange
}
code = match buf[4] {
`A` { KeyCode.up }
`B` { KeyCode.down }
`C` { KeyCode.right }
`D` { KeyCode.left }
`F` { KeyCode.end }
`H` { KeyCode.home }
`P` { KeyCode.f1 }
`Q` { KeyCode.f2 }
`R` { KeyCode.f3 }
`S` { KeyCode.f4 }
else { code }
}
// && buf[3] >= `2` && buf[3] <= `8` && buf[4] >= `A` && buf[4] <= `D`
}
return &Event{ typ: .key_down, code: code, utf8: single, modifiers: modifiers }, end
}

171
vlib/term/ui/ui.v 100644
View File

@ -0,0 +1,171 @@
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\\'
)
[inline]
pub fn (mut ctx Context) write(s string) {
if s == '' { return }
ctx.print_buf.push_many(s.str, s.len)
}
[inline]
pub fn (mut ctx Context) flush() {
$if windows {
// TODO
} $else {
// TODO: Diff the previous frame against this one, and only render things that changed?
C.write(C.STDOUT_FILENO, bsu.str, bsu.len)
C.write(C.STDOUT_FILENO, ctx.print_buf.data, ctx.print_buf.len)
C.write(C.STDOUT_FILENO, esu.str, esu.len)
ctx.print_buf.clear()
}
}
[inline]
pub fn (mut ctx Context) bold() {
ctx.write('\x1b[1m')
}
[inline]
pub fn (mut ctx Context) set_cursor_position(x int, y int) {
ctx.write('\x1b[$y;${x}H')
}
[inline]
pub fn (mut ctx Context) set_color(c Color) {
ctx.write('\x1b[38;2;${int(c.r)};${int(c.g)};${int(c.b)}m')
}
[inline]
pub fn (mut ctx Context) set_bg_color(c Color) {
ctx.write('\x1b[48;2;${int(c.r)};${int(c.g)};${int(c.b)}m')
}
[inline]
pub fn (mut ctx Context) reset_color() {
ctx.write('\x1b[39m')
}
[inline]
pub fn (mut ctx Context) reset_bg_color() {
ctx.write('\x1b[49m')
}
[inline]
pub fn (mut ctx Context) reset() {
ctx.write('\x1b[0m')
}
[inline]
pub fn (mut ctx Context) clear() {
ctx.write('\x1b[2J\x1b[3J')
}
// pub const (
// default_color = gx.rgb(183, 101, 94) // hopefully nobody actually tries to use this color...
// )
// pub struct DrawConfig {
// pub mut:
// fg_color gx.Color = default_color
// bg_color gx.Color = default_color
// }
[inline]
pub fn (mut ctx Context) draw_point(x int, y int) {
ctx.set_cursor_position(x, y)
ctx.write(' ')
}
[inline]
pub fn (mut ctx Context) draw_text(x int, y int, s string) {
ctx.set_cursor_position(x, y)
ctx.write(s)
}
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
}
}
}
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 } else { y2 }, if y > y2 { y } else { y2 }
for y_pos in min_y .. max_y + 1 {
ctx.draw_line(x, y_pos, x2, y_pos)
}
}
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)
}
[inline]
pub fn (mut ctx Context) horizontal_separator(y int) {
ctx.set_cursor_position(0, y)
ctx.write(strings.repeat(/* `⎽` */`-`, ctx.window_width))
}