From c315218ed1cb8858b35db7845e79b52045d83d43 Mon Sep 17 00:00:00 2001 From: spaceface777 Date: Fri, 13 Nov 2020 14:30:47 +0100 Subject: [PATCH] term.ui: allow setting the terminal title (#6809) --- examples/term.ui/event_viewer.v | 17 ++++---- examples/term.ui/term_drawing.v | 14 ++++--- vlib/term/ui/README.md | 35 +++++++++++++++- vlib/term/ui/input.v | 1 + vlib/term/ui/input_nix.c.v | 18 ++++++++- vlib/term/ui/input_windows.c.v | 16 +++++++- vlib/term/ui/termios_nix.c.v | 72 ++++++++------------------------- vlib/term/ui/ui.v | 13 ++---- 8 files changed, 103 insertions(+), 83 deletions(-) diff --git a/examples/term.ui/event_viewer.v b/examples/term.ui/event_viewer.v index a7bca5c69e..74889e08ba 100644 --- a/examples/term.ui/event_viewer.v +++ b/examples/term.ui/event_viewer.v @@ -6,15 +6,13 @@ mut: } 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') - } + mut app := &App(x) + app.tui.clear() + app.tui.set_cursor_position(0, 0) + app.tui.write('V term.input event viewer (press `esc` to exit)\n\n') + app.tui.write('$e') + app.tui.write('\n\nRaw event bytes: "${e.utf8.bytes().hex()}" = ${e.utf8.bytes()}') + app.tui.flush() if e.typ == .key_down && e.code == .escape { exit(0) } } @@ -24,6 +22,7 @@ app.tui = tui.init( user_data: app, event_fn: event + window_title: 'V term.ui event viewer' hide_cursor: true capture_events: true frame_rate: 60 diff --git a/examples/term.ui/term_drawing.v b/examples/term.ui/term_drawing.v index 1aae4b6063..a341689491 100644 --- a/examples/term.ui/term_drawing.v +++ b/examples/term.ui/term_drawing.v @@ -116,6 +116,7 @@ fn main() { event_fn: event frame_rate: frame_rate hide_cursor: true + window_title: 'V terminal pixelart drawing app' }) app.mouse_pos.x = 40 app.mouse_pos.y = 15 @@ -398,11 +399,13 @@ fn (mut app App) draw_footer() { app.tui.bold() app.tui.draw_text(3, wh - 1, '$select_size $app.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) - } + + // TODO: help button + // 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] @@ -436,6 +439,7 @@ fn (mut app App) footer_click(event &tui.Event) { return } idx := footer_y * 19 - 6 + event.x / 3 + if idx < 0 || idx > 56 { return } color := colors[idx / 19][idx % 19] if event.button == .primary { app.primary_color = color diff --git a/vlib/term/ui/README.md b/vlib/term/ui/README.md index 07964f4af2..21461c8bae 100644 --- a/vlib/term/ui/README.md +++ b/vlib/term/ui/README.md @@ -5,7 +5,39 @@ A V module for designing terminal UI apps #### Quickstart ```v -// todo +import term.ui as tui + +struct App { +mut: + tui &tui.Context = 0 +} + +fn event(e &tui.Event, x voidptr) { + mut app := &App(x) + println(e) +} + +fn frame(x voidptr) { + mut app := &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() +} + +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. @@ -22,6 +54,7 @@ See the `/examples/term.ui/` folder for more usage examples. - `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. + - `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. diff --git a/vlib/term/ui/input.v b/vlib/term/ui/input.v index 4fe5a7e8b8..c33a5f2c01 100644 --- a/vlib/term/ui/input.v +++ b/vlib/term/ui/input.v @@ -187,6 +187,7 @@ pub struct Config { frame_rate int = 30 use_x11 bool + window_title string hide_cursor bool capture_events bool // All kill signals diff --git a/vlib/term/ui/input_nix.c.v b/vlib/term/ui/input_nix.c.v index 95d661c6c2..11d131e9cf 100644 --- a/vlib/term/ui/input_nix.c.v +++ b/vlib/term/ui/input_nix.c.v @@ -9,12 +9,17 @@ pub fn init(cfg Config) &Context { cfg: cfg, read_buf: []byte{ cap: cfg.buffer_size } } + ctx.save_title() if cfg.hide_cursor { - s := '\x1b[?25l' - C.write(C.STDOUT_FILENO, s.str, s.len) + print('\x1b[?25l') } + if cfg.window_title != '' { + print('\x1b]0;$cfg.window_title\x07') + } + + // lmao unsafe { x := &ctx_ptr *x = ctx @@ -23,6 +28,15 @@ pub fn init(cfg Config) &Context { return ctx } +pub fn (mut ctx Context) save_title() { + // restore the previously saved terminal title + print('\x1b[22;0t') +} +pub fn (mut ctx Context) load_title() { + // restore the previously saved terminal title + print('\x1b[23;0t') +} + pub fn (mut ctx Context) run() { if ctx.cfg.use_x11 { ctx.fail('error: x11 backend not implemented yet') diff --git a/vlib/term/ui/input_windows.c.v b/vlib/term/ui/input_windows.c.v index 6cdf1e6625..86677858d2 100644 --- a/vlib/term/ui/input_windows.c.v +++ b/vlib/term/ui/input_windows.c.v @@ -1,10 +1,22 @@ module ui +const ( + not_implemented_yet = "term.input: error: Windows support isn't implemented yet" +) + pub fn init(cfg Config) &Context { - panic("term.input: error: Windows support isn't implemented yet") + panic(not_implemented_yet) return &Context{} } pub fn (mut ctx Context) run() { - panic("term.input: error: Windows support isn't implemented yet") + panic(not_implemented_yet) +} + +pub fn (mut ctx Context) save_title() { + panic(not_implemented_yet) +} + +pub fn (mut ctx Context) load_title() { + panic(not_implemented_yet) } diff --git a/vlib/term/ui/termios_nix.c.v b/vlib/term/ui/termios_nix.c.v index 71eed272a4..427e309ed2 100644 --- a/vlib/term/ui/termios_nix.c.v +++ b/vlib/term/ui/termios_nix.c.v @@ -41,6 +41,15 @@ fn get_terminal_size() (u16, u16) { return winsz.ws_row, winsz.ws_col } +fn restore_terminal_state() { + termios_reset() + mut c := ctx_ptr + if c != 0 { + c.load_title() + } + println('') +} + fn (mut ctx Context) termios_setup() { mut termios := get_termios() @@ -64,11 +73,12 @@ fn (mut ctx Context) termios_setup() { ctx.window_height, ctx.window_width = get_terminal_size() // Reset console on exit - C.atexit(termios_reset) - os.signal(C.SIGTSTP, termios_reset) + C.atexit(restore_terminal_state) + os.signal(C.SIGTSTP, restore_terminal_state) os.signal(C.SIGCONT, fn () { mut c := ctx_ptr if c != 0 { + c.save_title() c.termios_setup() c.window_height, c.window_width = get_terminal_size() mut event := &Event{ @@ -111,56 +121,6 @@ fn termios_reset() { /////////////////////////////////////////// -/* -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 @@ -252,7 +212,7 @@ fn escape_end(buf string) int { for { if i + 1 == buf.len { return buf.len } - if buf[i].is_letter() { + 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` { @@ -260,7 +220,8 @@ fn escape_end(buf string) int { } } 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 @@ -299,7 +260,8 @@ fn escape_sequence(buf_ string) (&Event, int) { // Mouse events // ---------------- - if buf.len > 2 && buf[1] == `<` { // Mouse control + // TODO: rxvt uses different escape sequences for mouse events :/ + if buf.len > 2 && buf[1] == `<` { split := buf[2..].split(';') if split.len < 3 { return &Event(0), 0 } diff --git a/vlib/term/ui/ui.v b/vlib/term/ui/ui.v index 85dc1af6bf..0c25a76456 100644 --- a/vlib/term/ui/ui.v +++ b/vlib/term/ui/ui.v @@ -79,15 +79,10 @@ 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) set_window_title(s string) { + print('\x1b]0;$s\x07') +} [inline] pub fn (mut ctx Context) draw_point(x int, y int) {