Skip to content

Commit

Permalink
add support for comands that also are queries and some helper functio…
Browse files Browse the repository at this point in the history
…ns for the parser
  • Loading branch information
apparebit committed Nov 14, 2024
1 parent fe7c93e commit 9bd941d
Show file tree
Hide file tree
Showing 2 changed files with 217 additions and 9 deletions.
183 changes: 175 additions & 8 deletions src/cmd.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
//! Controlling the terminal with ANSI escape sequences.
//!
//! This module defines [`Command`] as the common interface for sending commands
//! to terminals. The implementation supports only ANSI escape sequence, since
//! that is the preferred interface for controlling the terminal even on
//! Windows. The operating system has been supporting ANSI escape sequence since
//! Windows 10 TH2 (v1511).
//! [`Command`] is an instruction to change a terminal's screen, cursor,
//! content, or some such. Its implementation writes an ANSI escape sequence to
//! the terminal, even on Windows, which added support in Windows 10 TH2
//! (v1511). [`Query`] encapsulates the functionality for reading and parsing
//! the response returned by a terminal after receiving the corresponding
//! request. An implementation checks with [`Query::is_valid`] whether the
//! response had the right control and then uses [`Query::parse`] to parse the
//! payload of the terminal's response. [`Query::query`] builds on these two
//! required methods to coordinate between
//! [`TerminalAccess`](crate::term::TerminalAccess), [`VtScanner`], and
//! [`Query`].
//!
//! This module also defines a basic library of such commands. Each command
//! This modules further defines the core of a command library. Each command
//! implements the `Debug` and `Display` traits as well. The `Debug`
//! representation is the usual datatype representation, whereas the `Display`
//! representation is the ANSI escape sequence. As a result, all commands
//! defined by this module can be directly written to output, just like
//! defined by this module can be directly written to terminal output, just like
//! [`Style`](crate::style::Style) and [`ThemeEntry`](crate::theme::ThemeEntry).
//!
//! The core library includes the following commands:
Expand All @@ -28,8 +34,14 @@
//! [`EndBatchedOutput`], [`BeginBracketedPaste`] and [`EndBracketedPaste`],
//! also [`Link`].
//!
//! If a command starts with `Request` in its name, it is a query and implements
//! the [`Query`] trait in addition to [`Command`].
//!
#![allow(dead_code)]
use std::io::{BufRead, Error, ErrorKind, Result};

use crate::term::{Control, VtScanner};

/// A terminal command.
///
Expand All @@ -49,6 +61,37 @@ impl<C: Command> Command for &C {
}
}

/// A command that receives a response.
///
/// Since UTF-8 has more invariants than byte slices, this trait represents the
/// payload of ANSI escape sequences as `&[u8]`. Use [`VtScanner::to_str`] to
/// convert to string slice if needed.
pub trait Query {
/// The type of the response data.
type Response;

/// Determine whether the control is the expected control for the response.
fn is_valid(&self, control: Control) -> bool;

/// Parse the payload into a response object.
fn parse(&self, payload: &[u8]) -> Result<Self::Response>;

fn query(&self, reader: &mut impl BufRead, scanner: &mut VtScanner) -> Result<Self::Response> {
// To avoid a borrow checker error, we scan sequence first but do not
// retain the result. We access the control second and the text of the
// ANSI escape sequence third.
scanner.scan_bytes(reader)?;
let control = scanner.completed_control();
let payload = scanner.finished_bytes()?;

if control.is_none() || !self.is_valid(control.unwrap()) {
return Err(ErrorKind::InvalidInput.into());
}

self.parse(payload)
}
}

