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).
This commit is contained in:
Alexandre Pasmantier 2025-04-09 21:46:16 +00:00 committed by GitHub
parent b81873738a
commit 5bf3d20c83
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 98 additions and 77 deletions

View File

@ -30,7 +30,8 @@ fn draw(c: &mut Criterion) {
])); ]));
channel.find("television"); channel.find("television");
// Wait for the channel to finish loading // 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 { for _ in 0..5 {
// tick the matcher // tick the matcher
let _ = tv.channel.results(10, 0); let _ = tv.channel.results(10, 0);

View File

@ -6,7 +6,7 @@ use tracing::{debug, trace};
use crate::channels::entry::{Entry, PreviewType}; use crate::channels::entry::{Entry, PreviewType};
use crate::channels::{OnAir, TelevisionChannel}; use crate::channels::{OnAir, TelevisionChannel};
use crate::config::Config; use crate::config::{default_tick_rate, Config};
use crate::keymap::Keymap; use crate::keymap::Keymap;
use crate::render::UiState; use crate::render::UiState;
use crate::television::{Mode, Television}; use crate::television::{Mode, Television};
@ -16,12 +16,47 @@ use crate::{
render::{render, RenderingTask}, 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. /// The main application struct that holds the state of the application.
pub struct App { pub struct App {
keymap: Keymap, 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. /// The television instance that handles channels and entries.
television: Television, television: Television,
/// A flag that indicates whether the application should quit during the next frame. /// 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<UiState>, ui_state_tx: mpsc::UnboundedSender<UiState>,
/// Render task handle /// Render task handle
render_task: Option<tokio::task::JoinHandle<Result<()>>>, render_task: Option<tokio::task::JoinHandle<Result<()>>>,
/// Whether the application should automatically select the first entry if there is only one options: AppOptions,
/// entry available.
select_1: bool,
} }
/// The outcome of an action. /// The outcome of an action.
@ -99,14 +132,12 @@ impl App {
channel: TelevisionChannel, channel: TelevisionChannel,
config: Config, config: Config,
input: Option<String>, input: Option<String>,
select_1: bool, options: AppOptions,
no_remote: bool,
) -> Self { ) -> Self {
let (action_tx, action_rx) = mpsc::unbounded_channel(); let (action_tx, action_rx) = mpsc::unbounded_channel();
let (render_tx, render_rx) = mpsc::unbounded_channel(); let (render_tx, render_rx) = mpsc::unbounded_channel();
let (_, event_rx) = mpsc::unbounded_channel(); let (_, event_rx) = mpsc::unbounded_channel();
let (event_abort_tx, _) = mpsc::unbounded_channel(); let (event_abort_tx, _) = mpsc::unbounded_channel();
let tick_rate = config.application.tick_rate;
let keymap = Keymap::from(&config.keybindings); let keymap = Keymap::from(&config.keybindings);
debug!("{:?}", keymap); debug!("{:?}", keymap);
@ -116,12 +147,12 @@ impl App {
channel, channel,
config, config,
input, input,
no_remote, options.no_remote,
options.no_help,
); );
Self { Self {
keymap, keymap,
tick_rate,
television, television,
should_quit: false, should_quit: false,
should_suspend: false, should_suspend: false,
@ -134,7 +165,7 @@ impl App {
ui_state_rx, ui_state_rx,
ui_state_tx, ui_state_tx,
render_task: None, render_task: None,
select_1, options,
} }
} }
@ -160,7 +191,7 @@ impl App {
// Event loop // Event loop
if !headless { if !headless {
debug!("Starting backend event loop"); 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_rx = event_loop.rx;
self.event_abort_tx = event_loop.abort_tx; 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 // If `self.select_1` is true, the channel is not running, and there is
// only one entry available, automatically select the first entry. // 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.running()
&& self.television.channel.total_count() == 1 && self.television.channel.total_count() == 1
{ {
@ -406,8 +437,7 @@ mod test {
TelevisionChannel::Stdin(StdinChannel::new(PreviewType::None)), TelevisionChannel::Stdin(StdinChannel::new(PreviewType::None)),
Config::default(), Config::default(),
None, None,
true, AppOptions::default(),
false,
); );
app.television app.television
.results_picker .results_picker

View File

@ -1,6 +1,7 @@
use clap::{Parser, Subcommand, ValueEnum}; use clap::{Parser, Subcommand, ValueEnum};
#[derive(Parser, Debug)] #[allow(clippy::struct_excessive_bools)]
#[derive(Parser, Debug, Default)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
pub struct Cli { pub struct Cli {
/// Which channel shall we watch? /// Which channel shall we watch?
@ -112,6 +113,16 @@ pub struct Cli {
#[arg(long, default_value = "false", verbatim_doc_comment)] #[arg(long, default_value = "false", verbatim_doc_comment)]
pub no_remote: bool, 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)] #[command(subcommand)]
pub command: Option<Command>, pub command: Option<Command>,
} }

View File

@ -17,6 +17,7 @@ use crate::{
pub mod args; pub mod args;
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PostProcessedCli { pub struct PostProcessedCli {
pub channel: ParsedCliChannel, pub channel: ParsedCliChannel,
@ -31,6 +32,7 @@ pub struct PostProcessedCli {
pub keybindings: Option<KeyBindings>, pub keybindings: Option<KeyBindings>,
pub select_1: bool, pub select_1: bool,
pub no_remote: bool, pub no_remote: bool,
pub no_help: bool,
} }
impl Default for PostProcessedCli { impl Default for PostProcessedCli {
@ -48,6 +50,7 @@ impl Default for PostProcessedCli {
keybindings: None, keybindings: None,
select_1: false, select_1: false,
no_remote: false, no_remote: false,
no_help: false,
} }
} }
} }
@ -114,6 +117,7 @@ impl From<Cli> for PostProcessedCli {
keybindings, keybindings,
select_1: cli.select_1, select_1: cli.select_1,
no_remote: cli.no_remote, no_remote: cli.no_remote,
no_help: cli.no_help,
} }
} }
} }
@ -319,17 +323,9 @@ mod tests {
let cli = Cli { let cli = Cli {
channel: "files".to_string(), channel: "files".to_string(),
preview: Some("bat -n --color=always {}".to_string()), preview: Some("bat -n --color=always {}".to_string()),
no_preview: false,
delimiter: ":".to_string(), 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()), working_directory: Some("/home/user".to_string()),
autocomplete_prompt: None, ..Default::default()
select_1: false,
no_remote: false,
}; };
let post_processed_cli: PostProcessedCli = cli.into(); let post_processed_cli: PostProcessedCli = cli.into();
@ -345,8 +341,8 @@ mod tests {
delimiter: ":".to_string() delimiter: ":".to_string()
}) })
); );
assert_eq!(post_processed_cli.tick_rate, Some(50.0)); assert_eq!(post_processed_cli.tick_rate, None);
assert_eq!(post_processed_cli.frame_rate, Some(60.0)); assert_eq!(post_processed_cli.frame_rate, None);
assert_eq!( assert_eq!(
post_processed_cli.working_directory, post_processed_cli.working_directory,
Some("/home/user".to_string()) Some("/home/user".to_string())
@ -358,18 +354,8 @@ mod tests {
fn test_from_cli_no_args() { fn test_from_cli_no_args() {
let cli = Cli { let cli = Cli {
channel: ".".to_string(), channel: ".".to_string(),
preview: None,
no_preview: false,
delimiter: ":".to_string(), delimiter: ":".to_string(),
tick_rate: Some(50.0), ..Default::default()
frame_rate: Some(60.0),
keybindings: None,
input: None,
command: None,
working_directory: None,
autocomplete_prompt: None,
select_1: false,
no_remote: false,
}; };
let post_processed_cli: PostProcessedCli = cli.into(); let post_processed_cli: PostProcessedCli = cli.into();
@ -390,17 +376,8 @@ mod tests {
let cli = Cli { let cli = Cli {
channel: "files".to_string(), channel: "files".to_string(),
preview: Some(":files:".to_string()), preview: Some(":files:".to_string()),
no_preview: false,
delimiter: ":".to_string(), delimiter: ":".to_string(),
tick_rate: Some(50.0), ..Default::default()
frame_rate: Some(60.0),
keybindings: None,
input: None,
command: None,
working_directory: None,
autocomplete_prompt: None,
select_1: false,
no_remote: false,
}; };
let post_processed_cli: PostProcessedCli = cli.into(); let post_processed_cli: PostProcessedCli = cli.into();
@ -416,17 +393,8 @@ mod tests {
let cli = Cli { let cli = Cli {
channel: "files".to_string(), channel: "files".to_string(),
preview: Some(":env_var:".to_string()), preview: Some(":env_var:".to_string()),
no_preview: false,
delimiter: ":".to_string(), delimiter: ":".to_string(),
tick_rate: Some(50.0), ..Default::default()
frame_rate: Some(60.0),
keybindings: None,
input: None,
command: None,
working_directory: None,
autocomplete_prompt: None,
select_1: false,
no_remote: false,
}; };
let post_processed_cli: PostProcessedCli = cli.into(); let post_processed_cli: PostProcessedCli = cli.into();
@ -442,20 +410,12 @@ mod tests {
let cli = Cli { let cli = Cli {
channel: "files".to_string(), channel: "files".to_string(),
preview: Some(":env_var:".to_string()), preview: Some(":env_var:".to_string()),
no_preview: false,
delimiter: ":".to_string(), delimiter: ":".to_string(),
tick_rate: Some(50.0),
frame_rate: Some(60.0),
keybindings: Some( keybindings: Some(
"quit=\"esc\";select_next_entry=[\"down\",\"ctrl-j\"]" "quit=\"esc\";select_next_entry=[\"down\",\"ctrl-j\"]"
.to_string(), .to_string(),
), ),
input: None, ..Default::default()
command: None,
working_directory: None,
autocomplete_prompt: None,
select_1: false,
no_remote: false,
}; };
let post_processed_cli: PostProcessedCli = cli.into(); let post_processed_cli: PostProcessedCli = cli.into();

View File

@ -260,7 +260,7 @@ fn default_frame_rate() -> f64 {
60.0 60.0
} }
fn default_tick_rate() -> f64 { pub fn default_tick_rate() -> f64 {
50.0 50.0
} }

View File

@ -10,7 +10,7 @@ use television::cli::parse_channel;
use television::utils::clipboard::CLIPBOARD; use television::utils::clipboard::CLIPBOARD;
use tracing::{debug, error, info}; use tracing::{debug, error, info};
use television::app::App; use television::app::{App, AppOptions};
use television::channels::{ use television::channels::{
entry::PreviewType, stdin::Channel as StdinChannel, TelevisionChannel, entry::PreviewType, stdin::Channel as StdinChannel, TelevisionChannel,
}; };
@ -65,8 +65,13 @@ async fn main() -> Result<()> {
CLIPBOARD.with(<_>::default); CLIPBOARD.with(<_>::default);
debug!("Creating application..."); debug!("Creating application...");
let mut app = let options = AppOptions::new(
App::new(channel, config, args.input, args.select_1, args.no_remote); 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()?; stdout().flush()?;
debug!("Running application..."); debug!("Running application...");
let output = app.run(stdout().is_terminal(), false).await?; let output = app.run(stdout().is_terminal(), false).await?;

View File

@ -48,6 +48,7 @@ pub struct Television {
pub colorscheme: Colorscheme, pub colorscheme: Colorscheme,
pub ticks: u64, pub ticks: u64,
pub ui_state: UiState, pub ui_state: UiState,
pub no_help: bool,
} }
impl Television { impl Television {
@ -55,9 +56,10 @@ impl Television {
pub fn new( pub fn new(
action_tx: UnboundedSender<Action>, action_tx: UnboundedSender<Action>,
mut channel: TelevisionChannel, mut channel: TelevisionChannel,
config: Config, mut config: Config,
input: Option<String>, input: Option<String>,
no_remote: bool, no_remote: bool,
no_help: bool,
) -> Self { ) -> Self {
let mut results_picker = Picker::new(input.clone()); let mut results_picker = Picker::new(input.clone());
if config.ui.input_bar_position == InputPosition::Bottom { if config.ui.input_bar_position == InputPosition::Bottom {
@ -97,6 +99,10 @@ impl Television {
))) )))
}; };
if no_help {
config.ui.show_help_bar = false;
}
Self { Self {
action_tx, action_tx,
config, config,
@ -114,6 +120,7 @@ impl Television {
colorscheme, colorscheme,
ticks: 0, ticks: 0,
ui_state: UiState::default(), ui_state: UiState::default(),
no_help,
} }
} }
@ -592,6 +599,9 @@ impl Television {
self.handle_toggle_send_to_channel(); self.handle_toggle_send_to_channel();
} }
Action::ToggleHelp => { Action::ToggleHelp => {
if self.no_help {
return Ok(());
}
self.config.ui.show_help_bar = !self.config.ui.show_help_bar; self.config.ui.show_help_bar = !self.config.ui.show_help_bar;
} }
Action::TogglePreview => { Action::TogglePreview => {

View File

@ -1,7 +1,9 @@
use std::{collections::HashSet, path::PathBuf, time::Duration}; use std::{collections::HashSet, path::PathBuf, time::Duration};
use television::{ use television::{
action::Action, app::App, channels::TelevisionChannel, action::Action,
app::{App, AppOptions},
channels::TelevisionChannel,
config::default_config_from_file, config::default_config_from_file,
}; };
use tokio::{task::JoinHandle, time::timeout}; use tokio::{task::JoinHandle, time::timeout};
@ -39,7 +41,9 @@ fn setup_app(
config.application.tick_rate = 100.0; config.application.tick_rate = 100.0;
let input = None; 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 // retrieve the app's action channel handle in order to send a quit action
let tx = app.action_tx.clone(); let tx = app.action_tx.clone();