diff options
Diffstat (limited to 'v_windows/v/vlib/term/ui/termios_nix.c.v')
-rw-r--r-- | v_windows/v/vlib/term/ui/termios_nix.c.v | 530 |
1 files changed, 530 insertions, 0 deletions
diff --git a/v_windows/v/vlib/term/ui/termios_nix.c.v b/v_windows/v/vlib/term/ui/termios_nix.c.v new file mode 100644 index 0000000..fb5ff76 --- /dev/null +++ b/v_windows/v/vlib/term/ui/termios_nix.c.v @@ -0,0 +1,530 @@ +// Copyright (c) 2020-2021 Raúl Hernández. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +module ui + +import os +import time + +#include <termios.h> +#include <sys/ioctl.h> +#include <signal.h> + +fn C.tcgetattr(fd int, termios_p &C.termios) int + +fn C.tcsetattr(fd int, optional_actions int, termios_p &C.termios) int + +fn C.ioctl(fd int, request u64, arg voidptr) int + +struct C.termios { +mut: + c_iflag u32 + c_lflag u32 + c_cc [32]byte +} + +struct C.winsize { + ws_row u16 + ws_col u16 +} + +const ( + termios_at_startup = get_termios() +) + +[inline] +fn get_termios() C.termios { + mut t := C.termios{} + C.tcgetattr(C.STDIN_FILENO, &t) + return t +} + +[inline] +fn get_terminal_size() (u16, u16) { + winsz := C.winsize{} + C.ioctl(0, C.TIOCGWINSZ, &winsz) + return winsz.ws_row, winsz.ws_col +} + +fn restore_terminal_state_signal(_ os.Signal) { + restore_terminal_state() +} + +fn restore_terminal_state() { + termios_reset() + mut c := ctx_ptr + if c != 0 { + c.paused = true + load_title() + } + os.flush() +} + +fn (mut ctx Context) termios_setup() ? { + // store the current title, so restore_terminal_state can get it back + save_title() + + if !ctx.cfg.skip_init_checks && !(os.is_atty(C.STDIN_FILENO) != 0 + && os.is_atty(C.STDOUT_FILENO) != 0) { + return error('not running under a TTY') + } + + mut termios := get_termios() + + if ctx.cfg.capture_events { + // Set raw input mode by unsetting ICANON and ECHO, + // as well as disable e.g. ctrl+c and ctrl.z + termios.c_iflag &= ~u32(C.IGNBRK | C.BRKINT | C.PARMRK | C.IXON) + termios.c_lflag &= ~u32(C.ICANON | C.ISIG | C.ECHO | C.IEXTEN | C.TOSTOP) + } else { + // Set raw input mode by unsetting ICANON and ECHO + termios.c_lflag &= ~u32(C.ICANON | C.ECHO) + } + + if ctx.cfg.hide_cursor { + ctx.hide_cursor() + ctx.flush() + } + + if ctx.cfg.window_title != '' { + print('\x1b]0;$ctx.cfg.window_title\x07') + } + + if !ctx.cfg.skip_init_checks { + // prevent blocking during the feature detections, but allow enough time for the terminal + // to send back the relevant input data + termios.c_cc[C.VTIME] = 1 + termios.c_cc[C.VMIN] = 0 + C.tcsetattr(C.STDIN_FILENO, C.TCSAFLUSH, &termios) + // feature-test the SU spec + sx, sy := get_cursor_position() + print('$bsu$esu') + ex, ey := get_cursor_position() + if sx == ex && sy == ey { + // the terminal either ignored or handled the sequence properly, enable SU + ctx.enable_su = true + } else { + ctx.draw_line(sx, sy, ex, ey) + ctx.set_cursor_position(sx, sy) + ctx.flush() + } + // feature-test rgb (truecolor) support + ctx.enable_rgb = supports_truecolor() + } + // Prevent stdin from blocking by making its read time 0 + termios.c_cc[C.VTIME] = 0 + termios.c_cc[C.VMIN] = 0 + C.tcsetattr(C.STDIN_FILENO, C.TCSAFLUSH, &termios) + // enable mouse input + print('\x1b[?1003h\x1b[?1006h') + if ctx.cfg.use_alternate_buffer { + // switch to the alternate buffer + print('\x1b[?1049h') + // clear the terminal and set the cursor to the origin + print('\x1b[2J\x1b[3J\x1b[1;1H') + } + ctx.window_height, ctx.window_width = get_terminal_size() + + // Reset console on exit + C.atexit(restore_terminal_state) + os.signal_opt(.tstp, restore_terminal_state_signal) or {} + os.signal_opt(.cont, fn (_ os.Signal) { + mut c := ctx_ptr + if c != 0 { + c.termios_setup() or { panic(err) } + c.window_height, c.window_width = get_terminal_size() + mut event := &Event{ + typ: .resized + width: c.window_width + height: c.window_height + } + c.paused = false + c.event(event) + } + }) or {} + for code in ctx.cfg.reset { + os.signal_opt(code, fn (_ os.Signal) { + mut c := ctx_ptr + if c != 0 { + c.cleanup() + } + exit(0) + }) or {} + } + + os.signal_opt(.winch, fn (_ os.Signal) { + mut c := ctx_ptr + if c != 0 { + c.window_height, c.window_width = get_terminal_size() + + mut event := &Event{ + typ: .resized + width: c.window_width + height: c.window_height + } + c.event(event) + } + }) or {} + + os.flush() +} + +fn get_cursor_position() (int, int) { + print('\033[6n') + mut s := '' + unsafe { + buf := malloc_noscan(25) + len := C.read(C.STDIN_FILENO, buf, 24) + buf[len] = 0 + s = tos(buf, len) + } + a := s[2..].split(';') + if a.len != 2 { + return -1, -1 + } + return a[0].int(), a[1].int() +} + +fn supports_truecolor() bool { + // faster/simpler, but less reliable, check + if os.getenv('COLORTERM') in ['truecolor', '24bit'] { + return true + } + // set the bg color to some arbirtrary value (#010203), assumed not to be the default + print('\x1b[48:2:1:2:3m') + // andquery the current color + print('\x1bP\$qm\x1b\\') + mut s := '' + unsafe { + buf := malloc_noscan(25) + len := C.read(C.STDIN_FILENO, buf, 24) + buf[len] = 0 + s = tos(buf, len) + } + return s.contains('1:2:3') +} + +fn termios_reset() { + // C.TCSANOW ?? + C.tcsetattr(C.STDIN_FILENO, C.TCSAFLUSH, &ui.termios_at_startup) + print('\x1b[?1003l\x1b[?1006l\x1b[?25h') + c := ctx_ptr + if c != 0 && c.cfg.use_alternate_buffer { + print('\x1b[?1049l') + } + os.flush() +} + +/////////////////////////////////////////// +// TODO: do multiple sleep/read cycles, rather than one big one +fn (mut ctx Context) termios_loop() { + frame_time := 1_000_000 / ctx.cfg.frame_rate + mut init_called := false + mut sw := time.new_stopwatch(auto_start: false) + mut sleep_len := 0 + for { + if !init_called { + ctx.init() + init_called = true + } + // println('SLEEPING: $sleep_len') + if sleep_len > 0 { + time.sleep(sleep_len * time.microsecond) + } + if !ctx.paused { + sw.restart() + if ctx.cfg.event_fn != voidptr(0) { + unsafe { + len := C.read(C.STDIN_FILENO, &byte(ctx.read_buf.data) + ctx.read_buf.len, + ctx.read_buf.cap - ctx.read_buf.len) + ctx.resize_arr(ctx.read_buf.len + len) + } + if ctx.read_buf.len > 0 { + ctx.parse_events() + } + } + ctx.frame() + sw.pause() + e := sw.elapsed().microseconds() + sleep_len = frame_time - int(e) + + ctx.frame_count++ + } + } +} + +fn (mut ctx Context) parse_events() { + // Stop this from getting stuck in rare cases where something isn't parsed correctly + mut nr_iters := 0 + for ctx.read_buf.len > 0 { + nr_iters++ + if nr_iters > 100 { + ctx.shift(1) + } + mut event := &Event(0) + if ctx.read_buf[0] == 0x1b { + e, len := escape_sequence(ctx.read_buf.bytestr()) + event = e + ctx.shift(len) + } else { + event = single_char(ctx.read_buf.bytestr()) + ctx.shift(1) + } + if event != 0 { + ctx.event(event) + nr_iters = 0 + } + } +} + +fn single_char(buf string) &Event { + ch := buf[0] + + mut event := &Event{ + typ: .key_down + ascii: ch + code: KeyCode(ch) + utf8: buf + } + + match ch { + // special handling for `ctrl + letter` + // TODO: Fix assoc in V and remove this workaround :/ + // 1 ... 26 { event = Event{ ...event, code: KeyCode(96 | ch), modifiers: .ctrl } } + // 65 ... 90 { event = Event{ ...event, code: KeyCode(32 | ch), modifiers: .shift } } + // The bit `or`s here are really just `+`'s, just written in this way for a tiny performance improvement + // don't treat tab, enter as ctrl+i, ctrl+j + 1...8, 11...26 { + event = &Event{ + typ: event.typ + ascii: event.ascii + utf8: event.utf8 + code: KeyCode(96 | ch) + modifiers: .ctrl + } + } + 65...90 { + event = &Event{ + typ: event.typ + ascii: event.ascii + utf8: event.utf8 + code: KeyCode(32 | ch) + modifiers: .shift + } + } + else {} + } + + return event +} + +// Gets an entire, independent escape sequence from the buffer +// Normally, this just means reading until the first letter, but there are some exceptions... +fn escape_end(buf string) int { + mut i := 0 + for { + if i + 1 == buf.len { + return buf.len + } + + if buf[i].is_letter() || buf[i] == `~` { + if buf[i] == `O` && i + 2 <= buf.len { + n := buf[i + 1] + if (n >= `A` && n <= `D`) || (n >= `P` && n <= `S`) || n == `F` || n == `H` { + return i + 2 + } + } + return i + 1 + // escape hatch to avoid potential issues/crashes, although ideally this should never eval to true + } else if buf[i + 1] == 0x1b { + return i + 1 + } + i++ + } + // this point should be unreachable + assert false + return 0 +} + +fn escape_sequence(buf_ string) (&Event, int) { + end := escape_end(buf_) + single := buf_[..end] // read until the end of the sequence + buf := single[1..] // skip the escape character + + if buf.len == 0 { + return &Event{ + typ: .key_down + ascii: 27 + code: .escape + utf8: single + }, 1 + } + + if buf.len == 1 { + c := single_char(buf) + mut modifiers := c.modifiers + modifiers.set(.alt) + return &Event{ + typ: c.typ + ascii: c.ascii + code: c.code + utf8: single + modifiers: modifiers + }, 2 + } + // ---------------- + // Mouse events + // ---------------- + // Documentation: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking + if buf.len > 2 && buf[1] == `<` { + split := buf[2..].split(';') + if split.len < 3 { + return &Event(0), 0 + } + + typ, x, y := split[0].int(), split[1].int(), split[2].int() + lo := typ & 0b00011 + hi := typ & 0b11100 + + mut modifiers := Modifiers{} + if hi & 4 != 0 { + modifiers.set(.shift) + } + if hi & 8 != 0 { + modifiers.set(.alt) + } + if hi & 16 != 0 { + modifiers.set(.ctrl) + } + + match typ { + 0...31 { + last := buf[buf.len - 1] + button := if lo < 3 { MouseButton(lo + 1) } else { MouseButton.unknown } + event := if last == `m` || lo == 3 { + EventType.mouse_up + } else { + EventType.mouse_down + } + + return &Event{ + typ: event + x: x + y: y + button: button + modifiers: modifiers + utf8: single + }, end + } + 32...63 { + button, event := if lo < 3 { + MouseButton(lo + 1), EventType.mouse_drag + } else { + MouseButton.unknown, EventType.mouse_move + } + + return &Event{ + typ: event + x: x + y: y + button: button + modifiers: modifiers + utf8: single + }, end + } + 64...95 { + direction := if typ & 1 == 0 { Direction.down } else { Direction.up } + return &Event{ + typ: .mouse_scroll + x: x + y: y + direction: direction + modifiers: modifiers + utf8: single + }, end + } + else { + return &Event{ + typ: .unknown + utf8: single + }, end + } + } + } + // ---------------------------- + // Special key combinations + // ---------------------------- + + mut code := KeyCode.null + mut modifiers := Modifiers{} + match buf { + '[A', 'OA' { code = .up } + '[B', 'OB' { code = .down } + '[C', 'OC' { code = .right } + '[D', 'OD' { code = .left } + '[5~', '[[5~' { code = .page_up } + '[6~', '[[6~' { code = .page_down } + '[F', 'OF', '[4~', '[[8~' { code = .end } + '[H', 'OH', '[1~', '[[7~' { code = .home } + '[2~' { code = .insert } + '[3~' { code = .delete } + 'OP', '[11~' { code = .f1 } + 'OQ', '[12~' { code = .f2 } + 'OR', '[13~' { code = .f3 } + 'OS', '[14~' { code = .f4 } + '[15~' { code = .f5 } + '[17~' { code = .f6 } + '[18~' { code = .f7 } + '[19~' { code = .f8 } + '[20~' { code = .f9 } + '[21~' { code = .f10 } + '[23~' { code = .f11 } + '[24~' { code = .f12 } + else {} + } + + if buf == '[Z' { + code = .tab + modifiers.set(.shift) + } + + if buf.len == 5 && buf[0] == `[` && buf[1].is_digit() && buf[2] == `;` { + match buf[3] { + `2` { modifiers = .shift } + `3` { modifiers = .alt } + `4` { modifiers = .shift | .alt } + `5` { modifiers = .ctrl } + `6` { modifiers = .ctrl | .shift } + `7` { modifiers = .ctrl | .alt } + `8` { modifiers = .ctrl | .alt | .shift } + else {} + } + + if buf[1] == `1` { + match buf[4] { + `A` { code = KeyCode.up } + `B` { code = KeyCode.down } + `C` { code = KeyCode.right } + `D` { code = KeyCode.left } + `F` { code = KeyCode.end } + `H` { code = KeyCode.home } + `P` { code = KeyCode.f1 } + `Q` { code = KeyCode.f2 } + `R` { code = KeyCode.f3 } + `S` { code = KeyCode.f4 } + else {} + } + } else if buf[1] == `5` { + code = KeyCode.page_up + } else if buf[1] == `6` { + code = KeyCode.page_down + } + } + + return &Event{ + typ: .key_down + code: code + utf8: single + modifiers: modifiers + }, end +} |