xei-editor 0.2.9

xei (晴) — a modern terminal text editor in Rust with Vim-modal editing, syntax highlighting, file explorer, built-in terminal, and theming
use std::io::{Read, Write};
use std::path::PathBuf;
use std::process::{Child, Command, Stdio};
use std::sync::mpsc::{self, Receiver, TryRecvError};
use std::thread;

pub struct Terminal {
    pub open: bool,
    rows: Vec<Vec<Cell>>,
    cursor_row: usize,
    cursor_col: usize,
    cols: u16,
    rows_count: u16,
    scroll_offset: usize,
    child: Option<Child>,
    stdin: Option<Box<dyn Write + Send>>,
    rx: Option<Receiver<Vec<u8>>>,
    scrollback: Vec<Vec<Cell>>,
    fg: Color,
    bg: Color,
    bold: bool,
    pub started: bool,
}

#[derive(Clone)]
struct Cell {
    ch: char,
    fg: Option<Color>,
    bg: Option<Color>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Color {
    Default, Black, Red, Green, Yellow, Blue, Magenta, Cyan, White,
    BrightBlack, BrightRed, BrightGreen, BrightYellow,
    BrightBlue, BrightMagenta, BrightCyan, BrightWhite,
}

impl Color {
    fn to_ratatui(self) -> ratatui::style::Color {
        match self {
            Color::Default => ratatui::style::Color::Reset,
            Color::Black => ratatui::style::Color::Black,
            Color::Red => ratatui::style::Color::Red,
            Color::Green => ratatui::style::Color::Green,
            Color::Yellow => ratatui::style::Color::Yellow,
            Color::Blue => ratatui::style::Color::Blue,
            Color::Magenta => ratatui::style::Color::Magenta,
            Color::Cyan => ratatui::style::Color::Cyan,
            Color::White => ratatui::style::Color::White,
            Color::BrightBlack => ratatui::style::Color::Gray,
            Color::BrightRed => ratatui::style::Color::LightRed,
            Color::BrightGreen => ratatui::style::Color::LightGreen,
            Color::BrightYellow => ratatui::style::Color::LightYellow,
            Color::BrightBlue => ratatui::style::Color::LightBlue,
            Color::BrightMagenta => ratatui::style::Color::LightMagenta,
            Color::BrightCyan => ratatui::style::Color::LightCyan,
            Color::BrightWhite => ratatui::style::Color::White,
        }
    }
}

impl Default for Terminal {
    fn default() -> Self {
        let (cols, rows) = (80, 24);
        Self {
            open: false,
            rows: vec![vec![Cell { ch: ' ', fg: None, bg: None }; cols as usize]; rows as usize],
            cursor_row: 0, cursor_col: 0, cols, rows_count: rows, scroll_offset: 0,
            child: None, stdin: None, rx: None, scrollback: Vec::new(),
            fg: Color::Default, bg: Color::Default, bold: false, started: false,
        }
    }
}

impl Terminal {
    pub fn new() -> Self { Self::default() }

    pub fn start(&mut self, anchor: Option<&PathBuf>) {
        if self.started { return; }

        let cwd = anchor
            .and_then(|p| p.parent().map(|p| p.to_path_buf()))
            .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());

        let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".to_string());

        let mut child = match Command::new("/usr/bin/script")
            .args(["-q", "/dev/null", &shell])
            .stdin(Stdio::piped())
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .current_dir(&cwd)
            .env("TERM", "xterm-256color")
            .spawn()
        {
            Ok(c) => c,
            Err(_) => return,
        };

        let stdin_writer = child.stdin.take().unwrap();
        let mut stdout_reader = child.stdout.take().unwrap();

        let (tx, rx) = mpsc::channel::<Vec<u8>>();
        thread::spawn(move || {
            let mut buf = [0u8; 4096];
            loop {
                match stdout_reader.read(&mut buf) {
                    Ok(0) | Err(_) => break,
                    Ok(n) => {
                        if tx.send(buf[..n].to_vec()).is_err() { break; }
                    }
                }
            }
        });

        self.stdin = Some(Box::new(stdin_writer));
        self.rx = Some(rx);
        self.child = Some(child);
        self.open = true;
        self.started = true;
    }

    pub fn shutdown(&mut self) {
        if let Some(ref mut stdin) = self.stdin {
            let _ = stdin.write_all(b"exit\n");
            let _ = stdin.flush();
        }
        if let Some(mut child) = self.child.take() {
            let _ = child.kill();
            let _ = child.wait();
        }
        self.stdin = None;
        self.rx = None;
        self.started = false;
        self.open = false;
        self.rows = vec![vec![Cell { ch: ' ', fg: None, bg: None }; self.cols as usize]; self.rows_count as usize];
        self.cursor_row = 0;
        self.cursor_col = 0;
        self.scrollback.clear();
        self.fg = Color::Default;
        self.bg = Color::Default;
        self.bold = false;
    }

