Skip to content

Commit

Permalink
Implemented pretty printing of keybindings table
Browse files Browse the repository at this point in the history
  • Loading branch information
fritzrehde committed Feb 3, 2024
1 parent 2f2ee15 commit fcd3c6c
Show file tree
Hide file tree
Showing 9 changed files with 208 additions and 157 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.

8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,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 +264,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:
1. All environment variables set by `set-env` commands along with their values.
2. All keybindings along with the operations and the description they are mapped to.


## Tips

Expand Down
8 changes: 4 additions & 4 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" = { operations = "exit", description = "Exit watchbind" }
"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" }
"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
184 changes: 98 additions & 86 deletions src/config/keybindings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,16 @@ mod operations;
use anyhow::{bail, Context, Error, Result};
use derive_more::From;
use serde::Deserialize;
use std::io::Write;
use std::sync::Arc;
use std::{collections::HashMap, fmt, str};
use tabwriter::TabWriter;
use tokio::sync::Mutex;

use super::table::Table;
use crate::ui::EnvVariables;

pub use self::key::{KeyCode, KeyEvent, KeyModifier};
pub use self::operations::{OperationExecutable, OperationParsed, Operations, OperationsParsed};

use super::table::Table;

pub struct Keybindings(HashMap<KeyEvent, Operations>);

impl Keybindings {
Expand Down Expand Up @@ -72,59 +69,57 @@ impl KeybindingsParsed {
None => opt_b,
}
}
}

type KeyPrintable = String;
type OperationsPrintable = String;
type DescriptionPrintable = String;

#[derive(Debug, Clone, PartialEq, Eq, From)]
pub struct KeybindingsPrintable(HashMap<KeyPrintable, (OperationsPrintable, DescriptionPrintable)>);

impl KeybindingsPrintable {
pub fn display<U>(&self, display_width: U) -> String
where
usize: From<U>,
{
// TODO: inefficient to call to_string on key, ops and description everytime, save somewhere.
let rows_iter = self.0.iter().map(|(key, (operations, description))| {
[
key.to_string(),
operations.to_string(),
description.to_string(),
]
});
let column_names = ["key", "operations", "description"];

let rows_iter = self
.0
.iter()
.map(|(key, (operations, description))| [key, operations, description]);

Table::new(rows_iter)
.width(Some(display_width))
.left_margin(2)
.as_string()
}

/// Write formatted version (insert elastic tabstops) to a buffer.
fn write<W: Write>(&self, writer: W) -> Result<()> {
let mut tw = TabWriter::new(writer);
for (key, (operations, description)) in self.0.iter() {
writeln!(tw, "{}\t{}\t{}", key, description, operations)?;
}
tw.flush()?;
Ok(())
}

fn fmt(&self) -> Result<String> {
let mut buffer = vec![];
self.write(&mut buffer)?;
let written = String::from_utf8(buffer)?;
Ok(written)
.border()
.header(&column_names)
.make_string()
}
}

