Skip to content

Commit a70c730

Browse files
committed
Added keybindings-help-menu-format option
1 parent fcd3c6c commit a70c730

File tree

9 files changed

+139
-83
lines changed

9 files changed

+139
-83
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -267,8 +267,8 @@ This powerful combination allows you to set some initial state with `initial-env
267267
### Help menu
268268

269269
Watchbind supports a help menu that displays:
270-
1. All environment variables set by `set-env` commands along with their values.
271-
2. All keybindings along with the operations and the description they are mapped to.
270+
- All environment variables set by `set-env` commands along with their values.
271+
- 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`.
272272

273273

274274
## Tips

src/config/fields/field_selection.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ struct FieldSelection {
1616
end: Option<usize>,
1717
}
1818

19+
// TODO: should be (at least partially) generated by e.g. parse_display crate
1920
impl FromStr for FieldSelections {
2021
type Err = Error;
2122
fn from_str(s: &str) -> Result<Self, Self::Err> {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
use anyhow::{Error, Result};
2+
use derive_more::IntoIterator;
3+
use parse_display::{Display, FromStr};
4+
use serde::Deserialize;
5+
use std::str;
6+
7+
/// Specifies which columns should be included in the keybindings help menu,
8+
/// and in what order.
9+
#[derive(Debug, Deserialize, Clone, IntoIterator)]
10+
#[cfg_attr(test, derive(PartialEq))]
11+
pub struct KeybindingsHelpMenuFormat(#[into_iterator(ref)] Vec<KeybindingsHelpMenuColumn>);
12+
13+
#[derive(Debug, Deserialize, FromStr, Display, Clone)]
14+
#[cfg_attr(test, derive(PartialEq))]
15+
#[serde(rename_all = "kebab-case")]
16+
#[display(style = "kebab-case")]
17+
pub enum KeybindingsHelpMenuColumn {
18+
Key,
19+
Operations,
20+
Description,
21+
}
22+
23+
// TODO: should get generated by e.g. parse_display directly
24+
impl str::FromStr for KeybindingsHelpMenuFormat {
25+
type Err = Error;
26+
fn from_str(s: &str) -> Result<Self, Self::Err> {
27+
let help_menu_columns = s
28+
.split(',')
29+
.map(KeybindingsHelpMenuColumn::from_str)
30+
.collect::<Result<_, _>>()?;
31+
Ok(Self(help_menu_columns))
32+
}
33+
}

src/config/keybindings/mod.rs

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
mod help_menu_format;
12
mod key;
23
mod operations;
34

@@ -11,6 +12,7 @@ use tokio::sync::Mutex;
1112
use super::table::Table;
1213
use crate::ui::EnvVariables;
1314

15+
pub use self::help_menu_format::{KeybindingsHelpMenuColumn, KeybindingsHelpMenuFormat};
1416
pub use self::key::{KeyCode, KeyEvent, KeyModifier};
1517
pub use self::operations::{OperationExecutable, OperationParsed, Operations, OperationsParsed};
1618

@@ -75,46 +77,65 @@ type KeyPrintable = String;
7577
type OperationsPrintable = String;
7678
type DescriptionPrintable = String;
7779

78-
#[derive(Debug, Clone, PartialEq, Eq, From)]
79-
pub struct KeybindingsPrintable(HashMap<KeyPrintable, (OperationsPrintable, DescriptionPrintable)>);
80+
#[derive(Debug, Clone, From)]
81+
pub struct KeybindingsPrintable {
82+
keybindings: HashMap<KeyPrintable, (OperationsPrintable, DescriptionPrintable)>,
83+
format: KeybindingsHelpMenuFormat,
84+
header_column_names: Vec<String>,
85+
}
8086

8187
impl KeybindingsPrintable {
88+
pub fn new(keybindings_parsed: KeybindingsParsed, format: KeybindingsHelpMenuFormat) -> Self {
89+
let keybindings_printable = keybindings_parsed
90+
.0
91+
.into_iter()
92+
.map(|(key, (operations, description))| {
93+
(
94+
key.to_string(),
95+
(operations.to_string(), description.to_string()),
96+
)
97+
})
98+
.collect();
99+
100+
let header_column_names = (&format)
101+
.into_iter()
102+
.map(|column_name| column_name.to_string())
103+
.collect();
104+
105+
Self {
106+
keybindings: keybindings_printable,
107+
format,
108+
header_column_names,
109+
}
110+
}
111+
82112
pub fn display<U>(&self, display_width: U) -> String
83113
where
84114
usize: From<U>,
85115
{
86-
let column_names = ["key", "operations", "description"];
116+
let column_names = self.header_column_names.as_slice();
87117

88118
let rows_iter = self
89-
.0
119+
.keybindings
90120
.iter()
91-
.map(|(key, (operations, description))| [key, operations, description]);
121+
.map(|(key, (operations, description))| {
122+
// TODO: optimize, since currently O(n^2) (iterating format for every keybinding)
123+
(&self.format).into_iter().map(move |column| match column {
124+
KeybindingsHelpMenuColumn::Key => key,
125+
KeybindingsHelpMenuColumn::Operations => operations,
126+
KeybindingsHelpMenuColumn::Description => description,
127+
})
128+
});
92129

93130
Table::new(rows_iter)
94131
.width(Some(display_width))
95132
.left_margin(2)
133+
.header(column_names)
96134
.border()
97-
.header(&column_names)
98135
.make_string()
99136
}
100137
}
101138

102-
impl From<KeybindingsParsed> for KeybindingsPrintable {
103-
fn from(parsed: KeybindingsParsed) -> Self {
104-
let keybindings_printable = parsed
105-
.0
106-
.into_iter()
107-
.map(|(key, (operations, description))| {
108-
(
109-
key.to_string(),
110-
(operations.to_string(), description.to_string()),
111-
)
112-
})
113-
.collect();
114-
Self(keybindings_printable)
115-
}
116-
}
117-
118139
/// Keybindings parsed from TOML.
119140
#[derive(Debug, Deserialize, Clone)]
120141
#[cfg_attr(test, derive(PartialEq))]

src/config/mod.rs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,11 @@ use terminal_size::Width;
2121
#[cfg(test)]
2222
use derive_builder::Builder;
2323

24+
use crate::config::keybindings::{KeyCode, KeyModifier};
2425
use crate::config::style::PrettyColor;
25-
use crate::config::{
26-
keybindings::{KeyCode, KeyModifier},
27-
table::Table,
28-
};
2926
use crate::utils::possible_enum_values::PossibleEnumValues;
3027

31-
use self::keybindings::{KeybindingCli, KeybindingsToml};
28+
use self::keybindings::{KeybindingCli, KeybindingsHelpMenuFormat, KeybindingsToml};
3229
use self::style::{Boldness, Color, Style};
3330
use self::{
3431
fields::{FieldSelections, FieldSeparator},
@@ -41,6 +38,7 @@ pub use self::keybindings::{
4138
OperationParsed, Operations, OperationsParsed,
4239
};
4340
pub use self::style::Styles;
41+
pub use self::table::Table;
4442

4543
// TODO: don't have public members
4644

@@ -50,6 +48,7 @@ pub struct Config {
5048
pub watch_rate: Duration,
5149
pub styles: Styles,
5250
pub keybindings_parsed: KeybindingsParsed,
51+
pub keybindings_help_menu_format: KeybindingsHelpMenuFormat,
5352
pub header_lines: usize,
5453
pub fields: Fields,
5554
pub initial_env_ops: OperationsParsed,
@@ -155,6 +154,7 @@ impl TryFrom<PartialConfig> for Config {
155154
watch_rate: Duration::from_secs_f64(expect!(config, interval)),
156155
styles,
157156
keybindings_parsed: expect!(config, keybindings),
157+
keybindings_help_menu_format: expect!(config, keybindings_help_menu_format),
158158
header_lines: expect!(config, header_lines),
159159
fields: Fields::try_new(config.field_separator, config.field_selections)?,
160160
update_ui_while_blocking: expect!(config, update_ui_while_blocking),
@@ -186,6 +186,7 @@ pub struct PartialConfig {
186186
field_separator: Option<FieldSeparator>,
187187
update_ui_while_blocking: Option<bool>,
188188
keybindings: Option<KeybindingsParsed>,
189+
keybindings_help_menu_format: Option<KeybindingsHelpMenuFormat>,
189190
}
190191

191192
impl PartialConfig {
@@ -238,6 +239,9 @@ impl PartialConfig {
238239
.update_ui_while_blocking
239240
.or(other.update_ui_while_blocking),
240241
keybindings: KeybindingsParsed::merge(self.keybindings, other.keybindings),
242+
keybindings_help_menu_format: self
243+
.keybindings_help_menu_format
244+
.or(other.keybindings_help_menu_format),
241245
}
242246
}
243247

@@ -308,8 +312,9 @@ pub struct TomlFileConfig {
308312

309313
update_ui_while_blocking: Option<bool>,
310314

311-
// keybindings: Option<StringKeybindings>,
312315
keybindings: Option<KeybindingsToml>,
316+
317+
keybindings_help_menu_format: Option<KeybindingsHelpMenuFormat>,
313318
}
314319

315320
impl TomlFileConfig {
@@ -363,6 +368,7 @@ impl TryFrom<TomlFileConfig> for PartialConfig {
363368
.keybindings
364369
.map(KeybindingsParsed::try_from)
365370
.transpose()?,
371+
keybindings_help_menu_format: toml.keybindings_help_menu_format,
366372
})
367373
}
368374
}
@@ -394,6 +400,7 @@ impl TryFrom<CliArgs> for PartialConfig {
394400
.map(KeybindingsCli::from)
395401
.map(KeybindingsParsed::try_from)
396402
.transpose()?,
403+
keybindings_help_menu_format: cli.keybindings_help_menu_format,
397404
})
398405
}
399406
}
@@ -421,6 +428,8 @@ impl Default for PartialConfig {
421428
422429
"update-ui-while-blocking" = false
423430
431+
"keybindings-help-menu-format" = [ "key", "operations", "description" ]
432+
424433
[keybindings]
425434
"ctrl+c" = { operations = "exit", description = "Exit watchbind." }
426435
"q" = { operations = "exit", description = "Exit watchbind." }
@@ -594,6 +603,10 @@ pub struct CliArgs {
594603
/// Keybindings as comma-separated `KEY:OP[+OP]*` pairs, e.g. `q:select+exit,r:reload`.
595604
#[arg(short = 'b', long = "bind", value_name = "LIST", value_delimiter = ',')]
596605
keybindings: Option<Vec<KeybindingCli>>,
606+
607+
/// Format of keybindings help menu as comma-separated list, e.g. `key,operations,description`.
608+
#[arg(long, value_name = "FORMAT")]
609+
keybindings_help_menu_format: Option<KeybindingsHelpMenuFormat>,
597610
}
598611

599612
/// Convert [[&str, String]] to [[Cow::Borrowed(&str), Cow::Owned(&str)]].

src/config/table.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{fmt, ops::Deref};
1+
use std::fmt;
22
use tabled::{
33
builder::Builder,
44
settings::{
@@ -19,7 +19,7 @@ where
1919
width: Option<usize>,
2020
left_margin: Option<usize>,
2121
border: bool,
22-
header: Option<&'a [&'a str]>,
22+
header: Option<&'a [String]>,
2323
}
2424

2525
impl<'a, RowsIter, RowIter, TableItem> Table<'a, RowsIter, RowIter, TableItem>
@@ -60,7 +60,7 @@ where
6060
}
6161

6262
/// Use the first input row as a header.
63-
pub fn header(mut self, column_names: &'a [&'a str]) -> Self {
63+
pub fn header(mut self, column_names: &'a [String]) -> Self {
6464
self.header = Some(column_names);
6565
self
6666
}
@@ -85,15 +85,16 @@ where
8585
} else {
8686
table
8787
.with(Style::blank())
88-
// Add left margin for indent.
89-
.with(Margin::new(left_margin, 0, 0, 0))
90-
// Remove left padding, as it interferes with margin.
88+
// Remove left padding.
9189
.with(Padding::new(0, 1, 0, 0));
9290
}
9391

