Skip to content

Commit c94ce5a

Browse files
committed
Ensured shared environment for 'initial-env' command executions
1 parent bcd8c5e commit c94ce5a

File tree

9 files changed

+156
-43
lines changed

9 files changed

+156
-43
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ ranges = "0.3.3"
2929
tokio = { version = "1.33.0", features = ["full"] }
3030
futures = "0.3.29"
3131
ansi-to-tui = "3.1.0"
32+
once_cell = "1.18.0"
3233

3334
# Config for 'cargo dist'
3435
[workspace.metadata.dist]

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,12 @@ The environment variable `lines` set to all selected lines, or if none are selec
185185
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.
186186
If multiple lines are selected, they will be separated by newlines in `lines`.
187187

188+
### State management
189+
190+
The `set-env` and `unset-env` operations allow you to manage state through environment variables.
191+
Additionally, you can use the `initial-env` option to specify a list of `set-env` commands that will be executed **before** the first execution of the watched command.
192+
This powerful combination allows you to set some initial state with `initial-env`, reference that state directly in the watched command, and update the state with keybindings at runtime with `set-env`.
193+
188194
### Formatting with Field Separators and Field Selections
189195

190196
`watchbind` supports some extra formatting features reminiscent of the Unix `cut` command:

src/config/keybindings/operations/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ impl Operations {
3333
}
3434
}
3535

