From 826f85f6e8540f2091e60bc6c9fd74e9cf48e2b2 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 25 Mar 2026 16:42:17 +0300 Subject: [PATCH] term.ui: fix mouse text marking for copy/paste (fixes #16191) --- vlib/term/ui/README.md | 8 +++-- vlib/term/ui/input.v | 1 + vlib/term/ui/input_windows.c.v | 8 +++-- vlib/term/ui/mouse_config_test.v | 13 ++++++++ vlib/term/ui/termios_nix.c.v | 57 ++++++++++++-------------------- 5 files changed, 48 insertions(+), 39 deletions(-) create mode 100644 vlib/term/ui/mouse_config_test.v diff --git a/vlib/term/ui/README.md b/vlib/term/ui/README.md index 70daca04f..98bbd2dc4 100644 --- a/vlib/term/ui/README.md +++ b/vlib/term/ui/README.md @@ -68,6 +68,9 @@ See the `/examples/term.ui/` folder for more usage examples. - `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. +- `mouse_enabled bool` - enables terminal mouse tracking for `mouse_*` events. + Leave this off unless your app actually handles mouse input, so the terminal can still use the + mouse for regular text selection and copy/paste. - `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, @@ -89,11 +92,12 @@ 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). +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. \ No newline at end of file +nor a reliable way of converting these into `keydown` / `keyup` events. diff --git a/vlib/term/ui/input.v b/vlib/term/ui/input.v index 1659636d7..ef307ba00 100644 --- a/vlib/term/ui/input.v +++ b/vlib/term/ui/input.v @@ -228,6 +228,7 @@ pub: window_title string hide_cursor bool capture_events bool + mouse_enabled bool use_alternate_buffer bool = true skip_init_checks bool // All kill signals to set up exit listeners on: diff --git a/vlib/term/ui/input_windows.c.v b/vlib/term/ui/input_windows.c.v index d43b148d2..e2d63316e 100644 --- a/vlib/term/ui/input_windows.c.v +++ b/vlib/term/ui/input_windows.c.v @@ -56,8 +56,12 @@ pub fn init(cfg Config) &Context { if !C.SetConsoleMode(stdin_handle, 0x80) { 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) { + mut input_mode := u32(C.ENABLE_WINDOW_INPUT) + if ctx.cfg.mouse_enabled { + input_mode |= C.ENABLE_MOUSE_INPUT + } + // enable window input and optionally mouse input events. + if !C.SetConsoleMode(stdin_handle, input_mode) { panic('could not set raw input mode') } // store the current title, so restore_terminal_state can get it back diff --git a/vlib/term/ui/mouse_config_test.v b/vlib/term/ui/mouse_config_test.v new file mode 100644 index 000000000..480ebe0d0 --- /dev/null +++ b/vlib/term/ui/mouse_config_test.v @@ -0,0 +1,13 @@ +module ui + +fn test_mouse_tracking_is_disabled_by_default() { + cfg := Config{} + assert !cfg.mouse_enabled +} + +fn test_mouse_tracking_can_be_enabled_explicitly() { + cfg := Config{ + mouse_enabled: true + } + assert cfg.mouse_enabled +} diff --git a/vlib/term/ui/termios_nix.c.v b/vlib/term/ui/termios_nix.c.v index 88d4c512d..4b74264a4 100644 --- a/vlib/term/ui/termios_nix.c.v +++ b/vlib/term/ui/termios_nix.c.v @@ -70,7 +70,7 @@ fn (mut ctx Context) termios_setup() ! { ctx.flush() } - if ctx.cfg.window_title != '' && ctx.supports_window_title { + if ctx.cfg.window_title != '' { print('\x1b]0;${ctx.cfg.window_title}\x07') flush_stdout() } @@ -81,37 +81,32 @@ fn (mut ctx Context) termios_setup() ! { tios.c_cc[C.VTIME] = 1 tios.c_cc[C.VMIN] = 0 termios.tcsetattr(C.STDIN_FILENO, C.TCSAFLUSH, mut tios) - if ctx.supports_sync_updates { - // feature-test the SU spec - sx, sy := get_cursor_position() - print('${bsu}${esu}') - flush_stdout() - 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() - } - } - // linux console only advertises the base palette, so keep using the standard SGR colors there. - if ctx.enable_ansi256 { - // feature-test rgb (truecolor) support - ctx.enable_rgb = supports_truecolor() + // feature-test the SU spec + sx, sy := get_cursor_position() + print('${bsu}${esu}') + flush_stdout() + 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 tios.c_cc[C.VTIME] = 0 tios.c_cc[C.VMIN] = 0 termios.tcsetattr(C.STDIN_FILENO, C.TCSAFLUSH, mut tios) - // enable mouse input - if ctx.supports_sgr_mouse { + if ctx.cfg.mouse_enabled { + // enable mouse input print('\x1b[?1003h\x1b[?1006h') flush_stdout() } - if ctx.cfg.use_alternate_buffer && ctx.supports_alternate_buffer { + if ctx.cfg.use_alternate_buffer { // switch to the alternate buffer print('\x1b[?1049h') flush_stdout() @@ -210,19 +205,11 @@ fn termios_reset() { // C.TCSANOW ?? mut startup := termios_at_startup termios.tcsetattr(C.STDIN_FILENO, C.TCSAFLUSH, mut startup) + print('\x1b[?1003l\x1b[?1006l\x1b[?25h') + flush_stdout() c := ctx_ptr - if unsafe { c != 0 } { - if c.supports_sgr_mouse { - print('\x1b[?1003l\x1b[?1006l') - } - print('\x1b[?25h') - flush_stdout() - if c.cfg.use_alternate_buffer && c.supports_alternate_buffer { - print('\x1b[?1049l') - } - } else { - print('\x1b[?25h') - flush_stdout() + if unsafe { c != 0 } && c.cfg.use_alternate_buffer { + print('\x1b[?1049l') } os.flush() } -- 2.39.5