use alacritty_terminal::event::{Event, EventListener}; use alacritty_terminal::grid::Dimensions; use alacritty_terminal::term::{Config, Term}; use alacritty_terminal::vte::ansi::Processor; use alacritty_terminal::Grid; /// EventListener для alacritty_terminal — сюда прилетают события вроде /// "заголовок изменился", "нужен звонок" и т.п. Для MVP просто складываем /// в очередь, ui.rs потом разбирает. #[derive(Clone)] pub struct EventProxy(std::sync::mpsc::Sender); impl EventListener for EventProxy { fn send_event(&self, event: Event) { let _ = self.0.send(event); } } /// `Term::new`/`Term::resize` принимают не размер в пикселях (это отдельное /// понятие `WindowSize`, для другого — сообщать PTY реальный размер окна /// в пикселях, нам не нужное), а нечто, реализующее трейт `Dimensions` — /// то есть размер именно в колонках/строках символьной сетки. Такого /// готового публичного типа под рукой нет, поэтому просто заводим свой. struct GridSize { cols: usize, rows: usize, } impl Dimensions for GridSize { fn total_lines(&self) -> usize { self.rows } fn screen_lines(&self) -> usize { self.rows } fn columns(&self) -> usize { self.cols } } /// Держит состояние эмулируемого терминала: сетку ячеек с атрибутами, /// курсор, режимы (alt screen, wrap и т.д.) и VTE-парсер поверх неё. pub struct TerminalState { term: Term, parser: Processor, /// События от alacritty_terminal (смена заголовка через OSC, звонок, /// clipboard-запросы и т.п.) — очередь собирается, но пока никто её не /// читает: например, обновление имени таба по Event::Title ещё не /// подключено к ui.rs/app.rs. Оставлено как задел на будущее, а не /// удалено, поэтому и глушим dead_code явно, а не молчим о причине. #[allow(dead_code)] events: std::sync::mpsc::Receiver, } impl TerminalState { pub fn new(cols: u16, rows: u16) -> Self { let (tx, rx) = std::sync::mpsc::channel(); let proxy = EventProxy(tx); let size = GridSize { cols: cols as usize, rows: rows as usize }; let term = Term::new(Config::default(), &size, proxy); Self { term, parser: Processor::new(), events: rx } } /// Скормить парсеру очередную порцию байт, пришедших от транспорта. /// Именно тут живёт вся сложность интерпретации escape-последовательностей — /// alacritty_terminal берёт её на себя целиком. pub fn advance(&mut self, bytes: &[u8]) { for byte in bytes { self.parser.advance(&mut self.term, *byte); } } pub fn resize(&mut self, cols: u16, rows: u16) { let size = GridSize { cols: cols as usize, rows: rows as usize }; self.term.resize(size); } /// Текущее содержимое сетки для рендера в ratatui. pub fn grid(&self) -> &Grid { self.term.grid() } /// Позиция курсора в системе координат видимого вьюпорта: (line, column). /// line может быть отрицательной в теории (если вьюпорт проскроллен в /// историю), но у активного текстового курсора такого не бывает — /// проверка `row_idx as i32 == cursor.0` в ui.rs всё равно безопасна. pub fn cursor_position(&self) -> (i32, usize) { let point = self.term.grid().cursor.point; (point.line.0, point.column.0) } /// Ширина/высота видимой области терминала — то, что реально нужно /// пройти циклом в рендере, а не полагаться на то, что area в ratatui /// всегда совпадает по размеру (на случай рассинхрона после resize). pub fn columns(&self) -> usize { self.term.columns() } pub fn screen_lines(&self) -> usize { self.term.screen_lines() } /// См. комментарий у поля `events` — задел на будущее (обновление /// заголовка таба, звонок и т.п.), пока никем не вызывается. #[allow(dead_code)] pub fn drain_events(&mut self) -> Vec { self.events.try_iter().collect() } }