mirror of
https://github.com/alexpasmantier/television.git
synced 2025-07-29 06:11:37 +00:00
feat: add external actions support for multiple items
This commit is contained in:
parent
c514494f6d
commit
4986dd00d3
@ -12,8 +12,8 @@ env = { BAT_THEME = "ansi" }
|
||||
|
||||
[keybindings]
|
||||
shortcut = "f1"
|
||||
ctrl-f12 = "edit"
|
||||
ctrl-f12 = "actions:edit"
|
||||
|
||||
[actions.edit]
|
||||
description = "Opens the selected entry with Neovim"
|
||||
command = "nvim '{}'"
|
||||
description = "Opens the selected entries with Neovim"
|
||||
command = "nvim {}"
|
||||
|
@ -423,7 +423,61 @@ impl<'de> serde::Deserialize<'de> for Action {
|
||||
"watch_timer" => Action::WatchTimer,
|
||||
"select_prev_history" => Action::SelectPrevHistory,
|
||||
"select_next_history" => Action::SelectNextHistory,
|
||||
_ => Action::ExternalAction(s),
|
||||
s if s.starts_with("actions:") => {
|
||||
let action_name = &s[8..]; // Remove "actions:" prefix
|
||||
Action::ExternalAction(action_name.to_string())
|
||||
}
|
||||
_ => {
|
||||
return Err(serde::de::Error::unknown_variant(
|
||||
&s,
|
||||
&[
|
||||
"add_input_char",
|
||||
"delete_prev_char",
|
||||
"delete_prev_word",
|
||||
"delete_next_char",
|
||||
"delete_line",
|
||||
"go_to_prev_char",
|
||||
"go_to_next_char",
|
||||
"go_to_input_start",
|
||||
"go_to_input_end",
|
||||
"render",
|
||||
"resize",
|
||||
"clear_screen",
|
||||
"toggle_selection_down",
|
||||
"toggle_selection_up",
|
||||
"confirm_selection",
|
||||
"select_and_exit",
|
||||
"expect",
|
||||
"select_next_entry",
|
||||
"select_prev_entry",
|
||||
"select_next_page",
|
||||
"select_prev_page",
|
||||
"copy_entry_to_clipboard",
|
||||
"scroll_preview_up",
|
||||
"scroll_preview_down",
|
||||
"scroll_preview_half_page_up",
|
||||
"scroll_preview_half_page_down",
|
||||
"open_entry",
|
||||
"tick",
|
||||
"suspend",
|
||||
"resume",
|
||||
"quit",
|
||||
"toggle_remote_control",
|
||||
"toggle_help",
|
||||
"toggle_status_bar",
|
||||
"toggle_preview",
|
||||
"error",
|
||||
"no_op",
|
||||
"cycle_sources",
|
||||
"reload_source",
|
||||
"switch_to_channel",
|
||||
"watch_timer",
|
||||
"select_prev_history",
|
||||
"select_next_history",
|
||||
"actions:*",
|
||||
],
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(action)
|
||||
|
@ -686,54 +686,77 @@ impl App {
|
||||
Action::ExternalAction(ref action_name) => {
|
||||
debug!("External action triggered: {}", action_name);
|
||||
|
||||
// Handle external action execution
|
||||
if let Some(selected_entry) =
|
||||
// Handle external action execution for selected entries
|
||||
let selected_entries = if !self
|
||||
.television
|
||||
.channel
|
||||
.selected_entries()
|
||||
.is_empty()
|
||||
{
|
||||
// Use multi-selected entries
|
||||
self.television.channel.selected_entries().clone()
|
||||
} else if let Some(current_entry) =
|
||||
self.television.get_selected_entry()
|
||||
{
|
||||
debug!("Selected entry: {}", selected_entry.raw);
|
||||
|
||||
if let Some(action_spec) = self
|
||||
.television
|
||||
.channel_prototype
|
||||
.actions
|
||||
.get(action_name)
|
||||
{
|
||||
debug!(
|
||||
"Found action spec for: {}",
|
||||
action_name
|
||||
);
|
||||
// Store the external action info and exit - the command will be executed after terminal cleanup
|
||||
self.should_quit = true;
|
||||
self.render_tx.send(RenderingTask::Quit)?;
|
||||
return Ok(ActionOutcome::ExternalAction(
|
||||
action_spec.clone(),
|
||||
selected_entry.raw.clone(),
|
||||
));
|
||||
}
|
||||
|
||||
error!("Unknown action: {}", action_name);
|
||||
// List available actions for debugging
|
||||
let available_actions: Vec<&String> = self
|
||||
.television
|
||||
.channel_prototype
|
||||
.actions
|
||||
.keys()
|
||||
.collect();
|
||||
debug!(
|
||||
"Available actions: {:?}",
|
||||
available_actions
|
||||
);
|
||||
self.action_tx.send(Action::Error(format!(
|
||||
"Unknown action: {}",
|
||||
action_name
|
||||
)))?;
|
||||
// Use single entry under cursor
|
||||
std::iter::once(current_entry).collect()
|
||||
} else {
|
||||
error!("No entry selected for external action");
|
||||
debug!("No entries available for external action");
|
||||
self.action_tx.send(Action::Error(
|
||||
"No entry selected for external action"
|
||||
"No entry available for external action"
|
||||
.to_string(),
|
||||
))?;
|
||||
return Ok(ActionOutcome::None);
|
||||
};
|
||||
|
||||
debug!(
|
||||
"Selected {} entries for external action",
|
||||
selected_entries.len()
|
||||
);
|
||||
|
||||
if let Some(action_spec) = self
|
||||
.television
|
||||
.channel_prototype
|
||||
.actions
|
||||
.get(action_name)
|
||||
{
|
||||
debug!("Found action spec for: {}", action_name);
|
||||
// Store the external action info and exit - the command will be executed after terminal cleanup
|
||||
self.should_quit = true;
|
||||
self.render_tx.send(RenderingTask::Quit)?;
|
||||
// Concatenate entry values with space separator, quoting items with whitespace
|
||||
let concatenated_entries: String =
|
||||
selected_entries
|
||||
.iter()
|
||||
.map(|entry| {
|
||||
let raw = entry.raw.clone();
|
||||
if raw.chars().any(char::is_whitespace)
|
||||
{
|
||||
format!("'{}'", raw)
|
||||
} else {
|
||||
raw
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(" ");
|
||||
return Ok(ActionOutcome::ExternalAction(
|
||||
action_spec.clone(),
|
||||
concatenated_entries,
|
||||
));
|
||||
}
|
||||
error!("Unknown action: {}", action_name);
|
||||
// List available actions for debugging
|
||||
let available_actions: Vec<&String> = self
|
||||
.television
|
||||
.channel_prototype
|
||||
.actions
|
||||
.keys()
|
||||
.collect();
|
||||
debug!("Available actions: {:?}", available_actions);
|
||||
self.action_tx.send(Action::Error(format!(
|
||||
"Unknown action: {}",
|
||||
action_name
|
||||
)))?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
@ -160,7 +160,9 @@ impl CommandSpec {
|
||||
}
|
||||
|
||||
/// Execution mode for external actions
|
||||
#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize, PartialEq)]
|
||||
#[derive(
|
||||
Debug, Clone, Default, serde::Deserialize, serde::Serialize, PartialEq,
|
||||
)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ExecutionMode {
|
||||
/// Fork the command as a child process (current behavior, tv stays open)
|
||||
@ -171,7 +173,9 @@ pub enum ExecutionMode {
|
||||
}
|
||||
|
||||
/// Output handling mode for external actions
|
||||
#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize, PartialEq)]
|
||||
#[derive(
|
||||
Debug, Clone, Default, serde::Deserialize, serde::Serialize, PartialEq,
|
||||
)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum OutputMode {
|
||||
/// Inherit stdin/stdout/stderr (current behavior)
|
||||
@ -191,7 +195,7 @@ pub struct ActionSpec {
|
||||
pub command: CommandSpec,
|
||||
/// How to execute the command (fork vs become)
|
||||
#[serde(default)]
|
||||
pub become: bool,
|
||||
pub r#become: bool,
|
||||
/// How to handle command output
|
||||
#[serde(default)]
|
||||
pub output_mode: OutputMode,
|
||||
@ -818,8 +822,8 @@ mod tests {
|
||||
|
||||
[keybindings]
|
||||
shortcut = "f1"
|
||||
f8 = "thebatman"
|
||||
f9 = "lsman"
|
||||
f8 = "actions:thebatman"
|
||||
f9 = "actions:lsman"
|
||||
|
||||
[actions.thebatman]
|
||||
description = "cats the file"
|
||||
|
@ -3,7 +3,7 @@ use clap::Parser;
|
||||
use std::env;
|
||||
use std::io::{BufWriter, IsTerminal, Write, stdout};
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Stdio, exit};
|
||||
use std::process::exit;
|
||||
use television::{
|
||||
app::{App, AppOptions},
|
||||
cable::{Cable, cable_empty_exit, load_cable},
|
||||
@ -21,7 +21,7 @@ use television::{
|
||||
gh::update_local_channels,
|
||||
television::Mode,
|
||||
utils::clipboard::CLIPBOARD,
|
||||
utils::command::{execute_action, shell_command},
|
||||
utils::command::execute_action,
|
||||
utils::{
|
||||
shell::{
|
||||
Shell, completion_script, render_autocomplete_script_template,
|
||||
@ -116,16 +116,12 @@ async fn main() -> Result<()> {
|
||||
// Handle external action execution after terminal cleanup
|
||||
if let Some((action_spec, entry_value)) = output.external_action {
|
||||
debug!("Executing external action command after terminal cleanup");
|
||||
let template = action_spec.command.get_nth(0);
|
||||
|
||||
// Format the command template with the entry value
|
||||
let formatted_command =
|
||||
action_spec.command.get_nth(0).format(&entry_value)?;
|
||||
// Process the concatenated entry value through the template system
|
||||
let formatted_command = template.format(&entry_value)?;
|
||||
|
||||
debug!("External command: {}", formatted_command);
|
||||
|
||||
// Execute the command using the new action execution abstraction
|
||||
let status = execute_action(&action_spec, &formatted_command)?;
|
||||
|
||||
if !status.success() {
|
||||
eprintln!(
|
||||
"External command failed with exit code: {:?}",
|
||||
|
@ -35,7 +35,7 @@ pub fn shell_command<S>(
|
||||
}
|
||||
|
||||
/// Execute an external action with the appropriate execution mode and output handling
|
||||
///
|
||||
///
|
||||
/// Currently implements the existing behavior but designed to be extended with:
|
||||
/// - `become` flag for execve behavior
|
||||
/// - `output_mode` for different output handling modes
|
||||
@ -45,7 +45,7 @@ pub fn execute_action(
|
||||
) -> std::io::Result<std::process::ExitStatus> {
|
||||
// For now, preserve existing behavior regardless of the new flags
|
||||
// In the future, this will branch based on action_spec.become and action_spec.output_mode
|
||||
|
||||
|
||||
let mut cmd = shell_command(
|
||||
formatted_command,
|
||||
action_spec.command.interactive,
|
||||
@ -74,7 +74,7 @@ pub fn execute_action(
|
||||
}
|
||||
|
||||
// Execute based on become flag (future extension point)
|
||||
if action_spec.become {
|
||||
if action_spec.r#become {
|
||||
// Future: use execve to replace current process
|
||||
// For now, use normal execution
|
||||
let mut child = cmd.spawn()?;
|
||||
|
@ -45,8 +45,8 @@ env = { BAT_THEME = "ansi" }
|
||||
|
||||
[keybindings]
|
||||
shortcut = "f1"
|
||||
f8 = "thebatman"
|
||||
f9 = "lsman"
|
||||
f8 = "actions:thebatman"
|
||||
f9 = "actions:lsman"
|
||||
|
||||
[actions.thebatman]
|
||||
description = "show file content"
|
||||
@ -108,8 +108,8 @@ env = { BAT_THEME = "ansi" }
|
||||
|
||||
[keybindings]
|
||||
shortcut = "f1"
|
||||
f8 = "thebatman"
|
||||
f9 = "lsman"
|
||||
f8 = "actions:thebatman"
|
||||
f9 = "actions:lsman"
|
||||
|
||||
[actions.thebatman]
|
||||
description = "show file content"
|
||||
|
Loading…
x
Reference in New Issue
Block a user