WIP: NEW TERMINAL LIB

This commit is contained in:
2025-06-23 08:56:24 +03:00
parent e3c75903ff
commit b68a129d37
12 changed files with 1463 additions and 325 deletions
+24
View File
@@ -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"
+2
View File
@@ -0,0 +1,2 @@
all:
test
+1
View File
@@ -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
+25 -2
View File
@@ -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> {
+42 -20
View File
@@ -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);
+453 -300
View File
@@ -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) {
+8 -2
View File
@@ -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(())
})
})
}
}
+2 -1
View File
@@ -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) })
}
}
+1
View File
@@ -18,6 +18,7 @@ libc = "0.2.150"
yggdrasil-rt.workspace = true
[dev-dependencies]
pretty_assertions = "1.4.1"
tui.workspace = true
[features]
+898
View File
@@ -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);
}
}
+2
View File
@@ -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<()>;
+5
View File
@@ -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"]