feat(cli): add a --no-help flag to allow disabling showing 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).
This commit is contained in:
Alex Pasmantier 2025-04-09 23:37:20 +02:00
parent b81873738a
commit 07f0ae6e80
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();