lv-tui 0.4.0

A reactive TUI framework for Rust
Documentation
use std::cell::RefCell;
use std::collections::VecDeque;

use crate::buffer::DiffOp;
use crate::event::{Event, Key, KeyEvent, Modifiers, MouseEvent, MouseKind};
use crate::geom::Size;
use crate::Result;

/// 终端后端抽象
pub trait TerminalBackend {
    /// 进入终端 raw mode + alternate screen
    fn enter(&mut self) -> Result<()>;

    /// 退出终端 raw mode + alternate screen
    fn leave(&mut self) -> Result<()>;

    /// 获取当前终端大小
    fn size(&self) -> Result<Size>;

    /// 阻塞读取下一个事件
    fn read_event(&mut self) -> Result<Event>;

    /// 带超时的事件轮询,超时返回 Ok(None)
    fn poll_event(&mut self, timeout: std::time::Duration) -> Result<Option<Event>>;

    /// 将 diff ops 刷新到终端
    fn flush(&mut self, ops: &[DiffOp]) -> Result<()>;
}

/// 终端恢复守卫
///
/// Drop 时自动恢复终端状态,确保 panic unwind 安全。
pub struct TerminalGuard;

impl TerminalGuard {
    pub fn new() -> Self {
        Self
    }
}

impl Drop for TerminalGuard {
    fn drop(&mut self) {
        let _ = crossterm::terminal::disable_raw_mode();
        let _ = crossterm::execute!(
            std::io::stdout(),
            crossterm::terminal::LeaveAlternateScreen,
            crossterm::cursor::Show
        );
    }
}

/// 基于 crossterm 的终端后端实现
pub struct CrosstermBackend {
    stdout: std::io::Stdout,
    _guard: TerminalGuard,
}

impl CrosstermBackend {
    pub fn new() -> Result<Self> {
        Ok(Self {
            stdout: std::io::stdout(),
            _guard: TerminalGuard::new(),
        })
    }
}

impl TerminalBackend for CrosstermBackend {
    fn enter(&mut self) -> Result<()> {
        crossterm::terminal::enable_raw_mode()?;
        crossterm::execute!(
            self.stdout,
            crossterm::terminal::EnterAlternateScreen,
            crossterm::cursor::Hide,
            crossterm::event::EnableMouseCapture,
        )?;
        Ok(())
    }

    fn leave(&mut self) -> Result<()> {
        crossterm::execute!(
            self.stdout,
            crossterm::terminal::LeaveAlternateScreen,
            crossterm::cursor::Show,
            crossterm::event::DisableMouseCapture,
        )?;
        crossterm::terminal::disable_raw_mode()?;
        Ok(())
    }

    fn size(&self) -> Result<Size> {
        let (width, height) = crossterm::terminal::size()?;
        Ok(Size { width, height })
    }

    fn flush(&mut self, ops: &[DiffOp]) -> Result<()> {
        use crossterm::style::{Attribute, SetAttribute};

        for op in ops {
            // 移动光标
            crossterm::execute!(
                self.stdout,
                crossterm::cursor::MoveTo(op.pos.x, op.pos.y)
            )?;

            // 设置样式
            if let Some(fg) = op.cell.style.fg {
                crossterm::execute!(self.stdout, crossterm::style::SetForegroundColor(fg.into()))?;
            }
            if let Some(bg) = op.cell.style.bg {
                crossterm::execute!(self.stdout, crossterm::style::SetBackgroundColor(bg.into()))?;
            }
            if op.cell.style.bold {
                crossterm::execute!(self.stdout, SetAttribute(Attribute::Bold))?;
            }
            if op.cell.style.italic {
                crossterm::execute!(self.stdout, SetAttribute(Attribute::Italic))?;
            }
            if op.cell.style.underline {
                crossterm::execute!(self.stdout, SetAttribute(Attribute::Underlined))?;
            }

            // 打印字符
            crossterm::execute!(self.stdout, crossterm::style::Print(&op.cell.symbol))?;

            // 重置样式
            crossterm::execute!(
                self.stdout,
                crossterm::style::ResetColor,
                SetAttribute(Attribute::Reset)
            )?;
        }

        // flush 到终端
        use std::io::Write;
        self.stdout.flush()?;
        Ok(())
    }

