From 220671106e621454e2088ccf08bc9957f240bbec Mon Sep 17 00:00:00 2001 From: Alexandre Pasmantier <47638216+alexpasmantier@users.noreply.github.com> Date: Mon, 25 Nov 2024 21:32:41 +0100 Subject: [PATCH] feat(layout): allow reversing the layout and placing input bar on top (#76) --- .config/config.toml | 2 + crates/television/app.rs | 10 ++--- crates/television/config/ui.rs | 8 ++++ crates/television/main.rs | 2 +- crates/television/render.rs | 9 +--- crates/television/television.rs | 59 +++++++++++--------------- crates/television/ui/help.rs | 7 +-- crates/television/ui/input.rs | 9 ++-- crates/television/ui/layout.rs | 58 ++++++++++++++++++++++--- crates/television/ui/preview.rs | 13 +++--- crates/television/ui/remote_control.rs | 4 +- crates/television/ui/results.rs | 15 ++++--- 12 files changed, 117 insertions(+), 79 deletions(-) diff --git a/.config/config.toml b/.config/config.toml index 8b58926..dc5eca0 100644 --- a/.config/config.toml +++ b/.config/config.toml @@ -25,6 +25,8 @@ use_nerd_font_icons = false ui_scale = 100 # Whether to show the top help bar in the UI show_help_bar = true +# Where to place the input bar in the UI (top or bottom) +input_bar_position = "bottom" # Previewers settings # ---------------------------------------------------------------------------- diff --git a/crates/television/app.rs b/crates/television/app.rs index 68cb584..3bdc9d1 100644 --- a/crates/television/app.rs +++ b/crates/television/app.rs @@ -59,8 +59,6 @@ impl Keymap { /// The main application struct that holds the state of the application. pub struct App { - /// The configuration of the application. - config: Config, keymap: Keymap, // maybe move these two into config instead of passing them // via the cli? @@ -123,13 +121,12 @@ impl App { channel: TelevisionChannel, tick_rate: f64, frame_rate: f64, - passthrough_keybindings: Vec, + passthrough_keybindings: &[String], ) -> Result { let (action_tx, action_rx) = mpsc::unbounded_channel(); let (render_tx, _) = mpsc::unbounded_channel(); let (_, event_rx) = mpsc::unbounded_channel(); let (event_abort_tx, _) = mpsc::unbounded_channel(); - let television = Arc::new(Mutex::new(Television::new(channel))); let config = Config::new()?; let keymap = Keymap::from(&config.keybindings).with_mode_mappings( Mode::Channel, @@ -142,9 +139,10 @@ impl App { .collect(), )?; debug!("{:?}", keymap); + let television = + Arc::new(Mutex::new(Television::new(channel, config.clone()))); Ok(Self { - config, keymap, tick_rate, frame_rate, @@ -184,14 +182,12 @@ impl App { let (render_tx, render_rx) = mpsc::unbounded_channel(); self.render_tx = render_tx.clone(); let action_tx_r = self.action_tx.clone(); - let config_r = self.config.clone(); let television_r = self.television.clone(); let frame_rate = self.frame_rate; let rendering_task = tokio::spawn(async move { render( render_rx, action_tx_r, - config_r, television_r, frame_rate, is_output_tty, diff --git a/crates/television/config/ui.rs b/crates/television/config/ui.rs index 480c1b4..a614b5d 100644 --- a/crates/television/config/ui.rs +++ b/crates/television/config/ui.rs @@ -2,6 +2,8 @@ use config::ValueKind; use serde::Deserialize; use std::collections::HashMap; +use crate::ui::layout::InputPosition; + const DEFAULT_UI_SCALE: u16 = 90; #[derive(Clone, Debug, Deserialize)] @@ -9,6 +11,7 @@ pub struct UiConfig { pub use_nerd_font_icons: bool, pub ui_scale: u16, pub show_help_bar: bool, + pub input_bar_position: InputPosition, } impl Default for UiConfig { @@ -17,6 +20,7 @@ impl Default for UiConfig { use_nerd_font_icons: false, ui_scale: DEFAULT_UI_SCALE, show_help_bar: true, + input_bar_position: InputPosition::Bottom, } } } @@ -36,6 +40,10 @@ impl From for ValueKind { String::from("show_help_bar"), ValueKind::Boolean(val.show_help_bar).into(), ); + m.insert( + String::from("input_position"), + ValueKind::String(val.input_bar_position.to_string()).into(), + ); ValueKind::Table(m) } } diff --git a/crates/television/main.rs b/crates/television/main.rs index e133c50..91bb44a 100644 --- a/crates/television/main.rs +++ b/crates/television/main.rs @@ -45,7 +45,7 @@ async fn main() -> Result<()> { }, args.tick_rate, args.frame_rate, - args.passthrough_keybindings, + &args.passthrough_keybindings, ) { Ok(mut app) => { stdout().flush()?; diff --git a/crates/television/render.rs b/crates/television/render.rs index 6d7363c..93f710d 100644 --- a/crates/television/render.rs +++ b/crates/television/render.rs @@ -12,7 +12,7 @@ use tokio::{ }; use crate::television::Television; -use crate::{action::Action, config::Config, tui::Tui}; +use crate::{action::Action, tui::Tui}; #[derive(Debug)] pub enum RenderingTask { @@ -42,7 +42,6 @@ impl IoStream { pub async fn render( mut render_rx: mpsc::UnboundedReceiver, action_tx: mpsc::UnboundedSender, - config: Config, television: Arc>, frame_rate: f64, is_output_tty: bool, @@ -59,15 +58,11 @@ pub async fn render( debug!("Entering tui"); tui.enter()?; - debug!("Registering action handler and config handler"); + debug!("Registering action handler"); television .lock() .await .register_action_handler(action_tx.clone())?; - television - .lock() - .await - .register_config_handler(config.clone())?; // Rendering loop loop { diff --git a/crates/television/television.rs b/crates/television/television.rs index 67f7379..286f105 100644 --- a/crates/television/television.rs +++ b/crates/television/television.rs @@ -1,7 +1,7 @@ use crate::app::Keymap; use crate::picker::Picker; use crate::ui::input::actions::InputActionHandler; -use crate::ui::layout::{Dimensions, Layout}; +use crate::ui::layout::{Dimensions, InputPosition, Layout}; use crate::ui::spinner::Spinner; use crate::ui::spinner::SpinnerState; use crate::{action::Action, config::Config}; @@ -14,7 +14,6 @@ use television_channels::channels::{ remote_control::RemoteControl, OnAir, TelevisionChannel, UnitChannel, }; use television_channels::entry::{Entry, ENTRY_PLACEHOLDER}; -use television_previewers::previewers; use television_previewers::previewers::Previewer; use television_utils::strings::EMPTY_STRING; use tokio::sync::mpsc::UnboundedSender; @@ -54,23 +53,30 @@ pub struct Television { impl Television { #[must_use] - pub fn new(mut channel: TelevisionChannel) -> Self { + pub fn new(mut channel: TelevisionChannel, config: Config) -> Self { + let results_picker = match config.ui.input_bar_position { + InputPosition::Bottom => Picker::default().inverted(), + InputPosition::Top => Picker::default(), + }; + let previewer = Previewer::new(Some(config.previewers.clone().into())); + let keymap = Keymap::from(&config.keybindings); + channel.find(EMPTY_STRING); let spinner = Spinner::default(); Self { action_tx: None, - config: Config::default(), - keymap: Keymap::default(), + config, + keymap, channel, remote_control: TelevisionChannel::RemoteControl( RemoteControl::default(), ), mode: Mode::Channel, current_pattern: EMPTY_STRING.to_string(), - results_picker: Picker::default().inverted(), + results_picker, rc_picker: Picker::default(), results_area_height: 0, - previewer: Previewer::default(), + previewer, preview_scroll: None, preview_pane_height: 0, current_preview_total_lines: 0, @@ -220,24 +226,6 @@ impl Television { Ok(()) } - /// Register a configuration handler that provides configuration settings if necessary. - /// - /// # Arguments - /// * `config` - Configuration settings. - /// - /// # Returns - /// * `Result<()>` - An Ok result or an error. - pub fn register_config_handler(&mut self, config: Config) -> Result<()> { - self.config = config; - self.keymap = Keymap::from(&self.config.keybindings); - let previewer_config = - std::convert::Into::::into( - self.config.previewers.clone(), - ); - self.previewer.set_config(previewer_config); - Ok(()) - } - /// Update the state of the component based on a received action. /// /// # Arguments @@ -388,33 +376,34 @@ impl Television { area, !matches!(self.mode, Mode::Channel), self.config.ui.show_help_bar, + self.config.ui.input_bar_position, ); // help bar (metadata, keymaps, logo) - self.draw_help_bar(f, &layout)?; + self.draw_help_bar(f, &layout.help_bar)?; self.results_area_height = u32::from(layout.results.height - 2); // 2 for the borders self.preview_pane_height = layout.preview_window.height; - // top left block: results - self.draw_results_list(f, &layout)?; + // results list + self.draw_results_list(f, layout.results)?; - // bottom left block: input - self.draw_input_box(f, &layout)?; + // input box + self.draw_input_box(f, layout.input)?; let selected_entry = self .get_selected_entry(Some(Mode::Channel)) .unwrap_or(ENTRY_PLACEHOLDER); let preview = self.previewer.preview(&selected_entry); - // top right block: preview title + // preview title self.current_preview_total_lines = preview.total_lines(); - self.draw_preview_title_block(f, &layout, &preview)?; + self.draw_preview_title_block(f, layout.preview_title, &preview)?; - // bottom right block: preview content + // preview content self.draw_preview_content_block( f, - &layout, + layout.preview_window, selected_entry .line_number .map(|l| u16::try_from(l).unwrap_or(0)), @@ -423,7 +412,7 @@ impl Television { // remote control if matches!(self.mode, Mode::RemoteControl | Mode::SendToChannel) { - self.draw_remote_control(f, &layout.remote_control.unwrap())?; + self.draw_remote_control(f, layout.remote_control.unwrap())?; } Ok(()) } diff --git a/crates/television/ui/help.rs b/crates/television/ui/help.rs index 761537e..ed45feb 100644 --- a/crates/television/ui/help.rs +++ b/crates/television/ui/help.rs @@ -1,5 +1,4 @@ use crate::television::Television; -use crate::ui::layout::Layout; use crate::ui::logo::build_logo_paragraph; use crate::ui::mode::mode_color; use ratatui::layout::Rect; @@ -7,6 +6,8 @@ use ratatui::prelude::{Color, Style}; use ratatui::widgets::{Block, BorderType, Borders, Padding}; use ratatui::Frame; +use super::layout::HelpBarLayout; + pub fn draw_logo_block(f: &mut Frame, area: Rect, color: Color) { let logo_block = Block::default() .borders(Borders::ALL) @@ -24,9 +25,9 @@ impl Television { pub(crate) fn draw_help_bar( &self, f: &mut Frame, - layout: &Layout, + layout: &Option, ) -> color_eyre::Result<()> { - if let Some(help_bar) = layout.help_bar { + if let Some(help_bar) = layout { self.draw_metadata_block(f, help_bar.left); self.draw_keymaps_block(f, help_bar.middle)?; draw_logo_block(f, help_bar.right, mode_color(self.mode)); diff --git a/crates/television/ui/input.rs b/crates/television/ui/input.rs index d1a2639..7b7c7f2 100644 --- a/crates/television/ui/input.rs +++ b/crates/television/ui/input.rs @@ -1,9 +1,8 @@ use crate::television::Television; -use crate::ui::layout::Layout; use crate::ui::BORDER_COLOR; use color_eyre::eyre::Result; use ratatui::layout::{ - Alignment, Constraint, Direction, Layout as RatatuiLayout, + Alignment, Constraint, Direction, Layout as RatatuiLayout, Rect, }; use ratatui::prelude::{Span, Style}; use ratatui::style::Stylize; @@ -416,7 +415,7 @@ impl Television { pub(crate) fn draw_input_box( &mut self, f: &mut Frame, - layout: &Layout, + rect: Rect, ) -> Result<()> { let input_block = Block::default() .title_top(Line::from(" Pattern ").alignment(Alignment::Center)) @@ -425,12 +424,12 @@ impl Television { .border_style(Style::default().fg(BORDER_COLOR)) .style(Style::default()); - let input_block_inner = input_block.inner(layout.input); + let input_block_inner = input_block.inner(rect); if input_block_inner.area() == 0 { return Ok(()); } - f.render_widget(input_block, layout.input); + f.render_widget(input_block, rect); // split input block into 4 parts: prompt symbol, input, result count, spinner let total_count = self.channel.total_count(); diff --git a/crates/television/ui/layout.rs b/crates/television/ui/layout.rs index c45ffd8..23b3440 100644 --- a/crates/television/ui/layout.rs +++ b/crates/television/ui/layout.rs @@ -1,5 +1,8 @@ +use std::fmt::Display; + use ratatui::layout; use ratatui::layout::{Constraint, Direction, Rect}; +use serde::Deserialize; pub struct Dimensions { pub x: u16, @@ -41,6 +44,24 @@ impl HelpBarLayout { } } +#[derive(Debug, Clone, Copy, Deserialize, Default)] +pub enum InputPosition { + #[serde(rename = "top")] + Top, + #[serde(rename = "bottom")] + #[default] + Bottom, +} + +impl Display for InputPosition { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + InputPosition::Top => write!(f, "top"), + InputPosition::Bottom => write!(f, "bottom"), + } + } +} + pub struct Layout { pub help_bar: Option, pub results: Rect, @@ -75,6 +96,7 @@ impl Layout { area: Rect, with_remote: bool, with_help_bar: bool, + input_position: InputPosition, ) -> Self { let main_block = centered_rect(dimensions.x, dimensions.y, area); // split the main block into two vertical chunks (help bar + rest) @@ -127,23 +149,47 @@ impl Layout { .split(main_rect); // left block: results + input field + let results_constraints = + vec![Constraint::Min(3), Constraint::Length(3)]; + let left_chunks = layout::Layout::default() .direction(Direction::Vertical) - .constraints([Constraint::Min(3), Constraint::Length(3)]) + .constraints(match input_position { + InputPosition::Top => { + results_constraints.into_iter().rev().collect() + } + InputPosition::Bottom => results_constraints, + }) .split(vt_chunks[0]); + let (input, results) = match input_position { + InputPosition::Bottom => (left_chunks[1], left_chunks[0]), + InputPosition::Top => (left_chunks[0], left_chunks[1]), + }; // right block: preview title + preview + let preview_constraints = + vec![Constraint::Length(3), Constraint::Min(3)]; + let right_chunks = layout::Layout::default() .direction(Direction::Vertical) - .constraints([Constraint::Length(3), Constraint::Min(3)]) + .constraints(match input_position { + InputPosition::Top => { + preview_constraints.into_iter().rev().collect() + } + InputPosition::Bottom => preview_constraints, + }) .split(vt_chunks[1]); + let (preview_title, preview_window) = match input_position { + InputPosition::Bottom => (right_chunks[0], right_chunks[1]), + InputPosition::Top => (right_chunks[1], right_chunks[0]), + }; Self::new( help_bar_layout, - left_chunks[0], - left_chunks[1], - right_chunks[0], - right_chunks[1], + results, + input, + preview_title, + preview_window, if with_remote { Some(vt_chunks[2]) } else { diff --git a/crates/television/ui/preview.rs b/crates/television/ui/preview.rs index 2b0a549..c5f61d0 100644 --- a/crates/television/ui/preview.rs +++ b/crates/television/ui/preview.rs @@ -1,5 +1,4 @@ use crate::television::Television; -use crate::ui::layout::Layout; use crate::ui::BORDER_COLOR; use color_eyre::eyre::Result; use ratatui::layout::{Alignment, Rect}; @@ -26,7 +25,7 @@ impl Television { pub(crate) fn draw_preview_title_block( &self, f: &mut Frame, - layout: &Layout, + rect: Rect, preview: &Arc, ) -> Result<()> { let mut preview_title_spans = Vec::new(); @@ -44,7 +43,7 @@ impl Television { preview_title_spans.push(Span::styled( shrink_with_ellipsis( &preview.title, - layout.preview_window.width.saturating_sub(4) as usize, + rect.width.saturating_sub(4) as usize, ), Style::default().fg(DEFAULT_PREVIEW_TITLE_FG).bold(), )); @@ -57,14 +56,14 @@ impl Television { .border_style(Style::default().fg(BORDER_COLOR)), ) .alignment(Alignment::Left); - f.render_widget(preview_title, layout.preview_title); + f.render_widget(preview_title, rect); Ok(()) } pub(crate) fn draw_preview_content_block( &mut self, f: &mut Frame, - layout: &Layout, + rect: Rect, target_line: Option, preview: &Arc, ) { @@ -83,8 +82,8 @@ impl Television { bottom: 0, left: 1, }); - let inner = preview_outer_block.inner(layout.preview_window); - f.render_widget(preview_outer_block, layout.preview_window); + let inner = preview_outer_block.inner(rect); + f.render_widget(preview_outer_block, rect); //if let PreviewContent::Image(img) = &preview.content { // let image_component = StatefulImage::new(None); diff --git a/crates/television/ui/remote_control.rs b/crates/television/ui/remote_control.rs index 32c251f..9908e92 100644 --- a/crates/television/ui/remote_control.rs +++ b/crates/television/ui/remote_control.rs @@ -18,7 +18,7 @@ impl Television { pub fn draw_remote_control( &mut self, f: &mut Frame, - area: &Rect, + rect: Rect, ) -> Result<()> { let layout = Layout::default() .direction(Direction::Vertical) @@ -30,7 +30,7 @@ impl Television { ] .as_ref(), ) - .split(*area); + .split(rect); self.draw_rc_channels(f, &layout[0])?; self.draw_rc_input(f, &layout[1])?; draw_rc_logo(f, layout[2], mode_color(self.mode)); diff --git a/crates/television/ui/results.rs b/crates/television/ui/results.rs index 1643c60..a29a333 100644 --- a/crates/television/ui/results.rs +++ b/crates/television/ui/results.rs @@ -1,8 +1,8 @@ use crate::television::Television; -use crate::ui::layout::Layout; +use crate::ui::layout::InputPosition; use crate::ui::BORDER_COLOR; use color_eyre::eyre::Result; -use ratatui::layout::Alignment; +use ratatui::layout::{Alignment, Rect}; use ratatui::prelude::{Color, Line, Span, Style}; use ratatui::widgets::{ Block, BorderType, Borders, List, ListDirection, Padding, @@ -164,7 +164,7 @@ impl Television { pub(crate) fn draw_results_list( &mut self, f: &mut Frame, - layout: &Layout, + rect: Rect, ) -> Result<()> { let results_block = Block::default() .title_top(Line::from(" Results ").alignment(Alignment::Center)) @@ -181,21 +181,24 @@ impl Television { } let entries = self.channel.results( - layout.results.height.saturating_sub(2).into(), + rect.height.saturating_sub(2).into(), u32::try_from(self.results_picker.offset())?, ); let results_list = build_results_list( results_block, &entries, - ListDirection::BottomToTop, + match self.config.ui.input_bar_position { + InputPosition::Bottom => ListDirection::BottomToTop, + InputPosition::Top => ListDirection::TopToBottom, + }, None, self.config.ui.use_nerd_font_icons, ); f.render_stateful_widget( results_list, - layout.results, + rect, &mut self.results_picker.relative_state, ); Ok(())