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");
// 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);

View File

@ -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<UiState>,
/// Render task handle
render_task: Option<tokio::task::JoinHandle<Result<()>>>,
/// 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<String>,
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

View File

@ -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<Command>,
}

View File

@ -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<KeyBindings>,
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<Cli> 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();

View File

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

View File

@ -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?;

View File

@ -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<Action>,
mut channel: TelevisionChannel,
config: Config,
mut config: Config,
input: Option<String>,
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 => {

View File

@ -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();