Initial project import

This commit is contained in:
Agila Forge Importer committed 2026-07-13 15:20:32 +00:00
commit 23f2d29e32
11957 files changed
+1754577

No files matched your search

+130
View File
@@ -0,0 +1,130 @@
## Description
The `term` module is designed to provide the building blocks for building
very simple TUI apps. For more complex apps, you should really look at the
`term.ui` module, as it includes terminal events, is easier to use and
is much more performant for large draws.
## Usage
You can use the `term` module to either color the output on a terminal
or to decide on where to put the output in your terminal.
For example let's make a simple program which prints colored text in the middle of the terminal.
```v
import term
import os
fn main() {
term.clear() // clears the content in the terminal
width, height := term.get_terminal_size() // get the size of the terminal
term.set_cursor_position(x: width / 2, y: height / 2) // now we point the cursor to the middle of the terminal
println(term.strikethrough(term.bright_green('hello world'))) // Print green text
term.set_cursor_position(x: 0, y: height) // Sets the position of the cursor to the bottom of the terminal
// Keep prompting until the user presses the q key
for {
if var := os.input_opt('press q to quit: ') {
if var != 'q' {
continue
}
break
}
println('')
break
}
println('Goodbye.')
}
```
This simple program covers many of the principal aspects of the `term ` module.
## API
Here are some of the main functions of the `term` module. Note that the
coloring/styling functions like `ok_message`, `yellow`, `bold`, etc., do
*not* print anything by themselves — they return a new string with ANSI
escape codes embedded, which you can then pass to `println` (or similar)
to actually display the styled text on stdout.
```v
import term
import os
// returns the height and the width of the terminal
width, height := term.get_terminal_size()
println('terminal dimensions: width: ${width} height: ${height}')
mut output := ''
// returns the string as green text
output = 'ok_message() text ' + term.ok_message('is green')
println(output)
// returns the string as red text
output = 'fail_message() text ' + term.fail_message('is red')
println(output)
// returns the string as yellow text
output = 'warn_message() text ' + term.warn_message('is yellow')
println(output)
os.input('hit Enter to clear the console and continue')
// clears the entire terminal
term.clear()
// Set the color output of the text.
// The available colors are:
// black, white, blue, yellow,
// green, red, cyan, magenta,
// bright_black, bright_white, bright_blue, bright_yellow,
// bright_green, bright_red, bright_cyan, bright_magenta,
output = 'yellow() - ' + term.yellow('text')
println(output)
// transforms the given string into bold text
output = 'bold() - ' + term.bold('text')
println(output)
// puts a strikethrough into the given string
output = 'strikethrough() - ' + term.strikethrough('text')
println(output)
// underlines the given string
output = 'underline() - ' + term.underline('text')
println(output)
// colors the background of the output following the given color
// the available colors are: black, blue, yellow, green, cyan, gray
output = 'bg_green() - ' + term.bg_green('text')
println(output)
// sets the position of the cursor
term.set_cursor_position(x: 5, y: 10)
println('Cursor at (5,10)')
// flashes (blinks) the text
output = term.slow_blink('done')
println(output)
```
The `term` module also provides several lower-level cursor-control
helpers, which write directly to stdout:
```v
import term
// moves the cursor up
term.cursor_up(1)
// moves the cursor down
term.cursor_down(1)
// moves the cursor to the right
term.cursor_forward(2)
// moves the cursor to the left
term.cursor_back(2)
// hides the cursor
term.hide_cursor()
// shows the cursor
term.show_cursor()
```
+350
View File
@@ -0,0 +1,350 @@
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module term
import strings
// format_esc produces an ANSI escape code, for selecting the graphics rendition of the following text.
// Each of the attributes that can be passed in `code`, separated by `;`, will be in effect,
// until the terminal encounters another SGR ANSI escape code. For more details about the different
// codes, and their meaning, see: https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters
pub fn format_esc(code string) string {
return '\x1b[${code}m'
}
// format returns ANSI escape coded `msg` formatted with a preceding `open` and a succeeding `close`.
// For instance, `format('hi', '9', '29')` returns `'\x1b[9mhi\x1b[29m'`,
// or 'hi' with strikethrough, where `'\x1b[9m'` represents
// crossed out/strikethrough text and `'\x1b[29m'` turns off strikethrough.
pub fn format(msg string, open string, close string) string {
return '\x1b[${open}m${msg}\x1b[${close}m'
}
// format_rgb returns ANSI escape coded `msg` formatted with a preceding `open`, a succeeding `close` and the provided RGB colors `r`, `g`, and `b`.
pub fn format_rgb(r int, g int, b int, msg string, open string, close string) string {
return '\x1b[${open};2;${r};${g};${b}m${msg}\x1b[${close}m'
}
// rbg returns the `msg` with the foreground in the specified RGB color
// For example, `rgb(0, 255, 0, 'hi')` returns the `'hi'` string in
// lime color.
pub fn rgb(r int, g int, b int, msg string) string {
return format_rgb(r, g, b, msg, '38', '39')
}
// bg_rgb returns the `msg` with the background in the specified RGB color.
// For example, `bg_rgb(255, 0, 0, 'hi')` returns the text `'hi'` in
// red color.
pub fn bg_rgb(r int, g int, b int, msg string) string {
return format_rgb(r, g, b, msg, '48', '49')
}
// hex returns the `msg` with the foreground in the specified `hex` color.
// For example, `rgb(255, 'hi')` returns the `'hi'` string in
// blue color, which is `(0, 0, 255)` in RGB.
pub fn hex(hex int, msg string) string {
return format_rgb(hex >> 16, (hex >> 8) & 0xFF, hex & 0xFF, msg, '38', '39')
}
// hex returns the `msg` with the background in the specified `hex` color.
// For example, `bg_rgb(255, 'hi')` returns the `'hi'` string in
// a background of blue color, which is `(0, 0, 255)` in RGB.
pub fn bg_hex(hex int, msg string) string {
return format_rgb(hex >> 16, (hex >> 8) & 0xFF, hex & 0xFF, msg, '48', '49')
}
// reset resets all formatting for `msg`.
pub fn reset(msg string) string {
return format(msg, '0', '0')
}
// bold returns the given `msg` in bold.
pub fn bold(msg string) string {
return format(msg, '1', '22')
}
// dim returns the dimmed `msg`.
pub fn dim(msg string) string {
return format(msg, '2', '22')
}
// italic returns the given `msg` in italic.
pub fn italic(msg string) string {
return format(msg, '3', '23')
}
// underline returns the underlined `msg`.
pub fn underline(msg string) string {
return format(msg, '4', '24')
}
// slow_blink will surround the `msg` with ANSI escape codes for blinking (less than 150 times per minute).
pub fn slow_blink(msg string) string {
return format(msg, '5', '25')
}
// rapid_blink will surround the `msg` with ANSI escape codes for blinking (over 150 times per minute).
// Note that unlike slow_blink, this is not very widely supported.
pub fn rapid_blink(msg string) string {
return format(msg, '6', '26')
}
// inverse inverses the given `msg`.
pub fn inverse(msg string) string {
return format(msg, '7', '27')
}
// hidden hides the given `msg`.
pub fn hidden(msg string) string {
return format(msg, '8', '28')
}
// strikethrough returns the given `msg` in strikethrough.
pub fn strikethrough(msg string) string {
return format(msg, '9', '29')
}
// black formats the `msg` in black.
pub fn black(msg string) string {
return format(msg, '30', '39')
}
// red formats the `msg` in red.
pub fn red(msg string) string {
return format(msg, '31', '39')
}
// green formats the `msg` in green.
pub fn green(msg string) string {
return format(msg, '32', '39')
}
// yellow formats the `msg` in yellow.
pub fn yellow(msg string) string {
return format(msg, '33', '39')
}
// blue formats the `msg` in blue.
pub fn blue(msg string) string {
return format(msg, '34', '39')
}
// magenta formats the `msg` in magenta.
pub fn magenta(msg string) string {
return format(msg, '35', '39')
}
// cyan formats the `msg` in cyan.
pub fn cyan(msg string) string {
return format(msg, '36', '39')
}
// white formats the `msg` in white.
pub fn white(msg string) string {
return format(msg, '37', '39')
}
// bg_black formats the `msg` in black background.
pub fn bg_black(msg string) string {
return format(msg, '40', '49')
}
// bg_red formats the `msg` in red background.
pub fn bg_red(msg string) string {
return format(msg, '41', '49')
}
// bg_green formats the `msg` in green background.
pub fn bg_green(msg string) string {
return format(msg, '42', '49')
}
// bg_yellow formats the `msg` in yellow background.
pub fn bg_yellow(msg string) string {
return format(msg, '43', '49')
}
// bg_blue formats the `msg` in blue background.
pub fn bg_blue(msg string) string {
return format(msg, '44', '49')
}
// bg_magenta formats the `msg` in magenta background.
pub fn bg_magenta(msg string) string {
return format(msg, '45', '49')
}
// bg_cyan formats the `msg` in cyan background.
pub fn bg_cyan(msg string) string {
return format(msg, '46', '49')
}
// bg_white formats the `msg` in white background.
pub fn bg_white(msg string) string {
return format(msg, '47', '49')
}
// gray formats the `msg` in gray (equivalent to `bright_black`).
pub fn gray(msg string) string {
return bright_black(msg)
}
// bright_black formats the `msg` in bright black.
pub fn bright_black(msg string) string {
return format(msg, '90', '39')
}
// bright_red formats the `msg` in bright red.
pub fn bright_red(msg string) string {
return format(msg, '91', '39')
}
// bright_green formats the `msg` in bright green.
pub fn bright_green(msg string) string {
return format(msg, '92', '39')
}
// bright_yellow formats the `msg` in bright yellow.
pub fn bright_yellow(msg string) string {
return format(msg, '93', '39')
}
// bright_blue formats the `msg` in bright blue.
pub fn bright_blue(msg string) string {
return format(msg, '94', '39')
}
// bright_magenta formats the `msg` in bright magenta.
pub fn bright_magenta(msg string) string {
return format(msg, '95', '39')
}
// bright_cyan formats the `msg` in bright cyan.
pub fn bright_cyan(msg string) string {
return format(msg, '96', '39')
}
// bright_white formats the `msg` in bright white.
pub fn bright_white(msg string) string {
return format(msg, '97', '39')
}
// bright_bg_black formats the `msg` in bright black background.
pub fn bright_bg_black(msg string) string {
return format(msg, '100', '49')
}
// bright_bg_red formats the `msg` in bright red background.
pub fn bright_bg_red(msg string) string {
return format(msg, '101', '49')
}
// bright_bg_green formats the `msg` in bright green background.
pub fn bright_bg_green(msg string) string {
return format(msg, '102', '49')
}
// bright_bg_yellow formats the `msg` in bright yellow background.
pub fn bright_bg_yellow(msg string) string {
return format(msg, '103', '49')
}
// bright_bg_blue formats the `msg` in bright blue background.
pub fn bright_bg_blue(msg string) string {
return format(msg, '104', '49')
}
// bright_bg_magenta formats the `msg` in bright magenta background.
pub fn bright_bg_magenta(msg string) string {
return format(msg, '105', '49')
}
// bright_bg_cyan formats the `msg` in bright cyan background.
pub fn bright_bg_cyan(msg string) string {
return format(msg, '106', '49')
}
// bright_bg_white formats the `msg` in bright white background.
pub fn bright_bg_white(msg string) string {
return format(msg, '107', '49')
}
// highlight_command highlights the command with an on-brand background to make CLI commands immediately recognizable.
pub fn highlight_command(command string) string {
return bright_white(bg_cyan(' ${command} '))
}
pub enum TextStyle {
bold = 1
dim = 2
italic = 3
underline = 4
blink = 5
reverse = 7
}
pub enum FgColor {
black = 30
red = 31
green = 32
yellow = 33
blue = 34
magenta = 35
cyan = 36
white = 37
}
pub enum BgColor {
black = 40
red = 41
green = 42
yellow = 43
blue = 44
magenta = 45
cyan = 46
white = 47
}
@[params]
pub struct ColorConfig {
pub mut:
styles []TextStyle
fg ?FgColor
bg ?BgColor
custom string
}
// write_color appends the ANSI colorful string `s` to the buffer.
pub fn write_color(mut b strings.Builder, s string, config ColorConfig) {
mut codes := []string{cap: 3}
for style in config.styles {
codes << int(style).str()
}
if fg := config.fg {
codes << int(fg).str()
}
if bg := config.bg {
codes << int(bg).str()
}
if config.custom != '' {
codes << config.custom
}
if codes.len > 0 {
code_str := codes.join(';')
b.write_string('\x1b[${code_str}m${s}\x1b[0m')
} else {
b.write_string(s)
}
}
// writeln_color appends the ANSI colorful string `s`, and then a newline character.
pub fn writeln_color(mut b strings.Builder, s string, color ColorConfig) {
write_color(mut b, s, color)
b.writeln('')
}
+131
View File
@@ -0,0 +1,131 @@
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module term
// Sources for ANSI Control Sequences
// https://github.com/RajeshPatkarInstitute/Panim
// https://www.gnu.org/software/screen/manual/html_node/Control-Sequences.html
// https://en.wikipedia.org/wiki/ANSI_escape_code
// Support for Windows
// https://en.wikipedia.org/wiki/ANSI.SYS
// #include <windows.h>
// C.SetConsoleMode(C.ENABLE_VIRTUAL_TERMINAL_INPUT)
// Setting cursor to the given position
// x is the x coordinate
// y is the y coordinate
pub fn set_cursor_position(c Coord) {
print('\x1b[${c.y};${c.x}' + 'H')
flush_stdout()
}
// move the cursor relative to its current position.
// n is number of cells.
// direction: A is up / North
// direction: B is down / South
// direction: C is forward / East
// direction: D is backward / West
pub fn move(n int, direction string) {
print('\x1b[${n}${direction}')
flush_stdout()
}
// cursor_up moves the cursor up `n` lines.
pub fn cursor_up(n int) {
move(n, 'A')
}
// cursor_down moves the cursor down `n` lines.
pub fn cursor_down(n int) {
move(n, 'B')
}
// cursor_forward moves the cursor forward `n` character positions.
pub fn cursor_forward(n int) {
move(n, 'C')
}
// cursor_back moves the cursor back `n` characters.
pub fn cursor_back(n int) {
move(n, 'D')
}
// erase_display erases display characters based on the given parameter, `t`.
// `t` can be of the following values in string:
// `0`: current cursor position to end of the terminal window.
// `1`: current cursor position to beginning of the terminal
// window.
// `2`: clears the entire terminal window.
// `3`: clears the entire terminal window and also deletes the scrollback buffer.
pub fn erase_display(t string) {
print('\x1b[' + t + 'J')
flush_stdout()
}
// erase_to_end erases from the cursor to the end of the terminal window.
pub fn erase_toend() {
erase_display('0')
}
// erase_tobeg erases from the cursor to the beginning of the terminal window.
pub fn erase_tobeg() {
erase_display('1')
}
// erase_clear clears the entire terminal window and returns the cursor to the top left corner.
pub fn erase_clear() {
print('\033[H\033[J')
flush_stdout()
}
// erase_del_clear erases saved lines.
pub fn erase_del_clear() {
erase_display('3')
}
// erase_line erases the current line based on the given parameter, `t`.
// `t` can be of the following values in string:
// `0`: current cursor position to the end of the line.
// `1`: current cursor position to the beginning of the line.
// `2`: clears the entire line.
// Note: Cursor position does not change.
pub fn erase_line(t string) {
print('\x1b[' + t + 'K')
flush_stdout()
}
// erase_line_toend erases from the cursor position to the end of the line.
pub fn erase_line_toend() {
erase_line('0')
}
// erase_line_tobeg erases from the start of the line to the cursor position.
pub fn erase_line_tobeg() {
erase_line('1')
}
// erase_line_clear erases the entire line.
pub fn erase_line_clear() {
erase_line('2')
}
// show_cursor makes the cursor appear if it was not visible.
pub fn show_cursor() {
print('\x1b[?25h')
flush_stdout()
}
// Will make cursor invisible
pub fn hide_cursor() {
print('\x1b[?25l')
flush_stdout()
}
// clear_previous_line - useful for progressbars.
// It moves the cursor to start of line, then 1 line above,
// then erases the line. In effect the next println will overwrite
// the previous content.
pub fn clear_previous_line() {
print('\r\x1b[1A\x1b[2K')
flush_stdout()
}
@@ -0,0 +1,10 @@
module term
pub struct C.termios {
mut:
c_iflag int
c_oflag int
c_cflag int
c_lflag int
c_cc [20]u8
}
+11
View File
@@ -0,0 +1,11 @@
module term
pub struct C.termios {
mut:
c_iflag int
c_oflag int
c_cflag int
c_lflag int
// c_line byte
c_cc [10]int
}
+24
View File
@@ -0,0 +1,24 @@
module term
#const $tty = require('tty');
// get_terminal_size returns a number of columns and rows of terminal window.
pub fn get_terminal_size() (int, int) {
$if js_node {
cols := 0
rows := 0
#let sizes = $tty.WriteStream(1).getWindowSize();
#cols.val = sizes[0];
#rows.val = sizes[1];
return cols, rows
} $else {
return default_columns_size, default_rows_size
}
}
// clear clears current terminal screen.
pub fn clear() bool {
print('\x1b[2J')
print('\x1b[H')
return true
}
+233
View File
@@ -0,0 +1,233 @@
@[has_globals]
module term
import os
import strings.textscanner
const default_columns_size = 80
const default_rows_size = 25
// Coord - used by term.get_cursor_position and term.set_cursor_position.
pub struct Coord {
pub mut:
x int
y int
}
__global can_show_color_on_stdout_cache = 0
__global can_show_color_on_stderr_cache = 0
// can_show_color_on_stdout returns true, if colors are allowed in stdout.
// It returns false otherwise.
pub fn can_show_color_on_stdout() bool {
match can_show_color_on_stdout_cache {
1 { return true }
-1 { return false }
else {}
}
status := supports_escape_sequences(1)
can_show_color_on_stdout_cache = if status { 1 } else { -1 }
return status
}
// can_show_color_on_stderr returns true, if colors are allowed in stderr.
// It returns false otherwise.
pub fn can_show_color_on_stderr() bool {
match can_show_color_on_stderr_cache {
1 { return true }
-1 { return false }
else {}
}
status := supports_escape_sequences(2)
can_show_color_on_stderr_cache = if status { 1 } else { -1 }
return status
}
// failed returns a bold white on red version of the string `s`.
// If colors are not allowed, returns the string `s`
pub fn failed(s string) string {
if can_show_color_on_stdout() {
return bg_red(bold(white(s)))
}
return s
}
// ok_message returns a colored string with green color.
// If colors are not allowed, returns a given string.
pub fn ok_message(s string) string {
if can_show_color_on_stdout() {
return green('${s}')
}
return s
}
// fail_message returns a colored string with red color.
// If colors are not allowed, returns a given string.
pub fn fail_message(s string) string {
return failed('${s}')
}
// warn_message returns a colored string with yellow color.
// If colors are not allowed, returns a given string.
pub fn warn_message(s string) string {
if can_show_color_on_stdout() {
return bright_yellow('${s}')
}
return s
}
// colorize returns a colored string by running the specified `cfn` over the message `s`, but only if colored stdout is supported by the terminal.
// Example: term.colorize(term.yellow, 'the message')
pub fn colorize(cfn fn (string) string, s string) string {
if can_show_color_on_stdout() {
return cfn(s)
}
return s
}
// ecolorize returns a colored string by running the specified `cfn` over the message `s`, but only if colored stderr is supported by the terminal.
// Example: term.ecolorize(term.bright_red, 'the message')
pub fn ecolorize(cfn fn (string) string, s string) string {
if can_show_color_on_stderr() {
return cfn(s)
}
return s
}
// strip_ansi removes any ANSI sequences in the `text`.
pub fn strip_ansi(text string) string {
// This is a port of https://github.com/kilobyte/colorized-logs/blob/master/ansi2txt.c
// \e, [, 1, m, a, b, c, \e, [, 2, 2, m => abc
mut input := textscanner.new(text)
mut output := []u8{cap: text.len}
mut ch := 0
for ch != -1 {
ch = input.next()
if ch == 27 {
ch = input.next()
if ch == `[` {
for {
ch = input.next()
if ch in [`;`, `?`] || (ch >= `0` && ch <= `9`) {
continue
}
break
}
} else if ch == `]` {
ch = input.next()
if ch >= `0` && ch <= `9` {
for {
ch = input.next()
if ch == -1 || ch == 7 {
break
}
if ch == 27 {
ch = input.next()
break
}
}
}
} else if ch == `%` {
ch = input.next()
}
} else if ch != -1 {
output << u8(ch)
}
}
return output.bytestr()
}
// h_divider returns a horizontal divider line with a dynamic width, that depends on the current terminal settings.
// If an empty string is passed in, print enough spaces to make a new line.
pub fn h_divider(divider string) string {
cols, _ := get_terminal_size()
mut result := ''
if divider.len > 0 {
result = divider.repeat(1 + (cols / divider.len))
} else {
result = ' '.repeat(1 + cols)
}
return result[0..cols]
}
// header_left returns a horizontal divider line with a title text on the left.
// e.g: term.header_left('TITLE', '=')
// ==== TITLE =========================
pub fn header_left(text string, divider string) string {
plain_text := strip_ansi(text)
xcols, _ := get_terminal_size() // can get 0 in lldb/gdb
cols := imax(1, xcols)
relement := if divider.len > 0 { divider } else { ' ' }
hstart := relement.repeat(4)[0..4]
remaining_cols := imax(0, (cols - (hstart.len + 1 + plain_text.len + 1)))
hend := relement.repeat((remaining_cols + 1) / relement.len)[0..remaining_cols]
return '${hstart} ${text} ${hend}'
}
// header returns a horizontal divider line with a centered text in the middle.
// e.g: term.header('TEXT', '=')
// =============== TEXT ===============
pub fn header(text string, divider string) string {
if text.len == 0 {
return h_divider(divider)
}
xcols, _ := get_terminal_size()
cols := imax(1, xcols)
tlimit := imax(1, if cols > text.len + 2 + 2 * divider.len {
text.len
} else {
cols - 3 - 2 * divider.len
})
tlimit_aligned := if (tlimit % 2) != (cols % 2) { tlimit + 1 } else { tlimit }
tstart := imax(0, (cols - tlimit_aligned) / 2)
mut ln := ''
if divider.len > 0 {
ln = divider.repeat(1 + cols / divider.len)[0..cols]
} else {
ln = ' '.repeat(1 + cols)
}
if ln.len == 1 {
return ln + ' ' + text[0..tlimit] + ' ' + ln
}
return ln[0..tstart] + ' ' + text[0..tlimit] + ' ' + ln[tstart + tlimit + 2..cols]
}
fn imax(x int, y int) int {
return if x > y { x } else { y }
}
@[manualfree]
fn supports_escape_sequences(fd int) bool {
vcolors_override := os.getenv('VCOLORS')
defer {
unsafe { vcolors_override.free() }
}
if vcolors_override == 'always' {
return true
}
if vcolors_override == 'never' {
return false
}
env_term := os.getenv('TERM')
defer {
unsafe { env_term.free() }
}
if env_term == 'dumb' {
return false
}
$if windows {
env_conemu := os.getenv('ConEmuANSI')
defer {
unsafe { env_conemu.free() }
}
if env_conemu == 'ON' {
return true
}
// 4 is enable_virtual_terminal_processing
return (os.is_atty(fd) & 0x0004) > 0
} $else {
return os.is_atty(fd) > 0
}
}
+283
View File
@@ -0,0 +1,283 @@
module term
import os
import term.termios
import time
#include <sys/ioctl.h>
pub struct C.winsize {
pub:
ws_row u16
ws_col u16
ws_xpixel u16
ws_ypixel u16
}
fn C.ioctl(fd i32, request u64, args ...voidptr) i32
// get_terminal_size returns a number of columns and rows of terminal window.
pub fn get_terminal_size() (int, int) {
if os.is_atty(1) <= 0 || os.getenv('TERM') == 'dumb' {
return default_columns_size, default_rows_size
}
w := C.winsize{}
unsafe { C.ioctl(1, u64(C.TIOCGWINSZ), &w) }
return int(w.ws_col), int(w.ws_row)
}
// get_cursor_position returns a Coord containing the current cursor position.
pub fn get_cursor_position() !Coord {
if os.is_atty(1) <= 0 || os.getenv('TERM') == 'dumb' {
return Coord{0, 0}
}
mut old_state := termios.Termios{}
if termios.tcgetattr(0, mut old_state) != 0 {
return os.last_error()
}
defer {
// restore the old terminal state:
termios.tcsetattr(0, C.TCSANOW, mut old_state)
}
mut state := termios.Termios{}
if termios.tcgetattr(0, mut state) != 0 {
return os.last_error()
}
state.c_lflag &= termios.invert(termios.flag(int(C.ICANON) | int(C.ECHO)))
termios.tcsetattr(0, C.TCSANOW, mut state)
print('\e[6n')
flush_stdout()
mut x := 0
mut y := 0
mut stage := u8(0)
// ESC [ YYY `;` XXX `R`
for {
w := unsafe { C.getchar() }
if w < 0 {
return error_with_code('Failed to read from stdin', 888)
} else if w == `[` || w == `;` {
stage++
} else if `0` <= w && w <= `9` {
match stage {
// converting string values to int:
1 { y = y * 10 + int(w - `0`) }
2 { x = x * 10 + int(w - `0`) }
else {}
}
} else if w == `R` {
break
}
}
return Coord{x, y}
}
// set_terminal_title change the terminal title (usually that is the containing terminal window title).
pub fn set_terminal_title(title string) bool {
if os.is_atty(1) <= 0 || os.getenv('TERM') == 'dumb' {
return false
}
print('\033]0;')
print(title)
print('\007')
flush_stdout()
return true
}
// set_tab_title changes the terminal *tab title*, for terminal emulators like Konsole, that support several tabs.
pub fn set_tab_title(title string) bool {
if os.is_atty(1) <= 0 || os.getenv('TERM') == 'dumb' {
return false
}
print('\033]30;')
print(title)
print('\007')
flush_stdout()
return true
}
// clear clears current terminal screen.
pub fn clear() bool {
if os.is_atty(1) <= 0 || os.getenv('TERM') == 'dumb' {
return false
}
print('\x1b[2J')
print('\x1b[H')
flush_stdout()
return true
}
// supports_sixel returns `true` if the terminal supports Sixel graphics
//
// For more info on the sixel format:
// See https://en.wikipedia.org/wiki/Sixel
// See https://www.digiater.nl/openvms/decus/vax90b1/krypton-nasa/all-about-sixels.text
// For more info on terminal support:
// See https://www.arewesixelyet.com
pub fn supports_sixel() bool {
if os.is_atty(1) <= 0 || os.getenv('TERM') == 'dumb' {
return false
}
if os.getenv('TERM') == 'yaft' {
return true
}
mut old_state := termios.Termios{}
if termios.tcgetattr(0, mut old_state) != 0 {
return false
}
defer {
// restore the old terminal state:
termios.tcsetattr(0, C.TCSANOW, mut old_state)
}
mut state := termios.Termios{}
if termios.tcgetattr(0, mut state) != 0 {
return false
}
state.c_lflag &= termios.invert(termios.flag(int(C.ICANON) | int(C.ECHO)))
termios.tcsetattr(0, C.TCSANOW, mut state)
// Send a "Query Device Code"
print('\e[c')
flush_stdout()
// Terminal answers with a "Report Device Code" in the format `\e[<code>`
mut buf := []u8{cap: 64} // xterm's output can be long
for {
w := unsafe { C.getchar() }
if w < 0 {
return false
} else if w == `c` {
break
} else {
buf << u8(w)
}
}
sa := buf.bytestr().all_after('?').split(';')
// returns `true` if the digit literal "4" is found in a "slot" in the response, before the ending `c`.
return '4' in sa
}
// graphics_num_colors returns the number of color registers the terminal
// graphic attribute is set to use. This can be useful to know if the terminal
// is configured to support Sixel graphics.
//
// See "CSI ? Pi ; Pa ; Pv S" from https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
pub fn graphics_num_colors() u16 {
if os.is_atty(1) <= 0 || os.getenv('TERM') == 'dumb' {
return 0
}
if os.getenv('TERM') == 'yaft' {
return 256
}
mut old_state := termios.Termios{}
if termios.tcgetattr(0, mut old_state) != 0 {
return 0
}
defer {
// restore the old terminal state:
termios.tcsetattr(0, C.TCSANOW, mut old_state)
}
mut state := termios.Termios{}
if termios.tcgetattr(0, mut state) != 0 {
return 0
}
state.c_lflag &= termios.invert(termios.flag(int(C.ICANON) | int(C.ECHO)))
termios.tcsetattr(0, C.TCSANOW, mut state)
// Send "CSI ? Pi ; Pa ; Pv S"
print('\e[?1;1;0S')
flush_stdout()
mut buf := []u8{cap: 20}
for {
w := unsafe { C.getchar() }
if w < 0 {
return 0
} else if w == `S` {
break
} else if w == `;` {
buf.clear()
} else {
buf << u8(w)
}
}
return buf.bytestr().u16()
}
// enable_echo enable/disable echo input characters.
pub fn enable_echo(enable bool) {
mut state := termios.Termios{}
termios.tcgetattr(0, mut state)
if enable {
state.c_lflag |= C.ECHO
} else {
state.c_lflag &= ~C.ECHO
}
termios.tcsetattr(0, C.TCSANOW, mut state)
}
// KeyPressedParams contains the optional parameters that you can pass to key_pressed.
@[params]
pub struct KeyPressedParams {
pub mut:
blocking bool // whether to wait for a pressed key
echo bool // whether to output the pressed key to stdout
}
// key_pressed gives back a single character, read from the standard input.
// It returns -1 on error or no character in non-blocking mode
pub fn key_pressed(params KeyPressedParams) i64 {
mut state := termios.Termios{}
if termios.tcgetattr(0, mut state) != 0 {
return -1
}
mut old_state := state
defer {
// restore the old terminal state:
termios.tcsetattr(0, C.TCSANOW, mut old_state)
}
// disable line by line input
state.c_lflag &= ~C.ICANON
if params.echo {
state.c_lflag |= C.ECHO
} else {
state.c_lflag &= ~C.ECHO
}
termios.tcsetattr(0, C.TCSANOW, mut state)
mut ret := i64(0)
for {
pending := os.fd_is_pending(0)
if pending {
r := C.read(0, &ret, 8)
if r < 0 {
return r
} else {
return ret
}
}
if !params.blocking {
// in non-blocking mode, we need to return immediately
return -1
}
time.sleep(1 * time.millisecond)
}
return ret
}
+145
View File
@@ -0,0 +1,145 @@
import os
import term
import strings
fn test_get_terminal_size() {
cols, _ := term.get_terminal_size()
assert cols > 0
}
fn test_h_divider() {
divider := term.h_divider('-')
assert divider.len > 0
assert divider[0] == `-`
assert divider[divider.len - 1] == `-`
}
fn test_h_divider_multiple_characters() {
xdivider := term.h_divider('abc')
assert xdivider.len > 0
assert xdivider.contains('abcabc')
}
fn test_header() {
divider := term.h_divider('-')
term_width := divider.len
assert term_width > 0
empty_header := term.header('', '-')
short_header := term.header('reasonable header', '-')
very_long_header := term.header(['abc'].repeat(500).join(' '), '-')
very_long_header_2 := term.header(['abcd'].repeat(500).join(' '), '-')
/*
eprintln(divider)
eprintln(empty_header)
eprintln(short_header)
eprintln(term.header('another longer header', '_-/\\'))
eprintln(term.header('another longer header', '-'))
eprintln(term.header('short', '-'))
eprintln(term.header('12345', '-'))
eprintln(term.header('1234', '-'))
eprintln(term.header('123', '-'))
eprintln(term.header('12', '-'))
eprintln(term.header('1', '-'))
eprintln(very_long_header)
eprintln(divider)
eprintln(very_long_header_2)
eprintln(term.header(['abcd'].repeat(500).join(' '), '_-/\\'))
eprintln(term.header(['abcd'].repeat(500).join(' '), '_-//'))
eprintln(term.header('1', '_-/\\\/'))
eprintln(term.header('12', '_-/\\\/'))
eprintln(term.header('123', '_-/\\\/'))
eprintln(term.header('1234', '_-/\\/\\'))
eprintln(term.header('', '-'))
*/
assert term_width == empty_header.len
assert term_width == short_header.len
assert term_width == very_long_header.len
assert term_width == very_long_header_2.len
assert term_width == term.header('1234', '_-/\\/\\').len
}
fn test_get_cursor_position() {
original_position := term.get_cursor_position()!
cursor_position_1 := term.get_cursor_position()!
assert original_position.x == cursor_position_1.x
assert original_position.y == cursor_position_1.y
term.set_cursor_position(
x: 10
y: 11
)
cursor_position_2 := term.get_cursor_position()!
term.set_cursor_position(
x: 5
y: 6
)
cursor_position_3 := term.get_cursor_position()!
term.set_cursor_position(original_position)
eprintln('original_position: ${original_position}')
eprintln('cursor_position_2: ${cursor_position_2}')
eprintln('cursor_position_3: ${cursor_position_3}')
// 0,0 is returned on dumb terminals
if cursor_position_2.x == 0 && cursor_position_2.y == 0 {
return
}
if cursor_position_3.x == 0 && cursor_position_3.y == 0 {
return
}
assert cursor_position_2.x == 10
assert cursor_position_2.y == 11
assert cursor_position_3.x == 5
assert cursor_position_3.y == 6
}
fn test_set_terminal_title() {
// do not change the current terminal title outside of CI:
if os.getenv('CI') != 'true' {
return
}
term.set_terminal_title('v is awesome!')
dump(@FILE)
assert true
}
fn test_set_tab_title() {
// do not change the current terminal tab title outside of CI:
if os.getenv('CI') != 'true' {
return
}
term.set_tab_title('v is awesome!')
dump(@FILE)
assert true
}
fn test_strip_ansi() {
string_list := [
'abc',
term.bold('abc'),
term.yellow('abc'),
term.bold(term.red('abc')),
term.strikethrough(term.inverse(term.dim(term.bold(term.bright_bg_blue('abc'))))),
]
for s in string_list {
assert term.strip_ansi(s) == 'abc'
}
}
fn test_write_color() {
mut sb := strings.new_builder(100)
term.writeln_color(mut sb, 'hello')
term.writeln_color(mut sb, 'hello', fg: .red)
term.writeln_color(mut sb, 'hello', bg: .cyan)
term.writeln_color(mut sb, 'hello', styles: [.bold, .italic, .underline], fg: .red, bg: .cyan)
term.writeln_color(mut sb, 'hello', custom: '38;5;214')
output := sb.str()
println(output)
assert output.contains('\x1b[0m')
assert output.contains('\x1b[31m')
assert output.contains('\x1b[46m')
assert output.contains('\x1b[1;3;4;31;46m')
assert output.contains('\x1b[38;5;214m')
}
+212
View File
@@ -0,0 +1,212 @@
module term
import os
import time
#include <conio.h>
@[typedef]
pub struct C.COORD {
mut:
X i16
Y i16
}
@[typedef]
pub struct C.SMALL_RECT {
mut:
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
@[typedef]
pub struct C.CONSOLE_SCREEN_BUFFER_INFO {
mut:
dwSize C.COORD
dwCursorPosition C.COORD
wAttributes u16
srWindow C.SMALL_RECT
dwMaximumWindowSize C.COORD
}
pub union C.uChar {
mut:
UnicodeChar rune
AsciiChar u8
}
@[typedef]
pub struct C.CHAR_INFO {
mut:
Char C.uChar
Attributes u16
}
// ref - https://docs.microsoft.com/en-us/windows/console/getconsolescreenbufferinfo
fn C.GetConsoleScreenBufferInfo(handle C.HANDLE, info &C.CONSOLE_SCREEN_BUFFER_INFO) bool
// ref - https://docs.microsoft.com/en-us/windows/console/setconsoletitle
fn C.SetConsoleTitle(title &u16) bool
// ref - https://docs.microsoft.com/en-us/windows/console/setconsolecursorposition
fn C.SetConsoleCursorPosition(handle C.HANDLE, coord C.COORD) bool
// ref - https://docs.microsoft.com/en-us/windows/console/scrollconsolescreenbuffer
fn C.ScrollConsoleScreenBuffer(output C.HANDLE, scroll_rect &C.SMALL_RECT, clip_rect &C.SMALL_RECT, des C.COORD,
fill &C.CHAR_INFO) bool
// get_terminal_size returns a number of columns and rows of terminal window.
pub fn get_terminal_size() (int, int) {
if os.is_atty(1) > 0 && os.getenv('TERM') != 'dumb' {
info := C.CONSOLE_SCREEN_BUFFER_INFO{}
if C.GetConsoleScreenBufferInfo(C.GetStdHandle(C.STD_OUTPUT_HANDLE), &info) {
columns := int(info.srWindow.Right - info.srWindow.Left + 1)
rows := int(info.srWindow.Bottom - info.srWindow.Top + 1)
return columns, rows
}
}
return default_columns_size, default_rows_size
}
// get_cursor_position returns a Coord containing the current cursor position.
pub fn get_cursor_position() !Coord {
mut res := Coord{}
if os.is_atty(1) > 0 && os.getenv('TERM') != 'dumb' {
info := C.CONSOLE_SCREEN_BUFFER_INFO{}
if C.GetConsoleScreenBufferInfo(C.GetStdHandle(C.STD_OUTPUT_HANDLE), &info) {
res.x = info.dwCursorPosition.X
res.y = info.dwCursorPosition.Y
} else {
return os.last_error()
}
}
return res
}
// set_terminal_title changes the terminal title.
pub fn set_terminal_title(title string) bool {
wide_title := title.to_wide()
return C.SetConsoleTitle(wide_title)
}
// set_tab_title changes the terminal *tab title*, for terminal emulators that do support several tabs.
pub fn set_tab_title(title string) bool {
// TODO: investigate, whether there is an API for changing just the tab title on windows yet.
return set_terminal_title(title)
}
// clear clears current terminal screen.
// Implementation taken from https://docs.microsoft.com/en-us/windows/console/clearing-the-screen#example-2.
pub fn clear() bool {
hconsole := C.GetStdHandle(C.STD_OUTPUT_HANDLE)
mut csbi := C.CONSOLE_SCREEN_BUFFER_INFO{}
mut scrollrect := C.SMALL_RECT{}
mut scrolltarget := C.COORD{}
mut fill := C.CHAR_INFO{}
// Get the number of character cells in the current buffer.
if !C.GetConsoleScreenBufferInfo(hconsole, &csbi) {
return false
}
// Scroll the rectangle of the entire buffer.
scrollrect.Left = 0
scrollrect.Top = 0
scrollrect.Right = u16(csbi.dwSize.X)
scrollrect.Bottom = u16(csbi.dwSize.Y)
// Scroll it upwards off the top of the buffer with a magnitude of the entire height.
scrolltarget.X = 0
scrolltarget.Y = (0 - csbi.dwSize.Y)
// Fill with empty spaces with the buffer's default text attribute.
fill.Char.UnicodeChar = rune(` `)
fill.Attributes = csbi.wAttributes
// Do the scroll
C.ScrollConsoleScreenBuffer(hconsole, &scrollrect, C.NULL, scrolltarget, &fill)
// Move the cursor to the top left corner too.
csbi.dwCursorPosition.X = 0
csbi.dwCursorPosition.Y = 0
C.SetConsoleCursorPosition(hconsole, csbi.dwCursorPosition)
return true
}
// supports_sixel returns `true` if the terminal supports Sixel graphics
//
// For more info on the sixel format:
// See https://en.wikipedia.org/wiki/Sixel
// See https://www.digiater.nl/openvms/decus/vax90b1/krypton-nasa/all-about-sixels.text
// For more info on terminal support:
// See https://www.arewesixelyet.com
pub fn supports_sixel() bool {
// According to (2024) https://www.arewesixelyet.com/#windows-console there's no support
return false
}
// graphics_num_colors returns the number of color registers the terminal
// graphic attribute is set to use. This can be useful to know if the terminal
// is configured to support Sixel graphics.
//
// See "CSI ? Pi ; Pa ; Pv S" from https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
pub fn graphics_num_colors() u16 {
// Since this call is related to sixel terminal graphics and Windows Console and Terminal
// does not have support for querying the graphics setup this call returns 0
return 0
}
// enable_echo enable/disable echo input characters.
pub fn enable_echo(enable bool) {
// no need under windows, use key_pressed func's echo
}
fn C.kbhit() bool
fn C._getch() i32
fn C._getche() i32
// KeyPressedParams contains the optional parameters that you can pass to key_pressed.
@[params]
pub struct KeyPressedParams {
pub mut:
blocking bool // whether to wait for a pressed key
echo bool // whether to output the pressed key to stdout
}
// key_pressed gives back a single character, read from the standard input.
// It returns -1 on error or no character in non-blocking mode
pub fn key_pressed(params KeyPressedParams) i64 {
for {
if C.kbhit() {
res := if params.echo {
C._getche()
} else {
C._getch()
}
// see https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/getche-getwche?view=msvc-170
// > When _getche or _getwche reads a function key or an arrow key, the function must be called twice;
// > the first call returns 0 or 0xE0, and the second call returns the actual key code.
if res in [0, 0xe0] {
if C.kbhit() {
res2 := if params.echo {
C._getche()
} else {
C._getch()
}
return i64(u32(0xe0) << 16 | u32(res2))
}
}
return i64(res)
}
if !params.blocking {
// in non-blocking mode, we need to return immediately
return -1
}
time.sleep(1 * time.millisecond)
}
return 0
}
@@ -0,0 +1,99 @@
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
//
// Serves as more advanced input method
// based on the work of https://github.com/AmokHuginnsson/replxx
//
module termios
#include <termios.h>
#include <sys/ioctl.h>
// https://github.com/lattera/glibc/blob/master/sysdeps/unix/sysv/linux/bits/termios.h
const cclen = 32
type TcFlag = int
type Speed = int
type Cc = u8
// Termios stores the terminal options on Linux.
pub struct C.termios {
mut:
c_iflag TcFlag
c_oflag TcFlag
c_cflag TcFlag
c_lflag TcFlag
c_line Cc
c_cc [cclen]Cc
c_ispeed Speed
c_ospeed Speed
}
fn C.tcgetattr(fd i32, termios_p &C.termios) i32
fn C.tcsetattr(fd i32, optional_actions i32, const_termios_p &C.termios) i32
fn C.ioctl(fd i32, request u64, args ...voidptr) i32
// flag provides a termios flag of the correct size
// for the underlying C.termios structure
@[inline]
pub fn flag(value int) TcFlag {
return TcFlag(value)
}
// invert is a platform dependant way to bitwise NOT (~) TcFlag
// as its length varies across platforms
@[inline]
pub fn invert(value TcFlag) TcFlag {
return ~int(value)
}
pub struct Termios {
pub mut:
c_iflag TcFlag
c_oflag TcFlag
c_cflag TcFlag
c_lflag TcFlag
c_line Cc
c_cc [cclen]Cc
c_ispeed Speed
c_ospeed Speed
}
// tcgetattr is an unsafe wrapper around C.termios and keeps its semantic
@[inline]
pub fn tcgetattr(fd int, mut termios_p Termios) int {
unsafe {
return C.tcgetattr(fd, &C.termios(termios_p))
}
}
// tcsetattr is an unsafe wrapper around C.termios and keeps its semantic
@[inline]
pub fn tcsetattr(fd int, optional_actions int, mut termios_p Termios) int {
unsafe {
return C.tcsetattr(fd, optional_actions, &C.termios(termios_p))
}
}
// ioctl is an unsafe wrapper around C.ioctl and keeps its semantic
@[inline]
pub fn ioctl(fd int, request u64, arg voidptr) int {
unsafe {
return C.ioctl(fd, request, arg)
}
}
// set_state applies the flags in the `new_state` to the descriptor `fd`.
pub fn set_state(fd int, new_state Termios) int {
mut x := new_state
return tcsetattr(0, C.TCSANOW, mut x)
}
// disable_echo disables echoing characters as they are typed, when that Termios state is later set with termios.set_state(fd,t).
pub fn (mut t Termios) disable_echo() {
t.c_lflag &= invert(C.ECHO)
}
@@ -0,0 +1,97 @@
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
//
// Serves as more advanced input method
// based on the work of https://github.com/AmokHuginnsson/replxx
//
module termios
#include <termios.h>
#include <sys/ioctl.h>
// https://github.com/apple/darwin-xnu/blob/main/bsd/sys/termios.h
const cclen = 20
type TcFlag = usize
type Speed = usize
type Cc = u8
// Termios stores the terminal options
pub struct C.termios {
mut:
c_iflag TcFlag
c_oflag TcFlag
c_cflag TcFlag
c_lflag TcFlag
c_cc [cclen]Cc
c_ispeed Speed
c_ospeed Speed
}
fn C.tcgetattr(fd i32, termios_p &C.termios) i32
fn C.tcsetattr(fd i32, optional_actions i32, const_termios_p &C.termios) i32
fn C.ioctl(fd i32, request u64, args ...voidptr) i32
// flag provides a termios flag of the correct size
// for the underlying C.termios structure
@[inline]
pub fn flag(value int) TcFlag {
return usize(value)
}
// invert is a platform dependant way to bitwise NOT (~) TcFlag
// as its length varies across platforms
@[inline]
pub fn invert(value TcFlag) TcFlag {
return ~usize(value)
}
pub struct Termios {
pub mut:
c_iflag TcFlag
c_oflag TcFlag
c_cflag TcFlag
c_lflag TcFlag
c_cc [cclen]Cc
c_ispeed Speed
c_ospeed Speed
}
// tcgetattr is an unsafe wrapper around C.termios and keeps its semantic
@[inline]
pub fn tcgetattr(fd int, mut termios_p Termios) int {
unsafe {
return C.tcgetattr(fd, &C.termios(termios_p))
}
}
// tcsetattr is an unsafe wrapper around C.termios and keeps its semantic
@[inline]
pub fn tcsetattr(fd int, optional_actions int, mut termios_p Termios) int {
unsafe {
return C.tcsetattr(fd, optional_actions, &C.termios(termios_p))
}
}
// ioctl is an unsafe wrapper around C.ioctl and keeps its semantic
@[inline]
pub fn ioctl(fd int, request u64, arg voidptr) int {
unsafe {
return C.ioctl(fd, request, arg)
}
}
// set_state applies the flags in the `new_state` to the descriptor `fd`.
pub fn set_state(fd int, new_state Termios) int {
mut x := new_state
return tcsetattr(0, C.TCSANOW, mut x)
}
// disable_echo disables echoing characters as they are typed, when that Termios state is later set with termios.set_state(fd,t).
pub fn (mut t Termios) disable_echo() {
t.c_lflag &= invert(usize(C.ECHO))
}
@@ -0,0 +1,75 @@
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module termios
// not used but needed for function declarations
type TcFlag = int
type Speed = int
type Cc = u8
// Termios represents platform dependent flags representing the terminal state.
// Linux https://github.com/lattera/glibc/blob/master/sysdeps/unix/sysv/linux/bits/termios.h
// OpenBSD https://github.com/openbsd/src/blob/master/sys/sys/termios.h
// FreeBSD https://web.mit.edu/freebsd/head/sys/sys/_termios.h
// Solaris https://github.com/omniti-labs/illumos-omnios/blob/master/usr/src/uts/common/sys/termios.h
// DragonFly https://gitweb.dragonflybsd.org/dragonfly.git/blob_plain/HEAD:/sys/sys/_termios.h
// QNX https://github.com/vocho/openqnx/blob/master/trunk/lib/c/public/termios.h
pub struct Termios {
pub mut:
c_iflag TcFlag
c_oflag TcFlag
c_cflag TcFlag
c_lflag TcFlag
c_line Cc
c_cc [32]Cc
c_ispeed Speed
c_ospeed Speed
}
// flag provides a termios flag of the correct size for the underlying C.termios structure.
pub fn flag(value int) TcFlag {
return TcFlag(value)
}
// invert is a platform dependant way to bitwise NOT (~) TcFlag as its length varies across platforms.
// It is only implemented for Unix like OSes.
pub fn invert(value TcFlag) TcFlag {
return TcFlag(~int(value))
}
// tcgetattr is an unsafe wrapper around C.termios and keeps its semantic.
// It is only implemented for Unix like OSes
pub fn tcgetattr(fd int, mut t Termios) int {
$if wasm32_emscripten {
return 0
}
eprintln('term.termios: tcgetattr,tcsetattr,ioctl,set_state are not implemented for the platform')
eprintln('tcgetattr, fd: ${fd}, t: ${t}')
return 0
}
// tcsetattr is an unsafe wrapper around C.termios and keeps its semantic.
// It is only implemented for Unix like OSes.
pub fn tcsetattr(fd int, optional_actions int, mut t Termios) int {
eprintln('tcsetattr, fd: ${fd}, optional_actions: ${optional_actions}, t: ${t}')
return 0
}
// ioctl is an unsafe wrapper around C.ioctl and keeps its semantic.
// It is only implemented for Unix like OSes
pub fn ioctl(fd int, request u64, arg voidptr) int {
eprintln('ioctl, fd: ${fd}, request: ${request}, arg: ${arg}')
return 0
}
// set_state applies the flags in the `new_state` to the descriptor `fd`.
pub fn set_state(fd int, new_state Termios) int {
eprintln('set_state, fd: ${fd} | new_state: ${new_state}')
return 0
}
// disable_echo disables echoing characters as they are typed, when that Termios state is later set with termios.set_state(fd,t).
pub fn (mut t Termios) disable_echo() {
t.c_lflag &= invert(8)
}
@@ -0,0 +1,97 @@
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
//
// Serves as more advanced input method
// based on the work of https://github.com/AmokHuginnsson/replxx
//
module termios
#include <termios.h>
#include <sys/ioctl.h>
// https://gitweb.dragonflybsd.org/dragonfly.git/blob_plain/HEAD:/sys/sys/_termios.h
const cclen = 20
type TcFlag = int
type Speed = int
type Cc = u8
// Termios stores the terminal options
pub struct C.termios {
mut:
c_iflag TcFlag
c_oflag TcFlag
c_cflag TcFlag
c_lflag TcFlag
c_cc [cclen]Cc
c_ispeed Speed
c_ospeed Speed
}
fn C.tcgetattr(fd i32, termios_p &C.termios) i32
fn C.tcsetattr(fd i32, optional_actions i32, const_termios_p &C.termios) i32
fn C.ioctl(fd i32, request u64, args ...voidptr) i32
// flag provides a termios flag of the correct size
// for the underlying C.termios structure
@[inline]
pub fn flag(value int) TcFlag {
return int(value)
}
// invert is a platform dependant way to bitwise NOT (~) TcFlag
// as its length varies across platforms
@[inline]
pub fn invert(value TcFlag) TcFlag {
return ~int(value)
}
pub struct Termios {
pub mut:
c_iflag TcFlag
c_oflag TcFlag
c_cflag TcFlag
c_lflag TcFlag
c_cc [cclen]Cc
c_ispeed Speed
c_ospeed Speed
}
// tcgetattr is an unsafe wrapper around C.termios and keeps its semantic
@[inline]
pub fn tcgetattr(fd int, mut termios_p Termios) int {
unsafe {
return C.tcgetattr(fd, &C.termios(termios_p))
}
}
// tcsetattr is an unsafe wrapper around C.termios and keeps its semantic
@[inline]
pub fn tcsetattr(fd int, optional_actions int, mut termios_p Termios) int {
unsafe {
return C.tcsetattr(fd, optional_actions, &C.termios(termios_p))
}
}
// ioctl is an unsafe wrapper around C.ioctl and keeps its semantic.
@[inline]
pub fn ioctl(fd int, request u64, arg voidptr) int {
unsafe {
return C.ioctl(fd, request, arg)
}
}
// set_state applies the flags in the `new_state` to the descriptor `fd`.
pub fn set_state(fd int, new_state Termios) int {
mut x := new_state
return tcsetattr(0, C.TCSANOW, mut x)
}
// disable_echo disables echoing characters as they are typed, when that Termios state is later set with termios.set_state(fd,t).
pub fn (mut t Termios) disable_echo() {
t.c_lflag &= invert(C.ECHO)
}
@@ -0,0 +1,97 @@
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
//
// Serves as more advanced input method
// based on the work of https://github.com/AmokHuginnsson/replxx
//
module termios
#include <termios.h>
#include <sys/ioctl.h>
// https://web.mit.edu/freebsd/head/sys/sys/_termios.h
const cclen = 20
type TcFlag = int
type Speed = int
type Cc = u8
// Termios stores the terminal options
pub struct C.termios {
mut:
c_iflag TcFlag
c_oflag TcFlag
c_cflag TcFlag
c_lflag TcFlag
c_cc [cclen]Cc
c_ispeed Speed
c_ospeed Speed
}
fn C.tcgetattr(fd i32, termios_p &C.termios) i32
fn C.tcsetattr(fd i32, optional_actions i32, const_termios_p &C.termios) i32
fn C.ioctl(fd i32, request u64, args ...voidptr) i32
// flag provides a termios flag of the correct size
// for the underlying C.termios structure
@[inline]
pub fn flag(value int) TcFlag {
return int(value)
}
// invert is a platform dependant way to bitwise NOT (~) TcFlag
// as its length varies across platforms
@[inline]
pub fn invert(value TcFlag) TcFlag {
return ~int(value)
}
pub struct Termios {
pub mut:
c_iflag TcFlag
c_oflag TcFlag
c_cflag TcFlag
c_lflag TcFlag
c_cc [cclen]Cc
c_ispeed Speed
c_ospeed Speed
}
// tcgetattr is an unsafe wrapper around C.termios and keeps its semantic
@[inline]
pub fn tcgetattr(fd int, mut termios_p Termios) int {
unsafe {
return C.tcgetattr(fd, &C.termios(termios_p))
}
}
// tcsetattr is an unsafe wrapper around C.termios and keeps its semantic
@[inline]
pub fn tcsetattr(fd int, optional_actions int, mut termios_p Termios) int {
unsafe {
return C.tcsetattr(fd, optional_actions, &C.termios(termios_p))
}
}
// ioctl is an unsafe wrapper around C.ioctl and keeps its semantic
@[inline]
pub fn ioctl(fd int, request u64, arg voidptr) int {
unsafe {
return C.ioctl(fd, request, arg)
}
}
// set_state applies the flags in the `new_state` to the descriptor `fd`.
pub fn set_state(fd int, new_state Termios) int {
mut x := new_state
return tcsetattr(0, C.TCSANOW, mut x)
}
// disable_echo disables echoing characters as they are typed, when that Termios state is later set with termios.set_state(fd,t).
pub fn (mut t Termios) disable_echo() {
t.c_lflag &= invert(C.ECHO)
}
@@ -0,0 +1,99 @@
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
//
// Serves as more advanced input method
// based on the work of https://github.com/AmokHuginnsson/replxx
//
module termios
#include <termios.h>
#include <sys/ioctl.h>
// https://github.com/lattera/glibc/blob/master/sysdeps/unix/sysv/linux/bits/termios.h
const cclen = 32
type TcFlag = int
type Speed = int
type Cc = u8
// Termios stores the terminal options on Linux.
pub struct C.termios {
mut:
c_iflag TcFlag
c_oflag TcFlag
c_cflag TcFlag
c_lflag TcFlag
c_line Cc
c_cc [cclen]Cc
c_ispeed Speed
c_ospeed Speed
}
fn C.tcgetattr(fd i32, termios_p &C.termios) i32
fn C.tcsetattr(fd i32, optional_actions i32, const_termios_p &C.termios) i32
fn C.ioctl(fd i32, request u64, args ...voidptr) i32
// flag provides a termios flag of the correct size
// for the underlying C.termios structure
@[inline]
pub fn flag(value int) TcFlag {
return TcFlag(value)
}
// invert is a platform dependant way to bitwise NOT (~) TcFlag
// as its length varies across platforms
@[inline]
pub fn invert(value TcFlag) TcFlag {
return ~int(value)
}
pub struct Termios {
pub mut:
c_iflag TcFlag
c_oflag TcFlag
c_cflag TcFlag
c_lflag TcFlag
c_line Cc
c_cc [cclen]Cc
c_ispeed Speed
c_ospeed Speed
}
// tcgetattr is an unsafe wrapper around C.termios and keeps its semantic
@[inline]
pub fn tcgetattr(fd int, mut termios_p Termios) int {
unsafe {
return C.tcgetattr(fd, &C.termios(termios_p))
}
}
// tcsetattr is an unsafe wrapper around C.termios and keeps its semantic
@[inline]
pub fn tcsetattr(fd int, optional_actions int, mut termios_p Termios) int {
unsafe {
return C.tcsetattr(fd, optional_actions, &C.termios(termios_p))
}
}
// ioctl is an unsafe wrapper around C.ioctl and keeps its semantic
@[inline]
pub fn ioctl(fd int, request u64, arg voidptr) int {
unsafe {
return C.ioctl(fd, request, arg)
}
}
// set_state applies the flags in the `new_state` to the descriptor `fd`.
pub fn set_state(fd int, new_state Termios) int {
mut x := new_state
return tcsetattr(0, C.TCSANOW, mut x)
}
// disable_echo disables echoing characters as they are typed, when that Termios state is later set with termios.set_state(fd,t).
pub fn (mut t Termios) disable_echo() {
t.c_lflag &= invert(C.ECHO)
}
@@ -0,0 +1,97 @@
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
//
// Serves as more advanced input method
// based on the work of https://github.com/AmokHuginnsson/replxx
//
module termios
#include <termios.h>
#include <sys/ioctl.h>
// https://web.mit.edu/freebsd/head/sys/sys/_termios.h
const cclen = 20
type TcFlag = int
type Speed = int
type Cc = u8
// Termios stores the terminal options
pub struct C.termios {
mut:
c_iflag TcFlag
c_oflag TcFlag
c_cflag TcFlag
c_lflag TcFlag
c_cc [cclen]Cc
c_ispeed Speed
c_ospeed Speed
}
fn C.tcgetattr(fd i32, termios_p &C.termios) i32
fn C.tcsetattr(fd i32, optional_actions i32, const_termios_p &C.termios) i32
fn C.ioctl(fd i32, request u64, args ...voidptr) i32
// flag provides a termios flag of the correct size
// for the underlying C.termios structure
@[inline]
pub fn flag(value int) TcFlag {
return int(value)
}
// invert is a platform dependant way to bitwise NOT (~) TcFlag
// as its length varies across platforms
@[inline]
pub fn invert(value TcFlag) TcFlag {
return ~int(value)
}
pub struct Termios {
pub mut:
c_iflag TcFlag
c_oflag TcFlag
c_cflag TcFlag
c_lflag TcFlag
c_cc [cclen]Cc
c_ispeed Speed
c_ospeed Speed
}
// tcgetattr is an unsafe wrapper around C.termios and keeps its semantic
@[inline]
pub fn tcgetattr(fd int, mut termios_p Termios) int {
unsafe {
return C.tcgetattr(fd, &C.termios(termios_p))
}
}
// tcsetattr is an unsafe wrapper around C.termios and keeps its semantic
@[inline]
pub fn tcsetattr(fd int, optional_actions int, mut termios_p Termios) int {
unsafe {
return C.tcsetattr(fd, optional_actions, &C.termios(termios_p))
}
}
// ioctl is an unsafe wrapper around C.ioctl and keeps its semantic
@[inline]
pub fn ioctl(fd int, request u64, arg voidptr) int {
unsafe {
return C.ioctl(fd, request, arg)
}
}
// set_state applies the flags in the `new_state` to the descriptor `fd`.
pub fn set_state(fd int, new_state Termios) int {
mut x := new_state
return tcsetattr(0, C.TCSANOW, mut x)
}
// disable_echo disables echoing characters as they are typed, when that Termios state is later set with termios.set_state(fd,t).
pub fn (mut t Termios) disable_echo() {
t.c_lflag &= invert(C.ECHO)
}
@@ -0,0 +1,97 @@
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
//
// Serves as more advanced input method
// based on the work of https://github.com/AmokHuginnsson/replxx
//
module termios
#include <termios.h>
#include <sys/ioctl.h>
// https://github.com/openbsd/src/blob/master/sys/sys/termios.h
const cclen = 20
type TcFlag = int
type Speed = int
type Cc = u8
// Termios stores the terminal options
pub struct C.termios {
mut:
c_iflag TcFlag
c_oflag TcFlag
c_cflag TcFlag
c_lflag TcFlag
c_cc [cclen]Cc
c_ispeed Speed
c_ospeed Speed
}
fn C.tcgetattr(fd i32, termios_p &C.termios) i32
fn C.tcsetattr(fd i32, optional_actions i32, const_termios_p &C.termios) i32
fn C.ioctl(fd i32, request u64, args ...voidptr) i32
// flag provides a termios flag of the correct size
// for the underlying C.termios structure
@[inline]
pub fn flag(value int) TcFlag {
return int(value)
}
// invert is a platform dependant way to bitwise NOT (~) TcFlag
// as its length varies across platforms
@[inline]
pub fn invert(value TcFlag) TcFlag {
return ~int(value)
}
pub struct Termios {
pub mut:
c_iflag TcFlag
c_oflag TcFlag
c_cflag TcFlag
c_lflag TcFlag
c_cc [cclen]Cc
c_ispeed Speed
c_ospeed Speed
}
// tcgetattr is an unsafe wrapper around C.termios and keeps its semantic
@[inline]
pub fn tcgetattr(fd int, mut termios_p Termios) int {
unsafe {
return C.tcgetattr(fd, &C.termios(termios_p))
}
}
// tcsetattr is an unsafe wrapper around C.termios and keeps its semantic
@[inline]
pub fn tcsetattr(fd int, optional_actions int, mut termios_p Termios) int {
unsafe {
return C.tcsetattr(fd, optional_actions, &C.termios(termios_p))
}
}
// ioctl is an unsafe wrapper around C.ioctl and keeps its semantic
@[inline]
pub fn ioctl(fd int, request u64, arg voidptr) int {
unsafe {
return C.ioctl(fd, request, arg)
}
}
// set_state applies the flags in the `new_state` to the descriptor `fd`.
pub fn set_state(fd int, new_state Termios) int {
mut x := new_state
return tcsetattr(0, C.TCSANOW, mut x)
}
// disable_echo disables echoing characters as they are typed, when that Termios state is later set with termios.set_state(fd,t).
pub fn (mut t Termios) disable_echo() {
t.c_lflag &= invert(C.ECHO)
}
+99
View File
@@ -0,0 +1,99 @@
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
//
// Serves as more advanced input method
// based on the work of https://github.com/AmokHuginnsson/replxx
//
module termios
#include <termios.h>
#include <sys/ioctl.h>
// https://github.com/vocho/openqnx/blob/master/trunk/lib/c/public/termios.h
const cclen = 20
type TcFlag = int
type Speed = int
type Cc = u8
// Termios stores the terminal options
pub struct C.termios {
mut:
c_iflag TcFlag
c_oflag TcFlag
c_cflag TcFlag
c_lflag TcFlag
c_cc [cclen]Cc
reserved [3]u32
c_ispeed Speed
c_ospeed Speed
}
fn C.tcgetattr(fd i32, termios_p &C.termios) i32
fn C.tcsetattr(fd i32, optional_actions i32, const_termios_p &C.termios) i32
fn C.ioctl(fd i32, request u64, args ...voidptr) i32
// flag provides a termios flag of the correct size
// for the underlying C.termios structure
@[inline]
pub fn flag(value int) TcFlag {
return int(value)
}
// invert is a platform dependant way to bitwise NOT (~) TcFlag
// as its length varies across platforms
@[inline]
pub fn invert(value TcFlag) TcFlag {
return ~int(value)
}
pub struct Termios {
pub mut:
c_iflag TcFlag
c_oflag TcFlag
c_cflag TcFlag
c_lflag TcFlag
c_cc [cclen]Cc
reserved [3]u32
c_ispeed Speed
c_ospeed Speed
}
// tcgetattr is an unsafe wrapper around C.termios and keeps its semantic
@[inline]
pub fn tcgetattr(fd int, mut termios_p Termios) int {
unsafe {
return C.tcgetattr(fd, &C.termios(termios_p))
}
}
// tcsetattr is an unsafe wrapper around C.termios and keeps its semantic
@[inline]
pub fn tcsetattr(fd int, optional_actions int, mut termios_p Termios) int {
unsafe {
return C.tcsetattr(fd, optional_actions, &C.termios(termios_p))
}
}
// ioctl is an unsafe wrapper around C.ioctl and keeps its semantic
@[inline]
pub fn ioctl(fd int, request u64, arg voidptr) int {
unsafe {
return C.ioctl(fd, request, arg)
}
}
// set_state applies the flags in the `new_state` to the descriptor `fd`.
pub fn set_state(fd int, new_state Termios) int {
mut x := new_state
return tcsetattr(0, C.TCSANOW, mut x)
}
// disable_echo disables echoing characters as they are typed, when that Termios state is later set with termios.set_state(fd,t).
pub fn (mut t Termios) disable_echo() {
t.c_lflag &= invert(C.ECHO)
}
@@ -0,0 +1,93 @@
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
//
// Serves as more advanced input method
// based on the work of https://github.com/AmokHuginnsson/replxx
//
module termios
#include <termios.h>
#include <sys/ioctl.h>
// https://web.mit.edu/freebsd/head/sys/sys/_termios.h
const cclen = 20
type TcFlag = int
type Speed = int
type Cc = u8
// Termios stores the terminal options
pub struct C.termios {
mut:
c_iflag TcFlag
c_oflag TcFlag
c_cflag TcFlag
c_lflag TcFlag
c_cc [cclen]Cc
}
fn C.tcgetattr(fd i32, termios_p &C.termios) i32
fn C.tcsetattr(fd i32, optional_actions i32, const_termios_p &C.termios) i32
fn C.ioctl(fd i32, request u64, args ...voidptr) i32
// flag provides a termios flag of the correct size
// for the underlying C.termios structure
@[inline]
pub fn flag(value int) TcFlag {
return int(value)
}
// invert is a platform dependant way to bitwise NOT (~) TcFlag
// as its length varies across platforms
@[inline]
pub fn invert(value TcFlag) TcFlag {
return ~int(value)
}
pub struct Termios {
pub mut:
c_iflag TcFlag
c_oflag TcFlag
c_cflag TcFlag
c_lflag TcFlag
c_cc [cclen]Cc
}
// tcgetattr is an unsafe wrapper around C.termios and keeps its semantic
@[inline]
pub fn tcgetattr(fd int, mut termios_p Termios) int {
unsafe {
return C.tcgetattr(fd, &C.termios(termios_p))
}
}
// tcsetattr is an unsafe wrapper around C.termios and keeps its semantic
@[inline]
pub fn tcsetattr(fd int, optional_actions int, mut termios_p Termios) int {
unsafe {
return C.tcsetattr(fd, optional_actions, &C.termios(termios_p))
}
}
// ioctl is an unsafe wrapper around C.ioctl and keeps its semantic
@[inline]
pub fn ioctl(fd int, request u64, arg voidptr) int {
unsafe {
return C.ioctl(fd, request, arg)
}
}
// set_state applies the flags in the `new_state` to the descriptor `fd`.
pub fn set_state(fd int, new_state Termios) int {
mut x := new_state
return tcsetattr(0, C.TCSANOW, mut x)
}
// disable_echo disables echoing characters as they are typed, when that Termios state is later set with termios.set_state(fd,t).
pub fn (mut t Termios) disable_echo() {
t.c_lflag &= invert(C.ECHO)
}
@@ -0,0 +1,35 @@
module termios
fn test_portable() {
assert 123 == int(flag(123))
o := Termios{
c_lflag: flag(0xFFFF)
} // assume c_lflag exists everywhere
// dump( o.c_lflag )
mut n := o
n.c_lflag &= invert(1)
// dump( n.c_lflag )
assert n.c_lflag != o.c_lflag
n.disable_echo() // just assume it exists, and can be called everywhere
assert true
}
@[if !windows]
fn test_termios() {
mut original_term := Termios{}
tcgetattr(0, mut original_term)
println(original_term)
mut silent_term := original_term
silent_term.c_lflag &= invert(C.ECHO)
tcsetattr(0, C.TCSANOW, mut silent_term)
mut check_term := Termios{}
tcgetattr(0, mut check_term)
assert check_term.c_lflag == silent_term.c_lflag
tcsetattr(0, C.TCSANOW, mut original_term)
tcgetattr(0, mut check_term)
assert check_term.c_lflag == original_term.c_lflag
}
@@ -0,0 +1,65 @@
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
//
// TODO: Windows version needs to be implemented.
// Will serve as more advanced input method
// based on the work of https://github.com/AmokHuginnsson/replxx
//
module termios
type TcFlag = int
type Speed = int
type Cc = u8
// flag provides a termios flag of the correct size for the underlying C.termios structure.
// It is only implemented for Unix like OSes.
pub fn flag(value int) TcFlag {
return int(value)
}
// invert is a platform dependant way to bitwise NOT (~) TcFlag, as its length varies across platforms.
// It is only implemented for Unix like OSes.
pub fn invert(value TcFlag) TcFlag {
return ~int(value)
}
pub struct Termios {
pub mut:
c_iflag TcFlag
c_oflag TcFlag
c_cflag TcFlag
c_lflag TcFlag
c_line Cc
c_cc [32]Cc
c_ispeed Speed
c_ospeed Speed
}
// tcgetattr is an unsafe wrapper around C.termios and keeps its semantic.
// It is only implemented for Unix like OSes.
pub fn tcgetattr(fd int, mut termios_p Termios) int {
return -1
}
// tcsetattr is an unsafe wrapper around C.termios and keeps its semantic.
// It is only implemented for Unix like OSes.
pub fn tcsetattr(fd int, optional_actions int, mut termios_p Termios) int {
return -1
}
// ioctl is an unsafe wrapper around C.ioctl and keeps its semantic.
// It is only implemented for Unix like OSes.
@[inline]
pub fn ioctl(fd int, request u64, arg voidptr) int {
return -1
}
// set_state applies the flags in the `new_state` to the descriptor `fd`.
pub fn set_state(fd int, new_state Termios) int {
return -1
}
// disable_echo disables echoing characters as they are typed, when that Termios state is later set with termios.set_state(fd,t).
pub fn (mut t Termios) disable_echo() {
}
@@ -0,0 +1,7 @@
import term.ui
import term
fn test_term_and_term_ui_can_compile_together() {
println('${term.bold('hello')} world ${ui.color_table[0]}')
assert true
}
@@ -0,0 +1,7 @@
import term
import term.ui
fn test_term_and_term_ui_can_compile_together() {
println('${term.bold('hello')} world ${ui.color_table[0]}')
assert true
}
+103
View File
@@ -0,0 +1,103 @@
## `term.ui`
A V module for designing terminal UI apps
#### Quickstart
```v
import term.ui as tui
struct App {
mut:
tui &tui.Context = unsafe { nil }
}
fn event(e &tui.Event, x voidptr) {
if e.typ == .key_down && e.code == .escape {
exit(0)
}
}
fn frame(x voidptr) {
mut app := unsafe { &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()
}
fn main() {
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.
#### Configuration
- `user_data voidptr` - a pointer to any `user_data`, it will be passed as the last argument to
each callback. Used for accessing your app context from the different callbacks.
- `init_fn fn(voidptr)` - a callback that will be called after initialization
and before the first event / frame. Useful for initializing any user data.
- `frame_fn fn(voidptr)` - a callback that will be fired on each frame,
at a rate of `frame_rate` frames per second.
`event_fn fn(&Event, voidptr)` - a callback that will be fired for every event received.
- `cleanup_fn fn(voidptr)` - a callback that will be fired once, before the application exits.
- `fail_fn fn(string)` - a callback that will be fired
if a fatal error occurs during app initialization.
- `buffer_size int = 256` - the internal size of the read buffer.
Increasing it may help in case you're missing events, but you probably shouldn't lower
this value unless you make sure you're still receiving all events. In general,
higher frame rates work better with lower buffer sizes, and vice versa.
- `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.
- `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,
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.
#### FAQ
Q: My terminal (doesn't receive events / doesn't print anything / prints gibberish characters),
what's up with that?
A: Please check if your terminal. The module has been tested with `xterm`-based terminals on Linux
(like `gnome-terminal` and `konsole`), and `Terminal.app` and `iterm2` on macOS.
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).
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: `term.ui` emits `key_up` on Windows and on terminals that support the kitty keyboard
protocol. Legacy terminal input still only provides `key_down`, so `key_up` is not
available everywhere.
+89
View File
@@ -0,0 +1,89 @@
// 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]!
pub const color_table = init_color_table()
@[direct_array_access]
fn init_color_table() []u32 {
mut color_table_ := []u32{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] = ((u32(r) << 16) & 0xffffff) + ((u32(g) << 8) & 0xffff) +
(u32(b) & 0xff)
}
// grayscale
for i in 0 .. 24 {
r := 8 + (i * 10)
color_table_[i + 232] = ((u32(r) << 16) & 0xffffff) + ((u32(r) << 8) & 0xffff) +
(u32(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 := (u32(r) << 16) + (u32(g) << 8) + u32(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
}
@@ -0,0 +1,86 @@
module ui
pub 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]
pub struct C.INPUT_RECORD {
EventType u16
Event C.Event
}
pub union C.uChar {
mut:
UnicodeChar rune
AsciiChar u8
}
@[typedef]
pub struct C.KEY_EVENT_RECORD {
bKeyDown int
wRepeatCount u16
wVirtualKeyCode u16
wVirtualScanCode u16
uChar C.uChar
dwControlKeyState u32
}
@[typedef]
pub struct C.MOUSE_EVENT_RECORD {
dwMousePosition C.COORD
dwButtonState u32
dwControlKeyState u32
dwEventFlags u32
}
@[typedef]
pub struct C.WINDOW_BUFFER_SIZE_RECORD {
dwSize C.COORD
}
@[typedef]
pub struct C.MENU_EVENT_RECORD {
dwCommandId u32
}
@[typedef]
pub struct C.FOCUS_EVENT_RECORD {
bSetFocus int
}
@[typedef]
pub struct C.COORD {
mut:
X i16
Y i16
}
@[typedef]
pub struct C.SMALL_RECT {
mut:
Left u16
Top u16
Right u16
Bottom u16
}
@[typedef]
pub struct C.CONSOLE_SCREEN_BUFFER_INFO {
mut:
dwSize C.COORD
dwCursorPosition C.COORD
wAttributes u16
srWindow C.SMALL_RECT
dwMaximumWindowSize C.COORD
}
fn C.ReadConsoleInput(hConsoleInput C.HANDLE, lpBuffer &C.INPUT_RECORD, nLength u32, lpNumberOfEventsRead &u32) bool
fn C.GetNumberOfConsoleInputEvents(hConsoleInput C.HANDLE, lpcNumberOfEvents &u32) bool
fn C.GetConsoleScreenBufferInfo(handle C.HANDLE, info &C.CONSOLE_SCREEN_BUFFER_INFO) bool
+268
View File
@@ -0,0 +1,268 @@
// Copyright (c) 2020-2024 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
pub enum KeyCode {
null = 0
tab = 9
enter = 10
escape = 27
space = 32
backspace = 127
exclamation = 33
double_quote = 34
hashtag = 35
dollar = 36
percent = 37
ampersand = 38
single_quote = 39
left_paren = 40
right_paren = 41
asterisk = 42
plus = 43
comma = 44
minus = 45
period = 46
slash = 47
_0 = 48
_1 = 49
_2 = 50
_3 = 51
_4 = 52
_5 = 53
_6 = 54
_7 = 55
_8 = 56
_9 = 57
colon = 58
semicolon = 59
less_than = 60
equal = 61
greater_than = 62
question_mark = 63
at = 64
a = 97
b = 98
c = 99
d = 100
e = 101
f = 102
g = 103
h = 104
i = 105
j = 106
k = 107
l = 108
m = 109
n = 110
o = 111
p = 112
q = 113
r = 114
s = 115
t = 116
u = 117
v = 118
w = 119
x = 120
y = 121
z = 122
left_square_bracket = 91
backslash = 92
right_square_bracket = 93
caret = 94
underscore = 95
backtick = 96
left_curly_bracket = 123
vertical_bar = 124
right_curly_bracket = 125
tilde = 126
insert = 260
delete = 261
up = 262
down = 263
right = 264
left = 265
page_up = 266
page_down = 267
home = 268
end = 269
f1 = 290
f2 = 291
f3 = 292
f4 = 293
f5 = 294
f6 = 295
f7 = 296
f8 = 297
f9 = 298
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 enum Direction {
unknown
up
down
left
right
}
pub enum MouseButton {
unknown
left
middle
right
}
pub enum EventType {
unknown
mouse_down
mouse_up
mouse_move
mouse_drag
mouse_scroll
key_down
key_up
resized
}
@[flag]
pub enum Modifiers {
ctrl
shift
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
// Mouse event info
x int
y int
button MouseButton
direction Direction
// Keyboard event info
code KeyCode
modifiers Modifiers
ascii u8
utf8 string
// Resized event info
width int
height int
}
pub struct Context {
ExtraContext // contains fields specific to an implementation
pub:
cfg Config // the initial configuration, passed to ui.init()
mut:
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
window_height int
}
pub struct Config {
pub:
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
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:
reset []os.Signal = [.hup, .int, .quit, .ill, .abrt, .bus, .fpe, .kill, .segv, .pipe, .alrm, .term,
.stop]
}
@[inline]
fn (ctx &Context) init() {
f := ctx.cfg.init_fn or { return }
f(ctx.cfg.user_data)
}
@[inline]
fn (ctx &Context) frame() {
f := ctx.cfg.frame_fn or { return }
f(ctx.cfg.user_data)
}
@[inline]
fn (ctx &Context) cleanup() {
f := ctx.cfg.cleanup_fn or { return }
f(ctx.cfg.user_data)
}
@[inline]
fn (ctx &Context) fail(error string) {
f := ctx.cfg.fail_fn or { return }
f(error)
}
@[inline]
fn (ctx &Context) event(event &Event) {
f := ctx.cfg.event_fn or { return }
f(event, ctx.cfg.user_data)
}
+85
View File
@@ -0,0 +1,85 @@
// Copyright (c) 2020-2024 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.
@[has_globals]
module ui
struct ExtraContext {
mut:
read_buf []u8
// read_all_bytes causes all the raw bytes to be read as one event unit.
// This is cruicial for UTF-8 support since Unicode codepoints can span several bytes.
read_all_bytes bool = true
}
__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
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}
ctx_ptr = ctx
return ctx
}
@[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()
}
@[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()
}
// run sets up and starts the terminal.
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() or { panic(err) }
ctx.termios_loop()
}
}
// shifts the array left, to remove any data that was just read, and updates its len
// TODO: remove
@[inline]
fn (mut ctx Context) shift(len int) {
unsafe {
C.memmove(ctx.read_buf.data, &u8(ctx.read_buf.data) + len, ctx.read_buf.cap - len)
ctx.resize_arr(ctx.read_buf.len - len)
}
}
// TODO: don't actually do this, lmao
@[inline]
fn (mut ctx Context) resize_arr(size int) {
mut l := unsafe { &ctx.read_buf.len }
unsafe {
*l = size
_ = l
}
}
+103
View File
@@ -0,0 +1,103 @@
module ui
fn test_escape_sequence_parses_csi_u_plain_key() {
seq := '\x1b[97;1u'
event, len := escape_sequence(seq)
assert len == seq.len
assert event.typ == .key_down
assert event.code == .a
assert event.modifiers.is_empty()
assert event.utf8 == seq
}
fn test_escape_sequence_parses_csi_u_plain_key_without_modifier_param() {
seq := '\x1b[27u'
event, len := escape_sequence(seq)
assert len == seq.len
assert event.typ == .key_down
assert event.code == .escape
assert event.modifiers.is_empty()
assert event.utf8 == seq
}
fn test_escape_sequence_parses_csi_u_modified_key() {
seq := '\x1b[97;5u'
event, len := escape_sequence(seq)
assert len == seq.len
assert event.typ == .key_down
assert event.code == .a
assert event.modifiers.has(.ctrl)
assert !event.modifiers.has(.shift)
assert !event.modifiers.has(.alt)
assert event.utf8 == seq
}
fn test_escape_sequence_parses_csi_u_key_release() {
seq := '\x1b[97;1:3u'
event, len := escape_sequence(seq)
assert len == seq.len
assert event.typ == .key_up
assert event.code == .a
assert event.modifiers.is_empty()
assert event.utf8 == seq
}
fn test_escape_sequence_parses_csi_u_key_repeat_with_text() {
seq := '\x1b[97;1:2;97u'
event, len := escape_sequence(seq)
assert len == seq.len
assert event.typ == .key_down
assert event.code == .a
assert event.ascii == `a`
assert event.utf8 == 'a'
}
fn test_escape_sequence_parses_csi_u_shifted_text_with_associated_text() {
seq := '\x1b[97;2;65u'
event, len := escape_sequence(seq)
assert len == seq.len
assert event.typ == .key_down
assert event.code == .a
assert event.ascii == `A`
assert event.modifiers == .shift
assert event.utf8 == 'A'
}
fn test_escape_sequence_parses_csi_u_enter_key() {
seq := '\x1b[13;1u'
event, len := escape_sequence(seq)
assert len == seq.len
assert event.typ == .key_down
assert event.code == .enter
assert event.utf8 == seq
}
fn test_escape_sequence_parses_modify_other_keys_sequence() {
seq := '\x1b[27;3;97~'
event, len := escape_sequence(seq)
assert len == seq.len
assert event.typ == .key_down
assert event.code == .a
assert event.modifiers == .alt
assert event.utf8 == seq
}
fn test_escape_sequence_parses_modified_special_key_sequence() {
seq := '\x1b[3;5~'
event, len := escape_sequence(seq)
assert len == seq.len
assert event.typ == .key_down
assert event.code == .delete
assert event.modifiers == .ctrl
assert event.utf8 == seq
}
fn test_escape_sequence_parses_modified_special_key_release_sequence() {
seq := '\x1b[1;5:3A'
event, len := escape_sequence(seq)
assert len == seq.len
assert event.typ == .key_up
assert event.code == .up
assert event.modifiers == .ctrl
assert event.utf8 == seq
}
+344
View File
@@ -0,0 +1,344 @@
// Copyright (c) 2020-2024 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.
@[has_globals]
module ui
import os
import time
const buf_size = 64
__global ctx_ptr = &Context(unsafe { nil })
__global 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 unsafe { 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')
flush_stdout()
}
C.SetConsoleMode(ctx_ptr.stdin_handle, stdin_at_startup)
}
load_title()
os.flush()
}
// init initializes the context of a windows console given the `cfg`.
pub fn init(cfg Config) &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) {
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) {
panic('could not set raw input mode')
}
mut input_mode := u32(C.ENABLE_WINDOW_INPUT)
if ctx.cfg.mouse_enabled {
input_mode |= u32(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
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')
flush_stdout()
}
if ctx.cfg.hide_cursor {
ctx.hide_cursor()
ctx.flush()
}
if ctx.cfg.window_title != '' {
print('\x1b]0;${ctx.cfg.window_title}\x07')
flush_stdout()
}
ctx_ptr = ctx
at_exit(restore_terminal_state) or {}
for code in ctx.cfg.reset {
os.signal_opt(code, fn (_ os.Signal) {
mut c := ctx_ptr
if unsafe { c != 0 } {
c.cleanup()
}
exit(0)
}) or {}
}
ctx.stdin_handle = stdin_handle
ctx.stdout_handle = stdout_handle
return ctx
}
// run starts the windows console or restarts if it was paused.
pub fn (mut ctx Context) run() ! {
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.sleep(sleep_len * time.microsecond)
}
if !ctx.paused {
sw.restart()
if ctx.cfg.event_fn != none {
ctx.parse_events()
}
ctx.frame()
sw.pause()
e := sw.elapsed().microseconds()
sleep_len = frame_time - int(e)
ctx.frame_count++
}
}
}
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[0], 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 }
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 { unsafe { KeyCode(ch + 32) } } // letters
91...93 { KeyCode.null } // special keys
96...105 { unsafe { KeyCode(ch - 48) } } // numpad numbers
112...135 { unsafe { KeyCode(ch + 178) } } // f1 - f24
else { unsafe { KeyCode(ascii) } }
}
mut modifiers := unsafe { Modifiers(0) }
if e.dwControlKeyState & (0x1 | 0x2) != 0 {
modifiers.set(.alt)
}
if e.dwControlKeyState & (0x4 | 0x8) != 0 {
modifiers.set(.ctrl)
}
if e.dwControlKeyState & 0x10 != 0 {
modifiers.set(.shift)
}
event_type := if e.bKeyDown == 0 {
EventType.key_up
} else {
EventType.key_down
}
repeat_count := if event_type == .key_down && e.wRepeatCount > 0 {
int(e.wRepeatCount)
} else {
1
}
for _ in 0 .. repeat_count {
mut event := &Event{
typ: event_type
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 + 1
y := int(e.dwMousePosition.Y) - sb_info.srWindow.Top + 1
mut modifiers := unsafe { Modifiers(0) }
if e.dwControlKeyState & (0x1 | 0x2) != 0 {
modifiers.set(.alt)
}
if e.dwControlKeyState & (0x4 | 0x8) != 0 {
modifiers.set(.ctrl)
}
if e.dwControlKeyState & 0x10 != 0 {
modifiers.set(.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, 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 {}
}
}
}
@[inline]
fn save_title() {
// restore the previously saved terminal title
print('\x1b[22;0t')
flush_stdout()
}
@[inline]
fn load_title() {
// restore the previously saved terminal title
print('\x1b[23;0t')
flush_stdout()
}
+13
View File
@@ -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
}
+765
View File
@@ -0,0 +1,765 @@
// Copyright (c) 2020-2024 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 strings
import time
import term.termios
#include <signal.h>
pub struct C.winsize {
ws_row u16
ws_col u16
}
const termios_at_startup = get_termios()
const kitty_keyboard_flags = 0b10 | 0b1000 | 0b10000
@[inline]
fn get_termios() termios.Termios {
mut t := termios.Termios{}
termios.tcgetattr(C.STDIN_FILENO, mut t)
return t
}
@[inline]
fn get_terminal_size() (u16, u16) {
winsz := C.winsize{}
termios.ioctl(0, u64(termios.flag(C.TIOCGWINSZ)), voidptr(&winsz))
return winsz.ws_row, winsz.ws_col
}
fn restore_terminal_state_signal(_ os.Signal) {
restore_terminal_state()
}
fn restore_terminal_state() {
termios_reset()
mut c := ctx_ptr
if unsafe { c != 0 } {
c.paused = true
load_title()
}
os.flush()
}
fn (mut ctx Context) termios_setup() ! {
// store the current title, so restore_terminal_state can get it back
save_title()
if !ctx.cfg.skip_init_checks && !(os.is_atty(C.STDIN_FILENO) != 0
&& os.is_atty(C.STDOUT_FILENO) != 0) {
return error('not running under a TTY')
}
mut tios := get_termios()
if ctx.cfg.capture_events {
// Set raw input mode by unsetting ICANON and ECHO,
// as well as disable e.g. ctrl+c and ctrl.z
tios.c_iflag &=
termios.invert(termios.flag(int(C.IGNBRK) | int(C.BRKINT) | int(C.PARMRK) | int(C.IXON)))
tios.c_lflag &=
termios.invert(termios.flag(int(C.ICANON) | int(C.ISIG) | int(C.ECHO) | int(C.IEXTEN) | int(C.TOSTOP)))
} else {
// Set raw input mode by unsetting ICANON and ECHO
tios.c_lflag &= termios.invert(termios.flag(int(C.ICANON) | int(C.ECHO)))
}
if ctx.cfg.hide_cursor {
ctx.hide_cursor()
ctx.flush()
}
if ctx.cfg.window_title != '' {
print('\x1b]0;${ctx.cfg.window_title}\x07')
flush_stdout()
}
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
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()
}
// 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)
if ctx.cfg.mouse_enabled {
// enable mouse input
print('\x1b[?1003h\x1b[?1006h')
flush_stdout()
}
if ctx.cfg.use_alternate_buffer {
// switch to the alternate buffer
print('\x1b[?1049h')
flush_stdout()
// clear the terminal and set the cursor to the origin
print('\x1b[2J\x1b[3J\x1b[1;1H')
flush_stdout()
}
if ctx.cfg.capture_events {
// Ask supporting terminals to report press/repeat/release in CSI-u form.
print('\x1b[>${kitty_keyboard_flags}u')
flush_stdout()
}
ctx.window_height, ctx.window_width = get_terminal_size()
// Reset console on exit
at_exit(restore_terminal_state) or {}
os.signal_opt(.tstp, restore_terminal_state_signal) or {}
os.signal_opt(.cont, fn (_ os.Signal) {
mut c := ctx_ptr
if unsafe { c != 0 } {
c.termios_setup() or { panic(err) }
c.window_height, c.window_width = get_terminal_size()
mut event := &Event{
typ: .resized
width: c.window_width
height: c.window_height
}
c.paused = false
c.event(event)
}
}) or {}
for code in ctx.cfg.reset {
os.signal_opt(code, fn (_ os.Signal) {
mut c := ctx_ptr
if unsafe { c != 0 } {
c.cleanup()
}
exit(0)
}) or {}
}
os.signal_opt(.winch, fn (_ os.Signal) {
mut c := ctx_ptr
if unsafe { c != 0 } {
c.window_height, c.window_width = get_terminal_size()
mut event := &Event{
typ: .resized
width: c.window_width
height: c.window_height
}
c.event(event)
}
}) or {}
os.flush()
}
fn get_cursor_position() (int, int) {
print('\033[6n')
flush_stdout()
mut s := ''
unsafe {
buf := malloc_noscan(25)
len := C.read(C.STDIN_FILENO, buf, 24)
buf[len] = 0
s = tos(buf, len)
}
if s.len == 0 {
return -1, -1
}
a := s[2..].split(';')
if a.len != 2 {
return -1, -1
}
return a[0].int(), a[1].int()
}
fn supports_truecolor() bool {
// faster/simpler, but less reliable, check
if os.getenv('COLORTERM') in ['truecolor', '24bit'] {
return true
}
// set the bg color to some arbitrary value (#010203), assumed not to be the default
print('\x1b[48:2:1:2:3m')
flush_stdout()
// andquery the current color
print('\x1bP\$qm\x1b\\')
flush_stdout()
mut s := ''
unsafe {
buf := malloc_noscan(25)
len := C.read(C.STDIN_FILENO, buf, 24)
buf[len] = 0
s = tos(buf, len)
}
return s.contains('1:2:3')
}
fn termios_reset() {
// C.TCSANOW ??
mut startup := termios_at_startup
termios.tcsetattr(C.STDIN_FILENO, C.TCSAFLUSH, mut startup)
c := ctx_ptr
if unsafe { c != 0 } && c.cfg.capture_events {
// Pop the keyboard mode stack before leaving the current screen.
print('\x1b[<u')
}
print('\x1b[?1003l\x1b[?1006l\x1b[?25h')
flush_stdout()
if unsafe { c != 0 } && c.cfg.use_alternate_buffer {
print('\x1b[?1049l')
}
os.flush()
}
///////////////////////////////////////////
// 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
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
}
// println('SLEEPING: ${sleep_len}')
if sleep_len > 0 {
time.sleep(sleep_len * time.microsecond)
}
if !ctx.paused {
sw.restart()
if ctx.cfg.event_fn != none {
unsafe {
len := C.read(C.STDIN_FILENO, &u8(ctx.read_buf.data) + ctx.read_buf.len,
ctx.read_buf.cap - ctx.read_buf.len)
ctx.resize_arr(ctx.read_buf.len + len)
}
if ctx.read_buf.len > 0 {
ctx.parse_events()
}
}
ctx.frame()
sw.pause()
e := sw.elapsed().microseconds()
sleep_len = frame_time - int(e)
ctx.frame_count++
}
}
}
fn (mut ctx Context) parse_events() {
// Stop this from getting stuck in rare cases where something isn't parsed correctly
mut nr_iters := 0
for ctx.read_buf.len > 0 {
nr_iters++
if nr_iters > 100 {
ctx.shift(1)
}
mut event := &Event(unsafe { nil })
if ctx.read_buf[0] == 0x1b {
e, len := escape_sequence(ctx.read_buf.bytestr())
event = unsafe { e }
ctx.shift(len)
} else {
if ctx.read_all_bytes {
e, len := multi_char(ctx.read_buf.bytestr())
event = unsafe { e }
ctx.shift(len)
} else {
event = single_char(ctx.read_buf.bytestr())
ctx.shift(1)
}
}
if unsafe { event != 0 } {
ctx.event(event)
nr_iters = 0
}
}
}
fn single_char(buf string) &Event {
ch := buf[0]
mut event := &Event{
typ: .key_down
ascii: ch
code: unsafe { KeyCode(ch) }
utf8: ch.ascii_str()
}
match ch {
// special handling for `ctrl + letter`
// TODO: Fix assoc in V and remove this workaround :/
// 1 ... 26 { event = Event{ ...event, code: KeyCode(96 | ch), modifiers: .ctrl } }
// 65 ... 90 { event = Event{ ...event, code: KeyCode(32 | ch), modifiers: .shift } }
// The bit `or`s here are really just `+`'s, just written in this way for a tiny performance improvement
// don't treat tab, enter as ctrl+i, ctrl+j
1...8, 11...26 {
event = &Event{
typ: event.typ
ascii: event.ascii
utf8: event.utf8
code: unsafe { KeyCode(96 | ch) }
modifiers: .ctrl
}
}
65...90 {
event = &Event{
typ: event.typ
ascii: event.ascii
utf8: event.utf8
code: unsafe { KeyCode(32 | ch) }
modifiers: .shift
}
}
else {}
}
return event
}
fn multi_char(buf string) (&Event, int) {
ch := buf[0]
mut event := &Event{
typ: .key_down
ascii: ch
code: unsafe { KeyCode(ch) }
utf8: buf
}
match ch {
// special handling for `ctrl + letter`
// TODO: Fix assoc in V and remove this workaround :/
// 1 ... 26 { event = Event{ ...event, code: KeyCode(96 | ch), modifiers: .ctrl } }
// 65 ... 90 { event = Event{ ...event, code: KeyCode(32 | ch), modifiers: .shift } }
// The bit `or`s here are really just `+`'s, just written in this way for a tiny performance improvement
// don't treat tab, enter as ctrl+i, ctrl+j
1...8, 11...26 {
event = &Event{
typ: event.typ
ascii: event.ascii
utf8: event.utf8
code: unsafe { KeyCode(96 | ch) }
modifiers: .ctrl
}
}
65...90 {
event = &Event{
typ: event.typ
ascii: event.ascii
utf8: event.utf8
code: unsafe { KeyCode(32 | ch) }
modifiers: .shift
}
}
else {}
}
return event, buf.len
}
// Gets an entire, independent escape sequence from the buffer
// Normally, this just means reading until the first letter, but there are some exceptions...
fn escape_end(buf string) int {
mut i := 0
for {
if i + 1 == buf.len {
return buf.len
}
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` {
return i + 2
}
}
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
assert false
return 0
}
@[inline]
fn modifiers_from_report_param(param int) Modifiers {
flags := if param > 0 { param - 1 } else { 0 }
mut modifiers := unsafe { Modifiers(0) }
if flags & 0b001 != 0 {
modifiers.set(.shift)
}
if flags & 0b010 != 0 {
modifiers.set(.alt)
}
if flags & 0b100 != 0 {
modifiers.set(.ctrl)
}
return modifiers
}
@[inline]
fn key_event_type_from_report_param(param int) EventType {
return match param {
3 { .key_up }
else { .key_down }
}
}
@[inline]
fn parse_key_report_param(param string) (Modifiers, EventType) {
if param.len == 0 {
return unsafe { Modifiers(0) }, EventType.key_down
}
parts := param.split(':')
modifiers := modifiers_from_report_param(parts[0].int())
event_type := if parts.len > 1 {
key_event_type_from_report_param(parts[1].int())
} else {
EventType.key_down
}
return modifiers, event_type
}
fn utf8_from_reported_text(param string) string {
if param.len == 0 {
return ''
}
mut builder := strings.new_builder(param.len)
for part in param.split(':') {
codepoint := part.int()
if codepoint <= 0 || codepoint > 0x10ffff {
continue
}
builder.write_string(utf32_to_str(u32(codepoint)))
}
return builder.str()
}
@[inline]
fn event_from_reported_key(codepoint int, raw string, modifiers Modifiers, event_type EventType, text string) &Event {
mut utf8 := raw
if text.len > 0 {
utf8 = text
}
mut ascii := u8(0)
if text.len == 1 {
ascii = text[0]
}
if codepoint <= 0 || codepoint > 0x10ffff {
return &Event{
typ: event_type
ascii: ascii
utf8: utf8
modifiers: modifiers
}
}
if codepoint <= 255 {
ch := u8(codepoint)
if ch == `\r` {
return &Event{
typ: event_type
ascii: ch
code: .enter
utf8: utf8
modifiers: modifiers
}
}
base := single_char(ch.ascii_str())
event_ascii := if ascii != 0 { ascii } else { base.ascii }
return &Event{
typ: event_type
ascii: event_ascii
code: base.code
utf8: utf8
modifiers: base.modifiers | modifiers
}
}
return &Event{
typ: event_type
ascii: ascii
utf8: utf8
modifiers: modifiers
}
}
fn parse_csi_u_key_sequence(single string, buf string) &Event {
if buf.len < 4 || buf[0] != `[` || buf[buf.len - 1] != `u` {
return unsafe { nil }
}
parts := buf[1..buf.len - 1].split(';')
if parts.len < 1 || parts.len > 3 {
return unsafe { nil }
}
codepoint := parts[0].split(':')[0].int()
mut modifiers := unsafe { Modifiers(0) }
mut event_type := EventType.key_down
if parts.len > 1 {
modifiers, event_type = parse_key_report_param(parts[1])
}
text := if parts.len > 2 { utf8_from_reported_text(parts[2]) } else { '' }
return event_from_reported_key(codepoint, single, modifiers, event_type, text)
}
fn parse_modify_other_keys_sequence(single string, buf string) &Event {
if buf.len < 7 || buf[0] != `[` || buf[buf.len - 1] != `~` {
return unsafe { nil }
}
parts := buf[1..buf.len - 1].split(';')
if parts.len != 3 || parts[0] != '27' {
return unsafe { nil }
}
return event_from_reported_key(parts[2].int(), single,
modifiers_from_report_param(parts[1].int()), .key_down, '')
}
@[inline]
fn key_code_from_csi_final(final u8) KeyCode {
return match final {
`A` { .up }
`B` { .down }
`C` { .right }
`D` { .left }
`F` { .end }
`H` { .home }
`P` { .f1 }
`Q` { .f2 }
`R` { .f3 }
`S` { .f4 }
else { .null }
}
}
@[inline]
fn key_code_from_tilde_param(param string) KeyCode {
return match param {
'2' { .insert }
'3' { .delete }
'5' { .page_up }
'6' { .page_down }
'11' { .f1 }
'12' { .f2 }
'13' { .f3 }
'14' { .f4 }
'15' { .f5 }
'17' { .f6 }
'18' { .f7 }
'19' { .f8 }
'20' { .f9 }
'21' { .f10 }
'23' { .f11 }
'24' { .f12 }
else { .null }
}
}
fn parse_csi_modified_key_sequence(buf string) (KeyCode, Modifiers, EventType, bool) {
if buf.len < 5 || buf[0] != `[` {
return KeyCode.null, unsafe { Modifiers(0) }, EventType.key_down, false
}
final := buf[buf.len - 1]
params := buf[1..buf.len - 1].split(';')
if params.len != 2 {
return KeyCode.null, unsafe { Modifiers(0) }, EventType.key_down, false
}
modifiers, event_type := parse_key_report_param(params[1])
if final == `~` {
code := key_code_from_tilde_param(params[0])
return code, modifiers, event_type, code != .null
}
code := key_code_from_csi_final(final)
return code, modifiers, event_type, code != .null
}
fn escape_sequence(buf_ string) (&Event, int) {
end := escape_end(buf_)
single := buf_[..end] // read until the end of the sequence
buf := single[1..] // skip the escape character
if buf.len == 0 {
return &Event{
typ: .key_down
ascii: 27
code: .escape
utf8: single
}, 1
}
if buf.len == 1 {
c := single_char(buf)
mut modifiers := c.modifiers
modifiers.set(.alt)
return &Event{
typ: c.typ
ascii: c.ascii
code: c.code
utf8: single
modifiers: modifiers
}, 2
}
// ----------------
// Mouse events
// ----------------
// Documentation: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
if buf.len > 2 && buf[1] == `<` {
split := buf[2..].split(';')
if split.len < 3 {
return &Event(unsafe { nil }), 0
}
typ, x, y := split[0].int(), split[1].int(), split[2].int()
lo := typ & 0b00011
hi := typ & 0b11100
mut modifiers := unsafe { Modifiers(0) }
if hi & 4 != 0 {
modifiers.set(.shift)
}
if hi & 8 != 0 {
modifiers.set(.alt)
}
if hi & 16 != 0 {
modifiers.set(.ctrl)
}
match typ {
0...31 {
last := buf[buf.len - 1]
button := if lo < 3 { unsafe { MouseButton(lo + 1) } } else { MouseButton.unknown }
event := if last == `m` || lo == 3 {
EventType.mouse_up
} else {
EventType.mouse_down
}
return &Event{
typ: event
x: x
y: y
button: button
modifiers: modifiers
utf8: single
}, end
}
32...63 {
button, event := if lo < 3 {
unsafe { MouseButton(lo + 1), EventType.mouse_drag }
} else {
MouseButton.unknown, EventType.mouse_move
}
return &Event{
typ: event
x: x
y: y
button: button
modifiers: modifiers
utf8: single
}, end
}
64...95 {
direction := if typ & 1 == 0 { Direction.down } else { Direction.up }
return &Event{
typ: .mouse_scroll
x: x
y: y
direction: direction
modifiers: modifiers
utf8: single
}, end
}
else {
return &Event{
typ: .unknown
utf8: single
}, end
}
}
}
// ----------------------------
// Special key combinations
// ----------------------------
e := parse_csi_u_key_sequence(single, buf)
if unsafe { e != nil } {
return e, end
}
e2 := parse_modify_other_keys_sequence(single, buf)
if unsafe { e2 != nil } {
return e2, end
}
mut code := KeyCode.null
mut modifiers := unsafe { Modifiers(0) }
mut event_type := EventType.key_down
match buf {
'[A', 'OA' { code = .up }
'[B', 'OB' { code = .down }
'[C', 'OC' { code = .right }
'[D', 'OD' { code = .left }
'[5~', '[[5~' { code = .page_up }
'[6~', '[[6~' { code = .page_down }
'[F', 'OF', '[4~', '[[8~' { code = .end }
'[H', 'OH', '[1~', '[[7~' { code = .home }
'[2~' { code = .insert }
'[3~' { code = .delete }
'OP', '[11~' { code = .f1 }
'OQ', '[12~' { code = .f2 }
'OR', '[13~' { code = .f3 }
'OS', '[14~' { code = .f4 }
'[15~' { code = .f5 }
'[17~' { code = .f6 }
'[18~' { code = .f7 }
'[19~' { code = .f8 }
'[20~' { code = .f9 }
'[21~' { code = .f10 }
'[23~' { code = .f11 }
'[24~' { code = .f12 }
else {}
}
if buf == '[Z' {
code = .tab
modifiers.set(.shift)
}
if code == .null {
parsed_code, parsed_modifiers, parsed_event_type, ok := parse_csi_modified_key_sequence(buf)
if ok {
code = parsed_code
modifiers = parsed_modifiers
event_type = parsed_event_type
}
}
return &Event{
typ: event_type
code: code
utf8: single
modifiers: modifiers
}, end
}
+75
View File
@@ -0,0 +1,75 @@
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 {
mut pipe := os.pipe()!
fake_cursor_pos_data := '\033[45;70R'
written_bytes := pipe.write_string(fake_cursor_pos_data)!
if written_bytes == -1 {
pipe.close()
return error('error writing into pipe: ${C.strerror(C.errno)}')
}
C.close(pipe.write_fd)
if C.dup2(pipe.read_fd, C.STDIN_FILENO) == -1 {
C.close(pipe.read_fd)
return error('error redirecting stdin with dup2: ${C.strerror(C.errno)}')
}
C.close(pipe.read_fd)
cursor_pos_x, cursor_pos_y := get_cursor_position()
assert cursor_pos_x == 45
assert cursor_pos_y == 70
}
}
fn test_get_cursor_position_reads_empty_position_data() ! {
mut original_stdin_fd := -1
unsafe {
mut pipe := os.pipe()!
fake_cursor_pos_data := ''
written_bytes := pipe.write_string(fake_cursor_pos_data)!
if written_bytes == -1 {
pipe.close()
return error('error writing into pipe: ${C.strerror(C.errno)}')
}
C.close(pipe.write_fd)
if C.dup2(pipe.read_fd, C.STDIN_FILENO) == -1 {
C.close(pipe.read_fd)
return error('error redirecting stdin with dup2: ${C.strerror(C.errno)}')
}
C.close(pipe.read_fd)
cursor_pos_x, cursor_pos_y := get_cursor_position()
assert cursor_pos_x == -1
assert cursor_pos_y == -1
}
}
+284
View File
@@ -0,0 +1,284 @@
// Copyright (c) 2020-2024 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
pub struct Color {
pub:
r u8
g u8
b u8
}
// hex returns `c`'s RGB color in hex format.
pub fn (c Color) hex() string {
return '#${c.r.hex()}${c.g.hex()}${c.b.hex()}'
}
// Synchronized Updates spec, designed to avoid tearing during renders
// https://gitlab.com/gnachman/iterm2/-/wikis/synchronized-updates-spec
// The alternate `CSI ? 2026 h/l` sequences are preferred when no parameters
// are passed, since they are correctly interpreted by terminals like screen.
const bsu = '\x1b[?2026h'
const esu = '\x1b[?2026l'
// write puts the string `s` into the print buffer.
@[inline]
pub fn (mut ctx Context) write(s string) {
if s == '' {
return
}
unsafe { ctx.print_buf.push_many(s.str, s.len) }
}
// flush displays the accumulated print buffer to the screen.
@[inline]
pub fn (mut ctx Context) flush() {
// 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.
@[inline]
pub fn (mut ctx Context) bold() {
ctx.write('\x1b[1m')
}
// set_cursor_position positions the cusor at the given coordinates `x`,`y`.
@[inline]
pub fn (mut ctx Context) set_cursor_position(x int, y int) {
ctx.write('\x1b[${y};${x}H')
}
// show_cursor will make the cursor appear if it is not already visible
@[inline]
pub fn (mut ctx Context) show_cursor() {
ctx.write('\x1b[?25h')
}
// hide_cursor will make the cursor invisible
@[inline]
pub fn (mut ctx Context) hide_cursor() {
ctx.write('\x1b[?25l')
}
// set_color sets the current foreground color used by any succeeding `draw_*` calls.
@[inline]
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 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')
}
}
// set_color sets the current background color used by any succeeding `draw_*` calls.
@[inline]
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 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')
}
}
// reset_color sets the current foreground color back to it's default value.
@[inline]
pub fn (mut ctx Context) reset_color() {
ctx.write('\x1b[39m')
}
// reset_bg_color sets the current background color back to it's default value.
@[inline]
pub fn (mut ctx Context) reset_bg_color() {
ctx.write('\x1b[49m')
}
// reset restores the state of all colors and text formats back to their default values.
@[inline]
pub fn (mut ctx Context) reset() {
ctx.write('\x1b[0m')
}
// clear erases the entire terminal window and any saved lines.
@[inline]
pub fn (mut ctx Context) clear() {
ctx.write('\x1b[2J\x1b[3J')
}
// 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) {
ctx.set_cursor_position(x, y)
ctx.write(' ')
}
// draw_text draws the string `s`, starting from position `x`,`y`.
@[inline]
pub fn (mut ctx Context) draw_text(x int, y int, s string) {
ctx.set_cursor_position(x, y)
ctx.write(s)
}
// draw_line draws a line segment, starting at point `x`,`y`, and ending at point `x2`,`y2`.
pub fn (mut ctx Context) draw_line(x int, y int, x2 int, y2 int) {
min_x, min_y := if x < x2 { x } else { x2 }, if y < y2 { y } else { y2 }
max_x, _ := if x > x2 { x } else { x2 }, if y > y2 { y } else { y2 }
if y == y2 {
// Horizontal line, performance improvement
ctx.set_cursor_position(min_x, min_y)
ctx.write(strings.repeat(` `, max_x + 1 - min_x))
return
}
// Draw the various points with Bresenham's line algorithm:
mut x0, x1 := x, x2
mut y0, y1 := y, y2
sx := if x0 < x1 { 1 } else { -1 }
sy := if y0 < y1 { 1 } else { -1 }
dx := if x0 < x1 { x1 - x0 } else { x0 - x1 }
dy := if y0 < y1 { y0 - y1 } else { y1 - y0 } // reversed
mut err := dx + dy
for {
// res << Segment{ x0, y0 }
ctx.draw_point(x0, y0)
if x0 == x1 && y0 == y1 {
break
}
e2 := 2 * err
if e2 >= dy {
err += dy
x0 += sx
}
if e2 <= dx {
err += dx
y0 += sy
}
}
}
// draw_dashed_line draws a dashed line segment, starting at point `x`,`y`, and ending at point `x2`,`y2`.
pub fn (mut ctx Context) draw_dashed_line(x int, y int, x2 int, y2 int) {
// Draw the various points with Bresenham's line algorithm:
mut x0, x1 := x, x2
mut y0, y1 := y, y2
sx := if x0 < x1 { 1 } else { -1 }
sy := if y0 < y1 { 1 } else { -1 }
dx := if x0 < x1 { x1 - x0 } else { x0 - x1 }
dy := if y0 < y1 { y0 - y1 } else { y1 - y0 } // reversed
mut err := dx + dy
mut i := 0
for {
if i % 2 == 0 {
ctx.draw_point(x0, y0)
}
if x0 == x1 && y0 == y1 {
break
}
e2 := 2 * err
if e2 >= dy {
err += dy
x0 += sx
}
if e2 <= dx {
err += dx
y0 += sy
}
i++
}
}
// draw_rect draws a rectangle, starting at top left `x`,`y`, and ending at bottom right `x2`,`y2`.
pub fn (mut ctx Context) draw_rect(x int, y int, x2 int, y2 int) {
if y == y2 || x == x2 {
ctx.draw_line(x, y, x2, y2)
return
}
min_y, max_y := if y < y2 { y, y2 } else { y2, y }
for y_pos in min_y .. max_y + 1 {
ctx.draw_line(x, y_pos, x2, y_pos)
}
}
// draw_empty_dashed_rect draws a rectangle with dashed lines, starting at top left `x`,`y`, and ending at bottom right `x2`,`y2`.
pub fn (mut ctx Context) draw_empty_dashed_rect(x int, y int, x2 int, y2 int) {
if y == y2 || x == x2 {
ctx.draw_dashed_line(x, y, x2, y2)
return
}
min_x, max_x := if x < x2 { x, x2 } else { x2, x }
min_y, max_y := if y < y2 { y, y2 } else { y2, y }
ctx.draw_dashed_line(min_x, min_y, max_x, min_y)
ctx.draw_dashed_line(min_x, min_y, min_x, max_y)
if (max_y - min_y) & 1 == 0 {
ctx.draw_dashed_line(min_x, max_y, max_x, max_y)
} else {
ctx.draw_dashed_line(min_x + 1, max_y, max_x, max_y)
}
if (max_x - min_x) & 1 == 0 {
ctx.draw_dashed_line(max_x, min_y, max_x, max_y)
} else {
ctx.draw_dashed_line(max_x, min_y + 1, max_x, max_y)
}
}
// draw_empty_rect draws a rectangle with no fill, starting at top left `x`,`y`, and ending at bottom right `x2`,`y2`.
pub fn (mut ctx Context) draw_empty_rect(x int, y int, x2 int, y2 int) {
if y == y2 || x == x2 {
ctx.draw_line(x, y, x2, y2)
return
}
ctx.draw_line(x, y, x2, y)
ctx.draw_line(x, y2, x2, y2)
ctx.draw_line(x, y, x, y2)
ctx.draw_line(x2, y, x2, y2)
}
// horizontal_separator draws a horizontal separator, spanning the width of the screen.
@[inline]
pub fn (mut ctx Context) horizontal_separator(y int) {
ctx.set_cursor_position(0, y)
ctx.write(strings.repeat(`-`, ctx.window_width)) // /* `⎽` */
}
+8
View File
@@ -0,0 +1,8 @@
import term.ui
// This test just ensures that programs importing term.ui can compile
fn test_a_simple_term_ui_program_can_be_compiled() {
println(ui.color_table)
assert true
}
+54
View File
@@ -0,0 +1,54 @@
module term
// utf8_getchar returns an utf8 rune from standard input.
pub fn utf8_getchar() ?rune {
c := input_character()
if c == -1 {
return none
}
len := utf8_len(u8(~c))
if c < 0 {
return 0
} else if len == 0 {
return c
} else if len == 1 {
return -1
} else {
mut uc := c & ((1 << (7 - len)) - 1)
for i := 0; i + 1 < len; i++ {
c2 := input_character()
if c2 != -1 && (c2 >> 6) == 2 {
uc <<= 6
uc |= (c2 & 63)
} else if c2 == -1 {
return 0
} else {
return -1
}
}
return uc
}
}
// utf8_len calculates the length of a utf8 rune to read, according to its first byte.
pub fn utf8_len(c u8) int {
mut b := 0
mut x := c
if (x & 240) != 0 {
// 0xF0
x >>= 4
} else {
b += 4
}
if (x & 12) != 0 {
// 0x0C
x >>= 2
} else {
b += 2
}
if (x & 2) == 0 {
// 0x02
b++
}
return b
}