Skip to content

Commit

Permalink
feat: added support for user-defined keybinding descriptions (#97)
Browse files Browse the repository at this point in the history
- New TOML keybinding syntax was introduced, which allows specifying
  optional descriptions for each keybinding, as well as shorter syntax
  for specifying the operations.
- The UI of the help menu has been revamped. It now displays the
  keybindings hashmap, as well as the env variables, in an ascii table.
- A new config setting `keybindings-help-menu-format` has been
  introduced, which allows users to set exactly which columns should be
  shown (key, description, operations), and in which order.
  • Loading branch information
fritzrehde authored Feb 4, 2024
1 parent 34d3d3a commit 31856c0
Show file tree
Hide file tree
Showing 13 changed files with 614 additions and 211 deletions.
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 21 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,20 +105,25 @@ The operations are separated by `+` and executed in succession (one after the ot

#### Via TOML Config File

In a TOML config file, specify keybindings like so:
In a TOML config file, there are several different ways you can specify keybindings:
```toml
[keybindings]
"KEY" = [ "OP" ]
"KEY" = [ "OP", "OP", "OP" ]
"KEY" = [
"OP",
"OP"
]
# Single operation, without description
"KEY" = "OP"

# Single operation, with description
"KEY" = { description = "DESC", operations = "OP" }

# Multiple operations, without description
"KEY" = [ "OP_1", "OP_2", "OP_3" ]

# Multiple operations, with description
"KEY" = { description = "DESC", operations = [ "OP_1", "OP_2", "OP_3" ] }
```

This syntax differs from the command-line syntax because using the TOML array feature is more expressive and more native to the TOML file format.
Furthermore, this allows you to use the `+` character in your commands.
It also doesn't require escaping shell specific characters like `$` in (read more [in this section](#subshell)).
Furthermore, this allows you to use the `+` character in your commands, which is not possible with the CLI arguments (since that character is interpreted as a separator).
It also doesn't require escaping shell specific characters like `$` (read more about subshell behaviour [here](#subshell)).

You can find some keybinding examples in the [`examples/`](examples/) directory.

Expand Down Expand Up @@ -193,7 +198,7 @@ Operation | Description
`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. Note that `TUI-CMD` must spawn a full-screen TUI that covers the entire terminal, otherwise undefined behaviour will ensue.
`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.
`help-[show\|hide\|toggle]` | \[Show\|Hide\|Toggle the visibility of\] the help menu.

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.
Expand Down Expand Up @@ -264,6 +269,12 @@ The `set-env` and `unset-env` operations allow you to manage state through envir
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.
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`.

### Help menu

Watchbind supports a help menu that displays:
- All environment variables set by `set-env` commands along with their values.
- All keybindings along with the operations and the description they are mapped to, though you can customize what exactly gets displayed with `--keybindings-help-menu-format`.


## Tips

Expand Down
35 changes: 17 additions & 18 deletions examples/file-manager.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,52 +3,51 @@
# Note: the `printf`s used in some commands are necessary to remove the newlines produced by their nested commands

# Executed before the watched command is executed
initial-env = [ '''set-env pwd -- printf "$(pwd)"''' ]
initial-env = ['set-env pwd -- printf "$(pwd)"']

# All env variables are set as env variables in subshell where watched command is executed
watched-command = '''echo "$pwd"; ls "$pwd"'''
watched-command = 'echo "$pwd"; ls "$pwd"'
header-lines = 1

# Since we reload after each operation that changes the output, a small interval is not necessary.
# But we enable need periodic reloads in case some other processes manipulated the filesystem.
interval = 3

[keybindings]
# Move cursor up and down
"j" = [ "cursor down 1" ]
"k" = [ "cursor up 1" ]

# Delete (multiple) files
"d" = [
"d" = { description = "Delete (multiple) files", operations = [
'exec -- echo "$lines" | xargs -I {} rm "$pwd/{}"',
"reload"
]
"reload",
] }

# Open file (blocking)
"o" = [ 'exec -- echo "$lines" | xargs -I {} xdg-open "$pwd/{}"' ]
"o" = { description = "Open file (blocking)", operations = 'exec -- echo "$lines" | xargs -I {} xdg-open "$pwd/{}"' }

# Open file (non-blocking in background)
"O" = [ 'exec & -- echo "$lines" | xargs -I {} xdg-open "$pwd/{}"' ]
"O" = { description = "Open file (non-blocking in background)", operations = 'exec & -- echo "$lines" | xargs -I {} xdg-open "$pwd/{}"' }

# Edit text file in TUI editor
"e" = [ 'exec tui -- echo "$line" | xargs -I {} $EDITOR "$pwd/{}"' ]
"e" = { description = "Edit text file in TUI editor", operations = 'exec tui -- echo "$line" | xargs -I {} $EDITOR "$pwd/{}"' }

# Traverse out of directories
"h" = [
"h" = { description = "Traverse out of directories", operations = [
# Set $pwd to the parent dir of current dir
'set-env pwd -- printf "$(dirname "$pwd")"',
"reload"
]
"reload",
] }

# Traverse into directories
"l" = [
"l" = { description = "Traverse into directories", operations = [
# Only update $pwd if it is a directory
'set-env pwd -- new_pwd="$pwd/$line"; [ -d "$new_pwd" ] && pwd="$new_pwd"; printf "$pwd"',
"reload"
]
"reload",
] }

# Create a new file (with random name)
# "n" = [ "exec -- touch $(mktemp new_file_XXXXXX.txt)", "reload" ]

# TODO: read-into-env not yet supported

# Create a new file
# "n" = [
# "read-into-env NAME",
Expand Down
10 changes: 5 additions & 5 deletions examples/ls.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ selected-bg = "red"

[keybindings]
"esc" = [ "unselect-all", "help-hide" ]
"q" = [ "exit" ]
"j" = [ "cursor down 1" ]
"A" = [ "select-all" ]
"down" = [ "cursor down 1", "cursor down 1", "cursor down 1" ]
"J" = [ "cursor down 3" ]
"q" = { operations = "exit", description = "Exit watchbind." }
"j" = "cursor down 1"
"A" = { operations = "select-all", description = "Select all lines." }
"down" = { operations = [ "cursor down 1", "cursor down 1", "cursor down 1" ], description = "Move the cursor down 3 lines in 3 separate steps." }
"J" = { operations = "cursor down 3", description = "Move the cursor down 3 lines in one step." }
"K" = [
"cursor up 1",
"cursor up 1",
Expand Down
1 change: 1 addition & 0 deletions src/config/fields/field_selection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ struct FieldSelection {
end: Option<usize>,
}

// TODO: should be (at least partially) generated by e.g. parse_display crate
impl FromStr for FieldSelections {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Expand Down
33 changes: 33 additions & 0 deletions src/config/keybindings/help_menu_format.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use anyhow::{Error, Result};
use derive_more::IntoIterator;
use parse_display::{Display, FromStr};
use serde::Deserialize;
use std::str;

/// Specifies which columns should be included in the keybindings help menu,
/// and in what order.
#[derive(Debug, Deserialize, Clone, IntoIterator)]
#[cfg_attr(test, derive(PartialEq))]
pub struct KeybindingsHelpMenuFormat(#[into_iterator(ref)] Vec<KeybindingsHelpMenuColumn>);

#[derive(Debug, Deserialize, FromStr, Display, Clone)]
#[cfg_attr(test, derive(PartialEq))]
#[serde(rename_all = "kebab-case")]
#[display(style = "kebab-case")]
pub enum KeybindingsHelpMenuColumn {
Key,
Operations,
Description,
}

// TODO: should get generated by e.g. parse_display directly
impl str::FromStr for KeybindingsHelpMenuFormat {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let help_menu_columns = s
.split(',')
.map(KeybindingsHelpMenuColumn::from_str)
.collect::<Result<_, _>>()?;
Ok(Self(help_menu_columns))
}
}
Loading

0 comments on commit 31856c0

Please sign in to comment.