Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
@@ -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()
|
||||
```
|
||||
@@ -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('')
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)) // /* `⎽` */
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user