From 5a6880f5b6b00f3395b034b65a2c27a2e5ff62a4 Mon Sep 17 00:00:00 2001 From: Fritz Rehde Date: Mon, 22 Jan 2024 00:04:24 +0100 Subject: [PATCH] feat: allow customizing whether UI should update while blocking (#95) --- Cargo.lock | 27 +++++++++++++++++++ Cargo.toml | 1 + README.md | 14 +++++++--- src/config/mod.rs | 67 +++++++++++++++++++++++++++++++++-------------- src/ui/mod.rs | 12 +++++---- 5 files changed, 93 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c7ebe8f..eb1cd2b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -246,6 +246,26 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "const_format" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "crossterm" version = "0.27.0" @@ -1313,6 +1333,12 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + [[package]] name = "utf8parse" version = "0.2.1" @@ -1359,6 +1385,7 @@ dependencies = [ "ansi-to-tui", "anyhow", "clap", + "const_format", "crossterm", "derive-new", "derive_builder", diff --git a/Cargo.toml b/Cargo.toml index ed356fe..2fd6362 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ tabled = { version = "0.14.0", features = ["color"] } terminal_size = "0.3.0" owo-colors = "3.5.0" dirs = "5.0.1" +const_format = "0.2.32" # Config for 'cargo dist' [workspace.metadata.dist] diff --git a/README.md b/README.md index 329f780..8d22550 100644 --- a/README.md +++ b/README.md @@ -191,10 +191,10 @@ Operation | Description `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. +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`. +If multiple lines are selected, they will be separated by newlines in `$lines`. ### Styling @@ -251,7 +251,7 @@ Choose only specific fields to display. You can specify a comma-separated list of the indexes (starting at index 1) for individual fields (`X`), ranges (`X-Y`), or the capture of all fields from X onwards (`X-`). For instance, the field selection `1,3-4,6-` will display the first, third and fourth fields, as well as all fields from the sixth onwards. -**Important**: The `lines` passed to the `exec --` operations will remain unformatted, i.e. will not have the separators replaced with elastic tabstops and will not have non-selected fields ommitted. +**Important**: The `$lines` passed to the `exec --` operations will remain unformatted, i.e. will not have the separators replaced with elastic tabstops and will not have non-selected fields ommitted. ### State management @@ -314,3 +314,9 @@ But note that watchbind --bind "enter:notify-send $lines" ls ``` will not work as expected, because `$lines` will be replaced in the shell you are running the `watchbind` command from. + +### UI updates while blocking + +Watchbind can enter blocking states when a blocking subcommand is executed. +The default behaviour in a blocking state is to *not* display any new output lines received from the watched command. +However, this behaviour can easily be customized with the `--update-ui-while-blocking ` option. diff --git a/src/config/mod.rs b/src/config/mod.rs index a2234b4..8c01fec 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -46,6 +46,7 @@ pub struct Config { pub header_lines: usize, pub fields: Fields, pub initial_env_ops: OperationsParsed, + pub update_ui_while_blocking: bool, } const GLOBAL_CONFIG_FILE: &str = "config.toml"; @@ -104,18 +105,32 @@ fn global_config_file_path() -> Result { Ok(global_config_dir) } +/// Some `PartialConfig` fields **must** be set once everything has been +/// merged. Panic with this error message if that is not the case. +macro_rules! expect { + ($obj:expr, $field:ident) => { + $obj.$field.expect(const_format::formatcp!( + "Expected field '{}' to be set in the default TOML config", + stringify!($field) + )) + }; +} + impl TryFrom for Config { type Error = anyhow::Error; - fn try_from(toml: PartialConfig) -> Result { + fn try_from(config: PartialConfig) -> Result { let non_cursor_non_header_style = Style::new( - toml.non_cursor_non_header_fg, - toml.non_cursor_non_header_bg, - toml.non_cursor_non_header_boldness, + config.non_cursor_non_header_fg, + config.non_cursor_non_header_bg, + config.non_cursor_non_header_boldness, + ); + let cursor_style = Style::new(config.cursor_fg, config.cursor_bg, config.cursor_boldness); + let header_style = Style::new(config.header_fg, config.header_bg, config.header_boldness); + let selected_style = Style::new( + Color::Unspecified, + config.selected_bg, + Boldness::Unspecified, ); - let cursor_style = Style::new(toml.cursor_fg, toml.cursor_bg, toml.cursor_boldness); - let header_style = Style::new(toml.header_fg, toml.header_bg, toml.header_boldness); - let selected_style = - Style::new(Color::Unspecified, toml.selected_bg, Boldness::Unspecified); let styles = Styles::new( non_cursor_non_header_style, cursor_style, @@ -123,21 +138,19 @@ impl TryFrom for Config { selected_style, ); - // Some fields **must** contain a value in `TomlConfig::default()`. - // Panic with this error message if that is not the case. - let error_msg = "Should have a value in the default TOML config"; Ok(Self { - log_file: toml.log_file, - initial_env_ops: toml.initial_env_vars.unwrap_or_default().try_into()?, - watched_command: match toml.watched_command { + log_file: config.log_file, + initial_env_ops: config.initial_env_vars.unwrap_or_default().try_into()?, + watched_command: match config.watched_command { Some(command) => command, None => bail!("A command must be provided via command line or config file"), }, - watch_rate: Duration::from_secs_f64(toml.interval.expect(error_msg)), + watch_rate: Duration::from_secs_f64(expect!(config, interval)), styles, - keybindings_parsed: toml.keybindings.expect(error_msg), - header_lines: toml.header_lines.expect(error_msg), - fields: Fields::try_new(toml.field_separator, toml.field_selections)?, + keybindings_parsed: expect!(config, keybindings), + header_lines: expect!(config, header_lines), + fields: Fields::try_new(config.field_separator, config.field_selections)?, + update_ui_while_blocking: expect!(config, update_ui_while_blocking), }) } } @@ -164,6 +177,7 @@ pub struct PartialConfig { selected_bg: Color, field_selections: Option, field_separator: Option, + update_ui_while_blocking: Option, keybindings: Option, } @@ -213,6 +227,9 @@ impl PartialConfig { header_lines: self.header_lines.or(other.header_lines), field_separator: self.field_separator.or(other.field_separator), field_selections: self.field_selections.or(other.field_selections), + update_ui_while_blocking: self + .update_ui_while_blocking + .or(other.update_ui_while_blocking), keybindings: KeybindingsParsed::merge(self.keybindings, other.keybindings), } } @@ -221,7 +238,7 @@ impl PartialConfig { /// file type to be parsed from can be `global` or `local`. fn parse_from_optional_toml_file( opt_file: Option<&PathBuf>, - config_file_type: &'static str, + config_file_type: &str, ) -> Result> { match opt_file { Some(file) => { @@ -282,6 +299,8 @@ pub struct TomlFileConfig { field_selections: Option, field_separator: Option, + update_ui_while_blocking: Option, + keybindings: Option, } @@ -331,6 +350,7 @@ impl TryFrom for PartialConfig { header_lines: toml.header_lines, field_separator: toml.field_separator, field_selections: toml.field_selections, + update_ui_while_blocking: toml.update_ui_while_blocking, keybindings: toml .keybindings .map(KeybindingsParsed::try_from) @@ -360,6 +380,7 @@ impl TryFrom for PartialConfig { header_lines: cli.header_lines, field_separator: cli.field_separator, field_selections: cli.field_selections, + update_ui_while_blocking: cli.update_ui_while_blocking, keybindings: cli .keybindings .map(StringKeybindings::from) @@ -369,6 +390,7 @@ impl TryFrom for PartialConfig { } } +// TODO: add test that checks that the default config sets all values. impl Default for PartialConfig { fn default() -> Self { let default_toml = indoc! {r#" @@ -389,6 +411,8 @@ impl Default for PartialConfig { "selected-bg" = "magenta" + "update-ui-while-blocking" = false + [keybindings] "ctrl+c" = [ "exit" ] "q" = [ "exit" ] @@ -548,6 +572,11 @@ pub struct CliArgs { #[arg(short = 'f', long = "fields", value_name = "LIST")] field_selections: Option, + /// Whether to update the UI with new output from the watched command + /// while in a blocking state. + #[arg(long, value_name = "BOOL")] + update_ui_while_blocking: Option, + // TODO: replace with ClapKeybindings (currently panics, known clap bug) // TODO: replace with StringKeybindings once clap supports parsing into HashMap /// Keybindings as comma-separated `KEY:OP[+OP]*` pairs, e.g. `q:select+exit,r:reload`. diff --git a/src/ui/mod.rs b/src/ui/mod.rs index bcdc7fc..b0297f8 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -29,6 +29,7 @@ pub struct UI { keybindings: Arc, remaining_operations: Option, channels: Channels, + update_ui_while_blocking: bool, } /// After having blocked, there might be some remaining operations, that @@ -207,6 +208,7 @@ impl UI { reload_tx, polling_tx, }, + update_ui_while_blocking: config.update_ui_while_blocking, }; Ok((ui, polling_state)) @@ -317,11 +319,11 @@ impl UI { }, BlockingState::BlockedExecutingSubcommand => match event { Event::CommandOutput(lines) => { - // TODO: it's up for discussion if we really want this behaviour, need to find use-cases against first - - // We handle new output lines, but don't exit the - // blocking state. - self.state.update_lines(lines?)?; + if self.update_ui_while_blocking { + // We update the UI with the new output lines, + // but don't exit the blocking state. + self.state.update_lines(lines?)?; + } } Event::SubcommandCompleted(potential_error) => { potential_error?;