tenere 0.11.0

TUI interface for LLMs written in Rust
Documentation
use tokio::sync::mpsc::UnboundedSender;

use ratatui::{
    layout::{Alignment, Constraint, Direction, Layout, Rect},
    style::{Color, Style, Stylize},
    text::Text,
    widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap},
    Frame,
};

use crate::{
    app::FocusedBlock,
    event::Event,
    notification::{Notification, NotificationLevel},
};

#[derive(Debug, Default, Clone)]
pub struct Preview<'a> {
    pub text: Vec<Text<'a>>,
    pub scroll: usize,
}

#[derive(Debug, Default, Clone)]
pub struct History<'a> {
    block_height: usize,
    state: ListState,
    pub text: Vec<Vec<String>>,
    pub preview: Preview<'a>,
}

impl History<'_> {
    pub fn new() -> Self {
        Self {
            block_height: 0,
            state: ListState::default(),
            text: Vec::new(),
            preview: Preview::default(),
        }
    }

    pub fn move_to_bottom(&mut self) {
        if !self.text.is_empty() {
            self.state.select(Some(self.text.len() - 1));
        }
    }

    pub fn move_to_top(&mut self) {
        if !self.text.is_empty() {
            self.state.select(Some(0));
        }
    }

    pub fn scroll_down(&mut self) {
        if self.text.is_empty() {
            return;
        }
        let i = match self.state.selected() {
            Some(i) => {
                if i < self.text.len() - 1 {
                    i + 1
                } else {
                    i
                }
            }
            None => 0,
        };
        self.state.select(Some(i));
    }
    pub fn scroll_up(&mut self) {
        if self.text.is_empty() {
            return;
        }
        let i = match self.state.selected() {
            Some(i) => {
                if i > 1 {
                    i - 1
                } else {
                    0
                }
            }
            None => 0,
        };
        self.state.select(Some(i));
    }

    pub fn save(&mut self, archive_file_name: &str, sender: UnboundedSender<Event>) {
        if !self.text.is_empty() {
            match std::fs::write(
                archive_file_name,
                self.text[self.state.selected().unwrap_or(0)].join(""),
            ) {
                Ok(_) => {
                    let notif = Notification::new(
                        format!("Chat saved to `{}` file", archive_file_name),
                        NotificationLevel::Info,
                    );

                    sender.send(Event::Notification(notif)).unwrap();
                }
                Err(e) => {
                    let notif = Notification::new(e.to_string(), NotificationLevel::Error);

                    sender.send(Event::Notification(notif)).unwrap();
                }
            }
        }
    }

    pub fn render(&mut self, frame: &mut Frame, area: Rect, focused_block: FocusedBlock) {
        self.block_height = area.height as usize;

        if !self.text.is_empty() && self.state.selected().is_none() {
            *self.state.offset_mut() = 0;
            self.state.select(Some(0));
        }

        let (history_block, preview_block) = {
            let chunks = Layout::default()
                .direction(Direction::Horizontal)
                .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
                .split(area);
            (chunks[0], chunks[1])
        };

        let items = self
            .text
            .iter()
            .map(|chat| match chat.first() {
                Some(v) => ListItem::new(v.to_owned()),
                None => ListItem::new(""),
            })
            .collect::<Vec<ListItem>>();

        let list = List::new(items)
            .block(
                Block::default()
                    .borders(Borders::ALL)
                    .title(" History ")
                    .title_style(match focused_block {
                        FocusedBlock::History => Style::default().bold(),
                        _ => Style::default(),
                    })
                    .title_alignment(Alignment::Center)
                    .style(Style::default())
                    .border_style(match focused_block {
                        FocusedBlock::History => Style::default().fg(Color::Green),
                        _ => Style::default(),
                    }),
            )
            .highlight_style(Style::default().bg(Color::DarkGray));

        let preview = Paragraph::new(match self.state.selected() {
            Some(i) => self.preview.text[i].clone(),
            None => Text::raw(""),
        })
        .wrap(Wrap { trim: false })
        .scroll((self.preview.scroll as u16, 0))
        .block(
            Block::default()
                .title(" Preview ")
                .title_style(match focused_block {
                    FocusedBlock::Preview => Style::default().bold(),
                    _ => Style::default(),
                })
                .title_alignment(Alignment::Center)
                .borders(Borders::ALL)
                .style(Style::default())
                .border_style(match focused_block {
                    FocusedBlock::Preview => Style::default().fg(Color::Green),
                    _ => Style::default(),
                }),
        );

        frame.render_widget(Clear, area);
        frame.render_widget(preview, preview_block);
        frame.render_stateful_widget(list, history_block, &mut self.state);
    }
}