term.ui: native Windows console implementation (#8359)

pull/8376/head
spaceface 2021-01-27 13:52:39 +01:00 committed by GitHub
parent 2ada7b730e
commit e233911a7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 468 additions and 90 deletions

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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')
}

View File

@ -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')

View File

@ -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.