lv-tui 0.2.0

A reactive TUI framework for Rust, inspired by Textual and React
Documentation
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::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),
                    };

                    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,
            }
        }
    }
}