    pub fn write_input(&mut self, bytes: &[u8]) {
        if let Some(ref mut stdin) = self.stdin {
            let _ = stdin.write_all(bytes);
            let _ = stdin.flush();
        }
    }

    pub fn poll(&mut self) {
        let data = if let Some(ref rx) = self.rx {
            let mut all = Vec::new();
            loop {
                match rx.try_recv() {
                    Ok(part) => all.extend_from_slice(&part),
                    Err(TryRecvError::Empty) => break,
                    Err(TryRecvError::Disconnected) => break,
                }
            }
            if all.is_empty() { return; }
            all
        } else { return; };
        self.process_output(&data);
    }

    fn process_output(&mut self, data: &[u8]) {
        let mut i = 0;
        while i < data.len() {
            if data[i] == 0x1b && i + 1 < data.len() && data[i + 1] == b'[' {
                i = self.parse_csi(data, i + 2);
            } else if data[i] == b'\n' { self.newline(); i += 1; }
            else if data[i] == b'\r' { self.cursor_col = 0; i += 1; }
            else if data[i] == 0x08 { if self.cursor_col > 0 { self.cursor_col -= 1; } i += 1; }
            else if data[i] == b'\t' {
                self.write_char(' ');
                while self.cursor_col % 4 != 0 && self.cursor_col < self.cols as usize { self.write_char(' '); }
                i += 1;
            }
            else if data[i] == 0x07 { i += 1; }
            else if data[i] >= 0x20 { self.write_char(data[i] as char); i += 1; }
            else { i += 1; }
        }
    }

    fn parse_csi(&mut self, data: &[u8], start: usize) -> usize {
        let mut i = start;
        let mut nums = Vec::new();
        let mut cur = String::new();
        while i < data.len() {
            let b = data[i];
            if b == b'?' || b == b'>' {
                i += 1;
                while i < data.len() && !data[i].is_ascii_alphabetic() && data[i] != b'@' { i += 1; }
                if i < data.len() { i += 1; }
                return i;
            }
            if b.is_ascii_digit() { cur.push(b as char); }
            else if b == b';' { nums.push(cur.parse::<i32>().unwrap_or(0)); cur.clear(); }
            else if b.is_ascii_alphabetic() || b == b'@' || b == b'm' {
                nums.push(cur.parse::<i32>().unwrap_or(0));
                self.apply_csi(b as char, &nums);
                return i + 1;
            } else { return i; }
            i += 1;
        }
        i
    }

    fn apply_csi(&mut self, cmd: char, nums: &[i32]) {
        let n = |i: usize, d: i32| -> i32 { nums.get(i).copied().filter(|&v| v != 0).unwrap_or(d) };
        match cmd {
            'A' => self.cursor_row = self.cursor_row.saturating_sub(n(0,1).max(1) as usize),
            'B' => self.cursor_row = (self.cursor_row + n(0,1).max(1) as usize).min(self.rows_count as usize - 1),
            'C' => self.cursor_col = (self.cursor_col + n(0,1).max(1) as usize).min(self.cols as usize - 1),
            'D' => self.cursor_col = self.cursor_col.saturating_sub(n(0,1).max(1) as usize),
            'H'|'f' => {
                self.cursor_row = (n(0,1).max(1) as usize - 1).min(self.rows_count as usize - 1);
                self.cursor_col = (n(1,1).max(1) as usize - 1).min(self.cols as usize - 1);
            }
            'J' => {
                let m = n(0,0);
                if m == 2 || m == 3 {
                    for row in &mut self.rows { for c in row.iter_mut() { *c = Cell { ch: ' ', fg: None, bg: None }; } }
                    self.cursor_row = 0; self.cursor_col = 0;
                } else if m == 0 {
                    for c in self.cursor_col..self.cols as usize { self.rows[self.cursor_row][c] = Cell { ch: ' ', fg: None, bg: None }; }
                    for r in self.cursor_row+1..self.rows_count as usize { for c in 0..self.cols as usize { self.rows[r][c] = Cell { ch: ' ', fg: None, bg: None }; } }
                } else if m == 1 {
                    for r in 0..self.cursor_row { for c in 0..self.cols as usize { self.rows[r][c] = Cell { ch: ' ', fg: None, bg: None }; } }
                    for c in 0..=self.cursor_col { self.rows[self.cursor_row][c] = Cell { ch: ' ', fg: None, bg: None }; }
                }
            }
            'K' => {
                let m = n(0,0); let r = self.cursor_row;
                let range: Box<dyn Iterator<Item = usize>> = if m == 0 { Box::new(self.cursor_col..self.cols as usize) } else if m == 1 { Box::new(0..=self.cursor_col) } else { Box::new(0..self.cols as usize) };
                for c in range { self.rows[r][c] = Cell { ch: ' ', fg: None, bg: None }; }
            }
            'm' => {
                let modes: Vec<i32> = if nums.is_empty() { vec![0] } else { nums.to_vec() };
                let mut idx = 0;
                while idx < modes.len() {
                    match modes[idx] {
                        0 => { self.fg = Color::Default; self.bg = Color::Default; self.bold = false; }
                        1 => { self.bold = true; }
                        22 => { self.bold = false; }
                        30..=37 => { self.fg = ansi_to_color(modes[idx] - 30); }
                        38 => { if idx+2 < modes.len() && modes[idx+1] == 5 { self.fg = index_to_color(modes[idx+2]); idx += 2; } }
                        39 => { self.fg = Color::Default; }
                        40..=47 => { self.bg = ansi_to_color(modes[idx] - 40); }
                        48 => { if idx+2 < modes.len() && modes[idx+1] == 5 { self.bg = index_to_color(modes[idx+2]); idx += 2; } }
                        49 => { self.bg = Color::Default; }
                        90..=97 => { self.fg = bright_to_color(modes[idx] - 90); }
                        100..=107 => { self.bg = bright_to_color(modes[idx] - 100); }
                        _ => {}
                    }
                    idx += 1;
                }
            }
            _ => {}
        }
    }

