From e6396986990b5ef28de54cfd2620777e71c8fad9 Mon Sep 17 00:00:00 2001 From: dario_azzali <90763809+Darioazzali@users.noreply.github.com> Date: Thu, 19 Oct 2023 12:08:03 +0200 Subject: [PATCH] feat: added menu-view mode and layout switch (#102) Co-authored-by: Teo Stocco --- src/actors/console.rs | 162 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 141 insertions(+), 21 deletions(-) diff --git a/src/actors/console.rs b/src/actors/console.rs index 0d05d99..2519129 100644 --- a/src/actors/console.rs +++ b/src/actors/console.rs @@ -3,11 +3,14 @@ use chrono::prelude::*; use crossterm::event::KeyEvent; use ratatui::backend::Backend; use ratatui::layout::Rect; +use ratatui::prelude::Alignment; use ratatui::text::Line; +use ratatui::widgets::{List, ListItem, ListState}; use ratatui::Frame; +use std::borrow::Cow; use std::rc::Rc; -use std::str; use std::{cmp::min, collections::HashMap, io}; +use std::{str, usize}; use subprocess::ExitStatus; use ratatui::{ @@ -30,6 +33,37 @@ use crate::config::color::{ColorOption, Colorizer}; use super::command::{CommandActor, PoisonPill, Reload}; +const MENU_WIDTH: u16 = 30; +const MAX_CHARS: usize = (MENU_WIDTH - 6) as usize; + +enum LayoutDirection { + Horizontal, + Vertical, +} + +impl LayoutDirection { + fn get_opposite_orientation(&self) -> Self { + match self { + Self::Horizontal => Self::Vertical, + Self::Vertical => Self::Horizontal, + } + } +} + +enum AppMode { + Menu, + View, +} + +impl AppMode { + fn get_opposite_mode(&self) -> Self { + match self { + Self::View => Self::Menu, + Self::Menu => Self::View, + } + } +} + pub struct Panel { logs: Vec<(String, Style)>, lines: u16, @@ -59,12 +93,26 @@ pub struct ConsoleActor { arbiter: Arbiter, panels: HashMap, timestamp: bool, + layout_direction: LayoutDirection, + mode: AppMode, + list_state: ListState, } -pub fn chunks(f: &Frame) -> Rc<[Rect]> { +fn chunks(mode: &AppMode, direction: &LayoutDirection, f: &Frame) -> Rc<[Rect]> { + let chunks_constraints = match mode { + AppMode::Menu => match direction { + LayoutDirection::Horizontal => vec![Constraint::Min(0), Constraint::Length(3)], + LayoutDirection::Vertical => vec![Constraint::Min(0), Constraint::Length(MENU_WIDTH)], + }, + AppMode::View => vec![Constraint::Min(0)], + }; + let direction = match direction { + LayoutDirection::Horizontal => Direction::Vertical, + LayoutDirection::Vertical => Direction::Horizontal, + }; Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(0), Constraint::Length(3)].as_ref()) + .direction(direction) + .constraints(chunks_constraints) .split(f.size()) } @@ -80,6 +128,9 @@ impl ConsoleActor { arbiter: Arbiter::new(), panels: HashMap::default(), timestamp, + mode: AppMode::Menu, + layout_direction: LayoutDirection::Horizontal, + list_state: ListState::default().with_selected(Some(0)), } } @@ -107,7 +158,7 @@ impl ConsoleActor { pub fn get_log_height(&mut self) -> u16 { let frame = self.terminal.get_frame(); - chunks(&frame)[0].height + chunks(&self.mode, &self.layout_direction, &frame)[0].height } pub fn go_to(&mut self, panel_index: usize) { @@ -125,10 +176,12 @@ impl ConsoleActor { pub fn next(&mut self) { self.index = self.order[(self.idx() + 1) % self.order.len()].clone(); + self.list_state.select(Some(self.idx())) } pub fn previous(&mut self) { self.index = self.order[(self.idx() + self.order.len() - 1) % self.order.len()].clone(); + self.list_state.select(Some(self.idx())) } fn clean(&mut self) { @@ -146,7 +199,7 @@ impl ConsoleActor { if let Some(focused_panel) = &self.panels.get(&self.index) { self.terminal .draw(|f| { - let chunks = chunks(f); + let chunks = chunks(&self.mode, &self.layout_direction, f); let logs = &focused_panel.logs; let log_height = chunks[0].height; @@ -167,15 +220,32 @@ impl ConsoleActor { .scroll((maximum_scroll - min(maximum_scroll, focused_panel.shift), 0)); f.render_widget(paragraph, chunks[0]); - let /*mut*/ titles: Vec = self + //Format titles + let titles: Vec = self .order .iter() .map(|panel| { - let span = self.panels.get(panel).map(|p| match p.status { - Some(ExitStatus::Exited(0)) => Span::styled(format!("{}.", panel), Style::default().fg(Color::Green)), - Some(_) => Span::styled(format!("{}!", panel), Style::default().fg(Color::Red)), - None => Span::styled(format!("{}*", panel), Style::default()), - }).unwrap_or_else(|| Span::styled(panel, Style::default())); + let mut span = self + .panels + .get(panel) + .map(|p| match p.status { + Some(ExitStatus::Exited(0)) => Span::styled( + format!("{}.", panel), + Style::default().fg(Color::Green), + ), + Some(_) => Span::styled( + format!("{}!", panel), + Style::default().fg(Color::Red), + ), + None => Span::styled(format!("{}*", panel), Style::default()), + }) + .unwrap_or_else(|| Span::styled(panel, Style::default())); + // Replace the titles whoms length is greater than MAX_CHARS with an + // ellipse + span = Span::styled( + ellipse_if_too_long(span.content).into_owned(), + span.style, + ); Line::from(span) }) .collect(); @@ -188,20 +258,57 @@ impl ConsoleActor { focus.lines, f.size().width, )))); + */ - let tabs = Tabs::new(titles) - .block(Block::default().borders(Borders::ALL)) - .select(idx) - .highlight_style( - Style::default() - .add_modifier(Modifier::BOLD) - .bg(Color::DarkGray), - ); - f.render_widget(tabs, chunks[1]); + match self.mode { + AppMode::Menu => { + match self.layout_direction { + LayoutDirection::Horizontal => { + let tabs = Tabs::new(titles) + .block(Block::default().borders(Borders::ALL)) + .select(idx) + .highlight_style( + Style::default() + .add_modifier(Modifier::BOLD) + .bg(Color::DarkGray), + ); + f.render_widget(tabs, chunks[1]); + } + LayoutDirection::Vertical => { + let list = List::new( + titles + .into_iter() + .map(ListItem::new) + .collect::>(), + ) + .block( + Block::default() + .borders(Borders::ALL) + .title("Task List") + .title_alignment(Alignment::Center), + ) + .highlight_style( + Style::default() + .bg(Color::DarkGray) + .add_modifier(Modifier::BOLD), + ); + f.render_stateful_widget(list, chunks[1], &mut self.list_state) + } + }; + } + AppMode::View => {} + }; }) .unwrap(); } } + + pub fn switch_layout(&mut self) { + self.layout_direction = self.layout_direction.get_opposite_orientation(); + } + pub fn switch_mode(&mut self) { + self.mode = self.mode.get_opposite_mode(); + } } impl Actor for ConsoleActor { @@ -300,6 +407,8 @@ impl Handler for ConsoleActor { focused_panel.command.do_send(Reload::Manual); } } + KeyCode::Tab => self.switch_layout(), + KeyCode::Char('m') => self.switch_mode(), KeyCode::Right | KeyCode::Char('l') => { self.next(); } @@ -373,6 +482,17 @@ fn wrapped_lines(message: &String, width: u16) -> u16 { textwrap::wrap(str::from_utf8(&clean).unwrap(), width as usize).len() as u16 } +// Replace the character that are max that MAX_CHARS with an ellipse ... +fn ellipse_if_too_long(task_title: Cow<'_, str>) -> Cow { + if task_title.len() >= MAX_CHARS { + let mut task_title = task_title.to_string(); + task_title.replace_range(MAX_CHARS.., "..."); + Cow::Owned(task_title.to_string()) + } else { + task_title + } +} + /// Formats a message with a timestamp in `"{timestamp} {message}"`. fn format_message(message: &str, timestamp: &DateTime) -> String { format!("{} {}", timestamp.format("%H:%M:%S%.3f"), message)