From e233911a7b9afc4ff5a7d1555f0ec6f732fd9050 Mon Sep 17 00:00:00 2001 From: spaceface Date: Wed, 27 Jan 2021 13:52:39 +0100 Subject: [PATCH] term.ui: native Windows console implementation (#8359) --- examples/term.ui/text_editor.v | 7 +- vlib/term/term_windows.c.v | 46 ++--- vlib/term/ui/consoleapi_windows.c.v | 84 ++++++++ vlib/term/ui/input.v | 51 ++++- vlib/term/ui/input_nix.c.v | 52 ++--- vlib/term/ui/input_windows.c.v | 290 +++++++++++++++++++++++++++- vlib/term/ui/termios_nix.c.v | 6 +- vlib/term/ui/ui.v | 22 +-- 8 files changed, 468 insertions(+), 90 deletions(-) create mode 100644 vlib/term/ui/consoleapi_windows.c.v diff --git a/examples/term.ui/text_editor.v b/examples/term.ui/text_editor.v index 525797a8bc..6a537c293a 100644 --- a/examples/term.ui/text_editor.v +++ b/examples/term.ui/text_editor.v @@ -505,6 +505,9 @@ fn event(e &tui.Event, x voidptr) { .escape { exit(0) } + .enter { + buffer.put('\n') + } .backspace { buffer.del(-1) } @@ -552,8 +555,8 @@ fn event(e &tui.Event, x voidptr) { if e.code == .s { a.save() } - } else if e.modifiers in [tui.shift, 0] { - buffer.put(e.ascii.str()) + } else if e.modifiers in [tui.shift, 0] && e.code != .null { + buffer.put(e.ascii.ascii_str()) } } else { diff --git a/vlib/term/term_windows.c.v b/vlib/term/term_windows.c.v index 2b531e4af9..f3fc965216 100644 --- a/vlib/term/term_windows.c.v +++ b/vlib/term/term_windows.c.v @@ -2,31 +2,33 @@ module term import os -pub struct Coord16 { -pub: - x i16 - y i16 +[typedef] +struct C.COORD { + X i16 + Y i16 } -struct SmallRect { - left i16 - top i16 - right i16 - bottom i16 +[typedef] +struct C.SMALL_RECT { + Left u16 + Top u16 + Right u16 + Bottom u16 } // win: CONSOLE_SCREEN_BUFFER_INFO // https://docs.microsoft.com/en-us/windows/console/console-screen-buffer-info-str -struct ConsoleScreenBufferInfo { - dw_size Coord16 - dw_cursor_position Coord16 - w_attributes u16 - sr_window SmallRect - dw_maximum_window_size Coord16 +[typedef] +struct C.CONSOLE_SCREEN_BUFFER_INFO { + dwSize C.COORD + dwCursorPosition C.COORD + wAttributes u16 + srWindow C.SMALL_RECT + dwMaximumWindowSize C.COORD } // ref - https://docs.microsoft.com/en-us/windows/console/getconsolescreenbufferinfo -fn C.GetConsoleScreenBufferInfo(handle os.HANDLE, info &ConsoleScreenBufferInfo) bool +fn C.GetConsoleScreenBufferInfo(handle os.HANDLE, info &C.CONSOLE_SCREEN_BUFFER_INFO) bool // ref - https://docs.microsoft.com/en-us/windows/console/setconsoletitle fn C.SetConsoleTitle(title &u16) bool @@ -34,10 +36,10 @@ fn C.SetConsoleTitle(title &u16) bool // get_terminal_size returns a number of colums and rows of terminal window. pub fn get_terminal_size() (int, int) { if is_atty(1) > 0 && os.getenv('TERM') != 'dumb' { - info := ConsoleScreenBufferInfo{} + info := C.CONSOLE_SCREEN_BUFFER_INFO{} if C.GetConsoleScreenBufferInfo(C.GetStdHandle(C.STD_OUTPUT_HANDLE), &info) { - columns := int(info.sr_window.right - info.sr_window.left + 1) - rows := int(info.sr_window.bottom - info.sr_window.top + 1) + columns := int(info.srWindow.Right - info.srWindow.Left + 1) + rows := int(info.srWindow.Bottom - info.srWindow.Top + 1) return columns, rows } } @@ -48,10 +50,10 @@ pub fn get_terminal_size() (int, int) { pub fn get_cursor_position() Coord { mut res := Coord{} if is_atty(1) > 0 && os.getenv('TERM') != 'dumb' { - info := ConsoleScreenBufferInfo{} + info := C.CONSOLE_SCREEN_BUFFER_INFO{} if C.GetConsoleScreenBufferInfo(C.GetStdHandle(C.STD_OUTPUT_HANDLE), &info) { - res.x = info.dw_cursor_position.x - res.y = info.dw_cursor_position.y + res.x = info.dwCursorPosition.X + res.y = info.dwCursorPosition.Y } } return res diff --git a/vlib/term/ui/consoleapi_windows.c.v b/vlib/term/ui/consoleapi_windows.c.v new file mode 100644 index 0000000000..19d521a0d0 --- /dev/null +++ b/vlib/term/ui/consoleapi_windows.c.v @@ -0,0 +1,84 @@ +module ui + +import os + +union C.Event { + KeyEvent C.KEY_EVENT_RECORD + MouseEvent C.MOUSE_EVENT_RECORD + WindowBufferSizeEvent C.WINDOW_BUFFER_SIZE_RECORD + MenuEvent C.MENU_EVENT_RECORD + FocusEvent C.FOCUS_EVENT_RECORD +} + +[typedef] +struct C.INPUT_RECORD { + EventType u16 + Event C.Event +} + +union C.uChar { + UnicodeChar rune + AsciiChar byte +} + +[typedef] +struct C.KEY_EVENT_RECORD { + bKeyDown int + wRepeatCount u16 + wVirtualKeyCode u16 + wVirtualScanCode u16 + uChar C.uChar + dwControlKeyState u32 +} + +[typedef] +struct C.MOUSE_EVENT_RECORD { + dwMousePosition C.COORD + dwButtonState u32 + dwControlKeyState u32 + dwEventFlags u32 +} + +[typedef] +struct C.WINDOW_BUFFER_SIZE_RECORD { + dwSize C.COORD +} + +[typedef] +struct C.MENU_EVENT_RECORD { + dwCommandId u32 +} + +[typedef] +struct C.FOCUS_EVENT_RECORD { + bSetFocus int +} + +[typedef] +struct C.COORD { + X i16 + Y i16 +} + +[typedef] +struct C.SMALL_RECT { + Left u16 + Top u16 + Right u16 + Bottom u16 +} + +[typedef] +struct C.CONSOLE_SCREEN_BUFFER_INFO { + dwSize C.COORD + dwCursorPosition C.COORD + wAttributes u16 + srWindow C.SMALL_RECT + dwMaximumWindowSize C.COORD +} + +fn C.ReadConsoleInput() bool + +fn C.GetNumberOfConsoleInputEvents() bool + +fn C.GetConsoleScreenBufferInfo(handle os.HANDLE, info &C.CONSOLE_SCREEN_BUFFER_INFO) bool diff --git a/vlib/term/ui/input.v b/vlib/term/ui/input.v index 7a4792bb9d..4c89cc2bd1 100644 --- a/vlib/term/ui/input.v +++ b/vlib/term/ui/input.v @@ -1,4 +1,4 @@ -// Copyright (c) 2020 Raúl Hernández. All rights reserved. +// 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 @@ -108,6 +108,18 @@ pub enum KeyCode { 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 const ( @@ -164,10 +176,10 @@ pub: } pub struct Context { + ExtraContext // contains fields specific to an implementation pub: cfg Config mut: - read_buf []byte print_buf []byte paused bool enable_su bool @@ -198,3 +210,38 @@ pub struct Config { // All kill signals to set up exit listeners on reset []int = [1, 2, 3, 4, 6, 7, 8, 9, 11, 13, 14, 15, 19] } + +[inline] +fn (ctx &Context) init() { + if ctx.cfg.init_fn != voidptr(0) { + ctx.cfg.init_fn(ctx.cfg.user_data) + } +} + +[inline] +fn (ctx &Context) frame() { + if ctx.cfg.frame_fn != voidptr(0) { + ctx.cfg.frame_fn(ctx.cfg.user_data) + } +} + +[inline] +fn (ctx &Context) cleanup() { + if ctx.cfg.cleanup_fn != voidptr(0) { + ctx.cfg.cleanup_fn(ctx.cfg.user_data) + } +} + +[inline] +fn (ctx &Context) fail(error string) { + if ctx.cfg.fail_fn != voidptr(0) { + ctx.cfg.fail_fn(error) + } +} + +[inline] +fn (ctx &Context) event(event &Event) { + if ctx.cfg.event_fn != voidptr(0) { + ctx.cfg.event_fn(event, ctx.cfg.user_data) + } +} diff --git a/vlib/term/ui/input_nix.c.v b/vlib/term/ui/input_nix.c.v index 363a1920f8..c73c1b5b06 100644 --- a/vlib/term/ui/input_nix.c.v +++ b/vlib/term/ui/input_nix.c.v @@ -1,8 +1,14 @@ -// Copyright (c) 2020 Raúl Hernández. All rights reserved. +// Copyright (c) 2020-2021 Raúl Hernández. All rights reserved. // Use of this source code is governed by an MIT license // that can be found in the LICENSE file. module ui + +struct ExtraContext { +mut: + read_buf []byte +} + const ( ctx_ptr = &Context(0) ) @@ -10,8 +16,8 @@ const ( pub fn init(cfg Config) &Context { mut ctx := &Context{ cfg: cfg, - read_buf: []byte{ cap: cfg.buffer_size } } + ctx.read_buf = []byte{ cap: cfg.buffer_size } // lmao unsafe { @@ -22,11 +28,14 @@ pub fn init(cfg Config) &Context { return ctx } -pub fn (mut ctx Context) save_title() { +[inline] +fn save_title() { // restore the previously saved terminal title print('\x1b[22;0t') } -pub fn (mut ctx Context) load_title() { + +[inline] +fn load_title() { // restore the previously saved terminal title print('\x1b[23;0t') } @@ -57,38 +66,3 @@ 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) - } -} diff --git a/vlib/term/ui/input_windows.c.v b/vlib/term/ui/input_windows.c.v index b1f2485bec..e2b96806ae 100644 --- a/vlib/term/ui/input_windows.c.v +++ b/vlib/term/ui/input_windows.c.v @@ -1,25 +1,297 @@ -// Copyright (c) 2020 Raúl Hernández. All rights reserved. +// Copyright (c) 2020-2021 Raúl Hernández. All rights reserved. // Use of this source code is governed by an MIT license // that can be found in the LICENSE file. module ui +import os +import time + const ( - not_implemented_yet = "term.input: error: Windows support isn't implemented yet" + buf_size = 64 + ctx_ptr = &Context(0) + stdin_at_startup = u32(0) ) +struct ExtraContext { +mut: + stdin_handle C.HANDLE + stdout_handle C.HANDLE + read_buf [buf_size]C.INPUT_RECORD + mouse_down MouseButton +} + +fn restore_terminal_state() { + if 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') + } + C.SetConsoleMode(ctx_ptr.stdin_handle, stdin_at_startup) + } + load_title() + os.flush() +} + pub fn init(cfg Config) &Context { - panic(not_implemented_yet) - return &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) == 0 { + panic('could not get stdin console mode') + } + + // enable extended input flags (see https://stackoverflow.com/a/46802726) + // 0x80 == C.ENABLE_EXTENDED_FLAGS + if C.SetConsoleMode(stdin_handle, 0x80) == 0 { + panic('could not set raw input mode') + } + // enable window and mouse input events. + if C.SetConsoleMode(stdin_handle, C.ENABLE_WINDOW_INPUT | C.ENABLE_MOUSE_INPUT) == 0 { + panic('could not set raw input mode') + } + // store the current title, so restore_terminal_state can get it back + save_title() + + if ctx.cfg.use_alternate_buffer { + // switch to the alternate buffer + print('\x1b[?1049h') + // clear the terminal and set the cursor to the origin + print('\x1b[2J\x1b[3J\x1b[1;1H') + } + + if ctx.cfg.hide_cursor { + print('\x1b[?25l') + } + + if ctx.cfg.window_title != '' { + print('\x1b]0;$ctx.cfg.window_title\x07') + } + + unsafe { + x := &ctx_ptr + *x = ctx + } + + C.atexit(restore_terminal_state) + for code in ctx.cfg.reset { + os.signal(code, fn() { + mut c := ctx_ptr + if c != 0 { + c.cleanup() + } + exit(0) + }) + } + + ctx.stdin_handle = stdin_handle + ctx.stdout_handle = stdout_handle + return ctx } pub fn (mut ctx Context) run() ? { - panic(not_implemented_yet) + 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.usleep(sleep_len) + } + if !ctx.paused { + sw.restart() + if ctx.cfg.event_fn != voidptr(0) { + ctx.parse_events() + } + ctx.frame() + sw.pause() + e := sw.elapsed().microseconds() + sleep_len = frame_time - int(e) + ctx.frame_count++ + } + } } -pub fn (mut ctx Context) save_title() { - panic(not_implemented_yet) +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, buf_size, &nr_events) { + panic('could not read from stdin') + } + for i in 0 .. nr_events { + // print('E ') + match int(ctx.read_buf[i].EventType) { + C.KEY_EVENT { + e := unsafe { ctx.read_buf[i].Event.KeyEvent } + ch := e.wVirtualKeyCode + ascii := unsafe { e.uChar.AsciiChar } + if e.bKeyDown == 0 { continue } // we don't handle key_up events because they don't exist on linux... + // see: https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes + code := match int(ch) { + C.VK_BACK { KeyCode.backspace } + C.VK_RETURN { KeyCode.enter } + C.VK_PRIOR { KeyCode.page_up } + 14 ... 20 { KeyCode.null } + C.VK_NEXT { KeyCode.page_down } + C.VK_END { KeyCode.end } + C.VK_HOME { KeyCode.home } + C.VK_LEFT { KeyCode.left } + C.VK_UP { KeyCode.up } + C.VK_RIGHT { KeyCode.right } + C.VK_DOWN { KeyCode.down } + C.VK_INSERT { KeyCode.insert } + C.VK_DELETE { KeyCode.delete } + 65 ... 90 { KeyCode(ch + 32) } // letters + 91 ... 93 { KeyCode.null } // special keys + 96 ... 105 { KeyCode(ch - 48) } // numpad numbers + 112 ... 135 { KeyCode(ch + 178) } // f1 - f24 + else { KeyCode(ascii) } + } + + mut modifiers := u32(0) + if e.dwControlKeyState & (0x1 | 0x2) != 0 { modifiers |= alt } + if e.dwControlKeyState & (0x4 | 0x8) != 0 { modifiers |= ctrl } + if e.dwControlKeyState & 0x10 != 0 { modifiers |= shift } + + mut event := &Event{ + typ: .key_down + modifiers: modifiers + code: code + ascii: ascii + width: int(e.dwControlKeyState) + height: int(e.wVirtualKeyCode) + utf8: unsafe { e.uChar.UnicodeChar.str() } + } + ctx.event(event) + } + C.MOUSE_EVENT { + e := unsafe { ctx.read_buf[i].Event.MouseEvent } + sb_info := C.CONSOLE_SCREEN_BUFFER_INFO{} + if !C.GetConsoleScreenBufferInfo(ctx.stdout_handle, &sb_info) { + panic('could not get screenbuffer info') + } + x := e.dwMousePosition.X + y := int(e.dwMousePosition.Y) - sb_info.srWindow.Top + mut modifiers := u32(0) + if e.dwControlKeyState & (0x1 | 0x2) != 0 { modifiers |= alt } + if e.dwControlKeyState & (0x4 | 0x8) != 0 { modifiers |= ctrl } + if e.dwControlKeyState & 0x10 != 0 { modifiers |= shift } + // TODO: handle capslock/numlock/etc?? events exist for those keys + match int(e.dwEventFlags) { + C.MOUSE_MOVED { + mut button := match int(e.dwButtonState) { + 0 { MouseButton.unknown } + 1 { MouseButton.left } + 2 { MouseButton.right } + else { MouseButton.middle } + } + typ := if e.dwButtonState == 0 { + if ctx.mouse_down != .unknown { + button = ctx.mouse_down + ctx.mouse_down = .unknown + EventType.mouse_up + } else { + EventType.mouse_move + } + } else { + EventType.mouse_drag + } + ctx.event(&Event{ + typ: typ + x: x + y: y + button: button + modifiers: modifiers + }) + } C.MOUSE_WHEELED { + ctx.event(&Event{ + typ: .mouse_scroll + direction: if i16(e.dwButtonState >> 16) < 0 { Direction.up } else { Direction.down } + x: x + y: y + modifiers: modifiers + }) + } 0x0008 /* C.MOUSE_HWHEELED */ { + ctx.event(&Event{ + typ: .mouse_scroll + direction: if i16(e.dwButtonState >> 16) < 0 { Direction.right } else { Direction.left } + x: x + y: y + modifiers: modifiers + }) + } 0 /* CLICK */, C.DOUBLE_CLICK { + button := match int(e.dwButtonState) { + 0 { ctx.mouse_down } + 1 { MouseButton.left } + 2 { MouseButton.right } + else { MouseButton.middle } + } + ctx.mouse_down = button + ctx.event(&Event{ + typ: .mouse_down + x: x + y: y + button: button + modifiers: modifiers + }) + } else {} + } + } + C.WINDOW_BUFFER_SIZE_EVENT { + // e := unsafe { ctx.read_buf[i].Event.WindowBufferSizeEvent } + sb := C.CONSOLE_SCREEN_BUFFER_INFO{} + if !C.GetConsoleScreenBufferInfo(ctx.stdout_handle, &sb) { + panic('could not get screenbuffer info') + } + w := sb.srWindow.Right - sb.srWindow.Left + 1 + h := sb.srWindow.Bottom - sb.srWindow.Top + 1 + utf8 := '($ctx.window_width, $ctx.window_height) -> ($w, $h)' + if w != ctx.window_width || h != ctx.window_height { + ctx.window_width, ctx.window_height = w, h + mut event := &Event{ + typ: .resized + width: ctx.window_width + height: ctx.window_height + utf8: utf8 + } + ctx.event(event) + } + } + // C.MENU_EVENT { + // e := unsafe { ctx.read_buf[i].Event.MenuEvent } + // } + // C.FOCUS_EVENT { + // e := unsafe { ctx.read_buf[i].Event.FocusEvent } + // } + else {} + } + } } -pub fn (mut ctx Context) load_title() { - panic(not_implemented_yet) +[inline] +fn save_title() { + // restore the previously saved terminal title + print('\x1b[22;0t') +} + +[inline] +fn load_title() { + // restore the previously saved terminal title + print('\x1b[23;0t') } diff --git a/vlib/term/ui/termios_nix.c.v b/vlib/term/ui/termios_nix.c.v index c1b17d3336..f612711668 100644 --- a/vlib/term/ui/termios_nix.c.v +++ b/vlib/term/ui/termios_nix.c.v @@ -1,4 +1,4 @@ -// Copyright (c) 2020 Raúl Hernández. All rights reserved. +// 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 @@ -50,14 +50,14 @@ fn restore_terminal_state() { mut c := ctx_ptr if c != 0 { c.paused = true - c.load_title() + load_title() } os.flush() } fn (mut ctx Context) termios_setup() ? { // store the current title, so restore_terminal_state can get it back - ctx.save_title() + save_title() if !ctx.cfg.skip_init_checks && !(is_atty(C.STDIN_FILENO) != 0 && is_atty(C.STDOUT_FILENO) != 0) { return error('not running under a TTY') diff --git a/vlib/term/ui/ui.v b/vlib/term/ui/ui.v index e68b2598e5..dd99a5bdbb 100644 --- a/vlib/term/ui/ui.v +++ b/vlib/term/ui/ui.v @@ -1,4 +1,4 @@ -// Copyright (c) 2020 Raúl Hernández. All rights reserved. +// 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 @@ -35,19 +35,15 @@ pub fn (mut ctx Context) write(s string) { [inline] // flush displays the accumulated print buffer to the screen. pub fn (mut ctx Context) flush() { - $if windows { - // TODO - } $else { - // TODO: Diff the previous frame against this one, and only render things that changed? - if !ctx.enable_su { - C.write(C.STDOUT_FILENO, ctx.print_buf.data, ctx.print_buf.len) - } else { - 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() + // 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.