Skip to content

Commit

Permalink
Added new 'exec tui' operation for executing TUI commands
Browse files Browse the repository at this point in the history
  • Loading branch information
fritzrehde committed Nov 1, 2023
1 parent 8ef3562 commit fab908d
Show file tree
Hide file tree
Showing 6 changed files with 312 additions and 117 deletions.
35 changes: 19 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,22 +164,25 @@ All supported `OP` values:

Operation | Description
:-- | :--
`exit` | Quit watchbind
`reload` | Reload the watched command manually, resets interval timer
`cursor [down\|up] <N>` | Move cursor \[down\|up\] N number of lines
`cursor [first\|last]` | Move cursor to the \[first\|last\] line
`select` | Select line that cursor is currently on (i.e. add line that cursor is currently on to selected lines)
`unselect` | Unselect line that cursor is currently on
`toggle-selection` | Toggle selection of line that cursor is currently on
`select-all` | Select all lines
`unselect-all` | Unselect all currently selected lines
`exec -- <CMD>` | Execute `CMD` and block until termination
`exec & -- <CMD>` | Execute `CMD` as background process, i.e. don't block until command terminates
`set-env <ENV> -- <CMD>` | Blockingly execute `CMD`, and save its output to the environment variable `ENV`
`unset-env <ENV> -- <CMD>` | Unsets environment variable `ENV`
`help-[show\|hide\|toggle]` | \[Show\|Hide\|Toggle\] the help menu that shows all activated keybindings

All shell commands `CMD` will be executed in a subshell (i.e. `sh -c "CMD"`) that has the environment variable `line` set to the line the cursor is one and `lines` set to all selected lines or, if none are selected, the line the cursor is currently on.
`exit` | Quit watchbind.
`reload` | Reload the watched command manually, resets interval timer.
`cursor [down\|up] <N>` | Move cursor \[down\|up\] N number of lines.
`cursor [first\|last]` | Move cursor to the \[first\|last\] line.
`select` | Select line that cursor is currently on (i.e. add line that cursor is currently on to selected lines).
`unselect` | Unselect line that cursor is currently on.
`toggle-selection` | Toggle selection of line that cursor is currently on.
`select-all` | Select all lines.
`unselect-all` | Unselect all currently selected lines.
`exec -- <CMD>` | Execute `CMD` and block until termination.
`exec & -- <CMD>` | Execute `CMD` as background process, i.e. don't block until command terminates.
`exec tui -- <TUI-CMD>` | Execute a `TUI-CMD` that spawns a TUI (e.g. text editor). Watchbind's own TUI is replaced with `TUI-CMD`'s TUI until `TUI-CMD` terminates.
`set-env <ENV> -- <CMD>` | Blockingly execute `CMD`, and save its output to the environment variable `ENV`.
`unset-env <ENV> -- <CMD>` | Unsets environment variable `ENV`.
`help-[show\|hide\|toggle]` | \[Show\|Hide\|Toggle\] the help menu that shows all activated keybindings.

All `CMD` and `TUI-CMD` shell commands will be executed in a subshell (i.e. `sh -c "CMD"`) that has some environment variables set.
The environment variable `line` is set to the line the cursor is on.
The environment variable `lines` set to all selected lines, or if none are selected, the line the cursor is currently on.
All set environment variables `ENV` will be made available in all future spawned commands/processes, including the watched command, any executed subcommands, as well as commands executed in `set-env` operations.
If multiple lines are selected, they will be separated by newlines in `lines`.

