zellij/default-plugins/status-bar/src/main.rs
Aram Drevekenin 27c8986939
feat: multiple Select and Bulk Pane Actions (#4169)
* initial implementation with break panes to new tab

* break pane group left/right

* group embed/eject panes

* stack pane group on resize

* close pane group

* style(fmt): rustfmt

* fix tests

* group drag and ungroup with the mouse

* fix mouse hover for multiple clients

* fix for multiple clients

* multiple select plugin initial

* use real data in plugin

* adjust functionality

* fix some ux issues

* reflect group mouse group selections in plugin

* group/ungroup panes in Zellij

* highlight frames when marked by the plugin

* refactor: render function in plugin

* some ui responsiveness

* some more responsiveness and adjust hover text

* break out functionality

* stack functionality

* break panes left/right and close multiple panes

* fix(tab): only relayout the relevant layout when non-focused pane is closed

* status bar UI

* embed and float panes

* work

* fix some ui/ux issues

* refactor: move stuff around

* some responsiveness and fix search result browsing bug

* change plugin pane title

* differentiate group from focused pane

* add keyboard shortcut

* add ui to compact bar

* make boundary colors appear properly without pane frames

* get plugins to also display their frame color

* make hover shortcuts appear on command panes

* fix: do not render search string component if it's empty

* BeforeClose Event and unhighlight panes on exit

* some UI/UX fixes

* some fixes to the catppuccin-latte theme

* remove ungroup shortcut

* make some ui components opaque

* fix more opaque elements

* fix some issues with stacking pane order

* keyboard shortcuts for grouping

* config to opt out of advanced mouse actions

* make selected + focused frame color distinct

* group marking mode

* refactor: multiple-select plugin

* adjust stacking group behavior

* adjust flashing periods

* render common modifier in group controls

* add to compact bar

* adjust key hint wording

* add key to presets and default config

* some cleanups

* some refactoring

* fix tests

* fix plugin system tests

* tests: group/ungroup/hover

* test: BeforeClose plugin event

* new plugin assets

* style(fmt): rustfmt

* remove warnings

* tests: give plugin more time to load
2025-04-29 20:52:17 +02:00

926 lines
32 KiB
Rust

mod first_line;
mod one_line_ui;
mod second_line;
mod tip;
use ansi_term::{
ANSIString,
Colour::{Fixed, RGB},
Style,
};
use std::collections::BTreeMap;
use std::fmt::{Display, Error, Formatter};
use zellij_tile::prelude::actions::Action;
use zellij_tile::prelude::*;
use zellij_tile_utils::{palette_match, style};
use first_line::first_line;
use one_line_ui::one_line_ui;
use second_line::{
floating_panes_are_visible, fullscreen_panes_to_hide, keybinds,
locked_floating_panes_are_visible, locked_fullscreen_panes_to_hide, system_clipboard_error,
text_copied_hint,
};
use tip::utils::get_cached_tip_name;
// for more of these, copy paste from: https://en.wikipedia.org/wiki/Box-drawing_character
static ARROW_SEPARATOR: &str = "";
static MORE_MSG: &str = " ... ";
/// Shorthand for `Action::SwitchToMode(InputMode::Normal)`.
const TO_NORMAL: Action = Action::SwitchToMode(InputMode::Normal);
#[derive(Default)]
struct State {
tabs: Vec<TabInfo>,
tip_name: String,
mode_info: ModeInfo,
text_copy_destination: Option<CopyDestination>,
display_system_clipboard_failure: bool,
classic_ui: bool,
base_mode_is_locked: bool,
own_client_id: Option<ClientId>,
grouped_panes_count: Option<usize>,
}
register_plugin!(State);
#[derive(Default)]
pub struct LinePart {
part: String,
len: usize,
}
impl LinePart {
pub fn append(&mut self, to_append: &LinePart) {
self.part.push_str(&to_append.part);
self.len += to_append.len;
}
}
impl Display for LinePart {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
write!(f, "{}", self.part)
}
}
#[derive(Clone, Copy)]
pub struct ColoredElements {
pub selected: SegmentStyle,
pub unselected: SegmentStyle,
pub unselected_alternate: SegmentStyle,
pub disabled: SegmentStyle,
// superkey
pub superkey_prefix: Style,
pub superkey_suffix_separator: Style,
}
#[derive(Clone, Copy)]
pub struct SegmentStyle {
pub prefix_separator: Style,
pub char_left_separator: Style,
pub char_shortcut: Style,
pub char_right_separator: Style,
pub styled_text: Style,
pub suffix_separator: Style,
}
// I really hate this, but I can't come up with a good solution for this,
// we need different colors from palette for the default theme
// plus here we can add new sources in the future, like Theme
// that can be defined in the config perhaps
fn color_elements(palette: Styling, different_color_alternates: bool) -> ColoredElements {
let background = palette.text_unselected.background;
let foreground = palette.text_unselected.base;
let alternate_background_color = if different_color_alternates {
palette.ribbon_unselected.base
} else {
palette.ribbon_unselected.background
};
ColoredElements {
selected: SegmentStyle {
prefix_separator: style!(background, palette.ribbon_selected.background),
char_left_separator: style!(
palette.ribbon_selected.base,
palette.ribbon_selected.background
)
.bold(),
char_shortcut: style!(
palette.ribbon_selected.emphasis_0,
palette.ribbon_selected.background
)
.bold(),
char_right_separator: style!(
palette.ribbon_selected.base,
palette.ribbon_selected.background
)
.bold(),
styled_text: style!(
palette.ribbon_selected.base,
palette.ribbon_selected.background
)
.bold(),
suffix_separator: style!(palette.ribbon_selected.background, background).bold(),
},
unselected: SegmentStyle {
prefix_separator: style!(background, palette.ribbon_unselected.background),
char_left_separator: style!(
palette.ribbon_unselected.base,
palette.ribbon_unselected.background
)
.bold(),
char_shortcut: style!(
palette.ribbon_unselected.emphasis_0,
palette.ribbon_unselected.background
)
.bold(),
char_right_separator: style!(
palette.ribbon_unselected.base,
palette.ribbon_unselected.background
)
.bold(),
styled_text: style!(
palette.ribbon_unselected.base,
palette.ribbon_unselected.background
)
.bold(),
suffix_separator: style!(palette.ribbon_unselected.background, background).bold(),
},
unselected_alternate: SegmentStyle {
prefix_separator: style!(background, alternate_background_color),
char_left_separator: style!(background, alternate_background_color).bold(),
char_shortcut: style!(
palette.ribbon_unselected.emphasis_0,
alternate_background_color
)
.bold(),
char_right_separator: style!(background, alternate_background_color).bold(),
styled_text: style!(palette.ribbon_unselected.base, alternate_background_color).bold(),
suffix_separator: style!(alternate_background_color, background).bold(),
},
disabled: SegmentStyle {
prefix_separator: style!(background, palette.ribbon_unselected.background),
char_left_separator: style!(
palette.ribbon_unselected.base,
palette.ribbon_unselected.background
)
.dimmed()
.italic(),
char_shortcut: style!(
palette.ribbon_unselected.base,
palette.ribbon_unselected.background
)
.dimmed()
.italic(),
char_right_separator: style!(
palette.ribbon_unselected.base,
palette.ribbon_unselected.background
)
.dimmed()
.italic(),
styled_text: style!(
palette.ribbon_unselected.base,
palette.ribbon_unselected.background
)
.dimmed()
.italic(),
suffix_separator: style!(palette.ribbon_unselected.background, background),
},
superkey_prefix: style!(foreground, background).bold(),
superkey_suffix_separator: style!(background, background),
}
}
impl ZellijPlugin for State {
fn load(&mut self, configuration: BTreeMap<String, String>) {
// TODO: Should be able to choose whether to use the cache through config.
self.tip_name = get_cached_tip_name();
self.classic_ui = configuration
.get("classic")
.map(|c| c == "true")
.unwrap_or(false);
self.own_client_id = Some(get_plugin_ids().client_id);
set_selectable(false);
subscribe(&[
EventType::ModeUpdate,
EventType::TabUpdate,
EventType::PaneUpdate,
EventType::CopyToClipboard,
EventType::InputReceived,
EventType::SystemClipboardFailure,
]);
}
fn update(&mut self, event: Event) -> bool {
let mut should_render = false;
match event {
Event::ModeUpdate(mode_info) => {
if self.mode_info != mode_info {
should_render = true;
}
self.mode_info = mode_info;
self.base_mode_is_locked = self.mode_info.base_mode == Some(InputMode::Locked);
},
Event::TabUpdate(tabs) => {
if self.tabs != tabs {
should_render = true;
}
self.tabs = tabs;
},
Event::PaneUpdate(pane_manifest) => {
if let Some(own_client_id) = self.own_client_id {
let mut grouped_panes_count = 0;
for (_tab_index, pane_infos) in pane_manifest.panes {
for pane_info in pane_infos {
let is_in_pane_group =
pane_info.index_in_pane_group.get(&own_client_id).is_some();
if is_in_pane_group {
grouped_panes_count += 1;
}
}
}
if Some(grouped_panes_count) != self.grouped_panes_count {
if grouped_panes_count == 0 {
self.grouped_panes_count = None;
} else {
self.grouped_panes_count = Some(grouped_panes_count);
}
should_render = true;
}
}
},
Event::CopyToClipboard(copy_destination) => {
match self.text_copy_destination {
Some(text_copy_destination) => {
if text_copy_destination != copy_destination {
should_render = true;
}
},
None => {
should_render = true;
},
}
self.text_copy_destination = Some(copy_destination);
},
Event::SystemClipboardFailure => {
should_render = true;
self.display_system_clipboard_failure = true;
},
Event::InputReceived => {
if self.text_copy_destination.is_some()
|| self.display_system_clipboard_failure == true
{
should_render = true;
}
self.text_copy_destination = None;
self.display_system_clipboard_failure = false;
},
_ => {},
};
should_render
}
fn render(&mut self, rows: usize, cols: usize) {
let supports_arrow_fonts = !self.mode_info.capabilities.arrow_fonts;
let separator = if supports_arrow_fonts {
ARROW_SEPARATOR
} else {
""
};
let background = self.mode_info.style.colors.text_unselected.background;
if rows == 1 && !self.classic_ui {
let fill_bg = match background {
PaletteColor::Rgb((r, g, b)) => format!("\u{1b}[48;2;{};{};{}m\u{1b}[0K", r, g, b),
PaletteColor::EightBit(color) => format!("\u{1b}[48;5;{}m\u{1b}[0K", color),
};
let active_tab = self.tabs.iter().find(|t| t.active);
print!(
"{}{}",
one_line_ui(
&self.mode_info,
active_tab,
cols,
separator,
self.base_mode_is_locked,
self.text_copy_destination,
self.display_system_clipboard_failure,
self.grouped_panes_count,
),
fill_bg,
);
return;
}
//TODO: Switch to UI components here
let active_tab = self.tabs.iter().find(|t| t.active);
let first_line = first_line(&self.mode_info, active_tab, cols, separator);
let second_line = self.second_line(cols);
// [48;5;238m is white background, [0K is so that it fills the rest of the line
// [m is background reset, [0K is so that it clears the rest of the line
match background {
PaletteColor::Rgb((r, g, b)) => {
if rows > 1 {
println!("{}\u{1b}[48;2;{};{};{}m\u{1b}[0K", first_line, r, g, b);
} else {
if self.mode_info.mode == InputMode::Normal {
print!("{}\u{1b}[48;2;{};{};{}m\u{1b}[0K", first_line, r, g, b);
} else {
print!("\u{1b}[m{}\u{1b}[0K", second_line);
}
}
},
PaletteColor::EightBit(color) => {
if rows > 1 {
println!("{}\u{1b}[48;5;{}m\u{1b}[0K", first_line, color);
} else {
if self.mode_info.mode == InputMode::Normal {
print!("{}\u{1b}[48;5;{}m\u{1b}[0K", first_line, color);
} else {
print!("\u{1b}[m{}\u{1b}[0K", second_line);
}
}
},
}
if rows > 1 {
print!("\u{1b}[m{}\u{1b}[0K", second_line);
}
}
}
impl State {
fn second_line(&self, cols: usize) -> LinePart {
let active_tab = self.tabs.iter().find(|t| t.active);
if let Some(copy_destination) = self.text_copy_destination {
text_copied_hint(copy_destination)
} else if self.display_system_clipboard_failure {
system_clipboard_error(&self.mode_info.style.colors)
} else if let Some(active_tab) = active_tab {
if active_tab.is_fullscreen_active {
match self.mode_info.mode {
InputMode::Normal => fullscreen_panes_to_hide(
&self.mode_info.style.colors,
active_tab.panes_to_hide,
),
InputMode::Locked => locked_fullscreen_panes_to_hide(
&self.mode_info.style.colors,
active_tab.panes_to_hide,
),
_ => keybinds(&self.mode_info, &self.tip_name, cols),
}
} else if active_tab.are_floating_panes_visible {
match self.mode_info.mode {
InputMode::Normal => floating_panes_are_visible(&self.mode_info),
InputMode::Locked => {
locked_floating_panes_are_visible(&self.mode_info.style.colors)
},
_ => keybinds(&self.mode_info, &self.tip_name, cols),
}
} else {
keybinds(&self.mode_info, &self.tip_name, cols)
}
} else {
LinePart::default()
}
}
}
pub fn get_common_modifiers(mut keyvec: Vec<&KeyWithModifier>) -> Vec<KeyModifier> {
if keyvec.is_empty() {
return vec![];
}
let mut common_modifiers = keyvec.pop().unwrap().key_modifiers.clone();
for key in keyvec {
common_modifiers = common_modifiers
.intersection(&key.key_modifiers)
.cloned()
.collect();
}
common_modifiers.into_iter().collect()
}
/// Get key from action pattern(s).
///
/// This function takes as arguments a `keymap` that is a `Vec<(Key, Vec<Action>)>` and contains
/// all keybindings for the current mode and one or more `p` patterns which match a sequence of
/// actions to search for. If within the keymap a sequence of actions matching `p` is found, all
/// keys that trigger the action pattern are returned as vector of `Vec<Key>`.
pub fn action_key(
keymap: &[(KeyWithModifier, Vec<Action>)],
action: &[Action],
) -> Vec<KeyWithModifier> {
keymap
.iter()
.filter_map(|(key, acvec)| {
let matching = acvec
.iter()
.zip(action)
.filter(|(a, b)| a.shallow_eq(b))
.count();
if matching == acvec.len() && matching == action.len() {
Some(key.clone())
} else {
None
}
})
.collect::<Vec<KeyWithModifier>>()
}
/// Get multiple keys for multiple actions.
///
/// An extension of [`action_key`] that iterates over all action tuples and collects the results.
pub fn action_key_group(
keymap: &[(KeyWithModifier, Vec<Action>)],
actions: &[&[Action]],
) -> Vec<KeyWithModifier> {
let mut ret = vec![];
for action in actions {
ret.extend(action_key(keymap, action));
}
ret
}
/// Style a vector of [`Key`]s with the given [`Palette`].
///
/// Creates a line segment of style `<KEYS>`, with correct theming applied: The brackets have the
/// regular text color, the enclosed keys are painted green and bold. If the keys share a common
/// modifier (See [`get_common_modifier`]), it is printed in front of the keys, painted green and
/// bold, separated with a `+`: `MOD + <KEYS>`.
///
/// If multiple [`Key`]s are given, the individual keys are separated with a `|` char. This does
/// not apply to the following groups of keys which are treated specially and don't have a
/// separator between them:
///
/// - "hjkl"
/// - "HJKL"
/// - "←↓↑→"
/// - "←→"
/// - "↓↑"
///
/// The returned Vector of [`ANSIString`] is suitable for transformation into an [`ANSIStrings`]
/// type.
pub fn style_key_with_modifier(
keyvec: &[KeyWithModifier],
palette: &Styling,
background: Option<PaletteColor>,
) -> Vec<ANSIString<'static>> {
if keyvec.is_empty() {
return vec![];
}
let text_color = palette_match!(palette.text_unselected.base);
let green_color = palette_match!(palette.text_unselected.emphasis_2);
let orange_color = palette_match!(palette.text_unselected.emphasis_0);
let mut ret = vec![];
let common_modifiers = get_common_modifiers(keyvec.iter().collect());
let no_common_modifier = common_modifiers.is_empty();
let modifier_str = common_modifiers
.iter()
.map(|m| m.to_string())
.collect::<Vec<_>>()
.join("-");
let painted_modifier = if modifier_str.is_empty() {
Style::new().paint("")
} else {
if let Some(background) = background {
let background = palette_match!(background);
Style::new()
.fg(orange_color)
.on(background)
.bold()
.paint(modifier_str)
} else {
Style::new().fg(orange_color).bold().paint(modifier_str)
}
};
ret.push(painted_modifier);
// Prints key group start
let group_start_str = if no_common_modifier { "<" } else { " + <" };
if let Some(background) = background {
let background = palette_match!(background);
ret.push(
Style::new()
.fg(text_color)
.on(background)
.paint(group_start_str),
);
} else {
ret.push(Style::new().fg(text_color).paint(group_start_str));
}
// Prints the keys
let key = keyvec
.iter()
.map(|key| {
if no_common_modifier {
format!("{}", key)
} else {
let key_modifier_for_key = key
.key_modifiers
.iter()
.filter(|m| !common_modifiers.contains(m))
.map(|m| m.to_string())
.collect::<Vec<_>>()
.join(" ");
if key_modifier_for_key.is_empty() {
format!("{}", key.bare_key)
} else {
format!("{} {}", key_modifier_for_key, key.bare_key)
}
}
})
.collect::<Vec<String>>();
// Special handling of some pre-defined keygroups
let key_string = key.join("");
let key_separator = match &key_string[..] {
"HJKL" => "",
"hjkl" => "",
"←↓↑→" => "",
"←→" => "",
"↓↑" => "",
"[]" => "",
_ => "|",
};
for (idx, key) in key.iter().enumerate() {
if idx > 0 && !key_separator.is_empty() {
if let Some(background) = background {
let background = palette_match!(background);
ret.push(
Style::new()
.fg(text_color)
.on(background)
.paint(key_separator),
);
} else {
ret.push(Style::new().fg(text_color).paint(key_separator));
}
}
if let Some(background) = background {
let background = palette_match!(background);
ret.push(
Style::new()
.fg(green_color)
.on(background)
.bold()
.paint(key.clone()),
);
} else {
ret.push(Style::new().fg(green_color).bold().paint(key.clone()));
}
}
let group_end_str = ">";
if let Some(background) = background {
let background = palette_match!(background);
ret.push(
Style::new()
.fg(text_color)
.on(background)
.paint(group_end_str),
);
} else {
ret.push(Style::new().fg(text_color).paint(group_end_str));
}
ret
}
#[cfg(test)]
pub mod tests {
use super::*;
use ansi_term::unstyle;
use ansi_term::ANSIStrings;
fn big_keymap() -> Vec<(KeyWithModifier, Vec<Action>)> {
vec![
(KeyWithModifier::new(BareKey::Char('a')), vec![Action::Quit]),
(
KeyWithModifier::new(BareKey::Char('b')).with_ctrl_modifier(),
vec![Action::ScrollUp],
),
(
KeyWithModifier::new(BareKey::Char('d')).with_ctrl_modifier(),
vec![Action::ScrollDown],
),
(
KeyWithModifier::new(BareKey::Char('c')).with_alt_modifier(),
vec![Action::ScrollDown, Action::SwitchToMode(InputMode::Normal)],
),
(
KeyWithModifier::new(BareKey::Char('1')),
vec![TO_NORMAL, Action::SwitchToMode(InputMode::Locked)],
),
]
}
#[test]
fn common_modifier_with_ctrl_keys() {
let keyvec = vec![
KeyWithModifier::new(BareKey::Char('a')).with_ctrl_modifier(),
KeyWithModifier::new(BareKey::Char('b')).with_ctrl_modifier(),
KeyWithModifier::new(BareKey::Char('c')).with_ctrl_modifier(),
];
let ret = get_common_modifiers(keyvec.iter().collect());
assert_eq!(ret, vec![KeyModifier::Ctrl]);
}
#[test]
fn common_modifier_with_alt_keys_chars() {
let keyvec = vec![
KeyWithModifier::new(BareKey::Char('1')).with_alt_modifier(),
KeyWithModifier::new(BareKey::Char('t')).with_alt_modifier(),
KeyWithModifier::new(BareKey::Char('z')).with_alt_modifier(),
];
let ret = get_common_modifiers(keyvec.iter().collect());
assert_eq!(ret, vec![KeyModifier::Alt]);
}
#[test]
fn common_modifier_with_mixed_alt_ctrl_keys() {
let keyvec = vec![
KeyWithModifier::new(BareKey::Char('1')).with_ctrl_modifier(),
KeyWithModifier::new(BareKey::Char('t')).with_alt_modifier(),
KeyWithModifier::new(BareKey::Char('z')).with_alt_modifier(),
];
let ret = get_common_modifiers(keyvec.iter().collect());
assert_eq!(ret, vec![]); // no common modifiers
}
#[test]
fn common_modifier_with_any_keys() {
let keyvec = vec![
KeyWithModifier::new(BareKey::Char('1')),
KeyWithModifier::new(BareKey::Char('t')).with_alt_modifier(),
KeyWithModifier::new(BareKey::Char('z')).with_alt_modifier(),
];
let ret = get_common_modifiers(keyvec.iter().collect());
assert_eq!(ret, vec![]); // no common modifiers
}
#[test]
fn action_key_simple_pattern_match_exact() {
let keymap = &[(KeyWithModifier::new(BareKey::Char('f')), vec![Action::Quit])];
let ret = action_key(keymap, &[Action::Quit]);
assert_eq!(ret, vec![KeyWithModifier::new(BareKey::Char('f'))]);
}
#[test]
fn action_key_simple_pattern_match_pattern_too_long() {
let keymap = &[(KeyWithModifier::new(BareKey::Char('f')), vec![Action::Quit])];
let ret = action_key(keymap, &[Action::Quit, Action::ScrollUp]);
assert_eq!(ret, Vec::new());
}
#[test]
fn action_key_simple_pattern_match_pattern_empty() {
let keymap = &[(KeyWithModifier::new(BareKey::Char('f')), vec![Action::Quit])];
let ret = action_key(keymap, &[]);
assert_eq!(ret, Vec::new());
}
#[test]
fn action_key_long_pattern_match_exact() {
let keymap = big_keymap();
let ret = action_key(&keymap, &[Action::ScrollDown, TO_NORMAL]);
assert_eq!(
ret,
vec![KeyWithModifier::new(BareKey::Char('c')).with_alt_modifier()]
);
}
#[test]
fn action_key_long_pattern_match_too_short() {
let keymap = big_keymap();
let ret = action_key(&keymap, &[TO_NORMAL]);
assert_eq!(ret, Vec::new());
}
#[test]
fn action_key_group_single_pattern() {
let keymap = big_keymap();
let ret = action_key_group(&keymap, &[&[Action::Quit]]);
assert_eq!(ret, vec![KeyWithModifier::new(BareKey::Char('a'))]);
}
#[test]
fn action_key_group_two_patterns() {
let keymap = big_keymap();
let ret = action_key_group(&keymap, &[&[Action::ScrollDown], &[Action::ScrollUp]]);
// Mind the order!
assert_eq!(
ret,
vec![
KeyWithModifier::new(BareKey::Char('d')).with_ctrl_modifier(),
KeyWithModifier::new(BareKey::Char('b')).with_ctrl_modifier()
]
);
}
fn get_palette() -> Palette {
Palette::default()
}
#[test]
fn style_key_with_modifier_only_chars() {
let keyvec = vec![
KeyWithModifier::new(BareKey::Char('a')),
KeyWithModifier::new(BareKey::Char('b')),
KeyWithModifier::new(BareKey::Char('c')),
];
let palette = Styling::default();
let ret = style_key_with_modifier(&keyvec, &palette, None);
let ret = unstyle(&ANSIStrings(&ret));
assert_eq!(ret, "<a|b|c>".to_string())
}
#[test]
fn style_key_with_modifier_special_group_hjkl() {
let keyvec = vec![
KeyWithModifier::new(BareKey::Char('h')),
KeyWithModifier::new(BareKey::Char('j')),
KeyWithModifier::new(BareKey::Char('k')),
KeyWithModifier::new(BareKey::Char('l')),
];
let palette = Styling::default();
let ret = style_key_with_modifier(&keyvec, &palette, None);
let ret = unstyle(&ANSIStrings(&ret));
assert_eq!(ret, "<hjkl>".to_string())
}
#[test]
fn style_key_with_modifier_special_group_all_arrows() {
let keyvec = vec![
KeyWithModifier::new(BareKey::Left),
KeyWithModifier::new(BareKey::Down),
KeyWithModifier::new(BareKey::Up),
KeyWithModifier::new(BareKey::Right),
];
let palette = Styling::default();
let ret = style_key_with_modifier(&keyvec, &palette, None);
let ret = unstyle(&ANSIStrings(&ret));
assert_eq!(ret, "<←↓↑→>".to_string())
}
#[test]
fn style_key_with_modifier_special_group_left_right_arrows() {
let keyvec = vec![
KeyWithModifier::new(BareKey::Left),
KeyWithModifier::new(BareKey::Right),
];
let palette = Styling::default();
let ret = style_key_with_modifier(&keyvec, &palette, None);
let ret = unstyle(&ANSIStrings(&ret));
assert_eq!(ret, "<←→>".to_string())
}
#[test]
fn style_key_with_modifier_special_group_down_up_arrows() {
let keyvec = vec![
KeyWithModifier::new(BareKey::Down),
KeyWithModifier::new(BareKey::Up),
];
let palette = Styling::default();
let ret = style_key_with_modifier(&keyvec, &palette, None);
let ret = unstyle(&ANSIStrings(&ret));
assert_eq!(ret, "<↓↑>".to_string())
}
#[test]
fn style_key_with_modifier_common_ctrl_modifier_chars() {
let keyvec = vec![
KeyWithModifier::new(BareKey::Char('a')).with_ctrl_modifier(),
KeyWithModifier::new(BareKey::Char('b')).with_ctrl_modifier(),
KeyWithModifier::new(BareKey::Char('c')).with_ctrl_modifier(),
KeyWithModifier::new(BareKey::Char('d')).with_ctrl_modifier(),
];
let palette = Styling::default();
let ret = style_key_with_modifier(&keyvec, &palette, None);
let ret = unstyle(&ANSIStrings(&ret));
assert_eq!(ret, "Ctrl + <a|b|c|d>".to_string())
}
#[test]
fn style_key_with_modifier_common_alt_modifier_chars() {
let keyvec = vec![
KeyWithModifier::new(BareKey::Char('a')).with_alt_modifier(),
KeyWithModifier::new(BareKey::Char('b')).with_alt_modifier(),
KeyWithModifier::new(BareKey::Char('c')).with_alt_modifier(),
KeyWithModifier::new(BareKey::Char('d')).with_alt_modifier(),
];
let palette = Styling::default();
let ret = style_key_with_modifier(&keyvec, &palette, None);
let ret = unstyle(&ANSIStrings(&ret));
assert_eq!(ret, "Alt + <a|b|c|d>".to_string())
}
#[test]
fn style_key_with_modifier_common_alt_modifier_with_special_group_all_arrows() {
let keyvec = vec![
KeyWithModifier::new(BareKey::Left).with_alt_modifier(),
KeyWithModifier::new(BareKey::Down).with_alt_modifier(),
KeyWithModifier::new(BareKey::Up).with_alt_modifier(),
KeyWithModifier::new(BareKey::Right).with_alt_modifier(),
];
let palette = Styling::default();
let ret = style_key_with_modifier(&keyvec, &palette, None);
let ret = unstyle(&ANSIStrings(&ret));
assert_eq!(ret, "Alt + <←↓↑→>".to_string())
}
#[test]
fn style_key_with_modifier_ctrl_alt_char_mixed() {
let keyvec = vec![
KeyWithModifier::new(BareKey::Char('a')).with_alt_modifier(),
KeyWithModifier::new(BareKey::Char('b')).with_ctrl_modifier(),
KeyWithModifier::new(BareKey::Char('c')),
];
let palette = Styling::default();
let ret = style_key_with_modifier(&keyvec, &palette, None);
let ret = unstyle(&ANSIStrings(&ret));
assert_eq!(ret, "<Alt a|Ctrl b|c>".to_string())
}
#[test]
fn style_key_with_modifier_unprintables() {
let keyvec = vec![
KeyWithModifier::new(BareKey::Backspace),
KeyWithModifier::new(BareKey::Enter),
KeyWithModifier::new(BareKey::Char(' ')),
KeyWithModifier::new(BareKey::Tab),
KeyWithModifier::new(BareKey::PageDown),
KeyWithModifier::new(BareKey::Delete),
KeyWithModifier::new(BareKey::Home),
KeyWithModifier::new(BareKey::End),
KeyWithModifier::new(BareKey::Insert),
KeyWithModifier::new(BareKey::Tab),
KeyWithModifier::new(BareKey::Esc),
];
let palette = Styling::default();
let ret = style_key_with_modifier(&keyvec, &palette, None);
let ret = unstyle(&ANSIStrings(&ret));
assert_eq!(
ret,
"<BACKSPACE|ENTER|SPACE|TAB|PgDn|DEL|HOME|END|INS|TAB|ESC>".to_string()
)
}
#[test]
fn style_key_with_modifier_unprintables_with_common_ctrl_modifier() {
let keyvec = vec![
KeyWithModifier::new(BareKey::Enter).with_ctrl_modifier(),
KeyWithModifier::new(BareKey::Char(' ')).with_ctrl_modifier(),
KeyWithModifier::new(BareKey::Tab).with_ctrl_modifier(),
];
let palette = Styling::default();
let ret = style_key_with_modifier(&keyvec, &palette, None);
let ret = unstyle(&ANSIStrings(&ret));
assert_eq!(ret, "Ctrl + <ENTER|SPACE|TAB>".to_string())
}
#[test]
fn style_key_with_modifier_unprintables_with_common_alt_modifier() {
let keyvec = vec![
KeyWithModifier::new(BareKey::Enter).with_alt_modifier(),
KeyWithModifier::new(BareKey::Char(' ')).with_alt_modifier(),
KeyWithModifier::new(BareKey::Tab).with_alt_modifier(),
];
let palette = Styling::default();
let ret = style_key_with_modifier(&keyvec, &palette, None);
let ret = unstyle(&ANSIStrings(&ret));
assert_eq!(ret, "Alt + <ENTER|SPACE|TAB>".to_string())
}
}