From 9eea37a5b5fc0e5ad7e611bf786b806c3d0da22e Mon Sep 17 00:00:00 2001 From: alexpasmantier Date: Fri, 25 Oct 2024 23:29:07 +0200 Subject: [PATCH] top menus --- .config/config.toml | 3 - README.md | 8 +- crates/television/television.rs | 87 ++++++++---- crates/television/ui.rs | 2 + crates/television/ui/help.rs | 203 +++++++++++++--------------- crates/television/ui/layout.rs | 42 ++++-- crates/television/ui/logo.rs | 26 ++++ crates/television/ui/metadata.rs | 113 ++++++++++++++++ crates/television_derive/src/lib.rs | 2 +- 9 files changed, 329 insertions(+), 157 deletions(-) create mode 100644 crates/television/ui/logo.rs create mode 100644 crates/television/ui/metadata.rs diff --git a/.config/config.toml b/.config/config.toml index 1ad8fea..dd0d7dc 100644 --- a/.config/config.toml +++ b/.config/config.toml @@ -4,9 +4,7 @@ down = "SelectNextEntry" up = "SelectPrevEntry" ctrl-n = "SelectNextEntry" ctrl-p = "SelectPrevEntry" -alt-down = "ScrollPreviewHalfPageDown" ctrl-d = "ScrollPreviewHalfPageDown" -alt-up = "ScrollPreviewHalfPageUp" ctrl-u = "ScrollPreviewHalfPageUp" enter = "SelectEntry" ctrl-enter = "SendToChannel" @@ -20,4 +18,3 @@ ctrl-n = "SelectNextEntry" ctrl-p = "SelectPrevEntry" enter = "SelectEntry" ctrl-s = "ToggleChannelSelection" - diff --git a/README.md b/README.md index 6d56e8e..0992e1f 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,12 @@ _______________ |,----------. |\ || |=| | - || || | | - || . _o| | | + || | | | + || |o| | |`-----------' |/ - ~~~~~~~~~~~~~~~ + `--------------' __ __ _ _ - / /____ / /__ _ __(_)__ (_)__ ___ + / /____ / /__ _ __(_)__ (_)__ ___ / __/ -_) / -_) |/ / (_- UnitChannel { + UnitChannel::from(&self.channel) + } + /// FIXME: this needs rework pub fn change_channel(&mut self, channel: TelevisionChannel) { self.reset_preview_scroll(); @@ -304,19 +314,17 @@ impl Television { Action::ScrollPreviewUp => self.scroll_preview_up(1), Action::ScrollPreviewHalfPageDown => self.scroll_preview_down(20), Action::ScrollPreviewHalfPageUp => self.scroll_preview_up(20), - Action::ToggleChannelSelection => { - match self.mode { - Mode::Channel => { - self.reset_screen(); - self.mode = Mode::Guide; - } - Mode::Guide => { - self.reset_screen(); - self.mode = Mode::Channel; - } - Mode::SendToChannel => {} + Action::ToggleChannelSelection => match self.mode { + Mode::Channel => { + self.reset_screen(); + self.mode = Mode::Guide; } - } + Mode::Guide => { + self.reset_screen(); + self.mode = Mode::Channel; + } + Mode::SendToChannel => {} + }, Action::SelectEntry => { if let Some(entry) = self.get_selected_entry() { match self.mode { @@ -387,19 +395,38 @@ impl Television { }, ); - let help_block = Block::default() - .borders(Borders::NONE) + let metadata_block = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(Color::Blue)) + .padding(Padding::horizontal(1)) + .style(Style::default()); + + let metadata_table = self.build_metadata_table().block(metadata_block); + + f.render_widget(metadata_table, layout.help_bar_left); + + let keymaps_block = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(Color::Blue)) .style(Style::default()) - .padding(Padding::uniform(1)); + .padding(Padding::horizontal(1)); + + let keymaps_table = self.build_help_table()?.block(keymaps_block); + + f.render_widget(keymaps_table, layout.help_bar_middle); + + let logo_block = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(Color::Blue)) + .style(Style::default().fg(Color::Yellow)) + .padding(Padding::horizontal(1)); - let help_text = self - .build_help_paragraph()? - .style(Style::default().fg(Color::DarkGray).italic()) - .alignment(Alignment::Center) - .wrap(Wrap { trim: true }) - .block(help_block); + let logo_paragraph = build_logo_paragraph().block(logo_block); - f.render_widget(help_text, layout.help_bar); + f.render_widget(logo_paragraph, layout.help_bar_right); self.results_area_height = u32::from(layout.results.height); if let Some(preview_window) = layout.preview_window { @@ -489,7 +516,7 @@ impl Television { "> ", Style::default().fg(DEFAULT_INPUT_FG).bold(), )) - .block(arrow_block); + .block(arrow_block); f.render_widget(arrow, inner_input_chunks[0]); let interactive_input_block = Block::default(); @@ -527,8 +554,8 @@ impl Television { ), Style::default().fg(DEFAULT_RESULTS_COUNT_FG).italic(), )) - .block(result_count_block) - .alignment(Alignment::Right); + .block(result_count_block) + .alignment(Alignment::Right); f.render_widget(result_count_paragraph, inner_input_chunks[2]); // Make the cursor visible and ask tui-rs to put it at the @@ -537,8 +564,8 @@ impl Television { // Put cursor past the end of the input text inner_input_chunks[1].x + u16::try_from( - self.input.visual_cursor().max(scroll) - scroll, - )?, + self.input.visual_cursor().max(scroll) - scroll, + )?, // Move one line down, from the border to the input line inner_input_chunks[1].y, )); diff --git a/crates/television/ui.rs b/crates/television/ui.rs index 42dc15e..8dde02f 100644 --- a/crates/television/ui.rs +++ b/crates/television/ui.rs @@ -3,6 +3,8 @@ use ratatui::style::{Color, Style, Stylize}; pub mod help; pub mod input; pub mod layout; +pub mod logo; +pub mod metadata; pub mod preview; pub mod results; pub mod spinner; diff --git a/crates/television/ui/help.rs b/crates/television/ui/help.rs index 83c15bd..418fe90 100644 --- a/crates/television/ui/help.rs +++ b/crates/television/ui/help.rs @@ -1,8 +1,9 @@ use color_eyre::eyre::{OptionExt, Result}; use ratatui::{ + layout::Constraint, style::{Color, Style}, text::{Line, Span}, - widgets::Paragraph, + widgets::{Cell, Row, Table}, }; use std::collections::HashMap; @@ -12,142 +13,118 @@ use crate::{ television::{Mode, Television}, }; -const SEPARATOR: &str = " "; const ACTION_COLOR: Color = Color::DarkGray; const KEY_COLOR: Color = Color::LightYellow; impl Television { - pub fn build_help_paragraph<'a>(&self) -> Result> { + pub fn build_help_table<'a>(&self) -> Result> { match self.mode { - Mode::Channel => self.build_help_paragraph_for_channel(), - Mode::Guide => self.build_help_paragraph_for_channel_selection(), - Mode::SendToChannel => self.build_help_paragraph_for_channel(), + Mode::Channel => self.build_help_table_for_channel(), + Mode::Guide => self.build_help_table_for_channel_selection(), + Mode::SendToChannel => self.build_help_table_for_channel(), } } - fn build_help_paragraph_for_channel<'a>(&self) -> Result> { + fn build_help_table_for_channel<'a>(&self) -> Result> { let keymap = self.keymap_for_mode()?; - let mut lines = Vec::new(); - - // NAVIGATION and SELECTION line - let mut ns_line = Line::default(); // Results navigation - let prev = keys_for_action(keymap, Action::SelectPrevEntry); - let next = keys_for_action(keymap, Action::SelectNextEntry); - let results_spans = - build_spans_for_key_groups("↕ Results", vec![prev, next]); - - ns_line.extend(results_spans); - ns_line.push_span(Span::styled(SEPARATOR, Style::default())); + let prev = keys_for_action(keymap, &Action::SelectPrevEntry); + let next = keys_for_action(keymap, &Action::SelectNextEntry); + let results_row = Row::new(build_cells_for_key_groups( + "↕ Results navigation", + vec![prev, next], + )); // Preview navigation - let up_keys = keys_for_action(keymap, Action::ScrollPreviewHalfPageUp); + let up_keys = + keys_for_action(keymap, &Action::ScrollPreviewHalfPageUp); let down_keys = - keys_for_action(keymap, Action::ScrollPreviewHalfPageDown); - let preview_spans = - build_spans_for_key_groups("↕ Preview", vec![up_keys, down_keys]); - - ns_line.extend(preview_spans); - ns_line.push_span(Span::styled(SEPARATOR, Style::default())); + keys_for_action(keymap, &Action::ScrollPreviewHalfPageDown); + let preview_row = Row::new(build_cells_for_key_groups( + "↕ Preview navigation", + vec![up_keys, down_keys], + )); // Select entry - let select_entry_keys = keys_for_action(keymap, Action::SelectEntry); - let select_entry_spans = build_spans_for_key_groups( - "Select entry", + let select_entry_keys = keys_for_action(keymap, &Action::SelectEntry); + let select_entry_row = Row::new(build_cells_for_key_groups( + "✓ Select entry", vec![select_entry_keys], - ); - - ns_line.extend(select_entry_spans); - ns_line.push_span(Span::styled(SEPARATOR, Style::default())); + )); // Send to channel let send_to_channel_keys = - keys_for_action(keymap, Action::SendToChannel); - // TODO: add send icon - let send_to_channel_spans = - build_spans_for_key_groups("Send to", vec![send_to_channel_keys]); - - ns_line.extend(send_to_channel_spans); - ns_line.push_span(Span::styled(SEPARATOR, Style::default())); + keys_for_action(keymap, &Action::SendToChannel); + let send_to_channel_row = Row::new(build_cells_for_key_groups( + "⇉ Send results to", + vec![send_to_channel_keys], + )); // Switch channels let switch_channels_keys = - keys_for_action(keymap, Action::ToggleChannelSelection); - let switch_channels_spans = build_spans_for_key_groups( - "Switch channels", + keys_for_action(keymap, &Action::ToggleChannelSelection); + let switch_channels_row = Row::new(build_cells_for_key_groups( + "⨀ Switch channels", vec![switch_channels_keys], - ); - - ns_line.extend(switch_channels_spans); - lines.push(ns_line); + )); // MISC line (quit, help, etc.) - // let mut misc_line = Line::default(); - // - // // Quit - // let quit_keys = keys_for_action(keymap, Action::Quit); - // let quit_spans = build_spans_for_key_groups("Quit", vec![quit_keys]); - // - // misc_line.extend(quit_spans); - // - // lines.push(misc_line); - - Ok(Paragraph::new(lines)) + // Quit ⏼ + let quit_keys = keys_for_action(keymap, &Action::Quit); + let quit_row = + Row::new(build_cells_for_key_groups("⏼ Quit", vec![quit_keys])); + + let widths = vec![Constraint::Fill(1), Constraint::Fill(2)]; + + Ok(Table::new( + vec![ + results_row, + preview_row, + select_entry_row, + send_to_channel_row, + switch_channels_row, + quit_row, + ], + widths, + )) } - fn build_help_paragraph_for_channel_selection<'a>( - &self, - ) -> Result> { + fn build_help_table_for_channel_selection<'a>(&self) -> Result> { let keymap = self.keymap_for_mode()?; - let mut lines = Vec::new(); - - // NAVIGATION + SELECTION line - let mut ns_line = Line::default(); // Results navigation - let prev = keys_for_action(keymap, Action::SelectPrevEntry); - let next = keys_for_action(keymap, Action::SelectNextEntry); - let results_spans = - build_spans_for_key_groups("↕ Results", vec![prev, next]); - - ns_line.extend(results_spans); - ns_line.push_span(Span::styled(SEPARATOR, Style::default())); + let prev = keys_for_action(keymap, &Action::SelectPrevEntry); + let next = keys_for_action(keymap, &Action::SelectNextEntry); + let results_row = Row::new(build_cells_for_key_groups( + "↕ Results", + vec![prev, next], + )); // Select entry - let select_entry_keys = keys_for_action(keymap, Action::SelectEntry); - let select_entry_spans = build_spans_for_key_groups( + let select_entry_keys = keys_for_action(keymap, &Action::SelectEntry); + let select_entry_row = Row::new(build_cells_for_key_groups( "Select entry", vec![select_entry_keys], - ); - - ns_line.extend(select_entry_spans); - ns_line.push_span(Span::styled(SEPARATOR, Style::default())); + )); // Switch channels let switch_channels_keys = - keys_for_action(keymap, Action::ToggleChannelSelection); - let switch_channels_spans = build_spans_for_key_groups( + keys_for_action(keymap, &Action::ToggleChannelSelection); + let switch_channels_row = Row::new(build_cells_for_key_groups( "Switch channels", vec![switch_channels_keys], - ); - - ns_line.extend(switch_channels_spans); - - lines.push(ns_line); - - // MISC line (quit, help, etc.) - // let mut misc_line = Line::default(); + )); // Quit - // let quit_keys = keys_for_action(keymap, Action::Quit); - // let quit_spans = build_spans_for_key_groups("Quit", vec![quit_keys]); - - // misc_line.extend(quit_spans); - - // lines.push(misc_line); - - Ok(Paragraph::new(lines)) + let quit_keys = keys_for_action(keymap, &Action::Quit); + let quit_row = + Row::new(build_cells_for_key_groups("Quit", vec![quit_keys])); + + Ok(Table::new( + vec![results_row, select_entry_row, switch_channels_row, quit_row], + vec![Constraint::Fill(1), Constraint::Fill(2)], + )) } /// Get the keymap for the current mode. @@ -189,21 +166,23 @@ impl Television { /// /// assert_eq!(spans.len(), 5); /// ``` -fn build_spans_for_key_groups( +fn build_cells_for_key_groups( group_name: &str, key_groups: Vec>, -) -> Vec { - if key_groups.is_empty() || key_groups.iter().all(|keys| keys.is_empty()) { - return vec![]; +) -> Vec { + if key_groups.is_empty() || key_groups.iter().all(std::vec::Vec::is_empty) + { + return vec![group_name.into(), "No keybindings".into()]; } let non_empty_groups = key_groups.iter().filter(|keys| !keys.is_empty()); - let mut spans = vec![ - Span::styled( - group_name.to_owned() + ": ", - Style::default().fg(ACTION_COLOR), - ), - Span::styled("[", Style::default().fg(KEY_COLOR)), - ]; + let mut cells = vec![Cell::from(Span::styled( + group_name.to_owned() + ": ", + Style::default().fg(ACTION_COLOR), + ))]; + + let mut spans = Vec::new(); + //spans.push(Span::styled("[", Style::default().fg(KEY_COLOR))); + let key_group_spans: Vec = non_empty_groups .map(|keys| { let key_group = keys.join(", "); @@ -213,12 +192,14 @@ fn build_spans_for_key_groups( key_group_spans.iter().enumerate().for_each(|(i, span)| { spans.push(span.clone()); if i < key_group_spans.len() - 1 { - spans.push(Span::styled(" | ", Style::default().fg(KEY_COLOR))); + spans.push(Span::styled(" / ", Style::default().fg(KEY_COLOR))); } }); - spans.push(Span::styled("]", Style::default().fg(KEY_COLOR))); - spans + //spans.push(Span::styled("]", Style::default().fg(KEY_COLOR))); + cells.push(Cell::from(Line::from(spans))); + + cells } /// Get the keys for a given action. @@ -246,11 +227,11 @@ fn build_spans_for_key_groups( /// ``` fn keys_for_action( keymap: &HashMap, - action: Action, + action: &Action, ) -> Vec { keymap .iter() - .filter(|(_key, act)| **act == action) + .filter(|(_key, act)| *act == action) .map(|(key, _act)| format!("{key}")) .collect() } diff --git a/crates/television/ui/layout.rs b/crates/television/ui/layout.rs index d31ff59..904aeb7 100644 --- a/crates/television/ui/layout.rs +++ b/crates/television/ui/layout.rs @@ -19,7 +19,9 @@ impl Default for Dimensions { } pub struct Layout { - pub help_bar: Rect, + pub help_bar_left: Rect, + pub help_bar_middle: Rect, + pub help_bar_right: Rect, pub results: Rect, pub input: Rect, pub preview_title: Option, @@ -28,14 +30,18 @@ pub struct Layout { impl Layout { pub fn new( - help_bar: Rect, + help_bar_left: Rect, + help_bar_middle: Rect, + help_bar_right: Rect, results: Rect, input: Rect, preview_title: Option, preview_window: Option, ) -> Self { Self { - help_bar, + help_bar_left, + help_bar_middle, + help_bar_right, results, input, preview_title, @@ -52,9 +58,19 @@ impl Layout { // split the main block into two vertical chunks (help bar + rest) let hz_chunks = layout::Layout::default() .direction(Direction::Vertical) - .constraints([Constraint::Fill(1), Constraint::Length(5)]) + .constraints([Constraint::Length(9), Constraint::Fill(1)]) .split(main_block); + // split the help bar into three horizontal chunks (left + center + right) + let help_bar_chunks = layout::Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Fill(1), + Constraint::Fill(1), + Constraint::Length(24), + ]) + .split(hz_chunks[0]); + if with_preview { // split the main block into two vertical chunks let vt_chunks = layout::Layout::default() @@ -63,7 +79,7 @@ impl Layout { Constraint::Percentage(50), Constraint::Percentage(50), ]) - .split(hz_chunks[0]); + .split(hz_chunks[1]); // left block: results + input field let left_chunks = layout::Layout::default() @@ -78,7 +94,9 @@ impl Layout { .split(vt_chunks[1]); Self::new( - hz_chunks[1], + help_bar_chunks[0], + help_bar_chunks[1], + help_bar_chunks[2], left_chunks[0], left_chunks[1], Some(right_chunks[0]), @@ -89,9 +107,17 @@ impl Layout { let chunks = layout::Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Min(10), Constraint::Length(3)]) - .split(hz_chunks[0]); + .split(hz_chunks[1]); - Self::new(hz_chunks[1], chunks[0], chunks[1], None, None) + Self::new( + help_bar_chunks[0], + help_bar_chunks[1], + help_bar_chunks[2], + chunks[0], + chunks[1], + None, + None, + ) } } } diff --git a/crates/television/ui/logo.rs b/crates/television/ui/logo.rs new file mode 100644 index 0000000..414306f --- /dev/null +++ b/crates/television/ui/logo.rs @@ -0,0 +1,26 @@ +use ratatui::widgets::Paragraph; + +//const LOGO: &str = r" _______________ +// __ __ _ _ |,----------. |\ +// / /____ / /__ _ __(_)__ (_)__ ___ || |=| | +// / __/ -_) / -_) |/ / (_-() -> Paragraph<'a> { + let lines = LOGO + .lines() + .map(std::convert::Into::into) + .collect::>(); + let logo_paragraph = Paragraph::new(lines); + logo_paragraph +} diff --git a/crates/television/ui/metadata.rs b/crates/television/ui/metadata.rs new file mode 100644 index 0000000..6d4e9c1 --- /dev/null +++ b/crates/television/ui/metadata.rs @@ -0,0 +1,113 @@ +use ratatui::{ + layout::Constraint, + style::{Color, Style}, + text::Span, + widgets::{Cell, Row, Table}, +}; + +use crate::television::Television; + +// television 0.1.6 +// target triple: aarch64-apple-darwin +// build: 1.82.0 (2024-10-24) +// current_channel: git_repos +// current_mode: channel + +impl Television { + pub fn build_metadata_table<'a>(&self) -> Table<'a> { + let version_row = Row::new(vec![ + Cell::from(Span::styled( + "version: ", + Style::default().fg(Color::DarkGray), + )), + Cell::from(Span::styled( + env!("CARGO_PKG_VERSION"), + Style::default().fg(Color::LightYellow), + )), + ]); + + let target_triple_row = Row::new(vec![ + Cell::from(Span::styled( + "target triple: ", + Style::default().fg(Color::DarkGray), + )), + Cell::from(Span::styled( + env!("VERGEN_CARGO_TARGET_TRIPLE"), + Style::default().fg(Color::LightYellow), + )), + ]); + + let build_row = Row::new(vec![ + Cell::from(Span::styled( + "build: ", + Style::default().fg(Color::DarkGray), + )), + Cell::from(Span::styled( + env!("VERGEN_RUSTC_SEMVER"), + Style::default().fg(Color::LightYellow), + )), + Cell::from(Span::styled( + " (", + Style::default().fg(Color::DarkGray), + )), + Cell::from(Span::styled( + env!("VERGEN_BUILD_DATE"), + Style::default().fg(Color::LightYellow), + )), + Cell::from(Span::styled( + ")", + Style::default().fg(Color::DarkGray), + )), + ]); + + let current_dir_row = Row::new(vec![ + Cell::from(Span::styled( + "current directory: ", + Style::default().fg(Color::DarkGray), + )), + Cell::from(Span::styled( + std::env::current_dir() + .expect("Could not get current directory") + .display() + .to_string(), + Style::default().fg(Color::LightYellow), + )), + ]); + + let current_channel_row = Row::new(vec![ + Cell::from(Span::styled( + "current channel: ", + Style::default().fg(Color::DarkGray), + )), + Cell::from(Span::styled( + self.current_channel().to_string(), + Style::default().fg(Color::LightYellow), + )), + ]); + + let current_mode_row = Row::new(vec![ + Cell::from(Span::styled( + "current mode: ", + Style::default().fg(Color::DarkGray), + )), + Cell::from(Span::styled( + self.mode.to_string(), + Style::default().fg(Color::LightYellow), + )), + ]); + + let widths = vec![Constraint::Fill(1), Constraint::Fill(2)]; + + Table::new( + vec![ + version_row, + target_triple_row, + build_row, + current_dir_row, + current_channel_row, + current_mode_row, + ], + widths, + ) + } +} diff --git a/crates/television_derive/src/lib.rs b/crates/television_derive/src/lib.rs index b830aa7..0b27594 100644 --- a/crates/television_derive/src/lib.rs +++ b/crates/television_derive/src/lib.rs @@ -224,7 +224,7 @@ fn impl_unit_channel(ast: &syn::DeriveInput) -> TokenStream { // Generate a unit enum from the given enum let unit_enum = quote! { - #[derive(Debug, Clone, Copy, PartialEq, Eq)] + #[derive(Debug, Clone, Copy, PartialEq, Eq, Display)] pub enum UnitChannel { #( #variant_names,