Skip to content

feat: support for external actions #669

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cable/unix/files.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,9 @@ env = { BAT_THEME = "ansi" }

[keybindings]
shortcut = "f1"
ctrl-f12 = "actions:edit"

[actions.edit]
description = "Opens the selected entries with Neovim"
command = "nvim {}"
mode = "execute"
127 changes: 122 additions & 5 deletions television/action.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
use crate::{event::Key, screen::constants::ACTION_PREFIX};
use serde::{Deserialize, Serialize};
use serde_with::{OneOrMany, serde_as};
use std::fmt::Display;

use crate::event::Key;

/// The different actions that can be performed by the application.
#[derive(
Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash, PartialOrd, Ord,
)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Hash, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]
pub enum Action {
// input actions
Expand Down Expand Up @@ -118,6 +115,8 @@ pub enum Action {
/// Handle mouse click event at specific coordinates
#[serde(skip)]
MouseClickAt(u16, u16),
/// Execute an external action
ExternalAction(String),
}

/// Container for one or more actions that can be executed together.
Expand Down Expand Up @@ -365,10 +364,125 @@ impl Display for Action {
write!(f, "select_entry_at_position")
}
Action::MouseClickAt(_, _) => write!(f, "mouse_click_at"),
Action::ExternalAction(name) => write!(f, "{}", name),
}
}
}

impl<'de> serde::Deserialize<'de> for Action {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;

let action = match s.as_str() {
"add_input_char" => Action::AddInputChar(' '),
"delete_prev_char" => Action::DeletePrevChar,
"delete_prev_word" => Action::DeletePrevWord,
"delete_next_char" => Action::DeleteNextChar,
"delete_line" => Action::DeleteLine,
"go_to_prev_char" => Action::GoToPrevChar,
"go_to_next_char" => Action::GoToNextChar,
"go_to_input_start" => Action::GoToInputStart,
"go_to_input_end" => Action::GoToInputEnd,
"render" => Action::Render,
"resize" => Action::Resize(0, 0),
"clear_screen" => Action::ClearScreen,
"toggle_selection_down" => Action::ToggleSelectionDown,
"toggle_selection_up" => Action::ToggleSelectionUp,
"confirm_selection" => Action::ConfirmSelection,
"select_and_exit" => Action::SelectAndExit,
"expect" => Action::Expect(Key::Char(' ')),
"select_next_entry" => Action::SelectNextEntry,
"select_prev_entry" => Action::SelectPrevEntry,
"select_next_page" => Action::SelectNextPage,
"select_prev_page" => Action::SelectPrevPage,
"copy_entry_to_clipboard" => Action::CopyEntryToClipboard,
"scroll_preview_up" => Action::ScrollPreviewUp,
"scroll_preview_down" => Action::ScrollPreviewDown,
"scroll_preview_half_page_up" => Action::ScrollPreviewHalfPageUp,
"scroll_preview_half_page_down" => {
Action::ScrollPreviewHalfPageDown
}
"open_entry" => Action::OpenEntry,
"tick" => Action::Tick,
"suspend" => Action::Suspend,
"resume" => Action::Resume,
"quit" => Action::Quit,
"toggle_remote_control" => Action::ToggleRemoteControl,
"toggle_help" => Action::ToggleHelp,
"toggle_status_bar" => Action::ToggleStatusBar,
"toggle_preview" => Action::TogglePreview,
"error" => Action::Error(String::new()),
"no_op" => Action::NoOp,
"cycle_sources" => Action::CycleSources,
"reload_source" => Action::ReloadSource,
"switch_to_channel" => Action::SwitchToChannel(String::new()),
"watch_timer" => Action::WatchTimer,
"select_prev_history" => Action::SelectPrevHistory,
"select_next_history" => Action::SelectNextHistory,
s if s.starts_with(ACTION_PREFIX) => {
let action_name = s.trim_start_matches(ACTION_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)
}
}

impl Action {
/// Returns a user-friendly description of the action for help panels and UI display.
///
Expand Down Expand Up @@ -460,6 +574,9 @@ impl Action {
// Mouse actions
Action::SelectEntryAtPosition(_, _) => "Select at position",
Action::MouseClickAt(_, _) => "Mouse click",

// External actions
Action::ExternalAction(_) => "External action",
}
}
}
Expand Down
61 changes: 60 additions & 1 deletion television/app.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use crate::{
action::Action,
cable::Cable,
channels::{entry::Entry, prototypes::ChannelPrototype},
channels::{
entry::Entry,
prototypes::{ActionSpec, ChannelPrototype},
},
cli::PostProcessedCli,
config::{Config, DEFAULT_PREVIEW_SIZE, default_tick_rate},
event::{Event, EventLoop, InputEvent, Key, MouseInputEvent},
Expand Down Expand Up @@ -147,13 +150,15 @@ pub enum ActionOutcome {
EntriesWithExpect(FxHashSet<Entry>, Key),
Input(String),
None,
ExternalAction(ActionSpec, String),
}

/// The result of the application.
#[derive(Debug)]
pub struct AppOutput {
pub selected_entries: Option<FxHashSet<Entry>>,
pub expect_key: Option<Key>,
pub external_action: Option<(ActionSpec, String)>,
}

impl AppOutput {
Expand All @@ -162,20 +167,29 @@ impl AppOutput {
ActionOutcome::Entries(entries) => Self {
selected_entries: Some(entries),
expect_key: None,
external_action: None,
},
ActionOutcome::EntriesWithExpect(entries, expect_key) => Self {
selected_entries: Some(entries),
expect_key: Some(expect_key),
external_action: None,
},
ActionOutcome::Input(input) => Self {
selected_entries: Some(FxHashSet::from_iter([Entry::new(
input,
)])),
expect_key: None,
external_action: None,
},
ActionOutcome::None => Self {
selected_entries: None,
expect_key: None,
external_action: None,
},
ActionOutcome::ExternalAction(action_spec, entry_value) => Self {
selected_entries: None,
expect_key: None,
external_action: Some((action_spec, entry_value)),
},
}
}
Expand Down Expand Up @@ -669,6 +683,51 @@ impl App {
self.television.set_pattern("");
}
}
Action::ExternalAction(ref action_name) => {
debug!("External action triggered: {}", action_name);

if let Some(selected_entries) =
self.television.get_selected_entries()
{
if let Some(action_spec) = self
.television
.channel_prototype
.actions
.get(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,
));
}
}
debug!("No entries available for external action");
self.action_tx.send(Action::Error(
"No entry available for external action"
.to_string(),
))?;
return Ok(ActionOutcome::None);
}
_ => {}
}
// Check if we're switching from remote control to channel mode
Expand Down
Loading
Loading