diff --git a/userspace/Cargo.lock b/userspace/Cargo.lock index e6c55814..52ac7140 100644 --- a/userspace/Cargo.lock +++ b/userspace/Cargo.lock @@ -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" diff --git a/userspace/etc/Makefile b/userspace/etc/Makefile new file mode 100644 index 00000000..9d280ef1 --- /dev/null +++ b/userspace/etc/Makefile @@ -0,0 +1,2 @@ +all: + test diff --git a/userspace/graphics/term/Cargo.toml b/userspace/graphics/term/Cargo.toml index ec094e1e..d3cdc7a1 100644 --- a/userspace/graphics/term/Cargo.toml +++ b/userspace/graphics/term/Cargo.toml @@ -7,6 +7,7 @@ authors = ["Mark Poliakov "] [dependencies] libpsf.workspace = true libcolors = { workspace = true, features = ["client"] } +libterm.workspace = true logsink.workspace = true cross.workspace = true diff --git a/userspace/graphics/term/src/config.rs b/userspace/graphics/term/src/config.rs index 927e272d..7b25ac90 100644 --- a/userspace/graphics/term/src/config.rs +++ b/userspace/graphics/term/src/config.rs @@ -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, + pub env: HashMap, +} + #[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>(path: P) -> Self { - Self::load(path).unwrap_or_default() + pub fn load_or_default>(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>(path: P) -> Option { diff --git a/userspace/graphics/term/src/main.rs b/userspace/graphics/term/src/main.rs index af25c0e2..7650b5f8 100644 --- a/userspace/graphics/term/src/main.rs +++ b/userspace/graphics/term/src/main.rs @@ -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, +} + 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 = LazyLock::new(|| Config::load_or_default("/etc/term.conf")); +static CONFIG: LazyLock = LazyLock::new(|| { + let args = Args::parse(); + Config::load_or_default("/etc/term.conf", args) +}); fn run() -> Result { LazyLock::force(&CONFIG); diff --git a/userspace/graphics/term/src/state.rs b/userspace/graphics/term/src/state.rs index 800d720c..f24ce7af 100644 --- a/userspace/graphics/term/src/state.rs +++ b/userspace/graphics/term/src/state.rs @@ -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, - + esc_parser: EscapeParser, + // esc_state: EscapeState, + // esc_args: Vec, 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) { diff --git a/userspace/lib/cross/src/sys/unix/mod.rs b/userspace/lib/cross/src/sys/unix/mod.rs index 59f5c33d..259d7160 100644 --- a/userspace/lib/cross/src/sys/unix/mod.rs +++ b/userspace/lib/cross/src/sys/unix/mod.rs @@ -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(()) + }) + }) } } diff --git a/userspace/lib/cross/src/sys/yggdrasil/mod.rs b/userspace/lib/cross/src/sys/yggdrasil/mod.rs index 273688ac..993df51b 100644 --- a/userspace/lib/cross/src/sys/yggdrasil/mod.rs +++ b/userspace/lib/cross/src/sys/yggdrasil/mod.rs @@ -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) }) } } diff --git a/userspace/lib/libterm/Cargo.toml b/userspace/lib/libterm/Cargo.toml index 3c6dd4a7..21ce87dc 100644 --- a/userspace/lib/libterm/Cargo.toml +++ b/userspace/lib/libterm/Cargo.toml @@ -18,6 +18,7 @@ libc = "0.2.150" yggdrasil-rt.workspace = true [dev-dependencies] +pretty_assertions = "1.4.1" tui.workspace = true [features] diff --git a/userspace/lib/libterm/src/escape.rs b/userspace/lib/libterm/src/escape.rs new file mode 100644 index 00000000..5f0f433b --- /dev/null +++ b/userspace/lib/libterm/src/escape.rs @@ -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), + BgIndex(Option), + 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 { + 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 { + 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, 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, + 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, 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 { + 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 { + 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, 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 { + 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, 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); + } +} diff --git a/userspace/lib/libterm/src/lib.rs b/userspace/lib/libterm/src/lib.rs index 59124921..728c607b 100644 --- a/userspace/lib/libterm/src/lib.rs +++ b/userspace/lib/libterm/src/lib.rs @@ -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<()>; diff --git a/userspace/lib/libterm/tests/test-01 b/userspace/lib/libterm/tests/test-01 new file mode 100644 index 00000000..902ddc7a --- /dev/null +++ b/userspace/lib/libterm/tests/test-01 @@ -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"]