From 960264bdeb16ff056287d2869a388d3d5931a60c Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 25 Mar 2026 16:42:17 +0300 Subject: [PATCH] term.ui: add support for raw linux tty (fixes #25463) --- vlib/term/ui/input.v | 38 ++++++++++++++++++--- vlib/term/ui/input_nix.c.v | 16 ++++++++- vlib/term/ui/termios_nix.c.v | 59 +++++++++++++++++++++------------ vlib/term/ui/termios_nix_test.v | 18 ++++++++++ vlib/term/ui/ui.c.v | 28 ++++++++++++++-- 5 files changed, 130 insertions(+), 29 deletions(-) diff --git a/vlib/term/ui/input.v b/vlib/term/ui/input.v index ac4620579..1659636d7 100644 --- a/vlib/term/ui/input.v +++ b/vlib/term/ui/input.v @@ -149,6 +149,31 @@ pub enum Modifiers { alt } +struct TerminalCapabilities { + enable_ansi256 bool = true + supports_alternate_buffer bool = true + supports_sgr_mouse bool = true + supports_sync_updates bool = true + supports_window_title bool = true +} + +fn terminal_capabilities_for(term_name string) TerminalCapabilities { + if term_name == 'linux' { + return TerminalCapabilities{ + enable_ansi256: false + supports_alternate_buffer: false + supports_sgr_mouse: false + supports_sync_updates: false + supports_window_title: false + } + } + return TerminalCapabilities{} +} + +fn current_terminal_capabilities() TerminalCapabilities { + return terminal_capabilities_for(os.getenv('TERM')) +} + pub struct Event { pub: typ EventType @@ -172,10 +197,15 @@ pub struct Context { pub: cfg Config // the initial configuration, passed to ui.init() mut: - print_buf []u8 - paused bool - enable_su bool - enable_rgb bool + print_buf []u8 + paused bool + enable_su bool + enable_rgb bool + enable_ansi256 bool = true + supports_alternate_buffer bool = true + supports_sgr_mouse bool = true + supports_sync_updates bool = true + supports_window_title bool = true pub mut: frame_count u64 window_width int diff --git a/vlib/term/ui/input_nix.c.v b/vlib/term/ui/input_nix.c.v index ccade8e09..72c36ed10 100644 --- a/vlib/term/ui/input_nix.c.v +++ b/vlib/term/ui/input_nix.c.v @@ -16,8 +16,14 @@ __global ctx_ptr = &Context(unsafe { nil }) // init initializes the terminal console with Config `cfg`. pub fn init(cfg Config) &Context { + caps := current_terminal_capabilities() mut ctx := &Context{ - cfg: cfg + cfg: cfg + enable_ansi256: caps.enable_ansi256 + supports_alternate_buffer: caps.supports_alternate_buffer + supports_sgr_mouse: caps.supports_sgr_mouse + supports_sync_updates: caps.supports_sync_updates + supports_window_title: caps.supports_window_title } ctx.read_buf = []u8{cap: cfg.buffer_size} @@ -27,6 +33,10 @@ pub fn init(cfg Config) &Context { @[inline] fn save_title() { + mut ctx := ctx_ptr + if unsafe { ctx == 0 } || !ctx.supports_window_title { + return + } // restore the previously saved terminal title print('\x1b[22;0t') flush_stdout() @@ -34,6 +44,10 @@ fn save_title() { @[inline] fn load_title() { + mut ctx := ctx_ptr + if unsafe { ctx == 0 } || !ctx.supports_window_title { + return + } // restore the previously saved terminal title print('\x1b[23;0t') flush_stdout() diff --git a/vlib/term/ui/termios_nix.c.v b/vlib/term/ui/termios_nix.c.v index 6ef91ea43..88d4c512d 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 != '' { + if ctx.cfg.window_title != '' && ctx.supports_window_title { print('\x1b]0;${ctx.cfg.window_title}\x07') flush_stdout() } @@ -81,30 +81,37 @@ 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) - // 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() + 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 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 - print('\x1b[?1003h\x1b[?1006h') - flush_stdout() - if ctx.cfg.use_alternate_buffer { + if ctx.supports_sgr_mouse { + print('\x1b[?1003h\x1b[?1006h') + flush_stdout() + } + if ctx.cfg.use_alternate_buffer && ctx.supports_alternate_buffer { // switch to the alternate buffer print('\x1b[?1049h') flush_stdout() @@ -203,11 +210,19 @@ 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 } && c.cfg.use_alternate_buffer { - print('\x1b[?1049l') + 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() } os.flush() } diff --git a/vlib/term/ui/termios_nix_test.v b/vlib/term/ui/termios_nix_test.v index ab7049808..cda600cc2 100644 --- a/vlib/term/ui/termios_nix_test.v +++ b/vlib/term/ui/termios_nix_test.v @@ -2,6 +2,24 @@ module ui import os +fn test_terminal_capabilities_disable_xterm_features_for_linux_console() { + caps := terminal_capabilities_for('linux') + assert !caps.enable_ansi256 + assert !caps.supports_alternate_buffer + assert !caps.supports_sgr_mouse + assert !caps.supports_sync_updates + assert !caps.supports_window_title +} + +fn test_terminal_capabilities_keep_xterm_defaults() { + caps := terminal_capabilities_for('xterm-256color') + assert caps.enable_ansi256 + assert caps.supports_alternate_buffer + assert caps.supports_sgr_mouse + assert caps.supports_sync_updates + assert caps.supports_window_title +} + fn test_get_cursor_position_reads_valid_row_column_data() ! { mut original_stdin_fd := -1 unsafe { diff --git a/vlib/term/ui/ui.c.v b/vlib/term/ui/ui.c.v index aa7e87906..bacacbb42 100644 --- a/vlib/term/ui/ui.c.v +++ b/vlib/term/ui/ui.c.v @@ -75,8 +75,10 @@ pub fn (mut ctx Context) hide_cursor() { pub fn (mut ctx Context) set_color(c Color) { if ctx.enable_rgb { ctx.write('\x1b[38;2;${int(c.r)};${int(c.g)};${int(c.b)}m') - } else { + } else if ctx.enable_ansi256 { ctx.write('\x1b[38;5;${rgb2ansi(c.r, c.g, c.b)}m') + } else { + ctx.write('\x1b[${30 + rgb2basic_ansi(c.r, c.g, c.b)}m') } } @@ -85,8 +87,10 @@ pub fn (mut ctx Context) set_color(c Color) { pub fn (mut ctx Context) set_bg_color(c Color) { if ctx.enable_rgb { ctx.write('\x1b[48;2;${int(c.r)};${int(c.g)};${int(c.b)}m') - } else { + } else if ctx.enable_ansi256 { ctx.write('\x1b[48;5;${rgb2ansi(c.r, c.g, c.b)}m') + } else { + ctx.write('\x1b[${40 + rgb2basic_ansi(c.r, c.g, c.b)}m') } } @@ -117,10 +121,30 @@ pub fn (mut ctx Context) clear() { // set_window_title sets the string `s` as the window title. @[inline] pub fn (mut ctx Context) set_window_title(s string) { + if !ctx.supports_window_title { + return + } print('\x1b]0;${s}\x07') flush_stdout() } +fn rgb2basic_ansi(r int, g int, b int) int { + mut best_index := 0 + mut best_distance := -1 + for i in 0 .. 8 { + ref := color_table[i] + dr := r - int((ref >> 16) & 0xff) + dg := g - int((ref >> 8) & 0xff) + db := b - int(ref & 0xff) + distance := dr * dr + dg * dg + db * db + if best_distance == -1 || distance < best_distance { + best_distance = distance + best_index = i + } + } + return best_index +} + // draw_point draws a point at position `x`,`y`. @[inline] pub fn (mut ctx Context) draw_point(x int, y int) { -- 2.39.5