///|
/// Cursor position reported by the terminal.
///
/// Rows and columns are 1-based terminal coordinates.
pub struct CursorPosition {
row : Int
col : Int
} derive(Eq)
///|
/// Terminal event reported by the coordinated root `Tty` handle.
pub(all) enum Event {
Input(@public_input.InputEvent)
Resize(WindowSize)
} derive(Eq)
///|
/// Coordinated terminal input/output handle.
///
/// `Tty` is for terminal protocols that need to write a request and read the
/// response from the same terminal input stream. It does not model a screen.
#cfg(platform="windows")
struct Tty {
input : &Reader
input_fd : @async/types.Fd
output : &Writer
output_fd : @async/types.Fd
input_backend : WindowsInput
pending_input : Array[@public_input.InputEvent]
// Serializes output writes so a single `write`/`write_string` call lands on
// the terminal atomically with respect to other concurrent writers.
write_lock : @lock.Lock
}
///|
#cfg(not(platform="windows"))
struct Tty {
input : &Reader
input_fd : @async/types.Fd
output : &Writer
output_fd : @async/types.Fd
reader : @input.EventReader
pending_input : Array[@public_input.InputEvent]
// Serializes output writes so a single `write`/`write_string` call lands on
// the terminal atomically with respect to other concurrent writers.
write_lock : @lock.Lock
}
///|
/// Create a coordinated terminal handle from input and output handles.
#cfg(platform="windows")
pub fn[I : Reader, O : Writer] Tty::new(input : I, output : O) -> Tty raise {
let input = input as &Reader
let input_fd = input.fd()
let output = output as &Writer
let output_fd = output.fd()
let input_backend = WindowsInput::new(input_fd, input as &@async/io.Reader)
{
input,
input_fd,
output,
output_fd,
input_backend,
pending_input: [],
write_lock: @lock.Lock::new(),
}
}
///|
/// Create a coordinated terminal handle from input and output handles.
#cfg(not(platform="windows"))
pub fn[I : Reader, O : Writer] Tty::new(input : I, output : O) -> Tty raise {
let input = input as &Reader
let input_fd = input.fd()
let output = output as &Writer
let output_fd = output.fd()
{
input,
input_fd,
output,
output_fd,
reader: @input.EventReader::new(input as &@async/io.Reader),
pending_input: [],
write_lock: @lock.Lock::new(),
}
}
///|
/// Wrap process stdio as one coordinated terminal handle.
pub fn Tty::stdio() -> Tty raise {
Tty::new(@async/stdio.stdin, @async/stdio.stdout)
}
///|
/// Close the underlying input and output handles.
///
/// Closing stdio-backed handles is a no-op.
#cfg(platform="windows")
pub fn Tty::close(self : Self) -> Unit {
self.input_backend.close()
self.input.close()
self.output.close()
}
///|
/// Close the underlying input and output handles.
///
/// Closing stdio-backed handles is a no-op.
#cfg(not(platform="windows"))
pub fn Tty::close(self : Self) -> Unit {
self.input.close()
self.output.close()
}
///|
/// Read the next root terminal event.
///
/// `esc_timeout_ms` controls the short wait used to distinguish a standalone
/// Escape key from the start of a longer escape sequence.
pub async fn Tty::read_event(self : Self, esc_timeout_ms? : Int = 50) -> Event {
if self.pending_input.length() > 0 {
return Input(self.pending_input.remove(0))
}
self.read_event_from_sources(esc_timeout_ms)
}
///|
async fn read_decoded_input_event(
reader : @input.EventReader,
esc_timeout_ms : Int,
) -> Event {
for ;; {
match reader.read_event(esc_timeout_ms~) {
Input(event) => return Input(event)
CursorPosition(..) => ()
KeyboardEnhancementFlags(_) => ()
PrimaryDeviceAttributes(_) => ()
DynamicColor(..) => ()
}
}
}
///|
async fn Tty::read_internal_event(
self : Self,
esc_timeout_ms? : Int = 50,
) -> @input.Event {
self.read_internal_event_from_sources(esc_timeout_ms)
}
///|
/// Write bytes to the terminal output handle.
///
/// The write is serialized through an internal lock, so a single call lands on
/// the terminal atomically even when multiple tasks write concurrently. To make
/// a multi-call sequence atomic, assemble it into one buffer and issue a single
/// `write`.
pub async fn Tty::write(self : Self, data : &@async/io.Data) -> Unit {
self.write_lock.with_lock(() => self.output.write(data))
}
///|
/// Write a string to the terminal output handle.
///
/// The write is serialized through an internal lock, so a single call lands on
/// the terminal atomically even when multiple tasks write concurrently. To make
/// a multi-call sequence atomic, assemble it into one string and issue a single
/// `write_string`.
pub async fn Tty::write_string(self : Self, string : String) -> Unit {
self.write_lock.with_lock(() => self.output.write(string))
}
///|
/// Query the terminal cursor position.
///
/// The returned row and column are 1-based. User input received while waiting
/// for the terminal response is preserved for later `Tty::read_event` calls.
pub async fn Tty::query_cursor_position(
self : Self,
timeout_ms? : Int = 50,
) -> CursorPosition? {
@async.with_timeout_opt(timeout_ms, () => {
self.write(@vt.request_cursor_position)
for ;; {
match self.read_internal_event() {
CursorPosition(row~, col~) => return { row, col }
Input(event) => self.pending_input.push(event)
KeyboardEnhancementFlags(_) => ()
PrimaryDeviceAttributes(_) => ()
DynamicColor(..) => ()
}
}
})
}
///|
/// Query whether the terminal supports the kitty keyboard protocol.
///
/// User input received while waiting for terminal responses is preserved for
/// later `Tty::read_event` calls.
pub async fn Tty::query_kitty_keyboard_support(
self : Self,
timeout_ms? : Int = 50,
) -> Bool {
match
@async.with_timeout_opt(timeout_ms, () => {
self.write(@vt.query_keyboard_enhancement_flags)
self.write(@vt.request_primary_device_attributes)
for ;; {
match self.read_internal_event() {
KeyboardEnhancementFlags(_) => return true
PrimaryDeviceAttributes(_) => return false
CursorPosition(..) => ()
DynamicColor(..) => ()
Input(event) => self.pending_input.push(event)
}
}
}) {
Some(supported) => supported
None => false
}
}
///|
/// Query the terminal default foreground color.
///
/// The returned RGB components are 16-bit values in the range `0..65535`.
/// User input received while waiting for the terminal response is preserved for
/// later `Tty::read_event` calls.
pub async fn Tty::query_default_foreground_color(
self : Self,
timeout_ms? : Int = 50,
) -> @color.Rgb16? {
self.query_dynamic_color(10, @vt.query_default_foreground_color, timeout_ms~)
}
///|
/// Query the terminal default background color.
///
/// The returned RGB components are 16-bit values in the range `0..65535`.
/// User input received while waiting for the terminal response is preserved for
/// later `Tty::read_event` calls.
pub async fn Tty::query_default_background_color(
self : Self,
timeout_ms? : Int = 50,
) -> @color.Rgb16? {
self.query_dynamic_color(11, @vt.query_default_background_color, timeout_ms~)
}
///|
/// Query the terminal text cursor color.
///
/// The returned RGB components are 16-bit values in the range `0..65535`.
/// User input received while waiting for the terminal response is preserved for
/// later `Tty::read_event` calls.
pub async fn Tty::query_cursor_color(
self : Self,
timeout_ms? : Int = 50,
) -> @color.Rgb16? {
self.query_dynamic_color(12, @vt.query_cursor_color, timeout_ms~)
}
///|
async fn Tty::query_dynamic_color(
self : Self,
expected_code : Int,
request : Bytes,
timeout_ms~ : Int,
) -> @color.Rgb16? {
@async.with_timeout_opt(timeout_ms, () => {
self.write(request)
for ;; {
match self.read_internal_event() {
DynamicColor(code~, red~, green~, blue~) if code == expected_code =>
return { red, green, blue }
DynamicColor(..) => ()
CursorPosition(..) => ()
KeyboardEnhancementFlags(_) => ()
PrimaryDeviceAttributes(_) => ()
Input(event) => self.pending_input.push(event)
}
}
})
}