Skip to content
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

Allow opening another TUI (e.g. text editor) temporarily before returning to watchbind #69

Merged
merged 1 commit into from
Nov 1, 2023
Merged
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
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