    fn poll_event(&mut self, timeout: std::time::Duration) -> Result<Option<Event>> {
        if !crossterm::event::poll(timeout)? {
            return Ok(None);
        }
        self.read_event().map(Some)
    }

    fn read_event(&mut self) -> Result<Event> {
        use crossterm::event::{self, Event as CEvent, KeyCode, KeyModifiers};

        loop {
            match event::read()? {
                CEvent::Key(key_event) => {
                    let key = match key_event.code {
                        KeyCode::Char(c) => Key::Char(c),
                        KeyCode::Enter => Key::Enter,
                        KeyCode::Esc => Key::Esc,
                        KeyCode::Backspace => Key::Backspace,
                        KeyCode::Tab => Key::Tab,
                        KeyCode::BackTab => Key::Tab,
                        KeyCode::Up => Key::Up,
                        KeyCode::Down => Key::Down,
                        KeyCode::Left => Key::Left,
                        KeyCode::Right => Key::Right,
                        KeyCode::Delete => Key::Delete,
                        KeyCode::Home => Key::Home,
                        KeyCode::End => Key::End,
                        KeyCode::PageUp => Key::PageUp,
                        KeyCode::PageDown => Key::PageDown,
                        _ => continue, // 忽略不支持的按键
                    };

                    let modifiers = Modifiers {
                        ctrl: key_event.modifiers.contains(KeyModifiers::CONTROL),
                        alt: key_event.modifiers.contains(KeyModifiers::ALT),
                        shift: key_event.modifiers.contains(KeyModifiers::SHIFT)
                            || key_event.code == KeyCode::BackTab,
                    };

                    return Ok(Event::Key(KeyEvent { key, modifiers }));
                }
                CEvent::Resize(w, h) => {
                    return Ok(Event::Resize(Size {
                        width: w,
                        height: h,
                    }));
                }
                CEvent::Mouse(mouse_event) => {
                    use crossterm::event::MouseEventKind;
                    let kind = match mouse_event.kind {
                        MouseEventKind::Down(_) => MouseKind::Down,
                        MouseEventKind::Up(_) => MouseKind::Up,
                        MouseEventKind::ScrollDown => MouseKind::ScrollDown,
                        MouseEventKind::ScrollUp => MouseKind::ScrollUp,
                        _ => continue,
                    };
                    return Ok(Event::Mouse(MouseEvent {
                        x: mouse_event.column,
                        y: mouse_event.row,
                        kind,
                    }));
                }
                _ => continue,
            }
        }
    }
}

/// 无头后端 — 不依赖终端,事件来自内存队列,输出收集到 buffer
///
/// 用于单元测试和自动化测试场景。
pub struct HeadlessBackend {
    size: Size,
    events: RefCell<VecDeque<Event>>,
}

impl HeadlessBackend {
    /// Creates a headless backend with the given virtual terminal dimensions.
    pub fn new(width: u16, height: u16) -> Self {
        Self {
            size: Size { width, height },
            events: RefCell::new(VecDeque::new()),
        }
    }

    /// Pushes an event into the in-memory queue for the runtime to consume.
    pub fn push_event(&self, event: Event) {
        self.events.borrow_mut().push_back(event);
    }
}

impl TerminalBackend for HeadlessBackend {
    fn enter(&mut self) -> Result<()> {
        Ok(())
    }

    fn leave(&mut self) -> Result<()> {
        Ok(())
    }

    fn size(&self) -> Result<Size> {
        Ok(self.size)
    }

    fn flush(&mut self, _ops: &[DiffOp]) -> Result<()> {
        Ok(())
    }

    fn poll_event(&mut self, timeout: std::time::Duration) -> Result<Option<Event>> {
        // Check for queued events first
        let event = self.events.borrow_mut().pop_front();
        if event.is_some() {
            return Ok(event);
        }
        // No events — wait for the timeout so timers can fire
        std::thread::sleep(timeout);
        Ok(None)
    }

    fn read_event(&mut self) -> Result<Event> {
        self.events
            .borrow_mut()
            .pop_front()
            .ok_or_else(|| crate::Error::Terminal("no events in headless queue".into()))
    }
}