Expand Down
5 changes: 3 additions & 2 deletions examples/ls.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
watched-command = "ls -la"
watched-command = "ls"
interval = 3.0
bold = false
cursor-fg = "black"
Expand Down Expand Up @@ -27,4 +27,5 @@ header-lines = 1
"exec -- notify-send \"Executing echo\"",
"exec -- echo \"Trying to overwrite watchbind's lines with stdout\"",
"exec -- echo \"Trying to overwrite watchbind's lines with stderr\" >&2"
]
]
"e" = [ "exec tui -- nvim \"$line\"" ]
55 changes: 40 additions & 15 deletions src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ pub struct WithEnv {
pub struct NoOutput;
#[derive(Clone)]
pub struct WithOutput;
#[derive(Clone)]
pub struct InheritedIO;

#[derive(Clone)]
pub struct NonInterruptible;
Expand Down Expand Up @@ -61,46 +63,43 @@ pub struct CommandBuilder<B = NonBlocking, E = WithoutEnv, O = NoOutput, I = Non
// TODO: impl default trait so we don't have to duplicate this
impl CommandBuilder {
pub fn new(command: String) -> Self {
let sh = ["sh", "-c", &command];
let mut process = TokioCommand::new(sh[0]);
process.args(&sh[1..]);
process.stdout(Stdio::null());
process.stderr(Stdio::null());

CommandBuilder {
// TODO: i think we don't even need command anymore, just the tokiocommand
command,
blocking: NonBlocking,
output: NoOutput,
interruptible: NonInterruptible,
env: WithoutEnv,
tokio_command: Default::default(),
tokio_command: TokioCommandBuilder::default(),
}
}
}

/// Since we can't save a tokio::process::Command permanently and just clone it
/// on new executions (it doesn't implement Clone), we store most of what's
/// necessary to contruct it.
/// Since we can't save a tokio::process::Command permanently and clone it
/// on new executions (it doesn't implement Clone), we store what's
/// needed to construct it.
#[derive(Default, Clone)]
struct TokioCommandBuilder {
stdin: StdioClonable,
stdout: StdioClonable,
stderr: StdioClonable,
}

// TODO: this should be known at compile-time as well, not have a match statement
#[derive(Default, Clone)]
enum StdioClonable {
Piped,
#[default]
Null,
Piped,
Inherit,
}

impl From<&StdioClonable> for Stdio {
fn from(value: &StdioClonable) -> Self {
match value {
StdioClonable::Piped => Stdio::piped(),
StdioClonable::Null => Stdio::null(),
StdioClonable::Piped => Stdio::piped(),
StdioClonable::Inherit => Stdio::inherit(),
}
}
}
Expand Down Expand Up @@ -150,6 +149,21 @@ impl<B, E, O, I> CommandBuilder<B, E, O, I> {
}
}

pub fn inherited_io(mut self) -> CommandBuilder<B, E, InheritedIO, I> {
// Required so child process can inherit IO from parent for TUI to work.
self.tokio_command.stdin = StdioClonable::Inherit;
self.tokio_command.stdout = StdioClonable::Inherit;

CommandBuilder {
command: self.command,
blocking: self.blocking,
output: InheritedIO,
interruptible: self.interruptible,
env: self.env,
tokio_command: self.tokio_command,
}
}