36-
#[derive(IntoIterator, Eq, Ord, PartialEq, PartialOrd, From)]
36+
#[derive(IntoIterator, Eq, Ord, PartialEq, PartialOrd, From, Clone)]
3737
pub struct OperationsParsed(#[into_iterator(ref)] Vec<OperationParsed>);
3838

3939
impl TryFrom<Vec<String>> for OperationsParsed {

src/config/keybindings/operations/operation.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use tokio::sync::Mutex;
1414
/// The version of Operation used for parsing and displaying. The reason we
1515
/// can't parse directly into Operation is because any operations that execute
1616
/// something need to receive access to the globally set environment variables.
17-
#[derive(FromStr, Display, PartialEq, PartialOrd, Eq, Ord)]
17+
#[derive(FromStr, Display, PartialEq, PartialOrd, Eq, Ord, Clone)]
1818
#[display(style = "kebab-case")]
1919
pub enum OperationParsed {
2020
Exit,
@@ -114,13 +114,13 @@ impl Operation {
114114
Self::Reload => return Ok(RequestedAction::ReloadWatchedCommand),
115115
Self::Exit => return Ok(RequestedAction::Exit),
116116
Self::ExecuteNonBlocking(non_blocking_cmd) => {
117-
state.add_lines_to_env().await?;
117+
state.add_cursor_and_selected_lines_to_env().await;
118118
non_blocking_cmd.execute().await?;
119+
state.remove_cursor_and_selected_lines_from_env().await;
119120
}
120121
Self::ExecuteBlocking(blocking_cmd) => {
121-
state.add_lines_to_env().await?;
122+
state.add_cursor_and_selected_lines_to_env().await;
122123

123-
// TODO: these clones are preventable by using Arc<> (I think Arc<Mutex> isn't required because executing them doesn't mutate them)
124124
let blocking_cmd = Arc::clone(blocking_cmd);
125125
let event_tx = event_tx.clone();
126126
tokio::spawn(async move {
@@ -133,7 +133,7 @@ impl Operation {
133133
return Ok(RequestedAction::ExecutingBlockingSubcommand);
134134
}
135135
Self::ExecuteTUI(tui_cmd) => {
136-
state.add_lines_to_env().await?;
136+
state.add_cursor_and_selected_lines_to_env().await;
137137

138138
// Create channels for waiting until TUI has actually been hidden.
139139
let (tui_hidden_tx, mut tui_hidden_rx) = mpsc::channel(1);
@@ -153,7 +153,7 @@ impl Operation {
153153
return Ok(RequestedAction::ExecutingTUISubcommand(tui_hidden_tx));
154154
}
155155
Self::SetEnv(env_variable, blocking_cmd) => {
156-
state.add_lines_to_env().await?;
156+
state.add_cursor_and_selected_lines_to_env().await;
157157

158158
let blocking_cmd = blocking_cmd.clone();
159159
let env_variable = env_variable.clone();
@@ -179,7 +179,8 @@ impl Operation {
179179
Ok(RequestedAction::Continue)
180180
}
181181

182-
/// Convert the parsed form into the normal, runtime Operation form.
182+
/// Convert the parsed form into the normal, runtime Operation form. The
183+
/// `env_variables` is required so it can be passed to the `SetEnv` command.
183184
pub fn from_parsed(parsed: OperationParsed, env_variables: &Arc<Mutex<EnvVariables>>) -> Self {
184185
match parsed {
185186
OperationParsed::Exit => Self::Exit,

src/config/mod.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ pub struct Config {
2828
pub keybindings_parsed: KeybindingsParsed,
2929
pub header_lines: usize,
3030
pub fields: Fields,
31-
pub initial_env_variables: OperationsParsed,
31+
pub initial_env_ops: OperationsParsed,
3232
}
3333

3434
impl Config {
@@ -76,7 +76,7 @@ impl TryFrom<TomlConfig> for Config {
7676

7777
Ok(Self {
7878
log_file: toml.log_file,
79-
initial_env_variables: toml.initial_env_variables.unwrap_or_default().try_into()?,
79+
initial_env_ops: toml.initial_env_vars.unwrap_or_default().try_into()?,
8080
watched_command: match toml.watched_command {
8181
Some(command) => command,
8282
None => bail!("A command must be provided via command line or config file"),
@@ -100,7 +100,7 @@ pub struct TomlConfig {
100100
log_file: Option<PathBuf>,
101101

102102
#[serde(rename = "initial-env")]
103-
initial_env_variables: Option<Vec<String>>,
103+
initial_env_vars: Option<Vec<String>>,
104104

105105
watched_command: Option<String>,
106106
interval: Option<f64>,
@@ -151,7 +151,7 @@ impl TomlConfig {
151151
fn merge(self, other: Self) -> Self {
152152
Self {
153153
log_file: self.log_file.or(other.log_file),
154-
initial_env_variables: self.initial_env_variables.or(other.initial_env_variables),
154+
initial_env_vars: self.initial_env_vars.or(other.initial_env_vars),
155155
watched_command: self.watched_command.or(other.watched_command),
156156
interval: self.interval.or(other.interval),
157157
non_cursor_non_header_fg: self
@@ -182,7 +182,7 @@ impl From<ClapConfig> for TomlConfig {
182182
fn from(clap: ClapConfig) -> Self {
183183
Self {
184184
log_file: clap.log_file,
185-
initial_env_variables: clap.initial_env_variables,
185+
initial_env_vars: clap.initial_env_vars,
186186
watched_command: clap.watched_command.map(|s| s.join(" ")),
187187
interval: clap.interval,
188188
non_cursor_non_header_fg: clap.non_cursor_non_header_fg,
@@ -248,9 +248,9 @@ pub struct ClapConfig {
248248
#[arg(short, long, value_name = "FILE")]
249249
log_file: Option<PathBuf>,
250250

251-
/// Command to watch by executing periodically
251+
/// Comman-separated `set-env` operations to execute before first watched command execution
252252
#[arg(long = "initial-env", value_name = "LIST", value_delimiter = ',')]
253-
initial_env_variables: Option<Vec<String>>,
253+
initial_env_vars: Option<Vec<String>>,
254254

255255
/// Command to watch by executing periodically
256256
#[arg(trailing_var_arg(true))]

src/ui/mod.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,15 +157,18 @@ impl UI {
157157
async fn new(config: Config) -> Result<(Self, PollingState)> {
158158
let terminal_manager = Tui::new()?;
159159

160-
let env_variables = EnvVariables::generate_initial(config.initial_env_variables).await?;
160+
// Create `State`.
161161
let keybindings_str = config.keybindings_parsed.to_string();
162-
let state = State::new(
162+
let mut state = State::new(
163163
config.header_lines,
164164
config.fields,
165165
config.styles,
166166
keybindings_str,
167-
env_variables,
167+
EnvVariables::new(),
168168
);
169+
state
170+
.generate_initial_env_vars(config.initial_env_ops)
171+
.await?;
169172

170173
// TODO: room for optimization: we can probably get away with much smaller buffer sizes for some of our channels
171174

@@ -269,6 +272,9 @@ impl UI {
269272
Event::TUISubcommandCompleted(potential_error) => {
270273
potential_error?;
271274

275+
// Remove temporary env vars that were added just for execution.
276+
self.state.remove_cursor_and_selected_lines_from_env().await;
277+
272278
self.tui.restore()?;
273279
log::info!("Watchbind's TUI is shown.");
274280

@@ -311,13 +317,18 @@ impl UI {
311317
},
312318
BlockingState::BlockedExecutingSubcommand => match event {
313319
Event::CommandOutput(lines) => {
320+
// TODO: it's up for discussion if we really want this behaviour, need to find use-cases against first
321+
314322
// We handle new output lines, but don't exit the
315323
// blocking state.
316324
self.state.update_lines(lines?)?;
317325
}
318326
Event::SubcommandCompleted(potential_error) => {
319327
potential_error?;
320328

329+
// Remove temporary env vars that were added just for execution.
330+
self.state.remove_cursor_and_selected_lines_from_env().await;
331+
321332
if let ControlFlow::Exit = self.conclude_blocking().await? {
322333
break 'event_loop;
323334
}
@@ -336,7 +347,10 @@ impl UI {
336347
self.state.update_lines(lines?)?;
337348
}
338349
Event::SubcommandForEnvCompleted(new_env_variables) => {
339-
self.state.set_env(new_env_variables?).await;
350+
// Remove temporary env vars that were added just for execution.
351+
self.state.remove_cursor_and_selected_lines_from_env().await;
352+
353+
self.state.set_envs(new_env_variables?).await;
340354

341355
if let ControlFlow::Exit = self.conclude_blocking().await? {
342356
break 'event_loop;

src/ui/state/env_variables/mod.rs

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ mod env_variable;
22

33
use crate::{
44
command::CommandBuilder,
5-
config::{OperationParsed, OperationsParsed},
5+
config::{Operation, OperationParsed, Operations, OperationsParsed},
66
};
77
use anyhow::{bail, Result};
88
use itertools::Itertools;
@@ -15,7 +15,11 @@ pub use env_variable::EnvVariable;
1515
pub struct EnvVariables(HashMap<EnvVariable, String>);
1616

1717
impl EnvVariables {
18-
// TODO: maybe extend to also allow the general execution of normal commands before the initial watched command is executed (if there are use-cases for that)
18+
/// Create a new empty structure of environment variables.
19+
pub fn new() -> Self {
20+
Self(HashMap::new())
21+
}
22+
1923
/// Receive parsed operations, but only execute the "set-env" operations.
2024
pub async fn generate_initial(value: OperationsParsed) -> Result<Self> {
2125
// TODO: consider trying to use async iterators to do this in one iterator pass (instead of the mut hashmap) once stable
@@ -36,13 +40,41 @@ impl EnvVariables {
3640
Ok(Self(map))
3741
}
3842

43+
/// Receive parsed operations, but only execute the "set-env" operations.
44+
pub async fn generate_initial2(&mut self, operations: Operations) -> Result<()> {
45+
// TODO: consider trying to use async iterators to do this in one iterator pass (instead of the mut hashmap) once stable
46+
for op in operations.into_iter() {
47+
match op {
48+
Operation::SetEnv(env_variable, blocking_cmd) => {
49+
log::info!("executing cmd");
50+
51+
let cmd_output = blocking_cmd.execute().await?;
52+
53+
log::info!("finished executing cmd");
54+
55+
self.0.insert(env_variable, cmd_output);
56+
}
57+
// other_op => bail!("Invalid operation for env variable setup (only \"set-env\" operations allowed here): {}", other_op),
58+
_ => bail!("Only \"set-env\" operations allowed here."),
59+
}
60+
}
61+
Ok(())
62+
}
63+
3964
pub fn merge_new_envs(&mut self, env_variables: Self) {
4065
self.0.extend(env_variables.0);
4166
}
4267

68+
// TODO: expose EnvVariableValue type instead of String
69+
70+
/// Add an environment variable mapping.
71+
pub fn set_env(&mut self, env_var: EnvVariable, value: String) {
72+
self.0.insert(env_var, value);
73+
}
74+
4375
/// Unset/remove the specified environment variable.
44-
pub fn unset_env(&mut self, env: &EnvVariable) {
45-
self.0.remove(env);
76+
pub fn unset_env(&mut self, env_var: &EnvVariable) {
77+
self.0.remove(env_var);
4678
}
4779

4880
/// Write formatted version (insert elastic tabstops) to a buffer.

0 commit comments

Comments
 (0)