From e03ae1937205978e00144900aed3b67951c487e3 Mon Sep 17 00:00:00 2001 From: spaceface777 Date: Thu, 26 Nov 2020 00:28:57 +0100 Subject: [PATCH] term.ui: approximate colors into ansi if rgb isn't supported (#6951) --- examples/term.ui/term_drawing.v | 3 + vlib/term/ui/color.v | 88 +++++++++++++++++++++++++++++ vlib/term/ui/input.v | 34 ++++++----- vlib/term/ui/input_nix.c.v | 7 ++- vlib/term/ui/input_windows.c.v | 3 + vlib/term/ui/termios_nix.c.v | 99 ++++++++++++++++++++++++++++----- vlib/term/ui/ui.v | 15 ++++- 7 files changed, 215 insertions(+), 34 deletions(-) create mode 100644 vlib/term/ui/color.v diff --git a/examples/term.ui/term_drawing.v b/examples/term.ui/term_drawing.v index cec69ab59a..77314b7a75 100644 --- a/examples/term.ui/term_drawing.v +++ b/examples/term.ui/term_drawing.v @@ -1,3 +1,6 @@ +// Copyright (c) 2020 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 main import term.ui as tui diff --git a/vlib/term/ui/color.v b/vlib/term/ui/color.v new file mode 100644 index 0000000000..73b8f86151 --- /dev/null +++ b/vlib/term/ui/color.v @@ -0,0 +1,88 @@ +// radare - LGPL - Copyright 2013-2020 - pancake, xarkes +// ansi 256 color extension for r_cons +// https://en.wikipedia.org/wiki/ANSI_color + +module ui + +const ( + value_range = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]!! + color_table = init_color_table() +) + +[direct_array_access] +fn init_color_table() []int { + mut color_table := []int{len: 256} + // ansi colors + color_table[0] = 0x000000 + color_table[1] = 0x800000 + color_table[2] = 0x008000 + color_table[3] = 0x808000 + color_table[4] = 0x000080 + color_table[5] = 0x800080 + color_table[6] = 0x008080 + color_table[7] = 0xc0c0c0 + color_table[8] = 0x808080 + color_table[9] = 0xff0000 + color_table[10] = 0x00ff00 + color_table[11] = 0xffff00 + color_table[12] = 0x0000ff + color_table[13] = 0xff00ff + color_table[14] = 0x00ffff + color_table[15] = 0xffffff + // color palette + for i in 0 .. 216 { + r := value_range[(i / 36) % 6] + g := value_range[(i / 6) % 6] + b := value_range[i % 6] + color_table[i + 16] = ((r << 16) & 0xffffff) + ((g << 8) & 0xffff) + (b & 0xff) + } + // grayscale + for i in 0 .. 24 { + r := 8 + (i * 10) + color_table[i + 232] = ((r << 16) & 0xffffff) + ((r << 8) & 0xffff) + (r & 0xff) + } + return color_table +} + +fn clamp(x int, y int, z int) int { + if x < y { + return y + } + if x > z { + return z + } + return x +} + +fn approximate_rgb(r int, g int, b int) int { + grey := r > 0 && r < 255 && r == g && r == b + if grey { + return 232 + int(f64(r) / (255 / 24.1)) + } + k := int(256.0 / 6) + r2 := clamp(r / k, 0, 5) + g2 := clamp(g / k, 0, 5) + b2 := clamp(b / k, 0, 5) + return 16 + (r2 * 36) + (g2 * 6) + b2 +} + +fn lookup_rgb(r int, g int, b int) int { + color := (r << 16) + (g << 8) + b + // lookup extended colors only, coz non-extended can be changed by users. + for i in 16 .. 256 { + if color_table[i] == color { + return i + } + } + return -1 +} + +// converts an RGB color to an ANSI 256-color, approximating it to the nearest available color +// if an exact match is not found +fn rgb2ansi(r int, g int, b int) int { + c := lookup_rgb(r, g, b) + if c == -1 { + return approximate_rgb(r, g, b) + } + return c +} diff --git a/vlib/term/ui/input.v b/vlib/term/ui/input.v index 6a3e2f510d..7a4792bb9d 100644 --- a/vlib/term/ui/input.v +++ b/vlib/term/ui/input.v @@ -1,3 +1,6 @@ +// Copyright (c) 2020 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 pub enum KeyCode { @@ -164,11 +167,11 @@ pub struct Context { pub: cfg Config mut: - termios C.termios read_buf []byte print_buf []byte paused bool enable_su bool + enable_rgb bool pub mut: frame_count u64 window_width int @@ -176,21 +179,22 @@ pub mut: } pub struct Config { - user_data voidptr - init_fn fn(voidptr) - frame_fn fn(voidptr) - cleanup_fn fn(voidptr) - event_fn fn(&Event, voidptr) - fail_fn fn(string) + user_data voidptr + init_fn fn(voidptr) + frame_fn fn(voidptr) + cleanup_fn fn(voidptr) + event_fn fn(&Event, voidptr) + fail_fn fn(string) - buffer_size int = 256 - frame_rate int = 30 - use_x11 bool + buffer_size int = 256 + frame_rate int = 30 + use_x11 bool - window_title string - hide_cursor bool - capture_events bool + window_title string + hide_cursor bool + capture_events bool use_alternate_buffer bool = true - // All kill signals - reset []int = [1, 2, 3, 4, 6, 7, 8, 9, 11, 13, 14, 15, 19] + skip_init_checks bool + // All kill signals to set up exit listeners on + reset []int = [1, 2, 3, 4, 6, 7, 8, 9, 11, 13, 14, 15, 19] } diff --git a/vlib/term/ui/input_nix.c.v b/vlib/term/ui/input_nix.c.v index 459e8942d5..5559acd86d 100644 --- a/vlib/term/ui/input_nix.c.v +++ b/vlib/term/ui/input_nix.c.v @@ -1,3 +1,6 @@ +// Copyright (c) 2020 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 const ( @@ -28,12 +31,12 @@ pub fn (mut ctx Context) load_title() { print('\x1b[23;0t') } -pub fn (mut ctx Context) run() { +pub fn (mut ctx Context) run() ? { if ctx.cfg.use_x11 { ctx.fail('error: x11 backend not implemented yet') exit(1) } else { - ctx.termios_setup() + ctx.termios_setup()? ctx.termios_loop() } } diff --git a/vlib/term/ui/input_windows.c.v b/vlib/term/ui/input_windows.c.v index 86677858d2..eb1fb913d2 100644 --- a/vlib/term/ui/input_windows.c.v +++ b/vlib/term/ui/input_windows.c.v @@ -1,3 +1,6 @@ +// Copyright (c) 2020 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 const ( diff --git a/vlib/term/ui/termios_nix.c.v b/vlib/term/ui/termios_nix.c.v index 7a5572fe45..699c5f673d 100644 --- a/vlib/term/ui/termios_nix.c.v +++ b/vlib/term/ui/termios_nix.c.v @@ -1,3 +1,6 @@ +// Copyright (c) 2020 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 @@ -51,10 +54,14 @@ fn restore_terminal_state() { os.flush() } -fn (mut ctx Context) termios_setup() { +fn (mut ctx Context) termios_setup() ? { // store the current title, so restore_terminal_state can get it back ctx.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') + } + mut termios := get_termios() if ctx.cfg.capture_events { @@ -75,20 +82,28 @@ fn (mut ctx Context) termios_setup() { print('\x1b]0;$ctx.cfg.window_title\x07') } - C.tcsetattr(C.STDIN_FILENO, C.TCSAFLUSH, &termios) - // feature-test the SU spec - sx, sy := get_cursor_position() - print('$bsu$esu') - 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.cfg.skip_init_checks { + // prevent blocking during the feature detections, but allow enough time for the terminal + // to send back the relevant input data + termios.c_cc[C.VTIME] = 1 + termios.c_cc[C.VMIN] = 0 + C.tcsetattr(C.STDIN_FILENO, C.TCSAFLUSH, &termios) + // feature-test the SU spec + sx, sy := get_cursor_position() + print('$bsu$esu') + 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 termios.c_cc[C.VTIME] = 0 termios.c_cc[C.VMIN] = 0 @@ -101,7 +116,6 @@ fn (mut ctx Context) termios_setup() { // clear the terminal and set the cursor to the origin print('\x1b[2J\x1b[3J\x1b[1;1H') } - ctx.termios = termios ctx.window_height, ctx.window_width = get_terminal_size() // Reset console on exit @@ -179,6 +193,61 @@ fn get_cursor_position() (int, int) { return x, y } +fn supports_truecolor() bool { + // set the bg color to some arbirtrary value (#010203), assumed not to be the default + print('\x1b[48:2:1:2:3m') + + sx, sy := get_cursor_position() + // sequence to query the current cursor position + print('\x1bP\$qm\x1b\\') + color := get_current_bg_color() + ex, ey := get_cursor_position() + // if the terminal doesn't understand the "get current color", + // assume it doesn't support truecolor either + if !(sx == ex && sy == ey) { + println('>>> different pos: ($sx, $sy) -> ($ex, $ey)') + return false + } + // TODO: iTerm emits a different sequence, but it's compatible anyways, so + if color !in [0x010203, 0x01010203] { + println('>>> no match: $color') + return false + } + return true +} + +fn get_current_bg_color() int { + mut res := 0 + mut colon_cnt := 0 + mut cur_val := 0 + + for i := 0; i < 50 ; i++ { + ch := int(C.getchar()) + b := byte(ch) + + if b in [0, 255] { + return -1 + } else if b == `m` { + if colon_cnt > 1 { + res = (res << 8) | cur_val + cur_val = 0 + } + break + } else if b in [`:`, `;`] { + if colon_cnt > 1 { + res = (res << 8) | cur_val + cur_val = 0 + } + colon_cnt++ + } else if b.is_digit() { + if colon_cnt > 1 { + cur_val = cur_val * 10 + b - byte(`0`) + } + } + } + return res +} + fn termios_reset() { C.tcsetattr(C.STDIN_FILENO, C.TCSAFLUSH /* C.TCSANOW ?? */, &termios_at_startup) print('\x1b[?1003l\x1b[?1006l\x1b[?25h') diff --git a/vlib/term/ui/ui.v b/vlib/term/ui/ui.v index 7be42b68a9..7198946944 100644 --- a/vlib/term/ui/ui.v +++ b/vlib/term/ui/ui.v @@ -1,3 +1,6 @@ +// Copyright (c) 2020 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 strings @@ -57,12 +60,20 @@ pub fn (mut ctx Context) set_cursor_position(x int, y int) { [inline] pub fn (mut ctx Context) set_color(c Color) { - ctx.write('\x1b[38;2;${int(c.r)};${int(c.g)};${int(c.b)}m') + if ctx.enable_rgb { + ctx.write('\x1b[38;2;${int(c.r)};${int(c.g)};${int(c.b)}m') + } else { + ctx.write('\x1b[38;5;${rgb2ansi(c.r, c.g, c.b)}m') + } } [inline] pub fn (mut ctx Context) set_bg_color(c Color) { - ctx.write('\x1b[48;2;${int(c.r)};${int(c.g)};${int(c.b)}m') + if ctx.enable_rgb { + ctx.write('\x1b[48;2;${int(c.r)};${int(c.g)};${int(c.b)}m') + } else { + ctx.write('\x1b[48;5;${rgb2ansi(c.r, c.g, c.b)}m') + } } [inline]