pub fn interruptible(
self,
interrupt_rx: Receiver<InterruptSignal>,
Expand All @@ -171,8 +185,8 @@ impl<B, O, I> CommandBuilder<B, WithoutEnv, O, I> {
let sh = ["sh", "-c", &self.command];

let mut command = TokioCommand::new(sh[0]);

command.args(&sh[1..]);
command.stdin(&self.tokio_command.stdin);
command.stdout(&self.tokio_command.stdout);
command.stderr(&self.tokio_command.stderr);

Expand Down Expand Up @@ -248,6 +262,17 @@ impl CommandBuilder<Blocking, WithEnv, NoOutput, NonInterruptible> {
}
}

impl CommandBuilder<Blocking, WithEnv, InheritedIO, NonInterruptible> {
pub async fn execute(&self) -> Result<()> {
let mut child = self.create_shell_command().await.spawn()?;

let exit_status = child.wait().await?;
assert_child_exited_successfully(exit_status, &mut child.stderr).await?;

Ok(())
}
}

impl CommandBuilder<Blocking, WithoutEnv, WithOutput, NonInterruptible> {
pub async fn execute(&self) -> Result<String> {
let mut child = self.create_shell_command().await.spawn()?;
Expand Down Expand Up @@ -354,8 +379,8 @@ impl<B, E, O> CommandBuilder<B, E, O, Interruptible> {
}
}

/// Return error in case the exit status/code indicates failure, and include
/// stderr in error message.
/// Return an error in case the exit status/exit code indicates failure, and
/// include stderr in error message.
async fn assert_child_exited_successfully(
exit_status: ExitStatus,
stderr: &mut Option<tokio::process::ChildStderr>,
Expand Down
41 changes: 36 additions & 5 deletions src/config/keybindings/operations/operation.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use crate::command::{Blocking, CommandBuilder, NonBlocking, WithEnv, WithOutput};
use crate::command::{
Blocking, CommandBuilder, InheritedIO, NonBlocking, NonInterruptible, WithEnv, WithOutput,
};
use crate::ui::{EnvVariable, EnvVariables, Event, RequestedAction, State};
use anyhow::Result;
use parse_display::{Display, FromStr};
use std::str;
use std::sync::Arc;
use tokio::sync::mpsc::Sender;
use tokio::sync::mpsc::{self, Sender};
use tokio::sync::Mutex;

// TODO: use some rust pattern (with types) instead of hardcoded Operation{,Parsed} variants
Expand Down Expand Up @@ -33,6 +35,9 @@ pub enum OperationParsed {
#[display("exec & -- {0}")]
ExecuteNonBlocking(String),

#[display("exec tui -- {0}")]
ExecuteTUI(String),

#[display("set-env {0} -- {1}")]
SetEnv(EnvVariable, String),

Expand All @@ -51,13 +56,14 @@ pub enum Operation {
HelpToggle,
MoveCursor(MoveCursor),
SelectLine(SelectOperation),
// TODO: document why we have an Arc (probably because it's shared across threads, but why? is it even necessary to share across threads given async)
ExecuteBlocking(Arc<CommandBuilder<Blocking, WithEnv>>),
ExecuteNonBlocking(Arc<CommandBuilder<NonBlocking, WithEnv>>),
ExecuteTUI(Arc<CommandBuilder<Blocking, WithEnv, InheritedIO, NonInterruptible>>),
SetEnv(
EnvVariable,
Arc<CommandBuilder<Blocking, WithEnv, WithOutput>>,
),

UnsetEnv(EnvVariable),
ReadIntoEnv(EnvVariable),
}
Expand Down Expand Up @@ -115,7 +121,7 @@ impl Operation {
state.add_lines_to_env().await?;

// TODO: these clones are preventable by using Arc<> (I think Arc<Mutex> isn't required because executing them doesn't mutate them)
let blocking_cmd = blocking_cmd.clone();
let blocking_cmd = Arc::clone(blocking_cmd);
let event_tx = event_tx.clone();
tokio::spawn(async move {
let result = blocking_cmd.execute().await;
Expand All @@ -126,10 +132,29 @@ impl Operation {

return Ok(RequestedAction::ExecutingBlockingSubcommand);
}
Self::ExecuteTUI(tui_cmd) => {
state.add_lines_to_env().await?;

// Create channels for waiting until TUI has actually been hidden.
let (tui_hidden_tx, mut tui_hidden_rx) = mpsc::channel(1);

let tui_cmd = Arc::clone(tui_cmd);
let event_tx = event_tx.clone();
tokio::spawn(async move {
// Wait until TUI has actually been hidden.
let _ = tui_hidden_rx.recv().await;

let result = tui_cmd.execute().await;

// Ignore whether the sender has closed channel.
let _ = event_tx.send(Event::TUISubcommandCompleted(result)).await;
});

return Ok(RequestedAction::ExecutingTUISubcommand(tui_hidden_tx));
}
Self::SetEnv(env_variable, blocking_cmd) => {
state.add_lines_to_env().await?;

// TODO: these clones are preventable by using Arc<> (I think Arc<Mutex> isn't required because executing them doesn't mutate them)
let blocking_cmd = blocking_cmd.clone();
let env_variable = env_variable.clone();
let event_tx = event_tx.clone();
Expand Down Expand Up @@ -172,6 +197,12 @@ impl Operation {
OperationParsed::ExecuteNonBlocking(cmd) => Self::ExecuteNonBlocking(Arc::new(
CommandBuilder::new(cmd).with_env(env_variables.clone()),
)),
OperationParsed::ExecuteTUI(cmd) => Self::ExecuteTUI(Arc::new(
CommandBuilder::new(cmd)
.blocking()
.inherited_io()
.with_env(env_variables.clone()),
)),
OperationParsed::SetEnv(env_var, cmd) => Self::SetEnv(
env_var,
Arc::new(
Expand Down
Loading

0 comments on commit fab908d

Please sign in to comment.