Skip to content

Commit 834a3cc

Browse files
feat(actions): improve actions behavior and add support for exec (#689)
## 📺 PR Description This improves support for `fork` and adds the logic for the `exec` feature. I'm dropping the `OutputMode` logic for now as it doesn't seem that useful and kind of gets in the way right now. We might add that back later on. ## Checklist <!-- a quick pass through the following items to make sure you haven't forgotten anything --> - [ ] my commits **and PR title** follow the [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) format - [ ] if this is a new feature, I have added tests to consolidate the feature and prevent regressions - [ ] if this is a bug fix, I have added a test that reproduces the bug (if applicable) - [ ] I have added a reasonable amount of documentation to the code where appropriate
1 parent b2b60ee commit 834a3cc

File tree

7 files changed

+156
-108
lines changed

7 files changed

+156
-108
lines changed

cable/unix/files.toml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ env = { BAT_THEME = "ansi" }
1212

1313
[keybindings]
1414
shortcut = "f1"
15-
# ctrl-f12 = "actions:edit"
15+
f12 = "actions:edit"
1616

17-
# [actions.edit]
18-
# description = "Opens the selected entries with Neovim"
19-
# command = "nvim {}"
20-
# mode = "execute"
17+
[actions.edit]
18+
description = "Opens the selected entries with the default editor (falls back to vim)"
19+
command = "${EDITOR:-vim} '{}'"
20+
# use `mode = "fork"` if you want to return to tv afterwards
21+
mode = "execute"

docs/01-Users/07-channels.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,6 @@ separator = "\n"
278278
description = "View files with less"
279279
command = "less {}"
280280
mode = "fork"
281-
output_mode = "discard"
282281
separator = " "
283282
# example: inputs "file1" and "file 2" will generate the command
284283
# less file1 file 2
@@ -290,11 +289,8 @@ separator = " "
290289
- `description` - Optional description of what the action does
291290
- `command` - Command template to execute (supports [templating syntax](#templating-syntax))
292291
- `mode` - Execution mode:
293-
- `execute` - Run command and wait for completion (inherits terminal)
294-
- `fork` - Run command in background (**not yet implemented**)
295-
- `output_mode` - How to handle command output (when `mode = "fork"`, **not yet implemented**):
296-
- `capture` - Capture output for processing
297-
- `discard` - Discard output silently
292+
- `execute` - Run command and become the new process
293+
- `fork` - Run command in a subprocess, allowing you to return to tv upon completion
298294
- `separator` - Character(s) to use when joining **multiple selected entries** when using complex template processing,
299295
depending on the entries content it might be beneficial to change to another
300296
one (default: `" "` - space)

television/app.rs

Lines changed: 90 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
1+
use std::{thread::sleep, time::Duration};
2+
13
use crate::{
24
action::Action,
35
cable::Cable,
4-
channels::{entry::Entry, prototypes::ActionSpec},
6+
channels::{
7+
entry::Entry,
8+
prototypes::{ActionSpec, ExecutionMode},
9+
},
510
config::layers::LayeredConfig,
6-
event::{Event, EventLoop, InputEvent, Key, MouseInputEvent},
11+
event::{
12+
ControlEvent, Event, EventLoop, InputEvent, Key, MouseInputEvent,
13+
},
714
history::History,
815
render::{RenderingTask, UiState, render},
916
television::{Mode, Television},
1017
tui::{IoStream, Tui, TuiMode},
18+
utils::command::execute_action,
1119
};
1220
use anyhow::Result;
1321
use rustc_hash::FxHashSet;
@@ -19,8 +27,6 @@ pub struct App {
1927
pub television: Television,
2028
/// A flag that indicates whether the application should quit during the next frame.
2129
should_quit: bool,
22-
/// A flag that indicates whether the application should suspend during the next frame.
23-
should_suspend: bool,
2430
/// A sender channel for actions.
2531
///
2632
/// This is made public so that tests, for instance, can send actions to a running application.
@@ -29,8 +35,8 @@ pub struct App {
2935
action_rx: mpsc::UnboundedReceiver<Action>,
3036
/// The receiver channel for events.
3137
event_rx: mpsc::UnboundedReceiver<Event<Key>>,
32-
/// A sender channel to abort the event loop.
33-
event_abort_tx: mpsc::UnboundedSender<()>,
38+
/// A sender channel to control the event loop.
39+
event_control_tx: mpsc::UnboundedSender<ControlEvent>,
3440
/// A sender channel for rendering tasks.
3541
render_tx: mpsc::UnboundedSender<RenderingTask>,
3642
/// The receiver channel for rendering tasks.
@@ -131,11 +137,10 @@ impl App {
131137
let mut app = Self {
132138
television,
133139
should_quit: false,
134-
should_suspend: false,
135140
action_tx,
136141
action_rx,
137142
event_rx,
138-
event_abort_tx,
143+
event_control_tx: event_abort_tx,
139144
render_tx,
140145
render_rx,
141146
ui_state_rx,
@@ -287,7 +292,7 @@ impl App {
287292
let event_loop =
288293
EventLoop::new(self.television.merged_config.tick_rate);
289294
self.event_rx = event_loop.rx;
290-
self.event_abort_tx = event_loop.abort_tx;
295+
self.event_control_tx = event_loop.control_tx;
291296
}
292297

293298
// Start watch timer if configured
@@ -349,7 +354,7 @@ impl App {
349354
if self.should_quit {
350355
// send a termination signal to the event loop
351356
if !headless {
352-
self.event_abort_tx.send(())?;
357+
self.event_control_tx.send(ControlEvent::Abort)?;
353358
}
354359

355360
// persist search history
@@ -482,11 +487,9 @@ impl App {
482487
}
483488
}
484489
Action::Suspend => {
485-
self.should_suspend = true;
486490
self.render_tx.send(RenderingTask::Suspend)?;
487491
}
488492
Action::Resume => {
489-
self.should_suspend = false;
490493
self.render_tx.send(RenderingTask::Resume)?;
491494
}
492495
Action::SelectAndExit => {
@@ -579,23 +582,32 @@ impl App {
579582
.merged_config
580583
.channel_actions
581584
.get(action_name)
585+
.cloned()
582586
{
583-
// Store the external action info and exit - the command will be executed after terminal cleanup
584-
self.should_quit = true;
585-
self.render_tx.send(RenderingTask::Quit)?;
586-
// Pass entries directly to be processed by execute_action
587-
return Ok(ActionOutcome::ExternalAction(
588-
action_spec.clone(),
589-
selected_entries,
590-
));
587+
match action_spec.mode {
588+
// suspend the TUI and execute the action
589+
ExecutionMode::Fork => {
590+
self.run_external_command_fork(
591+
&action_spec,
592+
&selected_entries,
593+
)?;
594+
}
595+
// clean up and exit the TUI and execute the action
596+
ExecutionMode::Execute => {
597+
self.run_external_command_execute(
598+
&action_spec,
599+
&selected_entries,
600+
)?;
601+
}
602+
}
591603
}
604+
} else {
605+
debug!("No entries available for external action");
606+
self.action_tx.send(Action::Error(
607+
"No entry available for external action"
608+
.to_string(),
609+
))?;
592610
}
593-
debug!("No entries available for external action");
594-
self.action_tx.send(Action::Error(
595-
"No entry available for external action"
596-
.to_string(),
597-
))?;
598-
return Ok(ActionOutcome::None);
599611
}
600612
_ => {}
601613
}
@@ -622,6 +634,58 @@ impl App {
622634
Ok(ActionOutcome::None)
623635
}
624636

637+
fn run_external_command_fork(
638+
&self,
639+
action_spec: &ActionSpec,
640+
entries: &FxHashSet<Entry>,
641+
) -> Result<()> {
642+
// suspend the event loop
643+
self.event_control_tx
644+
.send(ControlEvent::Pause)
645+
.map_err(|e| {
646+
error!("Failed to suspend event loop: {}", e);
647+
anyhow::anyhow!("Failed to suspend event loop: {}", e)
648+
})?;
649+
650+
// execute the external command in a separate process
651+
execute_action(action_spec, entries).map_err(|e| {
652+
error!("Failed to execute external action: {}", e);
653+
anyhow::anyhow!("Failed to execute external action: {}", e)
654+
})?;
655+
// resume the event loop
656+
self.event_control_tx
657+
.send(ControlEvent::Resume)
658+
.map_err(|e| {
659+
anyhow::anyhow!("Failed to resume event loop: {}", e)
660+
})?;
661+
// resume the TUI (after the event loop so as not to produce any artifacts)
662+
self.render_tx.send(RenderingTask::Resume)?;
663+
664+
Ok(())
665+
}
666+
667+
fn run_external_command_execute(
668+
&mut self,
669+
action_spec: &ActionSpec,
670+
entries: &FxHashSet<Entry>,
671+
) -> Result<()> {
672+
// cleanup
673+
self.render_tx.send(RenderingTask::Quit)?;
674+
// wait for the rendering task to finish
675+
if let Some(rendering_task) = self.render_task.take() {
676+
while !rendering_task.is_finished() {
677+
sleep(Duration::from_millis(10));
678+
}
679+
}
680+
681+
execute_action(action_spec, entries).map_err(|e| {
682+
error!("Failed to execute external action: {}", e);
683+
anyhow::anyhow!("Failed to execute external action: {}", e)
684+
})?;
685+
686+
Ok(())
687+
}
688+
625689
/// Maybe select the first entry if there is only one entry available.
626690
fn maybe_select_1(&mut self) -> Option<ActionOutcome> {
627691
debug!("Automatically selecting the first entry");

television/channels/prototypes.rs

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -177,19 +177,6 @@ pub enum ExecutionMode {
177177
Execute,
178178
}
179179

180-
/// Output handling mode for external actions
181-
#[derive(
182-
Debug, Clone, Default, serde::Deserialize, serde::Serialize, PartialEq,
183-
)]
184-
#[serde(rename_all = "lowercase")]
185-
pub enum OutputMode {
186-
/// Capture output for processing
187-
Capture,
188-
/// Discard output silently
189-
#[default]
190-
Discard,
191-
}
192-
193180
fn default_separator() -> String {
194181
" ".to_string()
195182
}
@@ -203,10 +190,9 @@ pub struct ActionSpec {
203190
/// How to execute the command
204191
#[serde(default)]
205192
pub mode: ExecutionMode,
206-
/// How to handle command output
207-
#[serde(default)]
208-
pub output_mode: OutputMode,
209-
/// Separator to use when joining multiple selected entries
193+
/// Separator to use when formatting multiple entries into the command
194+
///
195+
/// Example: `rm file1+SEPARATOR+file2+SEPARATOR+file3`
210196
#[serde(default = "default_separator")]
211197
pub separator: String,
212198
// TODO: add `requirements` (see `prototypes::BinaryRequirement`)

television/event.rs

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ impl Display for Key {
212212
#[allow(clippy::module_name_repetitions)]
213213
pub struct EventLoop {
214214
pub rx: mpsc::UnboundedReceiver<Event<Key>>,
215-
pub abort_tx: mpsc::UnboundedSender<()>,
215+
pub control_tx: mpsc::UnboundedSender<ControlEvent>,
216216
}
217217

218218
struct PollFuture {
@@ -256,12 +256,21 @@ fn flush_existing_events() {
256256
}
257257
}
258258

259+
pub enum ControlEvent {
260+
/// Abort the event loop
261+
Abort,
262+
/// Pause the event loop
263+
Pause,
264+
/// Resume the event loop
265+
Resume,
266+
}
267+
259268
impl EventLoop {
260269
pub fn new(tick_rate: u64) -> Self {
261270
let (tx, rx) = mpsc::unbounded_channel();
262271
let tick_interval = Duration::from_secs_f64(1.0 / tick_rate as f64);
263272

264-
let (abort, mut abort_recv) = mpsc::unbounded_channel();
273+
let (control_tx, mut control_rx) = mpsc::unbounded_channel();
265274

266275
flush_existing_events();
267276

@@ -272,10 +281,38 @@ impl EventLoop {
272281

273282
tokio::select! {
274283
// if we receive a message on the abort channel, stop the event loop
275-
_ = abort_recv.recv() => {
276-
tx.send(Event::Closed).unwrap_or_else(|_| warn!("Unable to send Closed event"));
277-
tx.send(Event::Tick).unwrap_or_else(|_| warn!("Unable to send Tick event"));
278-
break;
284+
Some(control_event) = control_rx.recv() => {
285+
match control_event {
286+
ControlEvent::Abort => {
287+
debug!("Received Abort control event");
288+
tx.send(Event::Closed).unwrap_or_else(|_| warn!("Unable to send Closed event"));
289+
tx.send(Event::Tick).unwrap_or_else(|_| warn!("Unable to send Tick event"));
290+
break;
291+
},
292+
ControlEvent::Pause => {
293+
debug!("Received Pause control event");
294+
// Stop processing events until resumed
295+
while let Some(event) = control_rx.recv().await {
296+
match event {
297+
ControlEvent::Resume => {
298+
debug!("Received Resume control event");
299+
// flush any leftover events
300+
flush_existing_events();
301+
break; // Exit pause loop
302+
},
303+
ControlEvent::Abort => {
304+
debug!("Received Abort control event during Pause");
305+
tx.send(Event::Closed).unwrap_or_else(|_| warn!("Unable to send Closed event"));
306+
tx.send(Event::Tick).unwrap_or_else(|_| warn!("Unable to send Tick event"));
307+
return;
308+
},
309+
ControlEvent::Pause => {}
310+
}
311+
}
312+
},
313+
// these should always be captured by the pause loop
314+
ControlEvent::Resume => {},
315+
}
279316
},
280317
_ = signal::ctrl_c() => {
281318
debug!("Received SIGINT");
@@ -319,7 +356,7 @@ impl EventLoop {
319356
//tx,
320357
rx,
321358
//tick_rate,
322-
abort_tx: abort,
359+
control_tx,
323360
}
324361
}
325362
}

television/main.rs

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ use television::{
2020
gh::update_local_channels,
2121
television::Mode,
2222
utils::clipboard::CLIPBOARD,
23-
utils::command::execute_action,
2423
utils::{
2524
shell::{
2625
Shell, completion_script, render_autocomplete_script_template,
@@ -100,22 +99,6 @@ async fn main() -> Result<()> {
10099
let output = app.run(stdout().is_terminal(), false).await?;
101100
info!("App output: {:?}", output);
102101

103-
// Handle external action execution after terminal cleanup
104-
if let Some((action_spec, entries)) = output.external_action {
105-
debug!("Executing external action command after terminal cleanup");
106-
107-
let status = execute_action(&action_spec, &entries)?;
108-
if !status.success() {
109-
eprintln!(
110-
"External command failed with exit code: {:?}",
111-
status.code()
112-
);
113-
exit(1);
114-
}
115-
116-
exit(0);
117-
}
118-
119102
let stdout_handle = stdout().lock();
120103
let mut bufwriter = BufWriter::new(stdout_handle);
121104
if let Some(key) = output.expect_key {

0 commit comments

Comments
 (0)