mamegrep 0.1.0

A TUI tool for `$ git grep` to easily edit search patterns and view results
Documentation
use std::{io::Write, time::Duration};

use crossterm::{
    event::Event,
    style::{Attribute, Attributes, ContentStyle, StyledContent},
    terminal::{EnterAlternateScreen, LeaveAlternateScreen},
};
use orfail::OrFail;

use crate::canvas::{Frame, TokenPosition, TokenStyle};

#[derive(Debug)]
pub struct Terminal {
    size: TerminalSize,
    prev: Frame,
    show_cursor: bool,
}

impl Terminal {
    pub fn new() -> orfail::Result<Self> {
        crossterm::execute!(
            std::io::stdout(),
            EnterAlternateScreen,
            crossterm::cursor::Hide,
        )
        .or_fail()?;
        crossterm::terminal::enable_raw_mode().or_fail()?;

        let (cols, rows) = crossterm::terminal::size().or_fail()?;
        let size = TerminalSize {
            rows: rows as usize,
            cols: cols as usize,
        };

        Ok(Self {
            size,
            prev: Frame::new(size),
            show_cursor: false,
        })
    }

    pub fn show_cursor(&mut self, position: TokenPosition) -> orfail::Result<()> {
        if !self.show_cursor {
            crossterm::execute!(std::io::stdout(), crossterm::cursor::Show).or_fail()?;
            self.show_cursor = true;
        }
        crossterm::execute!(
            std::io::stdout(),
            crossterm::cursor::MoveTo(position.col as u16, position.row as u16)
        )
        .or_fail()?;
        Ok(())
    }

    pub fn hide_cursor(&mut self) -> orfail::Result<()> {
        if self.show_cursor {
            crossterm::execute!(std::io::stdout(), crossterm::cursor::Hide).or_fail()?;
            self.show_cursor = false;
        }
        Ok(())
    }

    pub fn size(&self) -> TerminalSize {
        self.size
    }

    pub fn next_event(&mut self) -> orfail::Result<Event> {
        let timeout = Duration::from_secs(1);
        while !crossterm::event::poll(timeout).or_fail()? {}

        let event = crossterm::event::read().or_fail()?;
        if let Event::Resize(cols, rows) = event {
            self.size.cols = cols as usize;
            self.size.rows = rows as usize;
        }

        Ok(event)
    }

    pub fn draw_frame(&mut self, frame: Frame) -> orfail::Result<()> {
        let stdout = std::io::stdout();
        let mut writer = stdout.lock();
        for (row, line) in frame.dirty_lines(&self.prev) {
            crossterm::queue!(
                writer,
                crossterm::cursor::MoveTo(0, row as u16),
                crossterm::terminal::Clear(crossterm::terminal::ClearType::CurrentLine)
            )
            .or_fail()?;

            for token in line.tokens() {
                let attributes = match token.style() {
                    TokenStyle::Plain => Attributes::none(),
                    TokenStyle::Bold => Attributes::none().with(Attribute::Bold),
                    TokenStyle::Dim => Attributes::none().with(Attribute::Dim),
                    TokenStyle::Underlined => Attributes::none().with(Attribute::Underlined),
                    TokenStyle::Reverse => Attributes::none().with(Attribute::Reverse),
                };
                let content = StyledContent::new(
                    ContentStyle {
                        attributes,
                        ..Default::default()
                    },
                    token.text(),
                );
                crossterm::queue!(writer, crossterm::style::PrintStyledContent(content))
                    .or_fail()?;
            }
        }

        writer.flush().or_fail()?;
        self.prev = frame;
        Ok(())
    }
}

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

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct TerminalSize {
    pub rows: usize,
    pub cols: usize,
}

impl TerminalSize {
    pub const EMPTY: Self = Self { cols: 0, rows: 0 };

    pub fn is_empty(self) -> bool {
        self.rows == 0 || self.cols == 0
    }
}