impl fmt::Display for KeybindingsParsed {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// let formatted = self.fmt().map_err(|_| fmt::Error)?;
// f.write_str(&formatted)

let rows_iter = self.0.iter().map(|(key, (operations, description))| {
[
key.to_string(),
operations.to_string(),
description.to_string(),
]
});
write!(f, "{}", Table::new(rows_iter).as_string())
impl From<KeybindingsParsed> for KeybindingsPrintable {
fn from(parsed: KeybindingsParsed) -> Self {
let keybindings_printable = parsed
.0
.into_iter()
.map(|(key, (operations, description))| {
(
key.to_string(),
(operations.to_string(), description.to_string()),
)
})
.collect();
Self(keybindings_printable)
}
}

/// Keybindings parsed from TOML.
#[derive(Debug, Deserialize, Clone)]
#[cfg_attr(test, derive(PartialEq))]
pub struct KeybindingsToml(HashMap<KeyToml, TomlOperationsWithDescription>);

impl TryFrom<KeybindingsToml> for KeybindingsParsed {
type Error = anyhow::Error;
fn try_from(value: KeybindingsToml) -> Result<Self, Self::Error> {
Expand All @@ -133,18 +128,18 @@ impl TryFrom<KeybindingsToml> for KeybindingsParsed {
.into_iter()
.map(|(key, operations)| {
let (operations, description) = match operations {
TomlOperationsDescription::OpsOnly(operations) => {
TomlOperationsWithDescription::OpsOnly(operations) => {
(operations, Description::default())
}
TomlOperationsDescription::OpsWithDesc {
TomlOperationsWithDescription::OpsWithDesc {
operations,
description,
} => (operations, Description(description)),
};
Ok((
key.0
.parse()
.with_context(|| format!("Invalid KeyEvent: {}", key.0))?,
.with_context(|| format!("Invalid key event: {}", key.0))?,
(Vec::<String>::from(operations).try_into()?, description),
))
})
Expand All @@ -153,43 +148,16 @@ impl TryFrom<KeybindingsToml> for KeybindingsParsed {
}
}

impl TryFrom<KeybindingsCli> for KeybindingsParsed {
type Error = anyhow::Error;
fn try_from(value: KeybindingsCli) -> Result<Self, Self::Error> {
let keybindings = value
.0
.into_iter()
.map(|KeybindingCli { key, operations }| {
Ok((
key.parse()
.with_context(|| format!("Invalid KeyEvent: {}", key))?,
(
operations.try_into()?,
// Descriptions are not available in `KeybindingsCli`.
Description::default(),
),
))
})
.collect::<Result<_>>()?;
Ok(Self(keybindings))
}
}

#[derive(Debug, Deserialize, Clone)]
#[cfg_attr(test, derive(PartialEq))]
pub struct KeybindingsToml(HashMap<KeyToml, TomlOperationsDescription>);

#[derive(Debug, Deserialize, Clone, PartialEq, Eq, Hash)]
struct KeyToml(String);

#[derive(Debug, Deserialize, Clone)]
#[cfg_attr(test, derive(PartialEq))]
#[serde(untagged)]
pub enum TomlOperationsDescription {
pub enum TomlOperationsWithDescription {
OpsOnly(TomlOperations),
OpsWithDesc {
operations: TomlOperations,
// TODO: replace with Description type directly
description: Option<String>,
},
}
Expand All @@ -214,6 +182,7 @@ impl From<TomlOperations> for Vec<String> {
#[derive(Debug, Deserialize, Clone, PartialEq, Eq, Default)]
struct Description(Option<String>);

// TODO: not very general
impl fmt::Display for Description {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.0 {
Expand All @@ -224,14 +193,39 @@ impl fmt::Display for Description {
}
}

// TODO: setup tests for KeybindingsCli

// TODO: setup tests for KeybindingsCli: test that both `-b a:c,b:d` and `-b a:c -b b:d` work (clap features)
// TODO: remove once clap supports parsing directly into HashMap<String, Vec<String>>
#[derive(Clone, From)]

/// Keybindings parsed from a CLI argument.
#[derive(Debug, Clone, From)]
#[cfg_attr(test, derive(PartialEq))]
pub struct KeybindingsCli(Vec<KeybindingCli>);

impl TryFrom<KeybindingsCli> for KeybindingsParsed {
type Error = anyhow::Error;
fn try_from(value: KeybindingsCli) -> Result<Self, Self::Error> {
let keybindings = value
.0
.into_iter()
.map(|KeybindingCli { key, operations }| {
Ok((
key.parse()
.with_context(|| format!("Invalid key event: {}", key))?,
(
operations.try_into()?,
// Descriptions are not available in `KeybindingsCli`.
Description::default(),
),
))
})
.collect::<Result<_>>()?;
Ok(Self(keybindings))
}
}

/// A keybinding parsed from a string in the format: `KEY:OP[+OP]*`
#[derive(Clone)]
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
pub struct KeybindingCli {
key: String,
operations: Vec<String>,
Expand Down Expand Up @@ -313,22 +307,22 @@ mod tests {
/// into an artificial map with a key `foo`.
#[derive(Debug, Deserialize, Clone)]
struct OperationsTomlWrapper {
foo: TomlOperationsDescription,
foo: TomlOperationsWithDescription,
}

#[test]
fn test_parse_toml_operations() {
let ops_only_single = r#"foo = "op""#;
assert_eq!(
TomlOperationsDescription::OpsOnly(TomlOperations::Single("op".to_string())),
TomlOperationsWithDescription::OpsOnly(TomlOperations::Single("op".to_string())),
toml::from_str::<OperationsTomlWrapper>(ops_only_single)
.unwrap()
.foo
);

let ops_only_multiple = r#"foo = [ "op 1", "op 2" ]"#;
assert_eq!(
TomlOperationsDescription::OpsOnly(TomlOperations::Multiple(vec![
TomlOperationsWithDescription::OpsOnly(TomlOperations::Multiple(vec![
"op 1".to_string(),
"op 2".to_string()
])),
Expand All @@ -339,7 +333,7 @@ mod tests {

let ops_without_desc = r#"foo = { operations = [ "op 1", "op 2" ] }"#;
assert_eq!(
TomlOperationsDescription::OpsWithDesc {
TomlOperationsWithDescription::OpsWithDesc {
operations: TomlOperations::Multiple(vec!["op 1".to_string(), "op 2".to_string()]),
description: None,
},
Expand All @@ -351,7 +345,7 @@ mod tests {
let long_with_desc =
r#"foo = { operations = [ "op 1", "op 2" ], description = "A custom description" }"#;
assert_eq!(
TomlOperationsDescription::OpsWithDesc {
TomlOperationsWithDescription::OpsWithDesc {
operations: TomlOperations::Multiple(vec!["op 1".to_string(), "op 2".to_string()]),
description: Some("A custom description".to_string())
},
Expand All @@ -364,22 +358,40 @@ mod tests {
#[test]
fn test_parse_keybindings_toml() {
let keybindings = indoc! {r#"
"a" = "op"
"a" = [ "op1", "op2" ]
"b" = "op"
"#};

assert_eq!(
KeybindingsToml(HashMap::from([
(
KeyToml("a".to_string()),
TomlOperationsDescription::OpsOnly(TomlOperations::Single("op".to_string()))
TomlOperationsWithDescription::OpsOnly(TomlOperations::Multiple(vec![
"op1".to_string(),
"op2".to_string()
]))
),
(
KeyToml("b".to_string()),
TomlOperationsDescription::OpsOnly(TomlOperations::Single("op".to_string()))
TomlOperationsWithDescription::OpsOnly(TomlOperations::Single(
"op".to_string()
))
)
])),
toml::from_str(keybindings).unwrap()
);
}

#[test]
fn test_parse_keybinding_cli() {
let keybinding = "a:op multiple words+op2";

assert_eq!(
KeybindingCli {
key: "a".to_string(),
operations: vec!["op multiple words".to_string(), "op2".to_string()]
},
keybinding.parse().unwrap()
);
}
}
Loading

0 comments on commit fcd3c6c

Please sign in to comment.