use crate::{
languages::spoken,
ui::tui::{
self,
events::Evt,
screens,
widgets::{ScrollLog, StatusBar, StatusMode},
Screen,
},
Error, Status,
};
use crossterm::event::{self, KeyCode};
use ratatui::{
buffer::Buffer,
layout::{Alignment, Constraint, Flex, Layout, Rect},
style::{Color, Style},
symbols::border::Set,
text::{Line, Span},
widgets::{block::Position, Block, Borders, Clear, Padding, StatefulWidget, Widget},
};
use std::{
collections::{HashMap, VecDeque},
sync::{Arc, Mutex, OnceLock},
};
use tokio::sync::mpsc::Sender;
const TOP_DIALOG_BORDER: Set = Set {
top_left: "┌",
top_right: "┐",
bottom_left: "│",
bottom_right: "│",
vertical_left: "│",
vertical_right: "│",
horizontal_top: "─",
horizontal_bottom: " ",
};
const STATUS_BORDER: Set = Set {
top_left: " ",
top_right: " ",
bottom_left: "└",
bottom_right: "┘",
vertical_left: " ",
vertical_right: " ",
horizontal_top: " ",
horizontal_bottom: "─",
};
static EMOJIS: OnceLock<HashMap<&'static str, String>> = OnceLock::new();
fn emoji() -> &'static HashMap<&'static str, String> {
EMOJIS.get_or_init(|| {
let mut map = HashMap::new();
map.insert("* ", "⭐".to_string());
map.insert("v ", "✅".to_string());
map.insert("x ", "❌".to_string());
map.insert("r ", "🚀".to_string());
map.insert("y ", "🎉".to_string());
map.insert("n ", "😢".to_string());
map.insert("! ", "❗".to_string());
map.insert("^ ", "⚠️ ".to_string());
map.insert("i ", "ℹ️ ".to_string());
map.insert("> ", "↪️ ".to_string());
map.insert("< ", "↩️ ".to_string());
map
})
}
#[derive(Clone, Debug)]
pub struct Log<'a> {
log: VecDeque<(Option<String>, String)>,
max_log: usize,
st: ScrollLog<'a>,
sb: StatusBar<'a>,
area: Rect,
centered: Rect,
spoken_language: Option<spoken::Code>,
on_enter: Option<Evt>,
}
impl Log<'_> {
pub fn new(max_log: usize) -> Self {
let mut st = ScrollLog::default();
st.scroll_newest();
let mut sb = StatusBar::new();
let block = Block::default()
.padding(Padding::horizontal(1))
.style(Style::default().fg(Color::DarkGray))
.borders(Borders::LEFT | Borders::RIGHT)
.border_set(TOP_DIALOG_BORDER);
sb.set_block(block);
Self {
log: VecDeque::default(),
max_log,
st,
sb,
area: Rect::default(),
centered: Rect::default(),
spoken_language: None,
on_enter: None,
}
}
fn recalculate_rect(&mut self, area: Rect) {
if self.area != area {
let [_, hc, _] = Layout::horizontal([
Constraint::Percentage(10),
Constraint::Min(1),
Constraint::Percentage(10),
])
.areas(area);
[_, self.centered, _] = Layout::vertical([
Constraint::Percentage(10),
Constraint::Min(1),
Constraint::Percentage(10),
])
.areas(hc);
self.area = area;
}
}
fn add_message<S: AsRef<str>>(&mut self, msg: S) {
if msg.as_ref().len() < 2 {
return;
}
let msg = msg.as_ref().to_string();
self.log
.push_back((emoji().get(&msg[0..2]).cloned(), msg[2..].to_string()));
if self.log.len() > self.max_log {
self.log.pop_front();
}
}
fn render_log(&mut self, area: Rect, buf: &mut Buffer) {
Widget::render(Clear, area, buf);
let [log_area, status_bar_area] =
Layout::vertical([Constraint::Percentage(100), Constraint::Min(1)]).areas(area);
let title = Line::from(vec![
Span::styled("─", Style::default().fg(Color::DarkGray)),
Span::styled("/ Log /", Style::default().fg(Color::White)),
]);
let block = Block::default()
.title(title)
.title_style(Style::default().fg(Color::White))
.padding(Padding::horizontal(1))
.style(Style::default().fg(Color::DarkGray))
.borders(Borders::LEFT | Borders::RIGHT | Borders::TOP)
.border_set(TOP_DIALOG_BORDER);
self.st.block(block);
self.st.style(Style::default().fg(Color::White));
StatefulWidget::render(&mut self.st, log_area, buf, &mut self.log);
Widget::render(&mut self.sb, status_bar_area, buf);
}
fn render_status(&mut self, area: Rect, buf: &mut Buffer) {
let keys = if self.on_enter.is_some() {
Span::styled(
"/ j,k scroll / ⤒ top / ⤓ bottom / ↵ continue / q quit /",
Style::default().fg(Color::White),
)
} else {
Span::styled(
"/ j,k scroll / ⤒ top / ⤓ bottom / ` back / q quit /",
Style::default().fg(Color::White),
)
};
let line = Line::from(vec![
Span::styled("─", Style::default().fg(Color::DarkGray)),
keys,
]);
let block = Block::default()
.title(line)
.title_style(Style::default().fg(Color::White))
.title_position(Position::Bottom)
.title_alignment(Alignment::Left)
.style(Style::default().fg(Color::DarkGray))
.borders(Borders::LEFT | Borders::BOTTOM | Borders::RIGHT)
.border_set(STATUS_BORDER)
.padding(Padding::horizontal(1));
Widget::render(block, area, buf);
}
pub async fn handle_ui_event(
&mut self,
event: tui::Event,
_to_ui: Sender<screens::Event>,
_status: Arc<Mutex<Status>>,
) -> Result<(), Error> {
match event {
tui::Event::Log(msg) => self.add_message(msg),
tui::Event::CommandStarted(mode, message) => {
match mode {
StatusMode::Blank => {
}
StatusMode::Messages => {
self.sb.set_messages(message);
}
StatusMode::Progress => {
self.sb.set_progress(message);
}
}
}
tui::Event::CommandOutput(message, progress) => {
self.add_message(&message);
if let Some(progress_val) = progress {
self.sb.update_progress(Some(message), progress_val);
} else {
self.sb.update_message(message);
}
}
tui::Event::CommandCompleted(result, success, failure) => {
self.sb.set_blank();
if result.success {
self.add_message(format!("y {}", result.last_line));
self.add_message("< Press ↵ Enter to continue");
self.on_enter = success;
} else {
self.add_message(format!("n {}", result.last_line));
self.add_message("< Press ↵ Enter to continue");
self.on_enter = failure;
}
}
_ => {}
}
Ok(())
}
pub async fn handle_input_event(
&mut self,
event: event::Event,
to_ui: Sender<screens::Event>,
_status: Arc<Mutex<Status>>,
) -> Result<(), Error> {
if let event::Event::Key(key) = event {
match key.code {
KeyCode::PageUp => self.st.scroll_oldest(),
KeyCode::PageDown => self.st.scroll_newest(),
KeyCode::Char('j') | KeyCode::Char('J') | KeyCode::Down => self.st.scroll_newer(),
KeyCode::Char('k') | KeyCode::Char('K') | KeyCode::Up => self.st.scroll_older(),
KeyCode::Char('`') => to_ui.send((None, tui::Event::ToggleLog).into()).await?,
KeyCode::Enter => {
if let Some(on_enter) = self.on_enter.take() {
to_ui.send(on_enter.into()).await?
}
}
_ => {}
}
}
Ok(())
}
}
#[async_trait::async_trait]
impl Screen for Log<'_> {
async fn handle_event(
&mut self,
event: screens::Event,
to_ui: Sender<screens::Event>,
status: Arc<Mutex<Status>>,
) -> Result<(), Error> {
match event {
screens::Event::Input(input_event) => {
let spoken = {
let status = status.lock().unwrap();
status.spoken_language()
};
if self.spoken_language != spoken {
self.spoken_language = spoken
}
self.handle_input_event(input_event, to_ui, status).await
}
screens::Event::Ui(_, ui_event) => self.handle_ui_event(ui_event, to_ui, status).await,
}
}
fn render_screen(&mut self, area: Rect, buf: &mut Buffer) -> Result<(), Error> {
self.recalculate_rect(area);
Widget::render(Clear, self.centered, buf);
let [list_area, status_area] =
Layout::vertical([Constraint::Percentage(100), Constraint::Min(1)])
.flex(Flex::End)
.areas(self.centered);
self.render_log(list_area, buf);
self.render_status(status_area, buf);
Ok(())
}
}