92+
// Add left margin for indent.
93+
table.with(Margin::new(left_margin, 0, 0, 0));
94+
9495
if let Some(column_names) = self.header {
9596
// Set the header to the column names.
96-
table.with(ColumnNames::new(column_names.iter().map(Deref::deref)));
97+
table.with(ColumnNames::new(column_names));
9798
}
9899

99100
if let Some(width) = self.width {

src/ui/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,10 @@ impl UI {
165165
config.header_lines,
166166
config.fields,
167167
config.styles,
168-
KeybindingsPrintable::from(config.keybindings_parsed.clone()),
168+
KeybindingsPrintable::new(
169+
config.keybindings_parsed.clone(),
170+
config.keybindings_help_menu_format,
171+
),
169172
EnvVariables::new(),
170173
);
171174
state

src/ui/state/env_variables/mod.rs

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
mod env_variable;
22

3-
use anyhow::Result;
4-
use itertools::Itertools;
5-
use std::{collections::HashMap, fmt, io::Write};
6-
use tabwriter::TabWriter;
3+
use std::collections::HashMap;
4+
5+
use crate::config::Table;
76

87
pub use self::env_variable::EnvVariable;
98

10-
#[derive(Default, Debug)]
9+
#[derive(Default, Debug, Clone)]
1110
pub struct EnvVariables(HashMap<EnvVariable, String>);
1211

1312
impl EnvVariables {
@@ -32,30 +31,23 @@ impl EnvVariables {
3231
self.0.remove(env_var);
3332
}
3433

35-
/// Write formatted version (insert elastic tabstops) to a buffer.
36-
fn write<W: Write>(&self, writer: W) -> Result<()> {
37-
let mut tw = TabWriter::new(writer);
38-
for (env_variable, value) in self.0.iter().sorted() {
39-
writeln!(tw, "{}\t= \"{}\"", env_variable, value)?;
40-
}
41-
tw.flush()?;
42-
Ok(())
43-
}
34+
pub fn display<U>(&self, display_width: U) -> String
35+
where
36+
usize: From<U>,
37+
{
38+
let column_names = &["environment variable".to_string(), "value".to_string()];
4439

45-
// TODO: code duplication from KeybindingsParsed
46-
fn fmt(&self) -> Result<String> {
47-
let mut buffer = vec![];
48-
self.write(&mut buffer)?;
49-
let written = String::from_utf8(buffer)?;
50-
Ok(written)
51-
}
52-
}
40+
let rows_iter = self
41+
.0
42+
.iter()
43+
.map(|(env_variable, value)| [env_variable.to_string(), value.to_owned()]);
5344

54-
// TODO: code duplication from KeybindingsParsed
55-
impl fmt::Display for EnvVariables {
56-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57-
let formatted = self.fmt().map_err(|_| fmt::Error)?;
58-
f.write_str(&formatted)
45+
Table::new(rows_iter)
46+
.width(Some(display_width))
47+
.left_margin(2)
48+
.header(column_names)
49+
.border()
50+
.make_string()
5951
}
6052
}
6153

0 commit comments

Comments
 (0)