WIP: NEW TERMINAL LIB
This commit is contained in:
Generated
+24
@@ -652,6 +652,12 @@ dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@@ -1369,6 +1375,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"cross",
|
||||
"libc",
|
||||
"pretty_assertions",
|
||||
"thiserror",
|
||||
"tui",
|
||||
"yggdrasil-rt",
|
||||
@@ -2063,6 +2070,16 @@ dependencies = [
|
||||
"zerocopy 0.7.35",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pretty_assertions"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
|
||||
dependencies = [
|
||||
"diff",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.29"
|
||||
@@ -2777,6 +2794,7 @@ dependencies = [
|
||||
"cross",
|
||||
"libcolors",
|
||||
"libpsf",
|
||||
"libterm",
|
||||
"log",
|
||||
"logsink",
|
||||
"rusttype",
|
||||
@@ -3661,6 +3679,12 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||
|
||||
[[package]]
|
||||
name = "yasync"
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
all:
|
||||
test
|
||||
@@ -7,6 +7,7 @@ authors = ["Mark Poliakov <mark@alnyan.me>"]
|
||||
[dependencies]
|
||||
libpsf.workspace = true
|
||||
libcolors = { workspace = true, features = ["client"] }
|
||||
libterm.workspace = true
|
||||
logsink.workspace = true
|
||||
cross.workspace = true
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
@@ -35,10 +36,26 @@ pub struct ColorsConfig {
|
||||
pub dim: ColorsGroupConfig,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ShellConfig {
|
||||
pub command: Vec<String>,
|
||||
pub env: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
pub struct Config {
|
||||
pub fonts: FontConfig,
|
||||
pub colors: ColorsConfig,
|
||||
pub shell: ShellConfig,
|
||||
}
|
||||
|
||||
impl Default for ShellConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
command: vec!["/bin/sh".into(), "-l".into()],
|
||||
env: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(rust_analyzer, target_os = "yggdrasil"))]
|
||||
@@ -112,8 +129,14 @@ impl ColorsGroupConfig {
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load_or_default<P: AsRef<Path>>(path: P) -> Self {
|
||||
Self::load(path).unwrap_or_default()
|
||||
pub fn load_or_default<P: AsRef<Path>>(path: P, args: crate::Args) -> Self {
|
||||
let mut config = Self::load(path).unwrap_or_default();
|
||||
|
||||
if !args.command.is_empty() {
|
||||
config.shell.command = args.command;
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
fn load<P: AsRef<Path>>(path: P) -> Option<Self> {
|
||||
|
||||
@@ -12,6 +12,7 @@ use std::{
|
||||
},
|
||||
};
|
||||
|
||||
use clap::Parser;
|
||||
use config::Config;
|
||||
use cross::{
|
||||
io::{Poll, PtyMaster, TerminalOptions, TerminalOptionsImpl, TerminalSize},
|
||||
@@ -59,6 +60,11 @@ pub struct Terminal<'a> {
|
||||
shell: Child,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
pub struct Args {
|
||||
command: Vec<String>,
|
||||
}
|
||||
|
||||
impl DrawState {
|
||||
pub fn new(fonts: Fonts, width: usize) -> Self {
|
||||
Self {
|
||||
@@ -72,6 +78,7 @@ impl DrawState {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn draw_character(
|
||||
dt: &mut [u32],
|
||||
cx: usize,
|
||||
@@ -104,7 +111,6 @@ impl DrawState {
|
||||
return;
|
||||
}
|
||||
|
||||
let c = cell.char as char;
|
||||
let font = match (cell.attrs.bold, cell.attrs.italic) {
|
||||
(true, true) => &fonts.bold_italic,
|
||||
(false, true) => &fonts.italic,
|
||||
@@ -112,7 +118,7 @@ impl DrawState {
|
||||
(false, false) => &fonts.regular,
|
||||
};
|
||||
|
||||
font.map_glyph(c, |x, y, v| {
|
||||
font.map_glyph(cell.char, |x, y, v| {
|
||||
let v = v.min(1.0);
|
||||
let r = blend(fg.r, bg.r, v);
|
||||
let g = blend(fg.g, bg.g, v);
|
||||
@@ -374,14 +380,39 @@ impl Terminal<'_> {
|
||||
|
||||
log::debug!("stdin = {pty_slave_stdin:?}, stdout = {pty_slave_stdout:?}, stderr = {pty_slave_stderr:?}");
|
||||
|
||||
let term_var = CONFIG
|
||||
.shell
|
||||
.env
|
||||
.get("TERM")
|
||||
.map(String::as_str)
|
||||
.unwrap_or("xterm-256color");
|
||||
|
||||
let mut command = if let Some((shell, args)) = CONFIG.shell.command.split_first() {
|
||||
let mut command = Command::new(shell);
|
||||
command.args(args);
|
||||
command
|
||||
} else {
|
||||
let mut command = Command::new("/bin/sh");
|
||||
command.arg("-l");
|
||||
command
|
||||
};
|
||||
|
||||
command.env("TERM", term_var);
|
||||
|
||||
for (key, value) in CONFIG.shell.env.iter() {
|
||||
if key == "TERM" {
|
||||
continue;
|
||||
}
|
||||
|
||||
command.env(key, value);
|
||||
}
|
||||
|
||||
let shell = unsafe {
|
||||
Command::new("/bin/sh")
|
||||
.arg("-l")
|
||||
command
|
||||
.stdin(Stdio::from_raw_fd(pty_slave_stdin))
|
||||
.stdout(Stdio::from_raw_fd(pty_slave_stdout))
|
||||
.stderr(Stdio::from_raw_fd(pty_slave_stderr))
|
||||
.create_session()?
|
||||
.create_process_group()?
|
||||
.spawn()?
|
||||
};
|
||||
|
||||
@@ -411,7 +442,7 @@ impl Terminal<'_> {
|
||||
let mut s = self.state.lock().unwrap();
|
||||
|
||||
for &ch in &buf[..len] {
|
||||
needs_redraw |= s.handle_shell_output(ch);
|
||||
needs_redraw |= s.handle_shell_output(&mut pty, ch);
|
||||
}
|
||||
|
||||
Ok(needs_redraw)
|
||||
@@ -433,18 +464,6 @@ impl Terminal<'_> {
|
||||
Some(_) => (),
|
||||
None => (),
|
||||
}
|
||||
// match self.poll.wait(None, true)? {
|
||||
// Some((fd, Ok(_))) if fd == self.conn_fd => {
|
||||
// self.application.poll_events()?;
|
||||
// }
|
||||
// Some((fd, Ok(_))) if fd == self.pty_master_fd => {
|
||||
// }
|
||||
// Some((_, Ok(_))) => {
|
||||
// todo!()
|
||||
// }
|
||||
// Some((_, Err(error))) => return Err(Error::from(error)),
|
||||
// None => (),
|
||||
// }
|
||||
}
|
||||
|
||||
Ok(ExitCode::SUCCESS)
|
||||
@@ -454,7 +473,7 @@ impl Terminal<'_> {
|
||||
match self.run_inner() {
|
||||
Ok(code) => code,
|
||||
Err(error) => {
|
||||
eprintln!("Error: {}", error);
|
||||
eprintln!("Error: {error}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
@@ -462,7 +481,10 @@ impl Terminal<'_> {
|
||||
}
|
||||
|
||||
static ABORT: AtomicBool = AtomicBool::new(false);
|
||||
static CONFIG: LazyLock<Config> = LazyLock::new(|| Config::load_or_default("/etc/term.conf"));
|
||||
static CONFIG: LazyLock<Config> = LazyLock::new(|| {
|
||||
let args = Args::parse();
|
||||
Config::load_or_default("/etc/term.conf", args)
|
||||
});
|
||||
|
||||
fn run() -> Result<ExitCode, Error> {
|
||||
LazyLock::force(&CONFIG);
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::{collections::VecDeque, io::Write};
|
||||
|
||||
use cross::io::PtyMaster;
|
||||
use libterm::escape::{
|
||||
CharacterAttribute, ControlSequence, EraseInDisplay, EraseInLine, EscapeParser, TerminalInput,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
attr::{CellAttributes, Color},
|
||||
@@ -31,15 +36,15 @@ pub struct Cursor {
|
||||
pub col: usize,
|
||||
}
|
||||
|
||||
struct CsiState {
|
||||
byte: u8,
|
||||
}
|
||||
|
||||
enum EscapeState {
|
||||
Normal,
|
||||
Escape,
|
||||
Csi(CsiState),
|
||||
}
|
||||
// struct CsiState {
|
||||
// byte: u8,
|
||||
// }
|
||||
//
|
||||
// enum EscapeState {
|
||||
// Normal,
|
||||
// Escape,
|
||||
// Csi(CsiState),
|
||||
// }
|
||||
|
||||
#[derive(Default)]
|
||||
struct Utf8Decoder {
|
||||
@@ -58,9 +63,9 @@ pub struct State {
|
||||
pub buffer: Buffer,
|
||||
|
||||
utf8_decode: Utf8Decoder,
|
||||
esc_state: EscapeState,
|
||||
esc_args: Vec<u32>,
|
||||
|
||||
esc_parser: EscapeParser,
|
||||
// esc_state: EscapeState,
|
||||
// esc_args: Vec<u32>,
|
||||
scroll: usize,
|
||||
pub cursor: Cursor,
|
||||
pub cursor_visible: bool,
|
||||
@@ -237,6 +242,26 @@ impl Buffer {
|
||||
self.rows[row].clear(fg, bg);
|
||||
}
|
||||
|
||||
pub fn insert_row(&mut self, after: usize, fg: Color, bg: Color) {
|
||||
if after >= self.height {
|
||||
return;
|
||||
}
|
||||
for i in (after + 2..self.height).rev() {
|
||||
self.rows.swap(i - 1, i);
|
||||
}
|
||||
self.rows[after] = GridRow::new(self.width, fg, bg);
|
||||
}
|
||||
|
||||
pub fn remove_row(&mut self, at: usize, fg: Color, bg: Color) {
|
||||
if at >= self.height {
|
||||
return;
|
||||
}
|
||||
for i in at..self.height - 1 {
|
||||
self.rows.swap(i, i + 1);
|
||||
}
|
||||
self.rows[self.height - 1].clear(fg, bg);
|
||||
}
|
||||
|
||||
pub fn set_row_dirty(&mut self, row: usize) {
|
||||
if row >= self.rows.len() {
|
||||
return;
|
||||
@@ -265,8 +290,7 @@ impl State {
|
||||
buffer: Buffer::new(width, height, default_attributes.fg, default_attributes.bg),
|
||||
utf8_decode: Utf8Decoder::default(),
|
||||
|
||||
esc_args: Vec::new(),
|
||||
esc_state: EscapeState::Normal,
|
||||
esc_parser: EscapeParser::new(),
|
||||
|
||||
cursor: Cursor { row: 0, col: 0 },
|
||||
cursor_visible: true,
|
||||
@@ -314,52 +338,176 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
fn putc_normal(&mut self, ch: char) -> bool {
|
||||
let mut redraw = match ch {
|
||||
// c if c >= 127 => {
|
||||
// let attr = CellAttributes {
|
||||
// fg: Color::Black,
|
||||
// bg: Color::Red,
|
||||
// bright: false,
|
||||
// };
|
||||
// self.buffer.set_cell(self.cursor, GridCell::new('?', attr));
|
||||
// self.cursor.col += 1;
|
||||
// true
|
||||
// }
|
||||
'\x1B' => {
|
||||
self.esc_state = EscapeState::Escape;
|
||||
self.esc_args.clear();
|
||||
self.esc_args.push(0);
|
||||
return false;
|
||||
}
|
||||
'\x7F' | '\x08' => {
|
||||
if self.cursor.col > 0 {
|
||||
self.cursor.col -= 1;
|
||||
} else if self.cursor.row > 0 {
|
||||
self.cursor.row -= 1;
|
||||
self.cursor.col = 0;
|
||||
}
|
||||
true
|
||||
}
|
||||
'\r' => {
|
||||
self.buffer.rows[self.cursor.row].dirty = true;
|
||||
self.cursor.col = 0;
|
||||
true
|
||||
}
|
||||
'\n' => {
|
||||
self.buffer.rows[self.cursor.row].dirty = true;
|
||||
self.cursor.row += 1;
|
||||
self.cursor.col = 0;
|
||||
true
|
||||
}
|
||||
_ => {
|
||||
self.buffer
|
||||
.set_cell(self.cursor, GridCell::new(ch as char, self.attributes));
|
||||
self.cursor.col += 1;
|
||||
true
|
||||
}
|
||||
};
|
||||
// fn putc_normal(&mut self, ch: char) -> bool {
|
||||
// let mut redraw = match ch {
|
||||
// // c if c >= 127 => {
|
||||
// // let attr = CellAttributes {
|
||||
// // fg: Color::Black,
|
||||
// // bg: Color::Red,
|
||||
// // bright: false,
|
||||
// // };
|
||||
// // self.buffer.set_cell(self.cursor, GridCell::new('?', attr));
|
||||
// // self.cursor.col += 1;
|
||||
// // true
|
||||
// // }
|
||||
// '\x1B' => {
|
||||
// self.esc_state = EscapeState::Escape;
|
||||
// self.esc_args.clear();
|
||||
// self.esc_args.push(0);
|
||||
// return false;
|
||||
// }
|
||||
// '\x7F' | '\x08' => {
|
||||
// }
|
||||
// '\r' => {
|
||||
// }
|
||||
// '\t' => {
|
||||
// }
|
||||
// '\n' => {
|
||||
// }
|
||||
// _ => {
|
||||
// }
|
||||
// };
|
||||
|
||||
// redraw
|
||||
// }
|
||||
|
||||
// fn handle_ctlseq(&mut self, byte: u8, c: char) -> bool {
|
||||
// let redraw = match c {
|
||||
// 'h' if byte == b'?' => match self.esc_args.get(0).copied().unwrap_or(0) {
|
||||
// 25 => {
|
||||
// // Cursor visible
|
||||
// self.cursor_visible = true;
|
||||
// true
|
||||
// }
|
||||
// 1049 => {
|
||||
// // Enter alternate mode
|
||||
// self.alternate = true;
|
||||
// true
|
||||
// }
|
||||
// _ => false,
|
||||
// },
|
||||
// 'l' if byte == b'?' => match self.esc_args.get(0).copied().unwrap_or(0) {
|
||||
// 25 => {
|
||||
// // Cursor not visible
|
||||
// self.cursor_visible = false;
|
||||
// true
|
||||
// }
|
||||
// 1049 => {
|
||||
// // Leave alternate mode
|
||||
// self.alternate = false;
|
||||
// self.clear();
|
||||
// self.cursor = Cursor { col: 0, row: 0 };
|
||||
// self.cursor_dirty = true;
|
||||
// true
|
||||
// }
|
||||
// _ => false,
|
||||
// },
|
||||
// 'q' => {
|
||||
// match self.esc_args.get(0).copied().unwrap_or(0) {
|
||||
// 3 | 4 => self.cursor_style = CursorStyle::Underline,
|
||||
// 5 | 6 => self.cursor_style = CursorStyle::Bar,
|
||||
// _ => self.cursor_style = CursorStyle::Block,
|
||||
// }
|
||||
// self.cursor_dirty = true;
|
||||
// true
|
||||
// }
|
||||
|
||||
// 'c' => {
|
||||
// // Send device attributes
|
||||
// false
|
||||
// }
|
||||
// // // Move cursor down
|
||||
// // 'B' => {
|
||||
// // let amount = self.esc_args[0].max(1);
|
||||
// // if self.cursor.row < self.buffer.height - 1 {
|
||||
// // let amount = amount.min((self.buffer.height - self.cursor.row) as u32 - 1);
|
||||
// // self.buffer.set_row_dirty(self.cursor.row);
|
||||
// // self.cursor.row += amount as usize;
|
||||
// // true
|
||||
// // } else {
|
||||
// // false
|
||||
// // }
|
||||
// // }
|
||||
// // Move back one character
|
||||
// 'D' => {
|
||||
// if self.cursor.col > 0 {
|
||||
// self.buffer.set_row_dirty(self.cursor.row);
|
||||
// self.cursor.col -= 1;
|
||||
// true
|
||||
// } else {
|
||||
// false
|
||||
// }
|
||||
// }
|
||||
// // Character attributes
|
||||
// 'm' => {
|
||||
// }
|
||||
// // Move cursor to position
|
||||
// 'f' => {
|
||||
// }
|
||||
// // Move cursor to home position (0; 0)
|
||||
// 'H' => {
|
||||
// }
|
||||
// '$' => {
|
||||
// return false;
|
||||
// }
|
||||
// // Clear rows/columns/screen
|
||||
// 'J' => match self.esc_args[0] {
|
||||
// },
|
||||
// 'X' => {
|
||||
// let amount = self.esc_args[0].max(1);
|
||||
// for i in 0..amount {
|
||||
// let x = i as usize + self.cursor.col;
|
||||
// if x >= self.buffer.width {
|
||||
// break;
|
||||
// }
|
||||
// self.buffer.set_cell(
|
||||
// Cursor {
|
||||
// col: x,
|
||||
// ..self.cursor
|
||||
// },
|
||||
// GridCell {
|
||||
// char: ' ',
|
||||
// attrs: self.attributes,
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
// true
|
||||
// }
|
||||
// 'K' => match self.esc_args[0] {
|
||||
// },
|
||||
// _ => {
|
||||
// let ch0 = if byte == b'(' { '(' } else { '[' };
|
||||
// let ch1 = if byte == b'?' { "?" } else { "" };
|
||||
// log::warn!("Unhandled ctlseq: ESC {ch0}{ch1} {:?} {c}", self.esc_args);
|
||||
// false
|
||||
// }
|
||||
// };
|
||||
|
||||
// self.esc_state = EscapeState::Normal;
|
||||
// redraw
|
||||
// }
|
||||
|
||||
// fn handle_ctlseq_byte(&mut self, byte: u8, c: char) -> bool {
|
||||
// match c {
|
||||
// '?' if byte == 0 => {
|
||||
// self.esc_state = EscapeState::Csi(CsiState { byte: b'?' });
|
||||
// false
|
||||
// }
|
||||
// c if let Some(digit) = c.to_digit(10) => {
|
||||
// let arg = self.esc_args.last_mut().unwrap();
|
||||
// *arg *= 10;
|
||||
// *arg += digit;
|
||||
// false
|
||||
// }
|
||||
// ';' => {
|
||||
// self.esc_args.push(0);
|
||||
// false
|
||||
// }
|
||||
// _ => self.handle_ctlseq(byte, c),
|
||||
// }
|
||||
// }
|
||||
|
||||
fn fix_cursor(&mut self, mut redraw: bool) -> bool {
|
||||
if self.alternate {
|
||||
self.cursor.col = self.cursor.col.min(self.buffer.width - 1);
|
||||
self.cursor.row = self.cursor.row.min(self.buffer.height - 1);
|
||||
@@ -385,217 +533,109 @@ impl State {
|
||||
redraw
|
||||
}
|
||||
|
||||
fn handle_ctlseq(&mut self, byte: u8, c: char) -> bool {
|
||||
let redraw = match c {
|
||||
'h' if byte == b'?' => match self.esc_args.get(0).copied().unwrap_or(0) {
|
||||
25 => {
|
||||
// Cursor visible
|
||||
self.cursor_visible = true;
|
||||
fn handle_control_sequence(&mut self, pty: &mut PtyMaster, seq: ControlSequence) -> bool {
|
||||
match seq {
|
||||
ControlSequence::SetCharacterAttribute(attr) => match attr {
|
||||
CharacterAttribute::Reset => {
|
||||
self.attributes = self.default_attributes;
|
||||
self.fg_index = Some(7);
|
||||
self.update_fg_color();
|
||||
true
|
||||
}
|
||||
1049 => {
|
||||
// Enter alternate mode
|
||||
self.alternate = true;
|
||||
CharacterAttribute::ForegroundRgb(r, g, b) => {
|
||||
self.attributes.fg = Color::new(r, g, b);
|
||||
self.fg_index = None;
|
||||
self.update_fg_color();
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
'l' if byte == b'?' => match self.esc_args.get(0).copied().unwrap_or(0) {
|
||||
25 => {
|
||||
// Cursor not visible
|
||||
self.cursor_visible = false;
|
||||
CharacterAttribute::FgIndex(Some(index)) => {
|
||||
self.fg_index = Some(index as u32);
|
||||
self.update_fg_color();
|
||||
true
|
||||
}
|
||||
1049 => {
|
||||
// Leave alternate mode
|
||||
self.alternate = false;
|
||||
self.clear();
|
||||
self.cursor = Cursor { col: 0, row: 0 };
|
||||
self.cursor_dirty = true;
|
||||
CharacterAttribute::FgIndex(None) => {
|
||||
self.fg_index = Some(7);
|
||||
self.update_fg_color();
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
'q' => {
|
||||
match self.esc_args.get(0).copied().unwrap_or(0) {
|
||||
3 | 4 => self.cursor_style = CursorStyle::Underline,
|
||||
5 | 6 => self.cursor_style = CursorStyle::Bar,
|
||||
_ => self.cursor_style = CursorStyle::Block,
|
||||
}
|
||||
self.cursor_dirty = true;
|
||||
true
|
||||
}
|
||||
|
||||
// Move back one character
|
||||
'D' => {
|
||||
if self.cursor.col > 0 {
|
||||
self.buffer.set_row_dirty(self.cursor.row);
|
||||
self.cursor.col -= 1;
|
||||
CharacterAttribute::BackgroundRgb(r, g, b) => {
|
||||
self.attributes.bg = Color::new(r, g, b);
|
||||
self.fg_index = None;
|
||||
self.update_fg_color();
|
||||
true
|
||||
} else {
|
||||
}
|
||||
CharacterAttribute::BgIndex(Some(index)) => {
|
||||
self.attributes.bg = Color::from_escape(&CONFIG, false, index as u32)
|
||||
.unwrap_or(self.default_attributes.bg);
|
||||
self.update_fg_color();
|
||||
true
|
||||
}
|
||||
CharacterAttribute::BgIndex(None) => {
|
||||
self.attributes.bg = self.default_attributes.bg;
|
||||
self.update_fg_color();
|
||||
true
|
||||
}
|
||||
CharacterAttribute::DefaultFgBg => {
|
||||
self.attributes.fg = self.default_attributes.fg;
|
||||
self.attributes.bg = self.default_attributes.bg;
|
||||
self.fg_index = Some(7);
|
||||
self.update_fg_color();
|
||||
true
|
||||
}
|
||||
CharacterAttribute::Bold => {
|
||||
self.attributes.bold = true;
|
||||
self.update_fg_color();
|
||||
true
|
||||
}
|
||||
CharacterAttribute::Normal | CharacterAttribute::Faint => {
|
||||
self.attributes.bold = false;
|
||||
self.update_fg_color();
|
||||
true
|
||||
}
|
||||
CharacterAttribute::Italicized(set) => {
|
||||
self.attributes.italic = set;
|
||||
false
|
||||
}
|
||||
}
|
||||
// Character attributes
|
||||
'm' => {
|
||||
let args = self.esc_args.clone();
|
||||
|
||||
if args.is_empty() {
|
||||
return false;
|
||||
CharacterAttribute::Underlined(set) => {
|
||||
self.attributes.underlined = set;
|
||||
false
|
||||
}
|
||||
|
||||
if args.len() == 5 && args[0] == 38 && args[1] == 2 {
|
||||
let r = args[2];
|
||||
let g = args[3];
|
||||
let b = args[4];
|
||||
let color = Color::new(r as u8, g as u8, b as u8);
|
||||
self.fg_index = None;
|
||||
self.attributes.fg = color;
|
||||
self.update_fg_color();
|
||||
self.esc_state = EscapeState::Normal;
|
||||
return false;
|
||||
CharacterAttribute::CrossedOut(set) => {
|
||||
self.attributes.strikethrough = set;
|
||||
false
|
||||
}
|
||||
if args.len() == 5 && args[0] == 48 && args[1] == 2 {
|
||||
let r = args[2];
|
||||
let g = args[3];
|
||||
let b = args[4];
|
||||
let color = Color::new(r as u8, g as u8, b as u8);
|
||||
self.attributes.bg = color;
|
||||
self.esc_state = EscapeState::Normal;
|
||||
return false;
|
||||
_ => {
|
||||
log::warn!("TODO: SetCharacterAttribute({attr:?})");
|
||||
false
|
||||
}
|
||||
},
|
||||
|
||||
for arg in args {
|
||||
match arg {
|
||||
// Reset
|
||||
0 => {
|
||||
self.attributes = self.default_attributes;
|
||||
self.fg_index = Some(7);
|
||||
}
|
||||
// Bold
|
||||
1 => {
|
||||
self.attributes.bold = true;
|
||||
self.update_fg_color();
|
||||
}
|
||||
// Faint (should be decreased intensity, but here just normal)
|
||||
2 => {
|
||||
self.attributes.bold = false;
|
||||
self.update_fg_color();
|
||||
}
|
||||
// Italicized
|
||||
3 => {
|
||||
self.attributes.italic = true;
|
||||
}
|
||||
// Underlined
|
||||
4 => {
|
||||
self.attributes.underlined = true;
|
||||
}
|
||||
// Strikethrough
|
||||
9 => {
|
||||
self.attributes.strikethrough = true;
|
||||
}
|
||||
// Normal (neither bold nor faint)
|
||||
22 => {
|
||||
self.attributes.bold = false;
|
||||
self.update_fg_color();
|
||||
}
|
||||
// Not italicized
|
||||
23 => {
|
||||
self.attributes.italic = false;
|
||||
}
|
||||
// Not underlined
|
||||
24 => {
|
||||
self.attributes.underlined = false;
|
||||
}
|
||||
// Not strikethrough
|
||||
29 => {
|
||||
self.attributes.strikethrough = false;
|
||||
}
|
||||
// Foreground color
|
||||
30..=39 => {
|
||||
let vt_color = arg % 10;
|
||||
if vt_color == 9 {
|
||||
self.fg_index = Some(7);
|
||||
} else {
|
||||
self.fg_index = Some(vt_color);
|
||||
}
|
||||
self.update_fg_color();
|
||||
}
|
||||
// Background color
|
||||
40..=49 => {
|
||||
let vt_color = arg % 10;
|
||||
if vt_color == 9 {
|
||||
self.attributes.bg = self.default_attributes.bg;
|
||||
} else {
|
||||
self.attributes.bg = Color::from_escape(&*CONFIG, false, vt_color)
|
||||
.unwrap_or(self.default_attributes.bg);
|
||||
}
|
||||
}
|
||||
// Set foreground color (8-color mode)
|
||||
90..=97 => {
|
||||
let vt_color = arg - 90;
|
||||
self.fg_index = Some(vt_color);
|
||||
self.update_fg_color();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
// Move cursor to position
|
||||
'f' => {
|
||||
let row = self.esc_args[0].clamp(1, self.buffer.height as u32) - 1;
|
||||
let col = self.esc_args[1].clamp(1, self.buffer.width as u32) - 1;
|
||||
|
||||
self.buffer.set_row_dirty(self.cursor.row);
|
||||
self.cursor = Cursor {
|
||||
row: row as _,
|
||||
col: col as _,
|
||||
};
|
||||
self.cursor_dirty = true;
|
||||
|
||||
true
|
||||
}
|
||||
// Move cursor to home position (0; 0)
|
||||
'H' => {
|
||||
let (row, col) = if self.esc_args.len() == 2 {
|
||||
(
|
||||
self.esc_args[0].saturating_sub(1),
|
||||
self.esc_args[1].saturating_sub(1),
|
||||
)
|
||||
} else {
|
||||
(0, 0)
|
||||
};
|
||||
self.buffer.set_row_dirty(self.cursor.row);
|
||||
self.cursor = Cursor {
|
||||
row: row as _,
|
||||
col: col as _,
|
||||
};
|
||||
self.cursor_dirty = true;
|
||||
true
|
||||
}
|
||||
'$' => {
|
||||
return false;
|
||||
}
|
||||
// Clear rows/columns/screen
|
||||
'J' => match self.esc_args[0] {
|
||||
// Erase lines down
|
||||
0 => false,
|
||||
// Erase lines up
|
||||
1 => false,
|
||||
// Erase all
|
||||
2 => {
|
||||
// Erase
|
||||
ControlSequence::EraseInDisplay(erase) => match erase {
|
||||
EraseInDisplay::All => {
|
||||
self.buffer
|
||||
.clear(self.default_attributes.fg, self.attributes.bg);
|
||||
self.cursor_dirty = true;
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
EraseInDisplay::Above => {
|
||||
for row in 0..self.cursor.row {
|
||||
self.buffer
|
||||
.erase_row(row, self.attributes.fg, self.attributes.bg);
|
||||
}
|
||||
true
|
||||
}
|
||||
EraseInDisplay::Below => {
|
||||
for row in self.cursor.row + 1..self.buffer.height {
|
||||
self.buffer
|
||||
.erase_row(row, self.attributes.fg, self.attributes.bg);
|
||||
}
|
||||
true
|
||||
}
|
||||
EraseInDisplay::SavedLines => false,
|
||||
},
|
||||
'K' => match self.esc_args[0] {
|
||||
// Erase to right
|
||||
0 => {
|
||||
ControlSequence::EraseInLine(erase) => match erase {
|
||||
EraseInLine::ToRight => {
|
||||
self.buffer.rows[self.cursor.row].erase_to_right(
|
||||
self.cursor.col,
|
||||
self.default_attributes.fg,
|
||||
@@ -603,8 +643,11 @@ impl State {
|
||||
);
|
||||
true
|
||||
}
|
||||
// Erase All
|
||||
2 => {
|
||||
EraseInLine::ToLeft => {
|
||||
log::warn!("TODO: Erase in line to left");
|
||||
false
|
||||
}
|
||||
EraseInLine::All => {
|
||||
self.buffer.erase_row(
|
||||
self.cursor.row,
|
||||
self.default_attributes.fg,
|
||||
@@ -612,62 +655,172 @@ impl State {
|
||||
);
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
_ => false,
|
||||
ControlSequence::EraseCharacters(times) => {
|
||||
log::warn!("TODO: EraseCharacters {times}");
|
||||
false
|
||||
}
|
||||
|
||||
// Scroll
|
||||
ControlSequence::InsertLines(count) => {
|
||||
log::warn!("XXX: InsertLines {count}");
|
||||
for _ in 0..count {
|
||||
self.buffer
|
||||
.insert_row(self.cursor.row, self.attributes.fg, self.attributes.bg);
|
||||
}
|
||||
true
|
||||
}
|
||||
ControlSequence::DeleteLines(count) => {
|
||||
log::warn!("XXX: DeleteLines {count}");
|
||||
for _ in 0..count {
|
||||
self.buffer
|
||||
.remove_row(self.cursor.row, self.attributes.fg, self.attributes.bg);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
// Cursor controls
|
||||
ControlSequence::SetCursorStyle(style) => {
|
||||
log::warn!("TODO: SetCursorStyle {style:?}");
|
||||
false
|
||||
}
|
||||
ControlSequence::MoveCursorUp(times) => {
|
||||
let new_row = self.cursor.row.saturating_sub(times as usize);
|
||||
self.cursor.row = new_row;
|
||||
self.cursor_dirty = true;
|
||||
true
|
||||
}
|
||||
ControlSequence::MoveCursorDown(times) => {
|
||||
let new_row = (self.cursor.row + times as usize).min(self.buffer.height - 1);
|
||||
self.cursor.row = new_row;
|
||||
self.cursor_dirty = true;
|
||||
true
|
||||
}
|
||||
ControlSequence::MoveCursorForward(times) => {
|
||||
let new_col = (self.cursor.col + times as usize).min(self.buffer.width - 1);
|
||||
self.cursor.col = new_col;
|
||||
self.cursor_dirty = true;
|
||||
true
|
||||
}
|
||||
ControlSequence::SetCursorPosition(row, col) => {
|
||||
let row = row.saturating_sub(1);
|
||||
let col = col.saturating_sub(1);
|
||||
self.buffer.set_row_dirty(self.cursor.row);
|
||||
self.cursor = Cursor {
|
||||
row: row as _,
|
||||
col: col as _,
|
||||
};
|
||||
self.cursor_dirty = true;
|
||||
true
|
||||
}
|
||||
|
||||
// "Device" controls
|
||||
ControlSequence::ReportCursorPosition => {
|
||||
write!(pty, "\x1B[{};{}R", self.cursor.row + 1, self.cursor.col + 1).ok();
|
||||
pty.flush().ok();
|
||||
false
|
||||
}
|
||||
ControlSequence::QueryKeyModifiers(_query) => {
|
||||
// TODO
|
||||
pty.write_all(b"\x1B[>0m").ok();
|
||||
pty.flush().ok();
|
||||
false
|
||||
}
|
||||
ControlSequence::Decrqm => {
|
||||
pty.write_all(b"\x1B[?0;0$y").ok();
|
||||
pty.flush().ok();
|
||||
false
|
||||
}
|
||||
ControlSequence::SendDeviceAttributes(ps) => {
|
||||
_ = ps;
|
||||
pty.write_all(b"\x1B[?1;2c").ok();
|
||||
pty.flush().ok();
|
||||
false
|
||||
}
|
||||
// Not handled yet
|
||||
ControlSequence::SetWindowParameter(_, _) => false,
|
||||
|
||||
_ => {
|
||||
log::warn!("TODO: {seq:?}");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_shell_output(&mut self, pty: &mut PtyMaster, ch: u8) -> bool {
|
||||
let Some(ch) = self.utf8_decode.push(ch) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
self.esc_state = EscapeState::Normal;
|
||||
redraw
|
||||
}
|
||||
|
||||
fn handle_ctlseq_byte(&mut self, byte: u8, c: char) -> bool {
|
||||
match c {
|
||||
'?' if byte == 0 => {
|
||||
self.esc_state = EscapeState::Csi(CsiState { byte: b'?' });
|
||||
false
|
||||
match self.esc_parser.push(ch) {
|
||||
Some(TerminalInput::Character(ch)) => {
|
||||
self.buffer
|
||||
.set_cell(self.cursor, GridCell::new(ch, self.attributes));
|
||||
self.cursor.col += 1;
|
||||
self.fix_cursor(true)
|
||||
}
|
||||
c if let Some(digit) = c.to_digit(10) => {
|
||||
let arg = self.esc_args.last_mut().unwrap();
|
||||
*arg *= 10;
|
||||
*arg += digit;
|
||||
false
|
||||
Some(TerminalInput::CarriageReturn) => {
|
||||
self.buffer.rows[self.cursor.row].dirty = true;
|
||||
self.cursor.col = 0;
|
||||
self.fix_cursor(true)
|
||||
}
|
||||
';' => {
|
||||
self.esc_args.push(0);
|
||||
false
|
||||
Some(TerminalInput::NewLine) => {
|
||||
self.buffer.rows[self.cursor.row].dirty = true;
|
||||
self.cursor.row += 1;
|
||||
self.cursor.col = 0;
|
||||
self.fix_cursor(true)
|
||||
}
|
||||
_ => self.handle_ctlseq(byte, c),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_shell_output(&mut self, ch: u8) -> bool {
|
||||
if ch == 0x07 {
|
||||
return false;
|
||||
Some(TerminalInput::Tab) => {
|
||||
self.buffer.rows[self.cursor.row].dirty = true;
|
||||
self.cursor.col = (self.cursor.col + 3) & !3;
|
||||
self.fix_cursor(true)
|
||||
}
|
||||
Some(TerminalInput::NewPage) => {
|
||||
todo!();
|
||||
}
|
||||
Some(TerminalInput::Backspace) => {
|
||||
if self.cursor.col > 0 {
|
||||
self.cursor.col -= 1;
|
||||
} else if self.cursor.row > 0 {
|
||||
self.cursor.row -= 1;
|
||||
self.cursor.col = 0;
|
||||
}
|
||||
self.fix_cursor(true)
|
||||
}
|
||||
Some(TerminalInput::Delete) => {
|
||||
todo!();
|
||||
}
|
||||
Some(TerminalInput::Bell) | Some(TerminalInput::VerticalTab) => false,
|
||||
Some(TerminalInput::Control(seq)) => self.handle_control_sequence(pty, seq),
|
||||
None => false,
|
||||
}
|
||||
|
||||
if let Some(ch) = self.utf8_decode.push(ch) {
|
||||
match self.esc_state {
|
||||
EscapeState::Normal => self.putc_normal(ch),
|
||||
EscapeState::Escape => match ch {
|
||||
'[' => {
|
||||
self.esc_state = EscapeState::Csi(CsiState { byte: 0 });
|
||||
false
|
||||
}
|
||||
'(' => {
|
||||
self.esc_state = EscapeState::Csi(CsiState { byte: b'(' });
|
||||
false
|
||||
}
|
||||
_ => {
|
||||
self.esc_state = EscapeState::Normal;
|
||||
false
|
||||
}
|
||||
},
|
||||
EscapeState::Csi(CsiState { byte }) => self.handle_ctlseq_byte(byte, ch),
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
// if ch == 0x07 {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// if let Some(ch) = self.utf8_decode.push(ch) {
|
||||
// match self.esc_state {
|
||||
// EscapeState::Normal => self.putc_normal(ch),
|
||||
// EscapeState::Escape => match ch {
|
||||
// '[' => {
|
||||
// self.esc_state = EscapeState::Csi(CsiState { byte: 0 });
|
||||
// false
|
||||
// }
|
||||
// '(' => {
|
||||
// self.esc_state = EscapeState::Csi(CsiState { byte: b'(' });
|
||||
// false
|
||||
// }
|
||||
// _ => {
|
||||
// self.esc_state = EscapeState::Normal;
|
||||
// false
|
||||
// }
|
||||
// },
|
||||
// EscapeState::Csi(CsiState { byte }) => self.handle_ctlseq_byte(byte, ch),
|
||||
// }
|
||||
// } else {
|
||||
// false
|
||||
// }
|
||||
}
|
||||
|
||||
pub fn invalidate_current_viewport(&mut self) {
|
||||
|
||||
@@ -66,7 +66,13 @@ impl CommandSpawnExt for Command {
|
||||
}
|
||||
|
||||
fn create_session(&mut self) -> io::Result<&mut Self> {
|
||||
// TODO
|
||||
Ok(self)
|
||||
Ok(unsafe {
|
||||
self.pre_exec(move || {
|
||||
if libc::setsid() < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ impl CommandSpawnExt for Command {
|
||||
}
|
||||
|
||||
fn create_session(&mut self) -> io::Result<&mut Self> {
|
||||
Ok(unsafe { self.gain_terminal(0) })
|
||||
let group = unsafe { runtime::rt::sys::create_process_group() };
|
||||
Ok(unsafe { self.process_group(group).gain_terminal(0) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ libc = "0.2.150"
|
||||
yggdrasil-rt.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1.4.1"
|
||||
tui.workspace = true
|
||||
|
||||
[features]
|
||||
|
||||
@@ -0,0 +1,898 @@
|
||||
#[derive(Default)]
|
||||
pub enum EscapeParser {
|
||||
#[default]
|
||||
Normal,
|
||||
Escape(EscapeState),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum TerminalInput {
|
||||
Character(char),
|
||||
Control(ControlSequence),
|
||||
Bell,
|
||||
Backspace,
|
||||
Tab,
|
||||
NewLine,
|
||||
VerticalTab,
|
||||
NewPage,
|
||||
CarriageReturn,
|
||||
Delete,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum SetWindowParameter {
|
||||
Icon,
|
||||
Title,
|
||||
Both,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum ControlSequence {
|
||||
SetCharacterAttribute(CharacterAttribute),
|
||||
SetCursorPosition(u32, u32),
|
||||
EraseInLine(EraseInLine),
|
||||
EraseInDisplay(EraseInDisplay),
|
||||
EraseCharacters(u32),
|
||||
SetCursorStyle(CursorStyle),
|
||||
MoveCursorUp(u32),
|
||||
MoveCursorDown(u32),
|
||||
MoveCursorForward(u32),
|
||||
|
||||
SetAlternateMode(bool),
|
||||
SetBracketedPasteMode(bool),
|
||||
SetApplicationCursor(bool),
|
||||
SetWindowParameter(SetWindowParameter, String),
|
||||
SetAlternateKeypad(bool),
|
||||
|
||||
SetScrollingRegion(u32, u32),
|
||||
InsertLines(u32),
|
||||
DeleteLines(u32),
|
||||
|
||||
QueryKeyModifiers(QueryKeyModifiers),
|
||||
Decrqm,
|
||||
DeviceStatusReport,
|
||||
ReportCursorPosition,
|
||||
SendDeviceAttributes(u32),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum EraseInLine {
|
||||
ToRight,
|
||||
ToLeft,
|
||||
All,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum EraseInDisplay {
|
||||
Below,
|
||||
Above,
|
||||
All,
|
||||
SavedLines,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum CursorStyle {
|
||||
Block(bool),
|
||||
Bar(bool),
|
||||
Underline(bool),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum CharacterAttribute {
|
||||
Reset,
|
||||
Bold,
|
||||
Faint,
|
||||
Normal,
|
||||
Italicized(bool),
|
||||
Underlined(bool),
|
||||
Blinking(bool),
|
||||
Inverse(bool),
|
||||
Hidden(bool),
|
||||
CrossedOut(bool),
|
||||
FgIndex(Option<u8>),
|
||||
BgIndex(Option<u8>),
|
||||
DefaultFgBg,
|
||||
ForegroundRgb(u8, u8, u8),
|
||||
BackgroundRgb(u8, u8, u8),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum QueryKeyModifiers {
|
||||
ModifyKeyboard,
|
||||
ModifyCursorKeys,
|
||||
ModifyFunctionKeys,
|
||||
ModifyKeypadKeys,
|
||||
ModifyOtherKeys,
|
||||
ModifyModifierKeys,
|
||||
ModifySpecialKeys,
|
||||
}
|
||||
|
||||
impl EscapeParser {
|
||||
pub const fn new() -> Self {
|
||||
Self::Normal
|
||||
}
|
||||
|
||||
pub fn push(&mut self, ch: char) -> Option<TerminalInput> {
|
||||
match self {
|
||||
Self::Normal => self.push_normal(ch),
|
||||
Self::Escape(state) => {
|
||||
let (ret, to_normal) = state.push(ch);
|
||||
if to_normal {
|
||||
*self = EscapeParser::Normal;
|
||||
}
|
||||
ret.map(TerminalInput::Control)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn push_normal(&mut self, ch: char) -> Option<TerminalInput> {
|
||||
match ch {
|
||||
// Ignored: NUL, SOH, STX, ETX, EOT, ENQ, ACK
|
||||
'\x00'..='\x06' => None,
|
||||
// BEL
|
||||
'\x07' => Some(TerminalInput::Bell),
|
||||
// BS
|
||||
'\x08' => Some(TerminalInput::Backspace),
|
||||
// TAB
|
||||
'\x09' => Some(TerminalInput::Tab),
|
||||
// LF
|
||||
'\x0A' => Some(TerminalInput::NewLine),
|
||||
// VT
|
||||
'\x0B' => Some(TerminalInput::VerticalTab),
|
||||
// FF
|
||||
'\x0C' => Some(TerminalInput::NewPage),
|
||||
// CR
|
||||
'\x0D' => Some(TerminalInput::CarriageReturn),
|
||||
// Ignored: SO, ..., SUB
|
||||
'\x0E'..='\x1A' => None,
|
||||
// ESC
|
||||
'\x1B' => {
|
||||
*self = Self::Escape(EscapeState::Esc);
|
||||
None
|
||||
}
|
||||
// Ignored: FS, ..., US
|
||||
'\x1C'..='\x1F' => None,
|
||||
// Printable
|
||||
'\x20'..='\x7E' => Some(TerminalInput::Character(ch)),
|
||||
// DEL
|
||||
'\x7F' => Some(TerminalInput::Delete),
|
||||
// Other characters >0x7F
|
||||
_ => Some(TerminalInput::Character(ch)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum EscapeState {
|
||||
Esc,
|
||||
G0Charset(char),
|
||||
Csi(CsiState),
|
||||
Osc(OscState),
|
||||
}
|
||||
|
||||
impl EscapeState {
|
||||
fn push(&mut self, ch: char) -> (Option<ControlSequence>, bool) {
|
||||
match self {
|
||||
Self::Esc => match ch {
|
||||
// ESC ETX: Switch to VT100 mode
|
||||
'\x03' => todo!(),
|
||||
// ESC ENQ: Return terminal status
|
||||
'\x05' => todo!(),
|
||||
// ESC FF: PAGE (Clear screen)
|
||||
'\x0C' => todo!(),
|
||||
// ESC SO: Begin 4015 APL mode
|
||||
// ESC SI: End 4015 APL mode
|
||||
'\x0E' | '\x0F' => todo!(),
|
||||
// ESC ETB: COPY (Save Tektronix Codes to file)
|
||||
'\x17' => todo!(),
|
||||
// ESC CAN: Bypass condition
|
||||
'\x18' => todo!(),
|
||||
// ESC SUB: GIN mode
|
||||
'\x1A' => todo!(),
|
||||
// ESC FS: Special Point Plot Mode
|
||||
'\x1C' => todo!(),
|
||||
// ESC ( C => Designate G0 Character Set
|
||||
'(' => {
|
||||
*self = Self::G0Charset('\0');
|
||||
(None, false)
|
||||
}
|
||||
// ESC ] => OSC
|
||||
']' => {
|
||||
*self = Self::Osc(OscState::new());
|
||||
(None, false)
|
||||
}
|
||||
// ESC [ => CSI
|
||||
'[' => {
|
||||
*self = Self::Csi(CsiState::new());
|
||||
(None, false)
|
||||
}
|
||||
// ESC D => Index (IND)
|
||||
'D' => todo!(),
|
||||
// ESC E => Next Line (NEL)
|
||||
'E' => todo!(),
|
||||
// ESC H => Tab Set (HTS)
|
||||
'H' => todo!(),
|
||||
// ESC M => Reverse Index (RI)
|
||||
'M' => todo!(),
|
||||
// ESC N => Single Shift Select of G2 Character Set (SS2)
|
||||
// ESC O => Single Shift Select of G3 Character Set (SS3)
|
||||
'N' | 'O' => todo!(),
|
||||
// ESC P => Device Control String (DCS)
|
||||
'P' => (None, true),
|
||||
// ESC V => Start of Guarded Area (SPA)
|
||||
'V' => todo!(),
|
||||
// ESC W => End of Guarded Area (EPA)
|
||||
'W' => todo!(),
|
||||
// ESC X => Start of String (SOS)
|
||||
'X' => todo!(),
|
||||
// ESC Z => Return Terminal ID (DECID)
|
||||
'Z' => todo!(),
|
||||
// ESC \ => String Terminator (ST)
|
||||
'\\' => (None, true),
|
||||
// ESC ^ => Privacy Message (PM)
|
||||
'^' => todo!(),
|
||||
// ESC _ => Application Program Command (APC)
|
||||
'_' => todo!(),
|
||||
|
||||
' ' | '#' | '%' | ')' | '*' | '+' | '-' | '.' | '/' => todo!(),
|
||||
|
||||
'6'..='9' => todo!(),
|
||||
|
||||
// ESC c => Full Reset
|
||||
'c' => todo!(),
|
||||
// ESC = => Enter alternate keypad mode
|
||||
'=' => (Some(ControlSequence::SetAlternateKeypad(true)), true),
|
||||
// ESC > => Leave alternate keypad mode
|
||||
'>' => (Some(ControlSequence::SetAlternateKeypad(false)), true),
|
||||
|
||||
// Unsupported
|
||||
'F' | 'l' | 'm' | 'n' | 'o' | '|' | '}' | '~' => todo!("ESC {ch}"),
|
||||
|
||||
_ => todo!(),
|
||||
},
|
||||
Self::G0Charset(g0) => match ch {
|
||||
'"' | '%' if *g0 == '\0' => {
|
||||
*g0 = ch;
|
||||
(None, false)
|
||||
}
|
||||
// TODO
|
||||
_ => (None, true),
|
||||
},
|
||||
Self::Csi(csi) => csi.push(ch),
|
||||
Self::Osc(osc) => osc.push(ch),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum CsiState {
|
||||
// CSI ...
|
||||
Normal(CsiArgs),
|
||||
// CSI # ...
|
||||
PrefixHash(CsiArgs),
|
||||
// CSI ? ...
|
||||
PrefixQuestion(CsiArgs),
|
||||
// CSI > ...
|
||||
PrefixGt(CsiArgs),
|
||||
// CSI = ...
|
||||
PrefixEq(CsiArgs),
|
||||
// CSI ! ...
|
||||
PrefixExcl(CsiArgs),
|
||||
// CSI & ...
|
||||
PrefixAnd(CsiArgs),
|
||||
// CSI " ...
|
||||
PrefixDquote(CsiArgs),
|
||||
}
|
||||
|
||||
pub struct CsiArgs {
|
||||
args: Vec<u32>,
|
||||
suffix: char,
|
||||
}
|
||||
|
||||
impl CsiArgs {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
args: Vec::new(),
|
||||
suffix: '\0',
|
||||
}
|
||||
}
|
||||
|
||||
fn push(&mut self, ch: char) -> bool {
|
||||
match ch {
|
||||
ch if ch.is_ascii_digit() => {
|
||||
let digit = (ch as u8 - b'0') as u32;
|
||||
if let Some(last) = self.args.last_mut() {
|
||||
*last *= 10;
|
||||
*last += digit;
|
||||
} else {
|
||||
self.args.push(digit);
|
||||
}
|
||||
true
|
||||
}
|
||||
';' => {
|
||||
self.args.push(0);
|
||||
true
|
||||
}
|
||||
' ' | '#' | '$' | '\'' | ',' | '?' if !self.args.is_empty() && self.suffix == '\0' => {
|
||||
self.suffix = ch;
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn get(&self, index: usize) -> u32 {
|
||||
self.args.get(index).copied().unwrap_or_default()
|
||||
}
|
||||
|
||||
fn get_or(&self, index: usize, default: u32) -> u32 {
|
||||
self.args.get(index).copied().unwrap_or(default)
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.args.is_empty() && self.suffix == '\0'
|
||||
}
|
||||
}
|
||||
|
||||
impl CsiState {
|
||||
const fn new() -> Self {
|
||||
Self::Normal(CsiArgs::new())
|
||||
}
|
||||
|
||||
fn push(&mut self, ch: char) -> (Option<ControlSequence>, bool) {
|
||||
match self {
|
||||
Self::Normal(args) => match (ch, args.suffix) {
|
||||
// CSI ? ...
|
||||
('?', _) if args.is_empty() => {
|
||||
*self = Self::PrefixQuestion(CsiArgs::new());
|
||||
(None, false)
|
||||
}
|
||||
|
||||
(_, _) if args.push(ch) => (None, false),
|
||||
// CSI Ps @ Insert Ps (Blank) Character(s) (default = 1)
|
||||
('@', '\0') => todo!(),
|
||||
// CSI Ps SP @ Shift left Ps column(s) (default = 1)
|
||||
('@', ' ') => todo!(),
|
||||
// CSI Ps A Cursor Up Ps Times (default = 1)
|
||||
('A', '\0') => (Some(ControlSequence::MoveCursorUp(args.get_or(0, 1))), true),
|
||||
// CSI Ps SP A Shift right Ps column(s) (default = 1)
|
||||
('A', ' ') => todo!(),
|
||||
// CSI Ps B Cursor Down Ps Times (default = 1)
|
||||
('B', '\0') => (
|
||||
Some(ControlSequence::MoveCursorDown(args.get_or(0, 1))),
|
||||
true,
|
||||
),
|
||||
// CSI Ps C Cursor Forward Ps Times (default = 1)
|
||||
('C', '\0') => (
|
||||
Some(ControlSequence::MoveCursorForward(args.get_or(0, 1))),
|
||||
true,
|
||||
),
|
||||
// CSI Ps D Cursor Backward Ps Times (default = 1)
|
||||
('D', '\0') => todo!(),
|
||||
// CSI Ps E Cursor Next Line Ps Times (default = 1)
|
||||
('E', '\0') => todo!(),
|
||||
// CSI Ps F Cursor Preceding Line Ps Times (default = 1)
|
||||
('F', '\0') => todo!(),
|
||||
// CSI Ps G Cursor Character Absolute [column] (default = [row, 1])
|
||||
('G', '\0') => todo!(),
|
||||
// CSI Ps ; Ps H Cursor Position [row;column] (default = [1,1])
|
||||
('H', '\0') => (
|
||||
Some(ControlSequence::SetCursorPosition(
|
||||
args.get_or(0, 1),
|
||||
args.get_or(1, 1),
|
||||
)),
|
||||
true,
|
||||
),
|
||||
// CSI Ps I Cursor Forward Tabulation Ps tab stops (default = 1)
|
||||
('I', '\0') => todo!(),
|
||||
// CSI Ps J Erase in Display
|
||||
('J', '\0') => match args.get(0) {
|
||||
// Ps = 3 ⇒ Erase Saved Lines
|
||||
3 => (
|
||||
Some(ControlSequence::EraseInDisplay(EraseInDisplay::SavedLines)),
|
||||
true,
|
||||
),
|
||||
// Ps = 2 ⇒ Erase All
|
||||
2 => (
|
||||
Some(ControlSequence::EraseInDisplay(EraseInDisplay::All)),
|
||||
true,
|
||||
),
|
||||
// Ps = 1 ⇒ Erase Above
|
||||
1 => (
|
||||
Some(ControlSequence::EraseInDisplay(EraseInDisplay::Above)),
|
||||
true,
|
||||
),
|
||||
// Ps = 0 ⇒ Erase Below (default)
|
||||
_ => (
|
||||
Some(ControlSequence::EraseInDisplay(EraseInDisplay::Below)),
|
||||
true,
|
||||
),
|
||||
},
|
||||
// CSI Ps K Erase in Line
|
||||
('K', '\0') => match args.get(0) {
|
||||
// Ps = 2 ⇒ Erase All
|
||||
2 => (Some(ControlSequence::EraseInLine(EraseInLine::All)), true),
|
||||
// Ps = 1 ⇒ Erase to Left
|
||||
1 => (
|
||||
Some(ControlSequence::EraseInLine(EraseInLine::ToLeft)),
|
||||
true,
|
||||
),
|
||||
// Ps = 0 ⇒ Erase to Right (default)
|
||||
_ => (
|
||||
Some(ControlSequence::EraseInLine(EraseInLine::ToRight)),
|
||||
true,
|
||||
),
|
||||
},
|
||||
// CSI Ps L Insert Ps Line(s) (default = 1)
|
||||
('L', '\0') => (Some(ControlSequence::InsertLines(args.get_or(0, 1))), true),
|
||||
// CSI Ps M Delete Ps Line(s) (default = 1)
|
||||
('M', '\0') => (Some(ControlSequence::DeleteLines(args.get_or(0, 1))), true),
|
||||
// CSI Ps P Delete Ps Character(s) (default = 1)
|
||||
('P', '\0') => todo!(),
|
||||
// CSI Pm # P Push current colors onto stack
|
||||
('P', '#') => todo!(),
|
||||
// CSI Pm # Q Pop colors from the stack
|
||||
('Q', '#') => todo!(),
|
||||
// CSI Ps S Scroll up Ps lines (default = 1)
|
||||
('S', '\0') => todo!(),
|
||||
// CSI Ps T Scroll down Ps lines (default = 1)
|
||||
// CSI Ps ; Ps ; Ps ; Ps ; Ps T
|
||||
// Initiate highlight mouse tracking
|
||||
// [func;startx;starty;firstrow;lastrow]
|
||||
('T', '\0') => todo!(),
|
||||
// CSI Ps X Erase Ps Character(s) (default = 1)
|
||||
('X', '\0') => (
|
||||
Some(ControlSequence::EraseCharacters(args.get_or(0, 1))),
|
||||
true,
|
||||
),
|
||||
// CSI Ps Z Cursor Backward Tabulation Ps tab stops (default = 1)
|
||||
('Z', '\0') => todo!(),
|
||||
// CSI Ps ^ Scroll down Ps lines (default = 1)
|
||||
('^', '\0') => todo!(),
|
||||
// CSI Ps ` Character Position Absolute [column] (default = [row,1])
|
||||
('`', '\0') => todo!(),
|
||||
// CSI Ps a Character Position Relative [columns] (default = [row,col+1])
|
||||
('a', '\0') => todo!(),
|
||||
// CSI Ps b Repeat the preceding graphic character Ps times
|
||||
('b', '\0') => todo!(),
|
||||
// CSI Ps c Send Device Attributes (Primary DA)
|
||||
// Ps = 0 ⇒ request attributes from terminal
|
||||
('c', '\0') => (
|
||||
Some(ControlSequence::SendDeviceAttributes(args.get(0))),
|
||||
true,
|
||||
),
|
||||
// CSI Ps d Line Position Absolute [row] (default = [1,column])
|
||||
('d', '\0') => todo!(),
|
||||
// CSI Ps e Line Position Relative [rows] (default = [row+1,column])
|
||||
('e', '\0') => todo!(),
|
||||
// CSI Ps ; Ps f Horizontal and Vertical Position [row;column] (default=[1,1])
|
||||
('f', '\0') => todo!(),
|
||||
// CSI Ps g Tab Clear
|
||||
// Ps = 0 ⇒ Clear Current Column (default)
|
||||
// Ps = 3 ⇒ Clear All
|
||||
('g', '\0') => todo!(),
|
||||
// CSI Pm h Set Mode
|
||||
// Ps = 2 ⇒ Keyboard Action Mode
|
||||
// Ps = 4 ⇒ Insert Mode
|
||||
// Ps = 12 ⇒ Send/receive
|
||||
// Ps = 20 ⇒ Automatic Newline
|
||||
('h', '\0') => todo!(),
|
||||
// CSI Ps i Media Copy
|
||||
// Ps = 0 ⇒ Print screen (default)
|
||||
// Ps = 4 ⇒ Turn off printer controller mode
|
||||
// Ps = 5 ⇒ Turn on printer controller mode
|
||||
// Ps = 10 ⇒ HTML screen dump
|
||||
// Ps = 11 ⇒ SVG screen dump
|
||||
('i', '\0') => todo!(),
|
||||
// CSI Pm l Reset Mode
|
||||
// Ps = 2 ⇒ Keyboard Action Mode
|
||||
// Ps = 4 ⇒ Replace Mode
|
||||
// Ps = 12 ⇒ Send/receive
|
||||
// Ps = 20 ⇒ Normal Linefeed
|
||||
('l', '\0') => todo!(),
|
||||
// CSI Pm m Character Attributes
|
||||
('m', '\0') => (Self::handle_sgr(args), true),
|
||||
// CSI Ps n Device Status Report
|
||||
('n', '\0') => match args.get_or(0, 5) {
|
||||
// Ps = 6 ⇒ Report Cursor Position
|
||||
6 => (Some(ControlSequence::ReportCursorPosition), true),
|
||||
// Ps = 5 ⇒ Status Report
|
||||
_ => (Some(ControlSequence::DeviceStatusReport), true),
|
||||
},
|
||||
// CSI Pl ; Pc " p Set conformance level
|
||||
// Pl = 6 1 ⇒ level 1
|
||||
// Pl = 6 2 ⇒ level 2
|
||||
// Pl = 6 3 ⇒ level 3
|
||||
// Pl = 6 4 ⇒ level 4
|
||||
// Pl = 6 5 ⇒ level 5
|
||||
// Pc = 0 ⇒ 8-bit controls
|
||||
// Pc = 1 ⇒ 7-bit controls
|
||||
// Pc = 2 ⇒ 8-bit controls
|
||||
('p', '"') => todo!(),
|
||||
// CSI Ps $ p Request ANSI mode
|
||||
('p', '$') => todo!(),
|
||||
// CSI Pm # p Push video attributes onto stack
|
||||
('p', '#') => todo!(),
|
||||
// CSI Ps SP q Set cursor style
|
||||
('q', ' ') => {
|
||||
let style = match args.get_or(0, 1) {
|
||||
// Ps = 0 ⇒ blinking block
|
||||
0 => CursorStyle::Block(true),
|
||||
// Ps = 2 ⇒ steady block
|
||||
2 => CursorStyle::Block(false),
|
||||
// Ps = 3 ⇒ blinking underline
|
||||
3 => CursorStyle::Underline(true),
|
||||
// Ps = 4 ⇒ steady underline
|
||||
4 => CursorStyle::Underline(false),
|
||||
// Ps = 5 ⇒ blinking bar
|
||||
5 => CursorStyle::Bar(true),
|
||||
// Ps = 6 ⇒ steady bar
|
||||
6 => CursorStyle::Bar(false),
|
||||
// Ps = 1 ⇒ blinking block (default)
|
||||
_ => CursorStyle::Block(true),
|
||||
};
|
||||
(Some(ControlSequence::SetCursorStyle(style)), true)
|
||||
}
|
||||
// CSI Ps " q Select character protection attribute
|
||||
('q', '"') => todo!(),
|
||||
// CSI Ps q Load LEDs
|
||||
('q', '\0') => todo!(),
|
||||
// CSI Pt ; Pl ; Pb ; Pr ; Pm $ r
|
||||
// Change Attributes in Rectangular Area
|
||||
('r', '$') => todo!(),
|
||||
// CSI Ps ; Ps r Set Scrolling Region [top;bottom]
|
||||
('r', '\0') => (
|
||||
Some(ControlSequence::SetScrollingRegion(
|
||||
args.get_or(0, 1),
|
||||
args.get_or(1, 1),
|
||||
)),
|
||||
true,
|
||||
),
|
||||
// CSI s
|
||||
// CSI Pl ; Pr s
|
||||
('s', '\0') => todo!(),
|
||||
// CSI Ps ; Ps ; Ps t
|
||||
// TODO
|
||||
('t', '\0') => (None, true),
|
||||
// CSI Ps SP t
|
||||
('t', ' ') => todo!(),
|
||||
// CSI Pt ; Pl ; Pb ; Pr ; Pm $ t
|
||||
('t', '$') => todo!(),
|
||||
// CSI u
|
||||
('u', '\0') => todo!(),
|
||||
// CSI Ps SP u
|
||||
('u', ' ') => todo!(),
|
||||
// CSI Pt ; Pl ; Pb ; Pr ; Pp ; Pt ; Pl ; Pp $ v
|
||||
('v', '$') => todo!(),
|
||||
// CSI Ps $ w
|
||||
('w', '$') => todo!(),
|
||||
// CSI Pt ; Pl ; Pb ; Pr ' w
|
||||
('w', '\'') => todo!(),
|
||||
// CSI Ps x
|
||||
('x', '\0') => todo!(),
|
||||
// CSI Ps * x
|
||||
('x', '*') => todo!(),
|
||||
// CSI Pc ; Pt ; Pl ; Pb ; Pr $ x
|
||||
('x', '$') => todo!(),
|
||||
// CSI Ps # y
|
||||
('y', '#') => todo!(),
|
||||
// CSI Pi ; Pg ; Pt; Pl ; Pb ; Pr * y
|
||||
('y', '*') => todo!(),
|
||||
// CSI Ps ; Pu ' z
|
||||
('z', '\'') => todo!(),
|
||||
// CSI Pt ; Pl ; Pb ; Pr $ z
|
||||
('z', '$') => todo!(),
|
||||
// CSI Pm ' {
|
||||
('{', '\'') => todo!(),
|
||||
// CSI Pm # {
|
||||
('{', '#') => todo!(),
|
||||
// CSI Pt ; Pl ; Pb ; Pr $ {
|
||||
('{', '$') => todo!(),
|
||||
// CSI Pt ; Pl ; Pb ; Pr # |
|
||||
('|', '#') => todo!(),
|
||||
// CSI Ps $ |
|
||||
('|', '$') => todo!(),
|
||||
// CSI Ps ' |
|
||||
('|', '\'') => todo!(),
|
||||
// CSI Ps * |
|
||||
('|', '*') => todo!(),
|
||||
// CSI Ps ; Pf ; Pb , |
|
||||
('|', ',') => todo!(),
|
||||
// CSI Ps ; Pf ; Pb , }
|
||||
('}', ',') => todo!(),
|
||||
// CSI Ps ' }
|
||||
('}', '\'') => todo!(),
|
||||
// CSI Ps $ }
|
||||
('}', '$') => todo!(),
|
||||
// CSI Ps ' ~
|
||||
('~', '\'') => todo!(),
|
||||
// CSI Ps $ ~
|
||||
('~', '$') => todo!(),
|
||||
_ => (None, true),
|
||||
},
|
||||
Self::PrefixQuestion(args) => match (ch, args.suffix) {
|
||||
(_, _) if args.push(ch) => (None, false),
|
||||
// CSI ? Ps J Erase in Display
|
||||
('J', '\0') => todo!("CSI ? Ps J"),
|
||||
// CSI ? Ps K Erase in Line
|
||||
('K', '\0') => todo!("CSI ? Ps K"),
|
||||
// CSI ? Pi ; Pa ; Pv S
|
||||
('S', '\0') => todo!("CSI ? Pi ; Pa ; Pv S"),
|
||||
// CSI ? 5 W
|
||||
('W', '\0') if args.get(0) == 5 => todo!("CSI ? 5 W"),
|
||||
// CSI ? Pp g
|
||||
('g', '\0') => todo!("CSI ? Pp g"),
|
||||
// CSI ? Pm h DEC Private Mode Set
|
||||
('h', '\0') => (Self::handle_decset(args.get(0), true), true),
|
||||
// CSI ? Ps i
|
||||
('i', '\0') => todo!("CSI ? Ps i"),
|
||||
// CSI ? Pm l
|
||||
('l', '\0') => (Self::handle_decset(args.get(0), false), true),
|
||||
// CSI ? Pp m Query key modifier options
|
||||
('m', '\0') => {
|
||||
let query = match args.get(0) {
|
||||
0 => QueryKeyModifiers::ModifyKeyboard,
|
||||
1 => QueryKeyModifiers::ModifyCursorKeys,
|
||||
2 => QueryKeyModifiers::ModifyFunctionKeys,
|
||||
3 => QueryKeyModifiers::ModifyKeypadKeys,
|
||||
4 => QueryKeyModifiers::ModifyOtherKeys,
|
||||
6 => QueryKeyModifiers::ModifyModifierKeys,
|
||||
7 => QueryKeyModifiers::ModifySpecialKeys,
|
||||
_ => return (None, true),
|
||||
};
|
||||
|
||||
(Some(ControlSequence::QueryKeyModifiers(query)), true)
|
||||
}
|
||||
// CSI ? Ps n
|
||||
('n', '\0') => todo!("CSI ? Ps n"),
|
||||
// CSI ? Ps $ p Request DEC private mode
|
||||
('p', '$') => (Some(ControlSequence::Decrqm), true),
|
||||
// CSI ? Pm r
|
||||
('r', '\0') => todo!("CSI ? Pm r"),
|
||||
// CSI ? Pm s
|
||||
('s', '\0') => todo!("CSI ? Pm s"),
|
||||
|
||||
_ => (None, true),
|
||||
},
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_sgr(args: &CsiArgs) -> Option<ControlSequence> {
|
||||
let pm = args.get(0);
|
||||
let ps = args.get(1);
|
||||
let attr = match (pm, ps) {
|
||||
// Ps = 0 ⇒ Normal (default)
|
||||
(0, _) => CharacterAttribute::Reset,
|
||||
// Ps = 1 ⇒ Bold
|
||||
(1, _) => CharacterAttribute::Bold,
|
||||
// Ps = 2 ⇒ Faint
|
||||
(2, _) => CharacterAttribute::Faint,
|
||||
// Ps = 3 ⇒ Italicized
|
||||
(3, _) => CharacterAttribute::Italicized(true),
|
||||
// Ps = 4 ⇒ Underlined
|
||||
(4, _) => CharacterAttribute::Underlined(true),
|
||||
// Ps = 5 ⇒ Blink
|
||||
(5, _) => CharacterAttribute::Blinking(true),
|
||||
// Ps = 7 ⇒ Inverse
|
||||
(7, _) => CharacterAttribute::Inverse(true),
|
||||
// Ps = 8 ⇒ Invisible
|
||||
(8, _) => CharacterAttribute::Hidden(true),
|
||||
// Ps = 9 ⇒ Crossed-out characters
|
||||
(9, _) => CharacterAttribute::CrossedOut(true),
|
||||
// Ps = 2 1 ⇒ Doubly underlined
|
||||
(21, _) => return None,
|
||||
// Ps = 2 2 ⇒ Normal (neither bold nor faint)
|
||||
(22, _) => CharacterAttribute::Normal,
|
||||
// Ps = 2 3 ⇒ Not italicized
|
||||
(23, _) => CharacterAttribute::Italicized(false),
|
||||
// Ps = 2 4 ⇒ Not underlined
|
||||
(24, _) => CharacterAttribute::Underlined(false),
|
||||
// Ps = 2 5 ⇒ Steady (not blinking)
|
||||
(25, _) => CharacterAttribute::Blinking(false),
|
||||
// Ps = 2 7 ⇒ Positive (not inverse)
|
||||
(27, _) => CharacterAttribute::Inverse(false),
|
||||
// Ps = 2 8 ⇒ Visible (not hidden)
|
||||
(28, _) => CharacterAttribute::Hidden(false),
|
||||
// Ps = 2 9 ⇒ Not crossed out
|
||||
(29, _) => CharacterAttribute::CrossedOut(false),
|
||||
// Ps = 3 x ⇒ Set foreground color
|
||||
(30..=37, _) => CharacterAttribute::FgIndex(Some(pm as u8 - 30)),
|
||||
(39, _) => CharacterAttribute::FgIndex(None),
|
||||
// Ps = 4 x ⇒ Set background color
|
||||
(40..=47, _) => CharacterAttribute::BgIndex(Some(pm as u8 - 30)),
|
||||
(49, _) => CharacterAttribute::BgIndex(None),
|
||||
// Ps = 9 x ⇒ Set foreground color
|
||||
(90..=97, _) => CharacterAttribute::FgIndex(Some(pm as u8 - 90)),
|
||||
// Ps = 1 0 x ⇒ Set background color
|
||||
(100..=107, _) => CharacterAttribute::BgIndex(Some(pm as u8 - 100)),
|
||||
// Ps = 38 ; 2 ; Pi ; Pr ; Pg ; Pb ⇒ Set foreground color (RGB)
|
||||
// 38 ; 2 ; Pr ; Pg ; Pb
|
||||
(38, 2) => match args.args.len() {
|
||||
// 38 ; 2 ; Pi ; Pr ; Pg ; Pb
|
||||
6 => CharacterAttribute::ForegroundRgb(
|
||||
args.get(3) as u8,
|
||||
args.get(4) as u8,
|
||||
args.get(5) as u8,
|
||||
),
|
||||
// 38 ; 2 ; Pr ; Pg ; Pb
|
||||
5 => CharacterAttribute::ForegroundRgb(
|
||||
args.get(2) as u8,
|
||||
args.get(3) as u8,
|
||||
args.get(4) as u8,
|
||||
),
|
||||
_ => return None,
|
||||
},
|
||||
// Ps = 48 ; 2 ; Pi ; Pr ; Pg ; Pb ⇒ Set background color (RGB)
|
||||
// 48 ; 2 ; Pr ; Pg ; Pb
|
||||
(48, 2) => match args.args.len() {
|
||||
// 48 ; 2 ; Pi ; Pr ; Pg ; Pb
|
||||
6 => CharacterAttribute::BackgroundRgb(
|
||||
args.get(3) as u8,
|
||||
args.get(4) as u8,
|
||||
args.get(5) as u8,
|
||||
),
|
||||
// 48 ; 2 ; Pr ; Pg ; Pb
|
||||
5 => CharacterAttribute::BackgroundRgb(
|
||||
args.get(2) as u8,
|
||||
args.get(3) as u8,
|
||||
args.get(4) as u8,
|
||||
),
|
||||
_ => return None,
|
||||
},
|
||||
(_, _) => return None,
|
||||
};
|
||||
|
||||
Some(ControlSequence::SetCharacterAttribute(attr))
|
||||
}
|
||||
|
||||
fn handle_decset(pm: u32, set: bool) -> Option<ControlSequence> {
|
||||
match pm {
|
||||
// Application cursor keys
|
||||
1 => Some(ControlSequence::SetApplicationCursor(set)),
|
||||
// Start blinking cursor TODO
|
||||
12 => None,
|
||||
// Show cursor TODO
|
||||
25 => None,
|
||||
// TODO Send Mouse X & Y on button press
|
||||
1000 => None,
|
||||
// TODO Use Cell Motion Mouse Tracking
|
||||
1002 => None,
|
||||
// TODO Send FocusIn/FocusOut events
|
||||
1004 => None,
|
||||
// TODO Enable SGR Mouse Mode
|
||||
1006 => None,
|
||||
// TODO Use Alternate Screen Buffer
|
||||
1047 => None,
|
||||
// TODO Save cursor as in DECSC
|
||||
1048 => todo!(),
|
||||
// TODO Save cursor as in DECSC and switch to alternate screen buffer, clearing it
|
||||
// first
|
||||
1049 => Some(ControlSequence::SetAlternateMode(set)),
|
||||
// Ps = 2 0 0 4 ⇒ Set bracketed paste mode
|
||||
2004 => Some(ControlSequence::SetBracketedPasteMode(set)),
|
||||
_ => todo!("Unhandled DECSET {pm}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum OscState {
|
||||
Ps(u32),
|
||||
Pt(u32, String),
|
||||
PtEsc(u32, String),
|
||||
}
|
||||
|
||||
impl OscState {
|
||||
const fn new() -> Self {
|
||||
Self::Ps(0)
|
||||
}
|
||||
|
||||
fn push(&mut self, ch: char) -> (Option<ControlSequence>, bool) {
|
||||
match self {
|
||||
Self::Ps(ps) if ch.is_ascii_digit() => {
|
||||
*ps *= 10;
|
||||
*ps += (ch as u8 - b'0') as u32;
|
||||
(None, false)
|
||||
}
|
||||
Self::Ps(ps) if ch == ';' => {
|
||||
*self = Self::Pt(*ps, String::new());
|
||||
(None, false)
|
||||
}
|
||||
// OSC Ps ; Pt BEL
|
||||
Self::Pt(ps, pt) if ch == '\x07' => {
|
||||
let pt = core::mem::take(pt);
|
||||
let control = Self::handle_sequence(*ps, pt);
|
||||
(control, true)
|
||||
}
|
||||
// OSC Ps ; Pt ST (Where ST = ESC \)
|
||||
Self::Pt(ps, pt) if ch == '\x1B' => todo!(),
|
||||
// OSC Ps ; Pt ST
|
||||
Self::PtEsc(ps, pt) if ch == '\\' => todo!(),
|
||||
Self::Pt(_, pt) => {
|
||||
pt.push(ch);
|
||||
(None, false)
|
||||
}
|
||||
_ => (None, true),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_sequence(ps: u32, pt: String) -> Option<ControlSequence> {
|
||||
match ps {
|
||||
// Ps = 0 ⇒ Change Icon Name and Window Title to Pt
|
||||
0 => Some(ControlSequence::SetWindowParameter(
|
||||
SetWindowParameter::Both,
|
||||
pt,
|
||||
)),
|
||||
// Ps = 1 ⇒ Change Icon Name to Pt
|
||||
1 => Some(ControlSequence::SetWindowParameter(
|
||||
SetWindowParameter::Icon,
|
||||
pt,
|
||||
)),
|
||||
// Ps = 2 ⇒ Change Window Title to Pt
|
||||
2 => Some(ControlSequence::SetWindowParameter(
|
||||
SetWindowParameter::Title,
|
||||
pt,
|
||||
)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fs;
|
||||
|
||||
use crate::escape::{ControlSequence, EscapeParser, SetWindowParameter, TerminalInput};
|
||||
|
||||
fn push_str(seq: &mut Vec<TerminalInput>, s: &str) {
|
||||
for ch in s.chars() {
|
||||
seq.push(TerminalInput::Character(ch));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tests() {
|
||||
let mut parser = EscapeParser::new();
|
||||
let input = fs::read_to_string("tests/test-01").unwrap();
|
||||
let mut events = vec![];
|
||||
|
||||
let mut expect = vec![];
|
||||
push_str(&mut expect, "Script started on 2025-06-22 14:45:00+03:00 ");
|
||||
push_str(
|
||||
&mut expect,
|
||||
"[COMMAND=\"/bin/bash\" TERM=\"xterm-256color\" ",
|
||||
);
|
||||
push_str(
|
||||
&mut expect,
|
||||
"TTY=\"/dev/pts/8\" COLUMNS=\"112\" LINES=\"47\"]",
|
||||
);
|
||||
expect.push(TerminalInput::NewLine);
|
||||
expect.push(TerminalInput::Control(ControlSequence::SetWindowParameter(
|
||||
SetWindowParameter::Both,
|
||||
"alnyan@zeus:~/build/ygg/userspace/graphics/term".into(),
|
||||
)));
|
||||
expect.push(TerminalInput::Control(
|
||||
ControlSequence::SetBracketedPasteMode(true),
|
||||
));
|
||||
push_str(&mut expect, "[alnyan@zeus term]$ ");
|
||||
expect.push(TerminalInput::Control(
|
||||
ControlSequence::SetBracketedPasteMode(false),
|
||||
));
|
||||
expect.push(TerminalInput::CarriageReturn);
|
||||
expect.push(TerminalInput::CarriageReturn);
|
||||
expect.push(TerminalInput::NewLine);
|
||||
push_str(&mut expect, "exit");
|
||||
expect.push(TerminalInput::CarriageReturn);
|
||||
expect.push(TerminalInput::NewLine);
|
||||
expect.push(TerminalInput::NewLine);
|
||||
push_str(&mut expect, "Script done on 2025-06-22 14:45:05+03:00 ");
|
||||
push_str(&mut expect, "[COMMAND_EXIT_CODE=\"0\"]");
|
||||
expect.push(TerminalInput::NewLine);
|
||||
|
||||
for ch in input.chars() {
|
||||
if let Some(event) = parser.push(ch) {
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
pretty_assertions::assert_eq!(&expect, &events);
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,8 @@ use unix as sys;
|
||||
#[cfg(any(feature = "tui", rust_analyzer))]
|
||||
mod tui;
|
||||
|
||||
pub mod escape;
|
||||
|
||||
pub trait RawTerminal {
|
||||
fn raw_enter_alternate_mode(&mut self) -> io::Result<()>;
|
||||
fn raw_leave_alternate_mode(&mut self) -> io::Result<()>;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
Script started on 2025-06-22 14:45:00+03:00 [COMMAND="/bin/bash" TERM="xterm-256color" TTY="/dev/pts/8" COLUMNS="112" LINES="47"]
|
||||
]0;alnyan@zeus:~/build/ygg/userspace/graphics/term[?2004h[alnyan@zeus term]$ [?2004l
|
||||
exit
|
||||
|
||||
Script done on 2025-06-22 14:45:05+03:00 [COMMAND_EXIT_CODE="0"]
|
||||
Reference in New Issue
Block a user