feat(cli): add a --no-remote flag to lock the application on the cli-invoked channel (#455)

This will disable the remote control panel and associated actions
entirely. This is useful when the remote control is not needed or when
the user wants `tv` to run in single-channel mode (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 19:55:12 +00:00 committed by GitHub
parent 4a584b437c
commit b81873738a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 84 additions and 33 deletions

View File

@ -100,6 +100,7 @@ impl App {
config: Config,
input: Option<String>,
select_1: bool,
no_remote: bool,
) -> Self {
let (action_tx, action_rx) = mpsc::unbounded_channel();
let (render_tx, render_rx) = mpsc::unbounded_channel();
@ -110,8 +111,13 @@ impl App {
debug!("{:?}", keymap);
let (ui_state_tx, ui_state_rx) = mpsc::unbounded_channel();
let television =
Television::new(action_tx.clone(), channel, config, input);
let television = Television::new(
action_tx.clone(),
channel,
config,
input,
no_remote,
);
Self {
keymap,
@ -401,6 +407,7 @@ mod test {
Config::default(),
None,
true,
false,
);
app.television
.results_picker

View File

@ -102,6 +102,16 @@ pub struct Cli {
#[arg(long, default_value = "false", verbatim_doc_comment)]
pub select_1: bool,
/// Disable the remote control.
///
/// This will disable the remote control panel and associated actions
/// entirely. This is useful when the remote control is not needed or
/// when the user wants `tv` to run in single-channel mode (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_remote: bool,
#[command(subcommand)]
pub command: Option<Command>,
}

View File

@ -30,6 +30,7 @@ pub struct PostProcessedCli {
pub autocomplete_prompt: Option<String>,
pub keybindings: Option<KeyBindings>,
pub select_1: bool,
pub no_remote: bool,
}
impl Default for PostProcessedCli {
@ -46,6 +47,7 @@ impl Default for PostProcessedCli {
autocomplete_prompt: None,
keybindings: None,
select_1: false,
no_remote: false,
}
}
}
@ -111,6 +113,7 @@ impl From<Cli> for PostProcessedCli {
autocomplete_prompt: cli.autocomplete_prompt,
keybindings,
select_1: cli.select_1,
no_remote: cli.no_remote,
}
}
}
@ -326,6 +329,7 @@ mod tests {
working_directory: Some("/home/user".to_string()),
autocomplete_prompt: None,
select_1: false,
no_remote: false,
};
let post_processed_cli: PostProcessedCli = cli.into();
@ -365,6 +369,7 @@ mod tests {
working_directory: None,
autocomplete_prompt: None,
select_1: false,
no_remote: false,
};
let post_processed_cli: PostProcessedCli = cli.into();
@ -395,6 +400,7 @@ mod tests {
working_directory: None,
autocomplete_prompt: None,
select_1: false,
no_remote: false,
};
let post_processed_cli: PostProcessedCli = cli.into();
@ -420,6 +426,7 @@ mod tests {
working_directory: None,
autocomplete_prompt: None,
select_1: false,
no_remote: false,
};
let post_processed_cli: PostProcessedCli = cli.into();
@ -448,6 +455,7 @@ mod tests {
working_directory: None,
autocomplete_prompt: None,
select_1: false,
no_remote: false,
};
let post_processed_cli: PostProcessedCli = cli.into();

View File

@ -65,7 +65,8 @@ async fn main() -> Result<()> {
CLIPBOARD.with(<_>::default);
debug!("Creating application...");
let mut app = App::new(channel, config, args.input, args.select_1);
let mut app =
App::new(channel, config, args.input, args.select_1, args.no_remote);
stdout().flush()?;
debug!("Running application...");
let output = app.run(stdout().is_terminal(), false).await?;

View File

@ -35,7 +35,7 @@ pub struct Television {
action_tx: UnboundedSender<Action>,
pub config: Config,
pub channel: TelevisionChannel,
pub remote_control: TelevisionChannel,
pub remote_control: Option<TelevisionChannel>,
pub mode: Mode,
pub current_pattern: String,
pub results_picker: Picker,
@ -57,6 +57,7 @@ impl Television {
mut channel: TelevisionChannel,
config: Config,
input: Option<String>,
no_remote: bool,
) -> Self {
let mut results_picker = Picker::new(input.clone());
if config.ui.input_bar_position == InputPosition::Bottom {
@ -87,13 +88,20 @@ impl Television {
None,
);
let remote_control = if no_remote {
None
} else {
Some(TelevisionChannel::RemoteControl(RemoteControl::new(
builtin_channels,
Some(cable_channels),
)))
};
Self {
action_tx,
config,
channel,
remote_control: TelevisionChannel::RemoteControl(
RemoteControl::new(builtin_channels, Some(cable_channels)),
),
remote_control,
mode: Mode::Channel,
current_pattern: EMPTY_STRING.to_string(),
results_picker,
@ -118,9 +126,9 @@ impl Television {
let builtin_channels = load_builtin_channels(Some(
&cable_channels.keys().collect::<Vec<_>>(),
));
self.remote_control = TelevisionChannel::RemoteControl(
self.remote_control = Some(TelevisionChannel::RemoteControl(
RemoteControl::new(builtin_channels, Some(cable_channels)),
);
));
}
pub fn dump_context(&self) -> Ctx {
@ -170,7 +178,7 @@ impl Television {
self.channel.find(pattern);
}
Mode::RemoteControl | Mode::SendToChannel => {
self.remote_control.find(pattern);
self.remote_control.as_mut().unwrap().find(pattern);
}
}
}
@ -188,6 +196,8 @@ impl Television {
if let Some(i) = self.rc_picker.selected() {
return self
.remote_control
.as_ref()
.unwrap()
.get_result(i.try_into().unwrap());
}
None
@ -217,9 +227,10 @@ impl Television {
Mode::Channel => {
(self.channel.result_count(), &mut self.results_picker)
}
Mode::RemoteControl | Mode::SendToChannel => {
(self.remote_control.total_count(), &mut self.rc_picker)
}
Mode::RemoteControl | Mode::SendToChannel => (
self.remote_control.as_ref().unwrap().total_count(),
&mut self.rc_picker,
),
};
if result_count == 0 {
return;
@ -236,9 +247,10 @@ impl Television {
Mode::Channel => {
(self.channel.result_count(), &mut self.results_picker)
}
Mode::RemoteControl | Mode::SendToChannel => {
(self.remote_control.total_count(), &mut self.rc_picker)
}
Mode::RemoteControl | Mode::SendToChannel => (
self.remote_control.as_ref().unwrap().total_count(),
&mut self.rc_picker,
),
};
if result_count == 0 {
return;
@ -368,18 +380,20 @@ impl Television {
pub fn update_rc_picker_state(&mut self) {
if self.rc_picker.selected().is_none()
&& self.remote_control.result_count() > 0
&& self.remote_control.as_ref().unwrap().result_count() > 0
{
self.rc_picker.select(Some(0));
self.rc_picker.relative_select(Some(0));
}
self.rc_picker.entries = self.remote_control.results(
// this'll be more than the actual rc height but it's fine
self.ui_state.layout.results.height.into(),
u32::try_from(self.rc_picker.offset()).unwrap(),
);
self.rc_picker.total_items = self.remote_control.total_count();
self.rc_picker.entries =
self.remote_control.as_mut().unwrap().results(
// this'll be more than the actual rc height but it's fine
self.ui_state.layout.results.height.into(),
u32::try_from(self.rc_picker.offset()).unwrap(),
);
self.rc_picker.total_items =
self.remote_control.as_ref().unwrap().total_count();
}
pub fn handle_input_action(&mut self, action: &Action) {
@ -408,6 +422,9 @@ impl Television {
}
pub fn handle_toggle_rc(&mut self) {
if self.remote_control.is_none() {
return;
}
match self.mode {
Mode::Channel => {
self.mode = Mode::RemoteControl;
@ -417,7 +434,7 @@ impl Television {
// this resets the RC picker
self.reset_picker_input();
self.init_remote_control();
self.remote_control.find(EMPTY_STRING);
self.remote_control.as_mut().unwrap().find(EMPTY_STRING);
self.reset_picker_selection();
self.mode = Mode::Channel;
}
@ -426,16 +443,19 @@ impl Television {
}
pub fn handle_toggle_send_to_channel(&mut self) {
if self.remote_control.is_none() {
return;
}
match self.mode {
Mode::Channel | Mode::RemoteControl => {
self.mode = Mode::SendToChannel;
self.remote_control = TelevisionChannel::RemoteControl(
self.remote_control = Some(TelevisionChannel::RemoteControl(
RemoteControl::with_transitions_from(&self.channel),
);
));
}
Mode::SendToChannel => {
self.reset_picker_input();
self.remote_control.find(EMPTY_STRING);
self.remote_control.as_mut().unwrap().find(EMPTY_STRING);
self.reset_picker_selection();
self.mode = Mode::Channel;
}
@ -462,12 +482,15 @@ impl Television {
}
Mode::RemoteControl => {
if let Some(entry) = self.get_selected_entry(None) {
let new_channel =
self.remote_control.zap(entry.name.as_str())?;
let new_channel = self
.remote_control
.as_ref()
.unwrap()
.zap(entry.name.as_str())?;
// this resets the RC picker
self.reset_picker_selection();
self.reset_picker_input();
self.remote_control.find(EMPTY_STRING);
self.remote_control.as_mut().unwrap().find(EMPTY_STRING);
self.mode = Mode::Channel;
self.change_channel(new_channel);
}
@ -479,7 +502,7 @@ impl Television {
.transition_to(entry.name.as_str().try_into()?);
self.reset_picker_selection();
self.reset_picker_input();
self.remote_control.find(EMPTY_STRING);
self.remote_control.as_mut().unwrap().find(EMPTY_STRING);
self.mode = Mode::Channel;
self.change_channel(new_channel);
}
@ -589,7 +612,9 @@ impl Television {
self.update_results_picker_state();
self.update_rc_picker_state();
if self.remote_control.is_some() {
self.update_rc_picker_state();
}
let selected_entry = self
.get_selected_entry(Some(Mode::Channel))

View File

@ -39,7 +39,7 @@ fn setup_app(
config.application.tick_rate = 100.0;
let input = None;
let mut app = App::new(chan, config, input, select_1);
let mut app = App::new(chan, config, input, select_1, false);
// retrieve the app's action channel handle in order to send a quit action
let tx = app.action_tx.clone();