diff --git a/benches/main/ui.rs b/benches/main/ui.rs index be1f46d..8e095f4 100644 --- a/benches/main/ui.rs +++ b/benches/main/ui.rs @@ -16,6 +16,7 @@ use television::{ action::Action, cable::Cable, channels::entry::{Entry, into_ranges}, + cli::PostProcessedCli, config::{Config, ConfigEnv}, screen::{colors::ResultsColorscheme, result_item::build_results_list}, television::Television, @@ -484,12 +485,8 @@ pub fn draw(c: &mut Criterion) { tx, channel_prototype, config, - None, - false, - false, - Some(50), - false, cable.clone(), + PostProcessedCli::default(), ); tv.find("television"); for _ in 0..5 { diff --git a/television/action.rs b/television/action.rs index 8ce4d2a..6f051ae 100644 --- a/television/action.rs +++ b/television/action.rs @@ -367,6 +367,101 @@ impl Display for Action { } } +impl Action { + /// Returns a user-friendly description of the action for help panels and UI display. + /// + /// This method provides human-readable descriptions of actions that are suitable + /// for display in help panels, tooltips, and other user interfaces. Unlike the + /// `Display` implementation which returns `snake_case` configuration names, this + /// method returns descriptive text. + /// + /// # Returns + /// + /// A static string slice containing the user-friendly description. + /// + /// # Examples + /// + /// ```rust + /// use television::action::Action; + /// + /// assert_eq!(Action::Quit.description(), "Quit"); + /// assert_eq!(Action::SelectNextEntry.description(), "Navigate down"); + /// assert_eq!(Action::TogglePreview.description(), "Toggle preview"); + /// ``` + pub fn description(&self) -> &'static str { + match self { + // Input actions + Action::AddInputChar(_) => "Add character", + Action::DeletePrevChar => "Delete previous char", + Action::DeletePrevWord => "Delete previous word", + Action::DeleteNextChar => "Delete next char", + Action::DeleteLine => "Delete line", + Action::GoToPrevChar => "Move cursor left", + Action::GoToNextChar => "Move cursor right", + Action::GoToInputStart => "Move to start", + Action::GoToInputEnd => "Move to end", + + // Rendering actions (typically not shown in help) + Action::Render => "Render", + Action::Resize(_, _) => "Resize", + Action::ClearScreen => "Clear screen", + + // Selection actions + Action::ToggleSelectionDown => "Toggle selection down", + Action::ToggleSelectionUp => "Toggle selection up", + Action::ConfirmSelection => "Select entry", + Action::SelectAndExit => "Select and exit", + + // Navigation actions + Action::SelectNextEntry => "Navigate down", + Action::SelectPrevEntry => "Navigate up", + Action::SelectNextPage => "Page down", + Action::SelectPrevPage => "Page up", + Action::CopyEntryToClipboard => "Copy to clipboard", + + // Preview actions + Action::ScrollPreviewUp => "Preview scroll up", + Action::ScrollPreviewDown => "Preview scroll down", + Action::ScrollPreviewHalfPageUp => "Preview scroll half page up", + Action::ScrollPreviewHalfPageDown => { + "Preview scroll half page down" + } + Action::OpenEntry => "Open entry", + + // Application actions + Action::Tick => "Tick", + Action::Suspend => "Suspend", + Action::Resume => "Resume", + Action::Quit => "Quit", + + // Toggle actions + Action::ToggleRemoteControl => "Toggle remote control", + Action::ToggleHelp => "Toggle help", + Action::ToggleStatusBar => "Toggle status bar", + Action::TogglePreview => "Toggle preview", + + // Error and no-op + Action::Error(_) => "Error", + Action::NoOp => "No operation", + + // Channel actions + Action::ToggleSendToChannel => "Toggle send to channel", + Action::CycleSources => "Cycle sources", + Action::ReloadSource => "Reload source", + Action::SwitchToChannel(_) => "Switch to channel", + Action::WatchTimer => "Watch timer", + + // History actions + Action::SelectPrevHistory => "Previous history", + Action::SelectNextHistory => "Next history", + + // Mouse actions + Action::SelectEntryAtPosition(_, _) => "Select at position", + Action::MouseClickAt(_, _) => "Mouse click", + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -448,4 +543,27 @@ mod tests { assert_eq!(map.get(&actions2), Some(&"single")); assert_eq!(map.get(&actions4), Some(&"multiple")); } + + #[test] + fn test_action_description() { + // Test that description() returns user-friendly text + assert_eq!(Action::Quit.description(), "Quit"); + assert_eq!(Action::SelectNextEntry.description(), "Navigate down"); + assert_eq!(Action::SelectPrevEntry.description(), "Navigate up"); + assert_eq!(Action::TogglePreview.description(), "Toggle preview"); + assert_eq!(Action::ToggleHelp.description(), "Toggle help"); + assert_eq!(Action::ConfirmSelection.description(), "Select entry"); + assert_eq!( + Action::CopyEntryToClipboard.description(), + "Copy to clipboard" + ); + + // Test that description() differs from Display (snake_case) + assert_ne!( + Action::SelectNextEntry.description(), + Action::SelectNextEntry.to_string() + ); + assert_eq!(Action::SelectNextEntry.to_string(), "select_next_entry"); + assert_eq!(Action::SelectNextEntry.description(), "Navigate down"); + } } diff --git a/television/app.rs b/television/app.rs index 858c039..33c2205 100644 --- a/television/app.rs +++ b/television/app.rs @@ -2,6 +2,7 @@ use crate::{ action::Action, cable::Cable, channels::{entry::Entry, prototypes::ChannelPrototype}, + cli::PostProcessedCli, config::{Config, DEFAULT_PREVIEW_SIZE, default_tick_rate}, event::{Event, EventLoop, InputEvent, Key, MouseInputEvent}, history::History, @@ -178,9 +179,9 @@ impl App { pub fn new( channel_prototype: ChannelPrototype, config: Config, - input: Option, options: AppOptions, cable_channels: Cable, + cli_args: &PostProcessedCli, ) -> Self { let (action_tx, action_rx) = mpsc::unbounded_channel(); let (render_tx, render_rx) = mpsc::unbounded_channel(); @@ -192,12 +193,8 @@ impl App { action_tx.clone(), channel_prototype, config, - input, - options.no_remote, - options.no_preview, - options.preview_size, - options.exact, cable_channels, + cli_args.clone(), ); // Create input map from the merged config that includes both key and event bindings diff --git a/television/config/keybindings.rs b/television/config/keybindings.rs index 2252973..1cb6f79 100644 --- a/television/config/keybindings.rs +++ b/television/config/keybindings.rs @@ -317,12 +317,18 @@ pub fn merge_bindings( new_bindings: &Bindings, ) -> Bindings where - K: Display + FromStr + Clone + Eq + Hash, + K: Display + FromStr + Clone + Eq + Hash + std::fmt::Debug, K::Err: Display, { + debug!("bindings before: {:?}", bindings.bindings); + + // Merge new bindings - they take precedence over existing ones for (key, actions) in &new_bindings.bindings { bindings.bindings.insert(key.clone(), actions.clone()); } + + debug!("bindings after: {:?}", bindings.bindings); + bindings } diff --git a/television/config/mod.rs b/television/config/mod.rs index f1e4a6e..47c4654 100644 --- a/television/config/mod.rs +++ b/television/config/mod.rs @@ -1,7 +1,11 @@ use crate::{ + action::Action, cable::CABLE_DIR_NAME, - channels::prototypes::{DEFAULT_PROTOTYPE_NAME, UiSpec}, + channels::prototypes::{DEFAULT_PROTOTYPE_NAME, Template, UiSpec}, + cli::PostProcessedCli, + features::FeatureFlags, history::DEFAULT_HISTORY_SIZE, + screen::keybindings::remove_action_bindings, }; use anyhow::{Context, Result}; use directories::ProjectDirs; @@ -239,14 +243,147 @@ impl Config { } } - pub fn merge_keybindings(&mut self, other: &KeyBindings) { + pub fn merge_channel_keybindings(&mut self, other: &KeyBindings) { self.keybindings = merge_bindings(self.keybindings.clone(), other); } + /// Apply CLI keybinding overrides. + pub fn apply_cli_keybinding_overrides( + &mut self, + cli_keybindings: &KeyBindings, + ) { + debug!("keybindings before: {:?}", self.keybindings); + + for (key, actions) in &cli_keybindings.bindings { + // Update the keybinding + self.keybindings.insert(*key, actions.clone()); + } + + debug!("keybindings after: {:?}", self.keybindings); + } + pub fn merge_event_bindings(&mut self, other: &EventBindings) { self.events = merge_bindings(self.events.clone(), other); } + /// Apply CLI overrides to this config + pub fn apply_cli_overrides(&mut self, args: &PostProcessedCli) { + debug!("Applying CLI overrides to config after channel merging"); + + if let Some(cable_dir) = &args.cable_dir { + self.application.cable_dir.clone_from(cable_dir); + } + if let Some(tick_rate) = args.tick_rate { + self.application.tick_rate = tick_rate; + } + if args.global_history { + self.application.global_history = true; + } + // Handle preview panel flags + if args.no_preview { + self.ui.features.disable(FeatureFlags::PreviewPanel); + remove_action_bindings( + &mut self.keybindings, + &Action::TogglePreview.into(), + ); + } else if args.hide_preview { + self.ui.features.hide(FeatureFlags::PreviewPanel); + } else if args.show_preview { + self.ui.features.enable(FeatureFlags::PreviewPanel); + } + + if let Some(ps) = args.preview_size { + self.ui.preview_panel.size = ps; + } + + // Handle status bar flags + if args.no_status_bar { + self.ui.features.disable(FeatureFlags::StatusBar); + remove_action_bindings( + &mut self.keybindings, + &Action::ToggleStatusBar.into(), + ); + } else if args.hide_status_bar { + self.ui.features.hide(FeatureFlags::StatusBar); + } else if args.show_status_bar { + self.ui.features.enable(FeatureFlags::StatusBar); + } + + // Handle remote control flags + if args.no_remote { + self.ui.features.disable(FeatureFlags::RemoteControl); + remove_action_bindings( + &mut self.keybindings, + &Action::ToggleRemoteControl.into(), + ); + } else if args.hide_remote { + self.ui.features.hide(FeatureFlags::RemoteControl); + } else if args.show_remote { + self.ui.features.enable(FeatureFlags::RemoteControl); + } + + // Handle help panel flags + if args.no_help_panel { + self.ui.features.disable(FeatureFlags::HelpPanel); + remove_action_bindings( + &mut self.keybindings, + &Action::ToggleHelp.into(), + ); + } else if args.hide_help_panel { + self.ui.features.hide(FeatureFlags::HelpPanel); + } else if args.show_help_panel { + self.ui.features.enable(FeatureFlags::HelpPanel); + } + + // Apply CLI keybinding overrides + if let Some(keybindings) = &args.keybindings { + self.apply_cli_keybinding_overrides(keybindings); + } + + self.ui.ui_scale = args.ui_scale.unwrap_or(self.ui.ui_scale); + if let Some(input_header) = &args.input_header { + if let Ok(t) = Template::parse(input_header) { + self.ui.input_bar.header = Some(t); + } + } + if let Some(input_prompt) = &args.input_prompt { + self.ui.input_bar.prompt.clone_from(input_prompt); + } + if let Some(preview_header) = &args.preview_header { + if let Ok(t) = Template::parse(preview_header) { + self.ui.preview_panel.header = Some(t); + } + } + if let Some(preview_footer) = &args.preview_footer { + if let Ok(t) = + crate::channels::prototypes::Template::parse(preview_footer) + { + self.ui.preview_panel.footer = Some(t); + } + } + if let Some(layout) = args.layout { + self.ui.orientation = layout; + } + if let Some(input_border) = args.input_border { + self.ui.input_bar.border_type = input_border; + } + if let Some(preview_border) = args.preview_border { + self.ui.preview_panel.border_type = preview_border; + } + if let Some(results_border) = args.results_border { + self.ui.results_panel.border_type = results_border; + } + if let Some(input_padding) = args.input_padding { + self.ui.input_bar.padding = input_padding; + } + if let Some(preview_padding) = args.preview_padding { + self.ui.preview_panel.padding = preview_padding; + } + if let Some(results_padding) = args.results_padding { + self.ui.results_panel.padding = results_padding; + } + } + pub fn apply_prototype_ui_spec(&mut self, ui_spec: &UiSpec) { // Apply simple copy fields (Copy types) if let Some(value) = ui_spec.ui_scale { diff --git a/television/draw.rs b/television/draw.rs index 5dddf08..8fca762 100644 --- a/television/draw.rs +++ b/television/draw.rs @@ -179,7 +179,7 @@ pub fn draw(ctx: Box, f: &mut Frame<'_>, area: Rect) -> Result { &ctx.config.ui, show_remote, ctx.tv_state.preview_state.enabled, - Some(&ctx.config.keybindings), + Some(&ctx.config), ctx.tv_state.mode, &ctx.colorscheme, ); @@ -247,7 +247,7 @@ pub fn draw(ctx: Box, f: &mut Frame<'_>, area: Rect) -> Result { draw_help_panel( f, help_area, - &ctx.config.keybindings, + &ctx.config, ctx.tv_state.mode, &ctx.colorscheme, ); diff --git a/television/event.rs b/television/event.rs index 49c79fc..7af748c 100644 --- a/television/event.rs +++ b/television/event.rs @@ -28,7 +28,9 @@ pub enum Event { Tick, } -#[derive(Debug, Clone, Copy, Serialize, PartialEq, PartialOrd, Eq, Hash)] +#[derive( + Debug, Clone, Copy, Serialize, PartialEq, PartialOrd, Eq, Hash, Ord, +)] pub enum Key { Backspace, Enter, diff --git a/television/main.rs b/television/main.rs index dce1a19..000067f 100644 --- a/television/main.rs +++ b/television/main.rs @@ -5,23 +5,20 @@ use std::io::{BufWriter, IsTerminal, Write, stdout}; use std::path::PathBuf; use std::process::exit; use television::{ - action::Action, app::{App, AppOptions}, cable::{Cable, cable_empty_exit, load_cable}, channels::prototypes::{ ChannelPrototype, CommandSpec, PreviewSpec, Template, UiSpec, }, - cli::post_process, cli::{ PostProcessedCli, args::{Cli, Command}, - guess_channel_from_prompt, list_channels, + guess_channel_from_prompt, list_channels, post_process, }, - config::{Config, ConfigEnv, merge_bindings, ui::InputBarConfig}, + config::{Config, ConfigEnv, ui::InputBarConfig}, errors::os_error_exit, features::FeatureFlags, gh::update_local_channels, - screen::keybindings::remove_action_bindings, television::Mode, utils::clipboard::CLIPBOARD, utils::{ @@ -56,7 +53,7 @@ async fn main() -> Result<()> { // override configuration values with provided CLI arguments debug!("Applying CLI overrides..."); - apply_cli_overrides(&args, &mut config); + config.apply_cli_overrides(&args); // handle subcommands debug!("Handling subcommands..."); @@ -98,13 +95,7 @@ async fn main() -> Result<()> { args.width, args.inline, ); - let mut app = App::new( - channel_prototype, - config, - args.input.clone(), - options, - cable, - ); + let mut app = App::new(channel_prototype, config, options, cable, &args); // If the user requested to show the remote control on startup, switch the // television into Remote Control mode before the application event loop @@ -135,121 +126,6 @@ async fn main() -> Result<()> { exit(0); } -/// Apply overrides from the CLI arguments to the configuration. -/// -/// This function mutates the configuration in place. -fn apply_cli_overrides(args: &PostProcessedCli, config: &mut Config) { - if let Some(cable_dir) = &args.cable_dir { - config.application.cable_dir.clone_from(cable_dir); - } - if let Some(tick_rate) = args.tick_rate { - config.application.tick_rate = tick_rate; - } - if args.global_history { - config.application.global_history = true; - } - // Handle preview panel flags - if args.no_preview { - config.ui.features.disable(FeatureFlags::PreviewPanel); - remove_action_bindings( - &mut config.keybindings, - &Action::TogglePreview.into(), - ); - } else if args.hide_preview { - config.ui.features.hide(FeatureFlags::PreviewPanel); - } else if args.show_preview { - config.ui.features.enable(FeatureFlags::PreviewPanel); - } - - if let Some(ps) = args.preview_size { - config.ui.preview_panel.size = ps; - } - - // Handle status bar flags - if args.no_status_bar { - config.ui.features.disable(FeatureFlags::StatusBar); - remove_action_bindings( - &mut config.keybindings, - &Action::ToggleStatusBar.into(), - ); - } else if args.hide_status_bar { - config.ui.features.hide(FeatureFlags::StatusBar); - } else if args.show_status_bar { - config.ui.features.enable(FeatureFlags::StatusBar); - } - - // Handle remote control flags - if args.no_remote { - config.ui.features.disable(FeatureFlags::RemoteControl); - remove_action_bindings( - &mut config.keybindings, - &Action::ToggleRemoteControl.into(), - ); - } else if args.hide_remote { - config.ui.features.hide(FeatureFlags::RemoteControl); - } else if args.show_remote { - config.ui.features.enable(FeatureFlags::RemoteControl); - } - - // Handle help panel flags - if args.no_help_panel { - config.ui.features.disable(FeatureFlags::HelpPanel); - remove_action_bindings( - &mut config.keybindings, - &Action::ToggleHelp.into(), - ); - } else if args.hide_help_panel { - config.ui.features.hide(FeatureFlags::HelpPanel); - } else if args.show_help_panel { - config.ui.features.enable(FeatureFlags::HelpPanel); - } - - if let Some(keybindings) = &args.keybindings { - config.keybindings = - merge_bindings(config.keybindings.clone(), keybindings); - } - config.ui.ui_scale = args.ui_scale.unwrap_or(config.ui.ui_scale); - if let Some(input_header) = &args.input_header { - if let Ok(t) = Template::parse(input_header) { - config.ui.input_bar.header = Some(t); - } - } - if let Some(input_prompt) = &args.input_prompt { - config.ui.input_bar.prompt.clone_from(input_prompt); - } - if let Some(preview_header) = &args.preview_header { - if let Ok(t) = Template::parse(preview_header) { - config.ui.preview_panel.header = Some(t); - } - } - if let Some(preview_footer) = &args.preview_footer { - if let Ok(t) = Template::parse(preview_footer) { - config.ui.preview_panel.footer = Some(t); - } - } - if let Some(layout) = args.layout { - config.ui.orientation = layout; - } - if let Some(input_border) = args.input_border { - config.ui.input_bar.border_type = input_border; - } - if let Some(preview_border) = args.preview_border { - config.ui.preview_panel.border_type = preview_border; - } - if let Some(results_border) = args.results_border { - config.ui.results_panel.border_type = results_border; - } - if let Some(input_padding) = args.input_padding { - config.ui.input_bar.padding = input_padding; - } - if let Some(preview_padding) = args.preview_padding { - config.ui.preview_panel.padding = preview_padding; - } - if let Some(results_padding) = args.results_padding { - config.ui.results_panel.padding = results_padding; - } -} - pub fn set_current_dir(path: &PathBuf) -> Result<()> { env::set_current_dir(path)?; Ok(()) @@ -821,7 +697,7 @@ mod tests { results_padding: Some(Padding::new(9, 10, 11, 12)), ..Default::default() }; - apply_cli_overrides(&args, &mut config); + config.apply_cli_overrides(&args); assert_eq!(config.application.tick_rate, 100_f64); assert!(!config.ui.features.is_enabled(FeatureFlags::PreviewPanel)); @@ -959,7 +835,7 @@ mod tests { ui_scale: Some(90), ..Default::default() }; - apply_cli_overrides(&args, &mut config); + config.apply_cli_overrides(&args); assert_eq!(config.ui.ui_scale, 90); @@ -967,7 +843,7 @@ mod tests { let mut config = Config::default(); config.ui.ui_scale = 70; let args = PostProcessedCli::default(); - apply_cli_overrides(&args, &mut config); + config.apply_cli_overrides(&args); assert_eq!(config.ui.ui_scale, 70); } diff --git a/television/screen/help_panel.rs b/television/screen/help_panel.rs index 0165c31..b429a00 100644 --- a/television/screen/help_panel.rs +++ b/television/screen/help_panel.rs @@ -1,7 +1,7 @@ use crate::{ - config::KeyBindings, + action::Action, + config::{Config, KeyBindings}, screen::colors::Colorscheme, - screen::keybindings::{ActionMapping, find_keys_for_single_action}, television::Mode, }; use ratatui::{ @@ -11,6 +11,7 @@ use ratatui::{ text::{Line, Span}, widgets::{Block, BorderType, Borders, Clear, Padding, Paragraph}, }; +use tracing::{debug, trace}; const MIN_PANEL_WIDTH: u16 = 25; const MIN_PANEL_HEIGHT: u16 = 5; @@ -19,7 +20,7 @@ const MIN_PANEL_HEIGHT: u16 = 5; pub fn draw_help_panel( f: &mut Frame<'_>, area: Rect, - keybindings: &KeyBindings, + config: &Config, tv_mode: Mode, colorscheme: &Colorscheme, ) { @@ -28,7 +29,7 @@ pub fn draw_help_panel( } // Generate content - let content = generate_help_content(keybindings, tv_mode, colorscheme); + let content = generate_help_content(config, tv_mode, colorscheme); // Clear the area first to create the floating effect f.render_widget(Clear, area); @@ -52,63 +53,161 @@ pub fn draw_help_panel( f.render_widget(paragraph, area); } -/// Adds keybinding lines for action mappings to the given lines vector -fn add_keybinding_lines_for_mappings( - lines: &mut Vec>, - keybindings: &KeyBindings, - mappings: &[ActionMapping], - mode: Mode, - colorscheme: &Colorscheme, -) { - for mapping in mappings { - for (action, description) in &mapping.actions { - let keys = find_keys_for_single_action(keybindings, action); - for key in keys { - lines.push(create_compact_keybinding_line( - &key, - description, - mode, - colorscheme, - )); +/// Checks if an action is relevant for the given mode +fn is_action_relevant_for_mode(action: &Action, mode: Mode) -> bool { + match mode { + Mode::Channel => { + // Channel mode - all actions except those specifically for remote mode switching + match action { + // Input actions - available in both modes + Action::AddInputChar(_) + | Action::DeletePrevChar + | Action::DeletePrevWord + | Action::DeleteNextChar + | Action::DeleteLine + | Action::GoToPrevChar + | Action::GoToNextChar + | Action::GoToInputStart + | Action::GoToInputEnd + // Navigation actions - available in both modes + | Action::SelectNextEntry + | Action::SelectPrevEntry + | Action::SelectNextPage + | Action::SelectPrevPage + // Selection actions - channel specific (multi-select) + | Action::ToggleSelectionDown + | Action::ToggleSelectionUp + | Action::ConfirmSelection + // Preview actions - channel specific + | Action::ScrollPreviewUp + | Action::ScrollPreviewDown + | Action::ScrollPreviewHalfPageUp + | Action::ScrollPreviewHalfPageDown + | Action::TogglePreview + // Channel-specific actions + | Action::CopyEntryToClipboard + | Action::ReloadSource + | Action::CycleSources + | Action::SelectPrevHistory + | Action::SelectNextHistory + // UI toggles - global + | Action::ToggleRemoteControl + | Action::ToggleHelp + | Action::ToggleStatusBar + // Application actions - global + | Action::Quit => true, + + // Skip actions not relevant to help or internal actions + Action::NoOp + | Action::Render + | Action::Resize(_, _) + | Action::ClearScreen + | Action::Tick + | Action::Suspend + | Action::Resume + | Action::Error(_) + | Action::OpenEntry + | Action::SwitchToChannel(_) + | Action::WatchTimer + | Action::SelectEntryAtPosition(_, _) + | Action::MouseClickAt(_, _) + | Action::ToggleSendToChannel + | Action::SelectAndExit => false, + } + } + Mode::RemoteControl => { + // Remote control mode - limited set of actions + match action { + // Input actions - available in both modes + Action::AddInputChar(_) + | Action::DeletePrevChar + | Action::DeletePrevWord + | Action::DeleteNextChar + | Action::DeleteLine + | Action::GoToPrevChar + | Action::GoToNextChar + | Action::GoToInputStart + | Action::GoToInputEnd + // Navigation actions - available in both modes + | Action::SelectNextEntry + | Action::SelectPrevEntry + | Action::SelectNextPage + | Action::SelectPrevPage + // Selection in remote mode - just confirm (no multi-select) + | Action::ConfirmSelection + // UI toggles - global + | Action::ToggleRemoteControl + | Action::ToggleHelp + | Action::ToggleStatusBar + // Application actions - global + | Action::Quit => true, + + // All other actions not relevant in remote control mode + _ => false, } } } } +/// Adds keybinding lines for specific keys to the given lines vector +fn add_keybinding_lines_for_keys( + lines: &mut Vec>, + keybindings: &KeyBindings, + mode: Mode, + colorscheme: &Colorscheme, + category_name: &str, +) { + // Collect all valid keybinding entries + let mut entries: Vec<(String, String)> = Vec::new(); + + for (key, actions) in keybindings.iter() { + for action in actions.as_slice() { + // Filter out NoOp actions (unbound keys) + // Filter out actions not relevant for current mode + if matches!(action, Action::NoOp) + || !is_action_relevant_for_mode(action, mode) + { + continue; + } + + let description = action.description(); + let key_string = key.to_string(); + entries.push((description.to_string(), key_string.clone())); + trace!( + "Added keybinding: {} -> {} ({})", + key_string, description, category_name + ); + } + } + + // Sort entries alphabetically by description + entries.sort_by(|a, b| a.0.cmp(&b.0)); + + // Create lines from sorted entries + for (description, key_string) in entries { + lines.push(create_compact_keybinding_line( + &key_string, + &description, + mode, + colorscheme, + )); + } +} + /// Generates the help content organized into global and mode-specific groups fn generate_help_content( - keybindings: &KeyBindings, + config: &Config, mode: Mode, colorscheme: &Colorscheme, ) -> Vec> { let mut lines = Vec::new(); - // Global keybindings section header - lines.push(Line::from(vec![Span::styled( - "Global", - Style::default() - .fg(colorscheme.help.metadata_field_name_fg) - .bold() - .underlined(), - )])); - - // Global actions using centralized system - let global_mappings = ActionMapping::global_actions(); - add_keybinding_lines_for_mappings( - &mut lines, - keybindings, - &global_mappings, - mode, - colorscheme, - ); - - // Add spacing between Global and mode-specific sections - lines.push(Line::from("")); + debug!("Generating help content for mode: {:?}", mode); // Mode-specific keybindings section header let mode_name = match mode { - Mode::Channel => "Channel", - Mode::RemoteControl => "Remote", + Mode::Channel => "Channel Mode", + Mode::RemoteControl => "Remote Control Mode", }; lines.push(Line::from(vec![Span::styled( @@ -119,26 +218,15 @@ fn generate_help_content( .underlined(), )])); - // Navigation actions (common to both modes) using centralized system - let nav_mappings = ActionMapping::navigation_actions(); - add_keybinding_lines_for_mappings( + add_keybinding_lines_for_keys( &mut lines, - keybindings, - &nav_mappings, - mode, - colorscheme, - ); - - // Mode-specific actions using centralized system - let mode_mappings = ActionMapping::mode_specific_actions(mode); - add_keybinding_lines_for_mappings( - &mut lines, - keybindings, - &mode_mappings, + &config.keybindings, mode, colorscheme, + mode_name, ); + debug!("Generated help content with {} total lines", lines.len()); lines } @@ -168,12 +256,12 @@ fn create_compact_keybinding_line( /// Calculates the required dimensions for the help panel based on content #[allow(clippy::cast_possible_truncation)] pub fn calculate_help_panel_size( - keybindings: &KeyBindings, + config: &Config, mode: Mode, colorscheme: &Colorscheme, ) -> (u16, u16) { // Generate content to count items and calculate width - let content = generate_help_content(keybindings, mode, colorscheme); + let content = generate_help_content(config, mode, colorscheme); // Calculate required width based on actual content let max_content_width = content @@ -183,10 +271,18 @@ pub fn calculate_help_panel_size( .unwrap_or(25); // Calculate dimensions with proper padding: - // - Width: content + 3 (2 borders + 1 padding) + // - Width: content + 4 (2 borders + 2 padding) // - Height: content lines + 2 (2 borders, no title or padding) - let required_width = (max_content_width + 3).max(25) as u16; + let required_width = (max_content_width + 4).max(25) as u16; let required_height = (content.len() + 2).max(8) as u16; + trace!( + "Help panel size calculation: {} lines, max width {}, final dimensions {}x{}", + content.len(), + max_content_width, + required_width, + required_height + ); + (required_width, required_height) } diff --git a/television/screen/keybindings.rs b/television/screen/keybindings.rs index 9831609..38652d1 100644 --- a/television/screen/keybindings.rs +++ b/television/screen/keybindings.rs @@ -1,175 +1,9 @@ use crate::{ action::{Action, Actions}, config::KeyBindings, - television::Mode, }; -use std::fmt::Display; -/// Centralized action descriptions to avoid duplication between keybinding panel and help bar -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum ActionCategory { - // Global actions - Quit, - ToggleFeature, - - // Navigation actions (common to both modes) - ResultsNavigation, - PreviewNavigation, - - // Selection actions - SelectEntry, - ToggleSelection, - - // Channel-specific actions - CopyEntryToClipboard, - ToggleRemoteControl, - CycleSources, - ReloadSource, -} - -impl Display for ActionCategory { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let description = match self { - ActionCategory::Quit => "Quit", - ActionCategory::ToggleFeature => "Toggle features", - ActionCategory::ResultsNavigation => "Results navigation", - ActionCategory::PreviewNavigation => "Preview navigation", - ActionCategory::SelectEntry => "Select entry", - ActionCategory::ToggleSelection => "Toggle selection", - ActionCategory::CopyEntryToClipboard => "Copy entry to clipboard", - ActionCategory::ToggleRemoteControl => "Toggle Remote control", - ActionCategory::CycleSources => "Cycle through sources", - ActionCategory::ReloadSource => "Reload source", - }; - write!(f, "{description}") - } -} - -/// Defines what actions belong to each category and their individual descriptions -pub struct ActionMapping { - pub category: ActionCategory, - pub actions: Vec<(Action, &'static str)>, -} - -impl ActionMapping { - /// Get all action mappings for global actions - pub fn global_actions() -> Vec { - vec![ - ActionMapping { - category: ActionCategory::Quit, - actions: vec![(Action::Quit, "Quit")], - }, - ActionMapping { - category: ActionCategory::ToggleFeature, - actions: vec![ - (Action::TogglePreview, "Toggle preview"), - (Action::ToggleHelp, "Toggle help"), - (Action::ToggleStatusBar, "Toggle status bar"), - ], - }, - ] - } - - /// Get all action mappings for navigation actions (common to both modes) - pub fn navigation_actions() -> Vec { - vec![ - ActionMapping { - category: ActionCategory::ResultsNavigation, - actions: vec![ - (Action::SelectPrevEntry, "Navigate up"), - (Action::SelectNextEntry, "Navigate down"), - (Action::SelectPrevPage, "Page up"), - (Action::SelectNextPage, "Page down"), - ], - }, - ActionMapping { - category: ActionCategory::PreviewNavigation, - actions: vec![ - (Action::ScrollPreviewHalfPageUp, "Preview scroll up"), - (Action::ScrollPreviewHalfPageDown, "Preview scroll down"), - ], - }, - ] - } - - /// Get mode-specific action mappings - pub fn mode_specific_actions(mode: Mode) -> Vec { - match mode { - Mode::Channel => vec![ - ActionMapping { - category: ActionCategory::SelectEntry, - actions: vec![ - (Action::ConfirmSelection, "Select entry"), - (Action::ToggleSelectionDown, "Toggle selection down"), - (Action::ToggleSelectionUp, "Toggle selection up"), - ], - }, - ActionMapping { - category: ActionCategory::CopyEntryToClipboard, - actions: vec![( - Action::CopyEntryToClipboard, - "Copy to clipboard", - )], - }, - ActionMapping { - category: ActionCategory::ToggleRemoteControl, - actions: vec![( - Action::ToggleRemoteControl, - "Remote Control", - )], - }, - ActionMapping { - category: ActionCategory::CycleSources, - actions: vec![(Action::CycleSources, "Cycle sources")], - }, - ActionMapping { - category: ActionCategory::ReloadSource, - actions: vec![(Action::ReloadSource, "Reload source")], - }, - ], - Mode::RemoteControl => vec![ - ActionMapping { - category: ActionCategory::SelectEntry, - actions: vec![(Action::ConfirmSelection, "Select entry")], - }, - ActionMapping { - category: ActionCategory::ToggleRemoteControl, - actions: vec![( - Action::ToggleRemoteControl, - "Back to Channel", - )], - }, - ], - } - } - - /// Get all actions for a specific category, flattened for help bar usage - pub fn actions_for_category(&self) -> &[Action] { - // This is a bit of a hack to return just the Action part of the tuples - // We'll need to handle this differently in the help bar system - &[] - } -} - -/// Extract keys for a single action from the new Key->Action keybindings format -pub fn find_keys_for_action( - keybindings: &KeyBindings, - target_action: &Actions, -) -> Vec { - keybindings - .bindings - .iter() - .filter_map(|(key, action)| { - if action == target_action { - Some(key.to_string()) - } else { - None - } - }) - .collect() -} - -/// Extract keys for a single action (convenience function) +/// Extract keys for a single action pub fn find_keys_for_single_action( keybindings: &KeyBindings, target_action: &Action, @@ -188,17 +22,6 @@ pub fn find_keys_for_single_action( .collect() } -/// Extract keys for multiple actions and return them as a flat vector -pub fn extract_keys_for_actions( - keybindings: &KeyBindings, - actions: &[Actions], -) -> Vec { - actions - .iter() - .flat_map(|action| find_keys_for_action(keybindings, action)) - .collect() -} - /// Remove all keybindings for a specific action from `KeyBindings` pub fn remove_action_bindings( keybindings: &mut KeyBindings, diff --git a/television/screen/layout.rs b/television/screen/layout.rs index e2a9010..6b7439b 100644 --- a/television/screen/layout.rs +++ b/television/screen/layout.rs @@ -1,5 +1,5 @@ use crate::{ - config::{KeyBindings, UiConfig}, + config::{Config, UiConfig}, features::FeatureFlags, screen::{ colors::Colorscheme, help_panel::calculate_help_panel_size, @@ -137,7 +137,7 @@ impl Layout { ui_config: &UiConfig, show_remote: bool, show_preview: bool, - keybindings: Option<&KeyBindings>, + config: Option<&Config>, mode: Mode, colorscheme: &Colorscheme, ) -> Self { @@ -401,12 +401,12 @@ impl Layout { area }; - if let Some(kb) = keybindings { + if let Some(cfg) = config { let (width, height) = - calculate_help_panel_size(kb, mode, colorscheme); + calculate_help_panel_size(cfg, mode, colorscheme); Some(bottom_right_rect(width, height, hp_area)) } else { - // Fallback to reasonable default if keybindings not available + // Fallback to reasonable default if config not available Some(bottom_right_rect(45, 25, hp_area)) } } else { diff --git a/television/television.rs b/television/television.rs index 4809324..ab52ee4 100644 --- a/television/television.rs +++ b/television/television.rs @@ -7,6 +7,7 @@ use crate::{ prototypes::ChannelPrototype, remote_control::{CableEntry, RemoteControl}, }, + cli::PostProcessedCli, config::{Config, Theme}, draw::{ChannelState, Ctx, TvState}, errors::os_error_exit, @@ -20,7 +21,6 @@ use crate::{ render::UiState, screen::{ colors::Colorscheme, - keybindings::remove_action_bindings, layout::InputPosition, spinner::{Spinner, SpinnerState}, }, @@ -79,8 +79,7 @@ pub struct Television { pub colorscheme: Colorscheme, pub ticks: u64, pub ui_state: UiState, - pub no_preview: bool, - pub preview_size: Option, + pub cli_args: PostProcessedCli, pub current_command_index: usize, pub channel_prototype: ChannelPrototype, } @@ -93,23 +92,24 @@ impl Television { action_tx: UnboundedSender, channel_prototype: ChannelPrototype, base_config: Config, - input: Option, - no_remote: bool, - no_preview: bool, - preview_size: Option, - exact: bool, cable_channels: Cable, + cli_args: PostProcessedCli, ) -> Self { let mut config = Self::merge_base_config_with_prototype_specs( &base_config, &channel_prototype, ); - // Apply CLI overrides after prototype merging to ensure they take precedence - Self::apply_cli_overrides(&mut config, no_preview, preview_size); + // Apply ALL CLI overrides (including keybindings) after channel merging + config.apply_cli_overrides(&cli_args); debug!("Merged config: {:?}", config); + // Extract CLI arguments + let input = cli_args.input.clone(); + let exact = cli_args.exact; + let no_remote = cli_args.no_remote; + let mut results_picker = Picker::new(input.clone()); if config.ui.input_bar.position == InputPosition::Bottom { results_picker = results_picker.inverted(); @@ -187,8 +187,7 @@ impl Television { colorscheme, ticks: 0, ui_state: UiState::default(), - no_preview, - preview_size, + cli_args, current_command_index: 0, channel_prototype, } @@ -201,7 +200,7 @@ impl Television { let mut config = base_config.clone(); // keybindings if let Some(keybindings) = &channel_prototype.keybindings { - config.merge_keybindings(&keybindings.bindings); + config.merge_channel_keybindings(&keybindings.bindings); } // ui if let Some(ui_spec) = &channel_prototype.ui { @@ -231,28 +230,6 @@ impl Television { config } - /// Apply CLI overrides to ensure they take precedence over channel prototype settings - fn apply_cli_overrides( - config: &mut Config, - no_preview: bool, - preview_size: Option, - ) { - // Handle preview panel flags - this mirrors the logic in main.rs but only for the subset - // of flags that Television manages directly - if no_preview { - config.ui.features.disable(FeatureFlags::PreviewPanel); - remove_action_bindings( - &mut config.keybindings, - &Action::TogglePreview.into(), - ); - } - - // Apply preview size regardless of preview state - if let Some(ps) = preview_size { - config.ui.preview_panel.size = ps; - } - } - fn setup_previewer( channel_prototype: &ChannelPrototype, ) -> Option<(UnboundedSender, UnboundedReceiver)> @@ -331,11 +308,7 @@ impl Television { &channel_prototype, ); // Reapply CLI overrides to ensure they persist across channel changes - Self::apply_cli_overrides( - &mut self.config, - self.no_preview, - self.preview_size, - ); + self.config.apply_cli_overrides(&self.cli_args); // Set preview state enabled based on both channel capability and UI configuration self.preview_state.enabled = channel_prototype.preview.is_some() && self @@ -944,29 +917,33 @@ mod test { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_cli_overrides() { + use crate::cli::PostProcessedCli; + let config = crate::config::Config::default(); let prototype = crate::channels::prototypes::ChannelPrototype::new( "test", "echo 1", ); + let cli_args = PostProcessedCli { + no_remote: true, + exact: true, + ..PostProcessedCli::default() + }; let tv = Television::new( tokio::sync::mpsc::unbounded_channel().0, prototype, config.clone(), - None, - true, - false, - Some(50), - true, Cable::from_prototypes(vec![]), + cli_args, ); assert_eq!(tv.matching_mode, MatchingMode::Substring); - assert!(!tv.no_preview); assert!(tv.remote_control.is_none()); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_channel_keybindings_take_precedence() { + use crate::cli::PostProcessedCli; + let mut config = crate::config::Config::default(); config .keybindings @@ -987,16 +964,17 @@ mod test { ) .unwrap(); + let cli_args = PostProcessedCli { + no_remote: true, + exact: true, + ..PostProcessedCli::default() + }; let tv = Television::new( tokio::sync::mpsc::unbounded_channel().0, prototype, config.clone(), - None, - true, - false, - Some(50), - true, Cable::from_prototypes(vec![]), + cli_args, ); assert_eq!( diff --git a/tests/app.rs b/tests/app.rs index 51b1da0..5dc678b 100644 --- a/tests/app.rs +++ b/tests/app.rs @@ -9,6 +9,7 @@ use television::{ app::{App, AppOptions}, cable::Cable, channels::prototypes::ChannelPrototype, + cli::PostProcessedCli, config::default_config_from_file, }; use tokio::{task::JoinHandle, time::timeout}; @@ -59,16 +60,21 @@ fn setup_app( None, false, ); + let cli_args = PostProcessedCli { + exact, + input: input.clone(), + ..PostProcessedCli::default() + }; let mut app = App::new( chan, config, - input, options, Cable::from_prototypes(vec![ ChannelPrototype::new("files", "find . -type f"), ChannelPrototype::new("dirs", "find . -type d"), ChannelPrototype::new("env", "printenv"), ]), + &cli_args, ); // retrieve the app's action channel handle in order to send a quit action