use eframe::egui::{self, Align2, Color32, FontId, Pos2, Rect, Stroke, Vec2};
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use rcmd_tui::app::{App, Exec, SubshellSession, SubshellStep};
use crate::grid::{Metrics, Palette};
use crate::keys::Input;
const CTRL_O: u8 = 0x0F;
pub struct TerminalPane {
parser: vt100::Parser,
session: Option<SubshellSession>,
size: (u16, u16),
fed_command: bool,
moving: bool,
}
impl TerminalPane {
pub fn new(cols: u16, rows: u16) -> Self {
Self {
parser: vt100::Parser::new(rows, cols, 0),
session: None,
size: (cols, rows),
fed_command: false,
moving: false,
}
}
pub fn open(&mut self, app: &mut App, exec: Exec, cols: u16, rows: u16) -> bool {
let fed_command = matches!(exec, Exec::Command(_));
let Some(session) = app.begin_subshell(exec) else {
return false;
};
self.resize(app, cols, rows);
self.parser.process(&app.take_subshell_output());
self.session = Some(session);
self.fed_command = fed_command;
self.moving = true;
true
}
pub fn is_open(&self) -> bool {
self.session.is_some()
}
pub fn close(&mut self) {
self.session = None;
}
pub fn step(&mut self, app: &mut App) -> bool {
self.moving = false;
let Some(session) = self.session.as_mut() else {
return false;
};
match app.step_subshell(session) {
SubshellStep::Output(bytes) => {
self.parser.process(&bytes);
self.moving = true;
true
}
SubshellStep::Waiting => true,
SubshellStep::Done => !self.fed_command && app.subshell_alive(),
}
}
pub fn repaint_after(&self) -> std::time::Duration {
std::time::Duration::from_millis(if self.moving { 16 } else { 100 })
}
pub fn resize(&mut self, app: &mut App, cols: u16, rows: u16) {
if (cols, rows) == self.size {
return;
}
self.size = (cols, rows);
self.parser.set_size(rows, cols);
app.resize_subshell(cols, rows);
}
pub fn feed(&mut self, app: &mut App, input: &[Input]) -> bool {
let mut bytes = Vec::new();
for event in input {
let Input::Key(key) = event else { continue };
encode(key, self.parser.screen().application_cursor(), &mut bytes);
}
match bytes.iter().position(|&b| b == CTRL_O) {
Some(at) => {
app.feed_subshell(&bytes[..at]);
false
}
None => {
app.feed_subshell(&bytes);
true
}
}
}
pub fn paint(
&self,
painter: &egui::Painter,
origin: Pos2,
metrics: Metrics,
font: &FontId,
palette: Palette,
) {
let screen = self.parser.screen();
let (rows, cols) = screen.size();
let cell_rect = |col: u16, row: u16| {
Rect::from_min_size(
Pos2::new(
origin.x + col as f32 * metrics.width,
origin.y + row as f32 * metrics.height,
),
Vec2::new(metrics.width, metrics.height),
)
};
for row in 0..rows {
let mut col = 0;
while col < cols {
let bg = self.colors(screen.cell(row, col)).1;
let mut end = col + 1;
while end < cols && self.colors(screen.cell(row, end)).1 == bg {
end += 1;
}
if bg != palette.bg {
painter.rect_filled(
cell_rect(col, row).union(cell_rect(end - 1, row)),
0.0,
bg,
);
}
col = end;
}
for col in 0..cols {
let Some(cell) = screen.cell(row, col) else {
continue;
};
if cell.is_wide_continuation() {
continue;
}
let contents = cell.contents();
if contents.trim().is_empty() && !cell.underline() {
continue;
}
let (fg, _) = self.colors(Some(cell));
let rect = cell_rect(col, row);
if !contents.trim().is_empty() {
painter.text(
rect.left_top(),
Align2::LEFT_TOP,
&contents,
font.clone(),
fg,
);
if cell.bold() {
painter.text(
rect.left_top() + Vec2::new(0.6, 0.0),
Align2::LEFT_TOP,
&contents,
font.clone(),
fg,
);
}
}
if cell.underline() {
painter.hline(rect.x_range(), rect.bottom() - 1.0, Stroke::new(1.0, fg));
}
}
}
if !screen.hide_cursor() {
let (row, col) = screen.cursor_position();
if row < rows && col < cols {
let rect = cell_rect(col, row);
let (fg, bg) = self.colors(screen.cell(row, col));
painter.rect_filled(rect, 0.0, fg);
if let Some(cell) = screen.cell(row, col)
&& !cell.contents().trim().is_empty()
{
painter.text(
rect.left_top(),
Align2::LEFT_TOP,
cell.contents(),
font.clone(),
bg,
);
}
}
}
}
fn colors(&self, cell: Option<&vt100::Cell>) -> (Color32, Color32) {
let default = default_palette();
let (mut fg, mut bg) = match cell {
Some(cell) => (
to_color32(cell.fgcolor(), default.fg),
to_color32(cell.bgcolor(), default.bg),
),
None => (default.fg, default.bg),
};
if cell.is_some_and(vt100::Cell::inverse) {
std::mem::swap(&mut fg, &mut bg);
}
(fg, bg)
}
}
fn default_palette() -> Palette {
Palette::default()
}
fn to_color32(color: vt100::Color, default: Color32) -> Color32 {
match color {
vt100::Color::Default => default,
vt100::Color::Idx(i) => crate::grid::indexed_color(i),
vt100::Color::Rgb(r, g, b) => Color32::from_rgb(r, g, b),
}
}
fn encode(key: &KeyEvent, application_cursor: bool, out: &mut Vec<u8>) {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
let alt = key.modifiers.contains(KeyModifiers::ALT);
if alt {
out.push(0x1b);
}
let arrow = |c: u8, out: &mut Vec<u8>| {
out.extend_from_slice(if application_cursor {
b"\x1bO"
} else {
b"\x1b["
});
out.push(c);
};
match key.code {
KeyCode::Char(c) => match ctrl {
true => match c {
' ' => out.push(0),
'?' => out.push(0x7f),
c if c.is_ascii() => out.push((c as u8) & 0x1f),
c => push_char(c, out),
},
false => push_char(c, out),
},
KeyCode::Enter => out.push(b'\r'),
KeyCode::Tab => out.push(b'\t'),
KeyCode::BackTab => out.extend_from_slice(b"\x1b[Z"),
KeyCode::Backspace => out.push(0x7f),
KeyCode::Esc => out.push(0x1b),
KeyCode::Up => arrow(b'A', out),
KeyCode::Down => arrow(b'B', out),
KeyCode::Right => arrow(b'C', out),
KeyCode::Left => arrow(b'D', out),
KeyCode::Home => arrow(b'H', out),
KeyCode::End => arrow(b'F', out),
KeyCode::Insert => out.extend_from_slice(b"\x1b[2~"),
KeyCode::Delete => out.extend_from_slice(b"\x1b[3~"),
KeyCode::PageUp => out.extend_from_slice(b"\x1b[5~"),
KeyCode::PageDown => out.extend_from_slice(b"\x1b[6~"),
KeyCode::F(n) => match n {
1..=4 => {
out.extend_from_slice(b"\x1bO");
out.push(b'P' + (n - 1));
}
5..=12 => {
const TAIL: [&[u8]; 8] = [
b"15~", b"17~", b"18~", b"19~", b"20~", b"21~", b"23~", b"24~",
];
out.extend_from_slice(b"\x1b[");
out.extend_from_slice(TAIL[(n - 5) as usize]);
}
_ => {}
},
_ => {}
}
}
fn push_char(c: char, out: &mut Vec<u8>) {
let mut buf = [0u8; 4];
out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
}
#[cfg(test)]
mod tests {
use super::*;
fn bytes(code: KeyCode, modifiers: KeyModifiers, application_cursor: bool) -> Vec<u8> {
let mut out = Vec::new();
encode(
&KeyEvent::new(code, modifiers),
application_cursor,
&mut out,
);
out
}
#[test]
fn a_letter_is_itself_and_ctrl_strips_it_down() {
assert_eq!(bytes(KeyCode::Char('a'), KeyModifiers::NONE, false), b"a");
assert_eq!(bytes(KeyCode::Char('c'), KeyModifiers::CONTROL, false), [3]);
assert_eq!(bytes(KeyCode::Char('d'), KeyModifiers::CONTROL, false), [4]);
assert_eq!(
bytes(KeyCode::Char('o'), KeyModifiers::CONTROL, false),
[CTRL_O]
);
}
#[test]
fn alt_is_an_escape_prefix() {
assert_eq!(
bytes(KeyCode::Char('f'), KeyModifiers::ALT, false),
b"\x1bf"
);
}
#[test]
fn the_arrows_follow_the_cursor_mode() {
assert_eq!(bytes(KeyCode::Up, KeyModifiers::NONE, false), b"\x1b[A");
assert_eq!(bytes(KeyCode::Up, KeyModifiers::NONE, true), b"\x1bOA");
assert_eq!(bytes(KeyCode::Home, KeyModifiers::NONE, false), b"\x1b[H");
}
#[test]
fn the_editing_keys_are_the_ones_terminfo_expects() {
assert_eq!(bytes(KeyCode::Enter, KeyModifiers::NONE, false), b"\r");
assert_eq!(bytes(KeyCode::Backspace, KeyModifiers::NONE, false), [0x7f]);
assert_eq!(
bytes(KeyCode::Delete, KeyModifiers::NONE, false),
b"\x1b[3~"
);
assert_eq!(bytes(KeyCode::F(1), KeyModifiers::NONE, false), b"\x1bOP");
assert_eq!(bytes(KeyCode::F(5), KeyModifiers::NONE, false), b"\x1b[15~");
assert_eq!(
bytes(KeyCode::F(12), KeyModifiers::NONE, false),
b"\x1b[24~"
);
}
#[test]
fn a_shell_screen_comes_out_of_the_parser() {
let mut parser = vt100::Parser::new(3, 20, 0);
parser.process(b"\x1b[31mred\x1b[0m plain");
let screen = parser.screen();
assert_eq!(screen.cell(0, 0).unwrap().contents(), "r");
assert_eq!(screen.cell(0, 0).unwrap().fgcolor(), vt100::Color::Idx(1));
assert_eq!(screen.cell(0, 4).unwrap().fgcolor(), vt100::Color::Default);
assert!(!screen.application_cursor());
parser.process(b"\x1b[?1h");
assert!(parser.screen().application_cursor());
}
}