imap-tools 1.8.2

A collection of tools to manipulate IMAP mailboxes
use crate::libs::render::{Renderer as RendererTrait, RendererError};
use exn::{Result, ResultExt as _};
use ratatui::{
    backend::CrosstermBackend,
    layout::Constraint,
    style::{Modifier, Style},
    widgets::{Block, Borders, Cell, Row, Table},
    Terminal, TerminalOptions, Viewport,
};
use std::{
    fmt::Display,
    io::{stdout, IsTerminal as _, Stdout},
};

#[cfg_attr(feature = "tracing", derive(Debug))]
pub struct Renderer<'a> {
    terminal: Terminal<CrosstermBackend<Stdout>>,
    table_rows: Vec<Row<'a>>,
    column_widths: Vec<u16>,
    headers: Row<'a>,
    title: &'static str,
}

impl RendererTrait for Renderer<'_> {
    #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", ret))]
    fn is_usable() -> bool {
        stdout().is_terminal()
    }

    #[cfg_attr(
        feature = "tracing",
        tracing::instrument(
            level = "trace",
            skip(title, _format, headers),
            ret,
            err(level = "info")
        )
    )]
    fn new(
        title: &'static str,
        _format: &'static str,
        headers: &[&'static str],
    ) -> Result<Self, RendererError> {
        let mut terminal = ratatui::try_init_with_options(TerminalOptions {
            viewport: Viewport::Inline(0),
        })
        .or_raise(|| RendererError("ratatui init failed".to_owned()))?;
        terminal
            .clear()
            .or_raise(|| RendererError("terminal clear failed".to_owned()))?;
        Ok(Self {
            terminal,
            table_rows: vec![],
            column_widths: vec![],
            title,
            headers: headers
                .iter()
                .map(|h| {
                    Cell::new((*h).to_owned()).style(Style::default().add_modifier(Modifier::BOLD))
                })
                .collect(),
        })
    }

    #[cfg_attr(
        feature = "tracing",
        tracing::instrument(level = "trace", skip(self, row), err(level = "info"))
    )]
    fn add_row(&mut self, row: &[&dyn Display]) -> Result<(), RendererError> {
        let str_row: Vec<_> = row.iter().map(std::string::ToString::to_string).collect();

        #[cfg(feature = "tracing")]
        tracing::trace!(row = ?str_row);

        let new_row = Row::new(str_row.iter().cloned().enumerate().map(|(idx, content)| {
            let mut style = Style::default();
            if idx == 0 {
                style = style.add_modifier(Modifier::BOLD);
            }
            Cell::new(content).style(style)
        }));
        self.table_rows.push(new_row);

        if self.column_widths.is_empty() {
            let column_count = row.len();

            self.column_widths = vec![0u16; column_count];
        }

        #[expect(clippy::indexing_slicing, reason = "it's ok")]
        for (idx, cell) in str_row.iter().enumerate() {
            let width = cell.len();
            let width = u16::try_from(width)
                .or_raise(|| RendererError(format!("failed converting {width} in a u16")))?;
            if width > self.column_widths[idx] {
                self.column_widths[idx] = width;
            }
        }

        // let area = self.terminal.get_frame().area();
        // self.terminal.set_cursor_position(area)?;
        self.terminal
            .clear()
            .or_raise(|| RendererError("terminal clear failed".to_owned()))?;
        let table_width = self.table_rows.len();
        let table_width = u16::try_from(table_width)
            .or_raise(|| RendererError(format!("convert {table_width} into a u16 failed")))?;
        self.terminal = ratatui::try_init_with_options(TerminalOptions {
            viewport: Viewport::Inline(table_width + 3),
        })
        .or_raise(|| RendererError("ratatui init failed".to_owned()))?;

        let rows = self.table_rows.clone();
        let widths = self.column_widths();
        let headers = self.headers.clone();

        self.terminal
            .draw(|frame| {
                let table = Table::new(rows, widths)
                    .header(headers)
                    .block(Block::default().title(self.title).borders(Borders::ALL));
                frame.render_widget(table, frame.area());
            })
            .or_raise(|| RendererError("termianl draw failed".to_owned()))?;

        Ok(())
    }
}

impl Renderer<'_> {
    #[cfg_attr(
        feature = "tracing",
        tracing::instrument(level = "trace", skip(self), ret)
    )]
    fn column_widths(&self) -> Vec<Constraint> {
        let last_idx = self.column_widths.len() - 1;
        self.column_widths
            .iter()
            .enumerate()
            .map(|(idx, width)| {
                if idx == last_idx {
                    Constraint::Min(0)
                } else {
                    Constraint::Length(width + 1)
                }
            })
            .collect()
    }
}

impl Drop for Renderer<'_> {
    #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))]
    fn drop(&mut self) {
        ratatui::restore();
    }
}