macro_rules! define_simple_command {
($name:ident $(: $selfish:ident)? { $repr:expr }) => {
impl crate::cmd::Command for $name {
Expand Down Expand Up @@ -117,6 +160,34 @@ macro_rules! define_one_num_suite {

define_simple_suite!(RequestTerminalId, "\x1b[>q");

impl Query for RequestTerminalId {
type Response = (Vec<u8>, Option<Vec<u8>>);

fn is_valid(&self, control: Control) -> bool {
matches!(control, Control::DCS)
}

fn parse(&self, payload: &[u8]) -> Result<Self::Response> {
let s = payload
.strip_prefix(b">|")
.ok_or_else(|| Error::from(ErrorKind::InvalidData))?;

if let Some(s) = s.strip_suffix(b")") {
let (n, v) = s
.iter()
.position(|byte| *byte == b'(')
.map(|index| s.split_at(index))
.ok_or_else(|| Error::from(ErrorKind::InvalidData))?;
let name = n.to_owned();
let version = v.to_owned();
Ok((name, Some(version)))
} else {
let name = s.to_owned();
Ok((name, None))
}
}
}

// --------------------------------- Window Management ---------------------------------

define_simple_suite!(SaveWindowTitleOnStack, "\x1b[22;2t");
Expand Down Expand Up @@ -167,9 +238,68 @@ define_one_num_suite!(MoveToRow, "\x1b[", "d");

define_simple_suite!(RequestCursorPosition, "\x1b[6n");

impl Query for RequestCursorPosition {
type Response = (u16, u16);

fn is_valid(&self, control: Control) -> bool {
matches!(control, Control::CSI)
}

fn parse(&self, payload: &[u8]) -> Result<Self::Response> {
let s = payload
.strip_prefix(b"\x1b[")
.and_then(|s| s.strip_suffix(b"R"))
.ok_or_else(|| Error::from(ErrorKind::InvalidData))?;

let params = VtScanner::split_params(s)?;
if params.len() != 2
|| params[0].is_none()
|| params[0].is_none()
|| (u16::MAX as u64) < params[0].unwrap()
|| (u16::MAX as u64) < params[1].unwrap()
{
return Err(ErrorKind::InvalidData.into());
}
Ok((params[0].unwrap() as u16, params[1].unwrap() as u16))
}
}

// -------------------------------- Content Management ---------------------------------

define_simple_suite!(RequestBatchMode, "\x1b[?2026$p");

/// The current batch processing mode.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BatchMode {
NotSupported = 0,
Enabled = 1,
Disabled = 2,
Undefined = 3,
PermanentlyDisabled = 4,
}

impl Query for RequestBatchMode {
type Response = BatchMode;

fn is_valid(&self, control: Control) -> bool {
matches!(control, Control::CSI)
}

fn parse(&self, payload: &[u8]) -> Result<Self::Response> {
let s = payload
.strip_prefix(b"\x1b[?2026;")
.and_then(|s| s.strip_suffix(b"$y"))
.ok_or_else(|| Error::from(ErrorKind::ConnectionRefused))?;
Ok(match VtScanner::to_u64(s)? {
0 => BatchMode::NotSupported,
1 => BatchMode::Enabled,
2 => BatchMode::Disabled,
4 => BatchMode::PermanentlyDisabled,
_ => BatchMode::Undefined,
})
}
}

define_simple_suite!(BeginBatchedOutput, "\x1b[?2026h");
define_simple_suite!(EndBatchedOutput, "\x1b[?2026l");

Expand Down Expand Up @@ -201,18 +331,42 @@ impl Link {
}
}

/// Create a new hyperlink for terminal display.
pub fn link(text: impl AsRef<str>, href: impl AsRef<str>, id: Option<&str>) -> Link {
Link::new(text, href, id)
}

define_command!(Link, self, f { f.write_str(&self.0) } );
define_display!(Link);

// --------------------------------- Style Management ----------------------------------

define_simple_suite!(ResetStyle, "\x1b[m");
define_simple_suite!(RequestActiveStyle, "\x1bP$qm\x1b\\");

impl Query for RequestActiveStyle {
type Response = Vec<u8>;

fn is_valid(&self, control: Control) -> bool {
matches!(control, Control::DCS)
}

fn parse(&self, payload: &[u8]) -> Result<Self::Response> {
let s = payload
.strip_prefix(b"\x1bP1$r")
.and_then(|s| s.strip_suffix(b"m"))
.ok_or_else(|| Error::from(ErrorKind::InvalidData))?;

Ok(s.to_owned())
}
}

// =====================================================================================

#[cfg(test)]
mod test {
use super::{BeginBatchedOutput, MoveLeft, MoveTo};
use super::{BeginBatchedOutput, MoveLeft, MoveTo, Query, RequestTerminalId};
use crate::term::VtScanner;

#[test]
fn test_size_and_display() {
Expand All @@ -224,4 +378,17 @@ mod test {
assert_eq!(format!("{}", MoveLeft(2)), "\x1b[2C");
assert_eq!(format!("{}", MoveTo(5, 7)), "\x1b[5;7H")
}

#[test]
fn test_parsing() -> std::io::Result<()> {
let mut input = b"\x1bP>|Terminal\x1b\\".as_slice();
let mut scanner = VtScanner::new();

let response = scanner.scan_bytes(&mut input)?;
let (name, version) = RequestTerminalId.parse(response)?;

assert_eq!(&name, b"Terminal");
assert_eq!(version, None);
Ok(())
}
}
43 changes: 42 additions & 1 deletion src/term/escape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1459,6 +1459,47 @@ impl VtScanner {
let bytes = self.scan_bytes(reader)?;
std::str::from_utf8(bytes).map_err(|e| Error::new(ErrorKind::InvalidData, e))
}

/// Convert the byte slice into a string slice.
pub fn to_str(payload: &[u8]) -> std::io::Result<&str> {
std::str::from_utf8(payload).map_err(|e| Error::new(ErrorKind::InvalidData, e))
}

/// Convert the byteslice to an unsigned 64-bit integer.
pub fn to_u64(payload: &[u8]) -> std::io::Result<u64> {
if 19 < payload.len() {
return Err(ErrorKind::InvalidData.into());
}

let mut result = 0_u64;
for byte in payload.iter().rev() {
if byte.is_ascii_digit() {
let value = u64::from(*byte) - u64::from('0');
result += 10 * result + value;
} else {
return Err(ErrorKind::InvalidData.into());
}
}

Ok(result)
}

/// Split the payload into a vector of parameters.
pub fn split_params(payload: &[u8]) -> std::io::Result<Vec<Option<u64>>> {
let mut result = Vec::new();
for elem in payload.split(|byte| matches!(byte, b';' | b':')) {
let elem = if elem.is_empty() {
None
} else {
let num = VtScanner::to_u64(elem)?;
Some(num)
};

result.push(elem);
}

Ok(result)
}
}

// ================================================================================================
Expand All @@ -1469,7 +1510,7 @@ mod test {
use std::mem::size_of;

#[test]
fn test() {
fn test_size() {
assert_eq!(size_of::<(State, Action)>(), 2);
}
}

0 comments on commit 9bd941d

Please sign in to comment.