From 5bf3d20c83d5ea0d5e4c8e146d40f0cc37423611 Mon Sep 17 00:00:00 2001 From: Alexandre Pasmantier <47638216+alexpasmantier@users.noreply.github.com> Date: Wed, 9 Apr 2025 21:46:16 +0000 Subject: [PATCH] feat(cli): add a `--no-help` flag to allow disabling showing the help panel (#456) This will disable the help panel and associated toggling actions entirely. This is useful when the help panel is not needed or when the user wants `tv` to run with a minimal interface (e.g. when using it as a file picker for a script or embedding it in a larger application). --- benches/main/draw.rs | 3 +- television/app.rs | 64 +++++++++++++++++++++++++++++----------- television/cli/args.rs | 13 +++++++- television/cli/mod.rs | 62 +++++++------------------------------- television/config/mod.rs | 2 +- television/main.rs | 11 +++++-- television/television.rs | 12 +++++++- tests/app.rs | 8 +++-- 8 files changed, 98 insertions(+), 77 deletions(-) diff --git a/benches/main/draw.rs b/benches/main/draw.rs index e3191c0..3b88bf3 100644 --- a/benches/main/draw.rs +++ b/benches/main/draw.rs @@ -30,7 +30,8 @@ fn draw(c: &mut Criterion) { ])); channel.find("television"); // Wait for the channel to finish loading - let mut tv = Television::new(tx, channel, config, None); + let mut tv = + Television::new(tx, channel, config, None, false, false); for _ in 0..5 { // tick the matcher let _ = tv.channel.results(10, 0); diff --git a/television/app.rs b/television/app.rs index b18a84b..524bbb1 100644 --- a/television/app.rs +++ b/television/app.rs @@ -6,7 +6,7 @@ use tracing::{debug, trace}; use crate::channels::entry::{Entry, PreviewType}; use crate::channels::{OnAir, TelevisionChannel}; -use crate::config::Config; +use crate::config::{default_tick_rate, Config}; use crate::keymap::Keymap; use crate::render::UiState; use crate::television::{Mode, Television}; @@ -16,12 +16,47 @@ use crate::{ render::{render, RenderingTask}, }; +pub struct AppOptions { + /// Whether the application should automatically select the first entry if there is only one + /// entry available. + pub select_1: bool, + /// Whether the application should disable the remote control feature. + pub no_remote: bool, + /// Whether the application should disable the help panel feature. + pub no_help: bool, + pub tick_rate: f64, +} + +impl Default for AppOptions { + fn default() -> Self { + Self { + select_1: false, + no_remote: false, + no_help: false, + tick_rate: default_tick_rate(), + } + } +} + +impl AppOptions { + pub fn new( + select_1: bool, + no_remote: bool, + no_help: bool, + tick_rate: f64, + ) -> Self { + Self { + select_1, + no_remote, + no_help, + tick_rate, + } + } +} + /// The main application struct that holds the state of the application. pub struct App { keymap: Keymap, - // maybe move these two into config instead of passing them - // via the cli? - tick_rate: f64, /// The television instance that handles channels and entries. television: Television, /// A flag that indicates whether the application should quit during the next frame. @@ -53,9 +88,7 @@ pub struct App { ui_state_tx: mpsc::UnboundedSender, /// Render task handle render_task: Option>>, - /// Whether the application should automatically select the first entry if there is only one - /// entry available. - select_1: bool, + options: AppOptions, } /// The outcome of an action. @@ -99,14 +132,12 @@ impl App { channel: TelevisionChannel, config: Config, input: Option, - select_1: bool, - no_remote: bool, + options: AppOptions, ) -> Self { let (action_tx, action_rx) = mpsc::unbounded_channel(); let (render_tx, render_rx) = mpsc::unbounded_channel(); let (_, event_rx) = mpsc::unbounded_channel(); let (event_abort_tx, _) = mpsc::unbounded_channel(); - let tick_rate = config.application.tick_rate; let keymap = Keymap::from(&config.keybindings); debug!("{:?}", keymap); @@ -116,12 +147,12 @@ impl App { channel, config, input, - no_remote, + options.no_remote, + options.no_help, ); Self { keymap, - tick_rate, television, should_quit: false, should_suspend: false, @@ -134,7 +165,7 @@ impl App { ui_state_rx, ui_state_tx, render_task: None, - select_1, + options, } } @@ -160,7 +191,7 @@ impl App { // Event loop if !headless { debug!("Starting backend event loop"); - let event_loop = EventLoop::new(self.tick_rate, true); + let event_loop = EventLoop::new(self.options.tick_rate, true); self.event_rx = event_loop.rx; self.event_abort_tx = event_loop.abort_tx; } @@ -210,7 +241,7 @@ impl App { // If `self.select_1` is true, the channel is not running, and there is // only one entry available, automatically select the first entry. - if self.select_1 + if self.options.select_1 && !self.television.channel.running() && self.television.channel.total_count() == 1 { @@ -406,8 +437,7 @@ mod test { TelevisionChannel::Stdin(StdinChannel::new(PreviewType::None)), Config::default(), None, - true, - false, + AppOptions::default(), ); app.television .results_picker diff --git a/television/cli/args.rs b/television/cli/args.rs index 2ffb11b..25318e2 100644 --- a/television/cli/args.rs +++ b/television/cli/args.rs @@ -1,6 +1,7 @@ use clap::{Parser, Subcommand, ValueEnum}; -#[derive(Parser, Debug)] +#[allow(clippy::struct_excessive_bools)] +#[derive(Parser, Debug, Default)] #[command(author, version, about, long_about = None)] pub struct Cli { /// Which channel shall we watch? @@ -112,6 +113,16 @@ pub struct Cli { #[arg(long, default_value = "false", verbatim_doc_comment)] pub no_remote: bool, + /// Disable the help panel. + /// + /// This will disable the help panel and associated toggling actions + /// entirely. This is useful when the help panel is not needed or + /// when the user wants `tv` to run with a minimal interface (e.g. when + /// using it as a file picker for a script or embedding it in a larger + /// application). + #[arg(long, default_value = "false", verbatim_doc_comment)] + pub no_help: bool, + #[command(subcommand)] pub command: Option, } diff --git a/television/cli/mod.rs b/television/cli/mod.rs index d897ab2..3b97580 100644 --- a/television/cli/mod.rs +++ b/television/cli/mod.rs @@ -17,6 +17,7 @@ use crate::{ pub mod args; +#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub struct PostProcessedCli { pub channel: ParsedCliChannel, @@ -31,6 +32,7 @@ pub struct PostProcessedCli { pub keybindings: Option, pub select_1: bool, pub no_remote: bool, + pub no_help: bool, } impl Default for PostProcessedCli { @@ -48,6 +50,7 @@ impl Default for PostProcessedCli { keybindings: None, select_1: false, no_remote: false, + no_help: false, } } } @@ -114,6 +117,7 @@ impl From for PostProcessedCli { keybindings, select_1: cli.select_1, no_remote: cli.no_remote, + no_help: cli.no_help, } } } @@ -319,17 +323,9 @@ mod tests { let cli = Cli { channel: "files".to_string(), preview: Some("bat -n --color=always {}".to_string()), - no_preview: false, delimiter: ":".to_string(), - tick_rate: Some(50.0), - frame_rate: Some(60.0), - keybindings: None, - input: None, - command: None, working_directory: Some("/home/user".to_string()), - autocomplete_prompt: None, - select_1: false, - no_remote: false, + ..Default::default() }; let post_processed_cli: PostProcessedCli = cli.into(); @@ -345,8 +341,8 @@ mod tests { delimiter: ":".to_string() }) ); - assert_eq!(post_processed_cli.tick_rate, Some(50.0)); - assert_eq!(post_processed_cli.frame_rate, Some(60.0)); + assert_eq!(post_processed_cli.tick_rate, None); + assert_eq!(post_processed_cli.frame_rate, None); assert_eq!( post_processed_cli.working_directory, Some("/home/user".to_string()) @@ -358,18 +354,8 @@ mod tests { fn test_from_cli_no_args() { let cli = Cli { channel: ".".to_string(), - preview: None, - no_preview: false, delimiter: ":".to_string(), - tick_rate: Some(50.0), - frame_rate: Some(60.0), - keybindings: None, - input: None, - command: None, - working_directory: None, - autocomplete_prompt: None, - select_1: false, - no_remote: false, + ..Default::default() }; let post_processed_cli: PostProcessedCli = cli.into(); @@ -390,17 +376,8 @@ mod tests { let cli = Cli { channel: "files".to_string(), preview: Some(":files:".to_string()), - no_preview: false, delimiter: ":".to_string(), - tick_rate: Some(50.0), - frame_rate: Some(60.0), - keybindings: None, - input: None, - command: None, - working_directory: None, - autocomplete_prompt: None, - select_1: false, - no_remote: false, + ..Default::default() }; let post_processed_cli: PostProcessedCli = cli.into(); @@ -416,17 +393,8 @@ mod tests { let cli = Cli { channel: "files".to_string(), preview: Some(":env_var:".to_string()), - no_preview: false, delimiter: ":".to_string(), - tick_rate: Some(50.0), - frame_rate: Some(60.0), - keybindings: None, - input: None, - command: None, - working_directory: None, - autocomplete_prompt: None, - select_1: false, - no_remote: false, + ..Default::default() }; let post_processed_cli: PostProcessedCli = cli.into(); @@ -442,20 +410,12 @@ mod tests { let cli = Cli { channel: "files".to_string(), preview: Some(":env_var:".to_string()), - no_preview: false, delimiter: ":".to_string(), - tick_rate: Some(50.0), - frame_rate: Some(60.0), keybindings: Some( "quit=\"esc\";select_next_entry=[\"down\",\"ctrl-j\"]" .to_string(), ), - input: None, - command: None, - working_directory: None, - autocomplete_prompt: None, - select_1: false, - no_remote: false, + ..Default::default() }; let post_processed_cli: PostProcessedCli = cli.into(); diff --git a/television/config/mod.rs b/television/config/mod.rs index fead734..429ab90 100644 --- a/television/config/mod.rs +++ b/television/config/mod.rs @@ -260,7 +260,7 @@ fn default_frame_rate() -> f64 { 60.0 } -fn default_tick_rate() -> f64 { +pub fn default_tick_rate() -> f64 { 50.0 } diff --git a/television/main.rs b/television/main.rs index e6c6120..09802ee 100644 --- a/television/main.rs +++ b/television/main.rs @@ -10,7 +10,7 @@ use television::cli::parse_channel; use television::utils::clipboard::CLIPBOARD; use tracing::{debug, error, info}; -use television::app::App; +use television::app::{App, AppOptions}; use television::channels::{ entry::PreviewType, stdin::Channel as StdinChannel, TelevisionChannel, }; @@ -65,8 +65,13 @@ async fn main() -> Result<()> { CLIPBOARD.with(<_>::default); debug!("Creating application..."); - let mut app = - App::new(channel, config, args.input, args.select_1, args.no_remote); + let options = AppOptions::new( + args.select_1, + args.no_remote, + args.no_help, + config.application.tick_rate, + ); + let mut app = App::new(channel, config, args.input, options); stdout().flush()?; debug!("Running application..."); let output = app.run(stdout().is_terminal(), false).await?; diff --git a/television/television.rs b/television/television.rs index c7333e0..637d483 100644 --- a/television/television.rs +++ b/television/television.rs @@ -48,6 +48,7 @@ pub struct Television { pub colorscheme: Colorscheme, pub ticks: u64, pub ui_state: UiState, + pub no_help: bool, } impl Television { @@ -55,9 +56,10 @@ impl Television { pub fn new( action_tx: UnboundedSender, mut channel: TelevisionChannel, - config: Config, + mut config: Config, input: Option, no_remote: bool, + no_help: bool, ) -> Self { let mut results_picker = Picker::new(input.clone()); if config.ui.input_bar_position == InputPosition::Bottom { @@ -97,6 +99,10 @@ impl Television { ))) }; + if no_help { + config.ui.show_help_bar = false; + } + Self { action_tx, config, @@ -114,6 +120,7 @@ impl Television { colorscheme, ticks: 0, ui_state: UiState::default(), + no_help, } } @@ -592,6 +599,9 @@ impl Television { self.handle_toggle_send_to_channel(); } Action::ToggleHelp => { + if self.no_help { + return Ok(()); + } self.config.ui.show_help_bar = !self.config.ui.show_help_bar; } Action::TogglePreview => { diff --git a/tests/app.rs b/tests/app.rs index 5b5f631..64e9e85 100644 --- a/tests/app.rs +++ b/tests/app.rs @@ -1,7 +1,9 @@ use std::{collections::HashSet, path::PathBuf, time::Duration}; use television::{ - action::Action, app::App, channels::TelevisionChannel, + action::Action, + app::{App, AppOptions}, + channels::TelevisionChannel, config::default_config_from_file, }; use tokio::{task::JoinHandle, time::timeout}; @@ -39,7 +41,9 @@ fn setup_app( config.application.tick_rate = 100.0; let input = None; - let mut app = App::new(chan, config, input, select_1, false); + let options = + AppOptions::new(select_1, false, false, config.application.tick_rate); + let mut app = App::new(chan, config, input, options); // retrieve the app's action channel handle in order to send a quit action let tx = app.action_tx.clone();