mirror of
https://github.com/alexpasmantier/television.git
synced 2025-07-29 06:11:37 +00:00
refactor(help): more informative help panel (#668)
## 📺 PR Description Rework the help panel to show live keybinding source information (global, channel) instead of hard-coded entries Before <img width="511" height="934" alt="image" src="https://github.com/user-attachments/assets/bea29e07-ec4a-443d-a468-be1e9f28a705" /> After <img width="690" height="1355" alt="image" src="https://github.com/user-attachments/assets/a930382b-e814-482a-a729-54ef2dc286c4" /> ## Env ### Command ```bash RUST_LOG=debug cargo run -q -- --cable-dir ./cable/unix --config-file ./.config/config.toml --show-help-panel --keybindings "f10 = \"toggle_help\"; f9 = \"toggle_status_bar\"" ``` ### Config file ```toml [metadata] name = "files" description = "A channel to select files and directories" requirements = ["fd", "bat"] [source] command = ["fd -t f", "fd -t f -H"] [preview] command = "bat -n --color=always '{}'" env = { BAT_THEME = "ansi" } [keybindings] shortcut = "f1" f9 = "toggle_preview" f8 = "quit" esc = false ``` ## Checklist <!-- a quick pass through the following items to make sure you haven't forgotten anything --> - [x] my commits **and PR title** follow the [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) format - [x] if this is a new feature, I have added tests to consolidate the feature and prevent regressions - [ ] if this is a bug fix, I have added a test that reproduces the bug (if applicable) - [x] I have added a reasonable amount of documentation to the code where appropriate
This commit is contained in:
parent
72c34731ec
commit
c8df96270a
@ -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 {
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -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<String>,
|
||||
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
|
||||
|
@ -317,12 +317,18 @@ pub fn merge_bindings<K>(
|
||||
new_bindings: &Bindings<K>,
|
||||
) -> Bindings<K>
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -179,7 +179,7 @@ pub fn draw(ctx: Box<Ctx>, f: &mut Frame<'_>, area: Rect) -> Result<Layout> {
|
||||
&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<Ctx>, f: &mut Frame<'_>, area: Rect) -> Result<Layout> {
|
||||
draw_help_panel(
|
||||
f,
|
||||
help_area,
|
||||
&ctx.config.keybindings,
|
||||
&ctx.config,
|
||||
ctx.tv_state.mode,
|
||||
&ctx.colorscheme,
|
||||
);
|
||||
|
@ -28,7 +28,9 @@ pub enum Event<I> {
|
||||
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,
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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(
|
||||
/// 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<Line<'static>>,
|
||||
keybindings: &KeyBindings,
|
||||
mappings: &[ActionMapping],
|
||||
mode: Mode,
|
||||
colorscheme: &Colorscheme,
|
||||
category_name: &str,
|
||||
) {
|
||||
for mapping in mappings {
|
||||
for (action, description) in &mapping.actions {
|
||||
let keys = find_keys_for_single_action(keybindings, action);
|
||||
for key in keys {
|
||||
// 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,
|
||||
description,
|
||||
&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<Line<'static>> {
|
||||
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)
|
||||
}
|
||||
|
@ -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<ActionMapping> {
|
||||
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<ActionMapping> {
|
||||
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<ActionMapping> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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,
|
||||
|
@ -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 {
|
||||
|
@ -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<u16>,
|
||||
pub cli_args: PostProcessedCli,
|
||||
pub current_command_index: usize,
|
||||
pub channel_prototype: ChannelPrototype,
|
||||
}
|
||||
@ -93,23 +92,24 @@ impl Television {
|
||||
action_tx: UnboundedSender<Action>,
|
||||
channel_prototype: ChannelPrototype,
|
||||
base_config: Config,
|
||||
input: Option<String>,
|
||||
no_remote: bool,
|
||||
no_preview: bool,
|
||||
preview_size: Option<u16>,
|
||||
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<u16>,
|
||||
) {
|
||||
// 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<PreviewRequest>, UnboundedReceiver<Preview>)>
|
||||
@ -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!(
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user