    fn newline(&mut self) {
        if self.cursor_row + 1 >= self.rows_count as usize {
            self.scrollback.push(self.rows[0].clone());
            self.rows.remove(0);
            self.rows.push(vec![Cell { ch: ' ', fg: None, bg: None }; self.cols as usize]);
        } else { self.cursor_row += 1; }
        self.cursor_col = 0;
    }

    fn write_char(&mut self, ch: char) {
        if self.cursor_col >= self.cols as usize { self.newline(); }
        let fg = if self.fg != Color::Default { Some(self.fg) } else { None };
        let bg = if self.bg != Color::Default { Some(self.bg) } else { None };
        self.rows[self.cursor_row][self.cursor_col] = Cell { ch, fg, bg };
        self.cursor_col += 1;
    }

    pub fn visible_rows(&self) -> Vec<Vec<(String, Option<ratatui::style::Color>, Option<ratatui::style::Color>)>> {
        self.rows.iter().map(|row| row.iter().map(|cell| (cell.ch.to_string(), cell.fg.map(|c| c.to_ratatui()), cell.bg.map(|c| c.to_ratatui()))).collect()).collect()
    }

    pub fn scroll(&self) -> usize { self.scroll_offset }
    pub fn scroll_up(&mut self, a: usize) { self.scroll_offset = self.scroll_offset.saturating_add(a).min(self.scrollback.len()); }
    pub fn scroll_down(&mut self, a: usize) { self.scroll_offset = self.scroll_offset.saturating_sub(a); }
    pub fn scrollback_len(&self) -> usize { self.scrollback.len() }

    pub fn visible_scrollback(&self) -> Vec<Vec<(String, Option<ratatui::style::Color>, Option<ratatui::style::Color>)>> {
        self.scrollback.iter().map(|row| row.iter().map(|cell| (cell.ch.to_string(), cell.fg.map(|c| c.to_ratatui()), None)).collect()).collect()
    }

    pub fn cursor_position(&self) -> (u16, u16) { (self.cursor_col as u16, self.cursor_row as u16) }
}

fn ansi_to_color(i: i32) -> Color { match i { 0=>Color::Black,1=>Color::Red,2=>Color::Green,3=>Color::Yellow,4=>Color::Blue,5=>Color::Magenta,6=>Color::Cyan,7=>Color::White, _=>Color::Default } }
fn bright_to_color(i: i32) -> Color { match i { 0=>Color::BrightBlack,1=>Color::BrightRed,2=>Color::BrightGreen,3=>Color::BrightYellow,4=>Color::BrightBlue,5=>Color::BrightMagenta,6=>Color::BrightCyan,7=>Color::BrightWhite, _=>Color::Default } }
fn index_to_color(i: i32) -> Color { match i { 0..=7=>ansi_to_color(i), 8..=15=>bright_to_color(i-8), _=>Color::Default } }