mod first_line; mod second_line; mod tip; use ansi_term::{ ANSIString, Colour::{Fixed, RGB}, Style, }; 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 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, tip_name: String, mode_info: ModeInfo, text_copy_destination: Option, display_system_clipboard_failure: bool, } 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: Palette, different_color_alternates: bool) -> ColoredElements { let background = match palette.theme_hue { ThemeHue::Dark => palette.black, ThemeHue::Light => palette.white, }; let foreground = match palette.theme_hue { ThemeHue::Dark => palette.white, ThemeHue::Light => palette.black, }; let alternate_background_color = if different_color_alternates { match palette.theme_hue { ThemeHue::Dark => palette.white, ThemeHue::Light => palette.black, } } else { palette.fg }; match palette.source { PaletteSource::Default => ColoredElements { selected: SegmentStyle { prefix_separator: style!(background, palette.green), char_left_separator: style!(background, palette.green).bold(), char_shortcut: style!(palette.red, palette.green).bold(), char_right_separator: style!(background, palette.green).bold(), styled_text: style!(background, palette.green).bold(), suffix_separator: style!(palette.green, background).bold(), }, unselected: SegmentStyle { prefix_separator: style!(background, palette.fg), char_left_separator: style!(background, palette.fg).bold(), char_shortcut: style!(palette.red, palette.fg).bold(), char_right_separator: style!(background, palette.fg).bold(), styled_text: style!(background, palette.fg).bold(), suffix_separator: style!(palette.fg, background), }, unselected_alternate: SegmentStyle { prefix_separator: style!(background, alternate_background_color), char_left_separator: style!(background, alternate_background_color).bold(), char_shortcut: style!(palette.red, alternate_background_color).bold(), char_right_separator: style!(background, alternate_background_color).bold(), styled_text: style!(background, alternate_background_color).bold(), suffix_separator: style!(alternate_background_color, background), }, disabled: SegmentStyle { prefix_separator: style!(background, palette.fg), char_left_separator: style!(background, palette.fg).dimmed().italic(), char_shortcut: style!(background, palette.fg).dimmed().italic(), char_right_separator: style!(background, palette.fg).dimmed().italic(), styled_text: style!(background, palette.fg).dimmed().italic(), suffix_separator: style!(palette.fg, background), }, superkey_prefix: style!(foreground, background).bold(), superkey_suffix_separator: style!(background, background), }, PaletteSource::Xresources => ColoredElements { selected: SegmentStyle { prefix_separator: style!(background, palette.green), char_left_separator: style!(palette.fg, palette.green).bold(), char_shortcut: style!(palette.red, palette.green).bold(), char_right_separator: style!(palette.fg, palette.green).bold(), styled_text: style!(background, palette.green).bold(), suffix_separator: style!(palette.green, background).bold(), }, unselected: SegmentStyle { prefix_separator: style!(background, palette.fg), char_left_separator: style!(background, palette.fg).bold(), char_shortcut: style!(palette.red, palette.fg).bold(), char_right_separator: style!(background, palette.fg).bold(), styled_text: style!(background, palette.fg).bold(), suffix_separator: style!(palette.fg, background), }, unselected_alternate: SegmentStyle { prefix_separator: style!(background, alternate_background_color), char_left_separator: style!(background, alternate_background_color).bold(), char_shortcut: style!(palette.red, alternate_background_color).bold(), char_right_separator: style!(background, alternate_background_color).bold(), styled_text: style!(background, alternate_background_color).bold(), suffix_separator: style!(alternate_background_color, background), }, disabled: SegmentStyle { prefix_separator: style!(background, palette.fg), char_left_separator: style!(background, palette.fg).dimmed(), char_shortcut: style!(background, palette.fg).dimmed(), char_right_separator: style!(background, palette.fg).dimmed(), styled_text: style!(background, palette.fg).dimmed(), suffix_separator: style!(palette.fg, background), }, superkey_prefix: style!(background, palette.fg).bold(), superkey_suffix_separator: style!(palette.fg, background), }, } } impl ZellijPlugin for State { fn load(&mut self) { // TODO: Should be able to choose whether to use the cache through config. self.tip_name = get_cached_tip_name(); set_selectable(false); subscribe(&[ EventType::ModeUpdate, EventType::TabUpdate, 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; }, Event::TabUpdate(tabs) => { if self.tabs != tabs { should_render = true; } self.tabs = tabs; }, 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 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); let background = match self.mode_info.style.colors.theme_hue { ThemeHue::Dark => self.mode_info.style.colors.black, ThemeHue::Light => self.mode_info.style.colors.white, }; // [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(&self.mode_info.style.colors, 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() } } } /// Get a common modifier key from a key vector. /// /// Iterates over all keys and returns any found common modifier key. Possible modifiers that will /// be detected are "Ctrl" and "Alt". pub fn get_common_modifier(keyvec: Vec<&Key>) -> Option { let mut modifier = ""; let mut new_modifier; for key in keyvec.iter() { match key { Key::Ctrl(_) => new_modifier = "Ctrl", Key::Alt(_) => new_modifier = "Alt", _ => return None, } if modifier.is_empty() { modifier = new_modifier; } else if modifier != new_modifier { // Prefix changed! return None; } } match modifier.is_empty() { true => None, false => Some(modifier.to_string()), } } /// Get key from action pattern(s). /// /// This function takes as arguments a `keymap` that is a `Vec<(Key, Vec)>` 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`. pub fn action_key(keymap: &[(Key, Vec)], action: &[Action]) -> Vec { 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) } else { None } }) .collect::>() } /// 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: &[(Key, Vec)], actions: &[&[Action]]) -> Vec { 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 ``, 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 + `. /// /// 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: &[Key], palette: &Palette, background: Option, ) -> Vec> { // Nothing to do, quit... if keyvec.is_empty() { return vec![]; } let text_color = palette_match!(match palette.theme_hue { ThemeHue::Dark => palette.white, ThemeHue::Light => palette.black, }); let green_color = palette_match!(palette.green); let orange_color = palette_match!(palette.orange); let mut ret = vec![]; // Prints modifier key let modifier_str = match get_common_modifier(keyvec.iter().collect()) { Some(modifier) => modifier, None => "".to_string(), }; let no_modifier = modifier_str.is_empty(); 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_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_modifier { format!("{}", key) } else { match key { Key::Ctrl(c) => format!("{}", Key::Char(*c)), Key::Alt(c) => format!("{}", c), _ => format!("{}", key), } } }) .collect::>(); // 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; use zellij_tile::prelude::CharOrArrow; use zellij_tile::prelude::Direction; fn big_keymap() -> Vec<(Key, Vec)> { vec![ (Key::Char('a'), vec![Action::Quit]), (Key::Ctrl('b'), vec![Action::ScrollUp]), (Key::Ctrl('d'), vec![Action::ScrollDown]), ( Key::Alt(CharOrArrow::Char('c')), vec![Action::ScrollDown, Action::SwitchToMode(InputMode::Normal)], ), ( Key::Char('1'), vec![TO_NORMAL, Action::SwitchToMode(InputMode::Locked)], ), ] } #[test] fn common_modifier_with_ctrl_keys() { let keyvec = vec![Key::Ctrl('a'), Key::Ctrl('b'), Key::Ctrl('c')]; let ret = get_common_modifier(keyvec.iter().collect()); assert_eq!(ret, Some("Ctrl".to_string())); } #[test] fn common_modifier_with_alt_keys_chars() { let keyvec = vec![ Key::Alt(CharOrArrow::Char('1')), Key::Alt(CharOrArrow::Char('t')), Key::Alt(CharOrArrow::Char('z')), ]; let ret = get_common_modifier(keyvec.iter().collect()); assert_eq!(ret, Some("Alt".to_string())); } #[test] fn common_modifier_with_alt_keys_arrows() { let keyvec = vec![ Key::Alt(CharOrArrow::Direction(Direction::Left)), Key::Alt(CharOrArrow::Direction(Direction::Right)), ]; let ret = get_common_modifier(keyvec.iter().collect()); assert_eq!(ret, Some("Alt".to_string())); } #[test] fn common_modifier_with_alt_keys_arrows_and_chars() { let keyvec = vec![ Key::Alt(CharOrArrow::Direction(Direction::Left)), Key::Alt(CharOrArrow::Direction(Direction::Right)), Key::Alt(CharOrArrow::Char('t')), Key::Alt(CharOrArrow::Char('z')), ]; let ret = get_common_modifier(keyvec.iter().collect()); assert_eq!(ret, Some("Alt".to_string())); } #[test] fn common_modifier_with_mixed_alt_ctrl_keys() { let keyvec = vec![ Key::Alt(CharOrArrow::Direction(Direction::Left)), Key::Alt(CharOrArrow::Char('z')), Key::Ctrl('a'), Key::Ctrl('1'), ]; let ret = get_common_modifier(keyvec.iter().collect()); assert_eq!(ret, None); } #[test] fn common_modifier_with_any_keys() { let keyvec = vec![Key::Backspace, Key::Char('f'), Key::Down]; let ret = get_common_modifier(keyvec.iter().collect()); assert_eq!(ret, None); } #[test] fn common_modifier_with_ctrl_and_normal_keys() { let keyvec = vec![Key::Ctrl('a'), Key::Char('f'), Key::Down]; let ret = get_common_modifier(keyvec.iter().collect()); assert_eq!(ret, None); } #[test] fn common_modifier_with_alt_and_normal_keys() { let keyvec = vec![Key::Alt(CharOrArrow::Char('a')), Key::Char('f'), Key::Down]; let ret = get_common_modifier(keyvec.iter().collect()); assert_eq!(ret, None); } #[test] fn action_key_simple_pattern_match_exact() { let keymap = &[(Key::Char('f'), vec![Action::Quit])]; let ret = action_key(keymap, &[Action::Quit]); assert_eq!(ret, vec![Key::Char('f')]); } #[test] fn action_key_simple_pattern_match_pattern_too_long() { let keymap = &[(Key::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 = &[(Key::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![Key::Alt(CharOrArrow::Char('c'))]); } #[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![Key::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![Key::Ctrl('d'), Key::Ctrl('b')]); } fn get_palette() -> Palette { Palette::default() } #[test] fn style_key_with_modifier_only_chars() { let keyvec = vec![Key::Char('a'), Key::Char('b'), Key::Char('c')]; let palette = get_palette(); 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_hjkl() { let keyvec = vec![ Key::Char('h'), Key::Char('j'), Key::Char('k'), Key::Char('l'), ]; let palette = get_palette(); 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_hjkl_broken() { // Sorted the wrong way let keyvec = vec![ Key::Char('h'), Key::Char('k'), Key::Char('j'), Key::Char('l'), ]; let palette = get_palette(); 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_all_arrows() { let keyvec = vec![ Key::Char('←'), Key::Char('↓'), Key::Char('↑'), Key::Char('→'), ]; let palette = get_palette(); 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![Key::Char('←'), Key::Char('→')]; let palette = get_palette(); 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![Key::Char('↓'), Key::Char('↑')]; let palette = get_palette(); 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![ Key::Ctrl('a'), Key::Ctrl('b'), Key::Ctrl('c'), Key::Ctrl('d'), ]; let palette = get_palette(); let ret = style_key_with_modifier(&keyvec, &palette, None); let ret = unstyle(&ANSIStrings(&ret)); assert_eq!(ret, "Ctrl + ".to_string()) } #[test] fn style_key_with_modifier_common_alt_modifier_chars() { let keyvec = vec![ Key::Alt(CharOrArrow::Char('a')), Key::Alt(CharOrArrow::Char('b')), Key::Alt(CharOrArrow::Char('c')), Key::Alt(CharOrArrow::Char('d')), ]; let palette = get_palette(); 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_common_alt_modifier_with_special_group_all_arrows() { let keyvec = vec![ Key::Alt(CharOrArrow::Direction(Direction::Left)), Key::Alt(CharOrArrow::Direction(Direction::Down)), Key::Alt(CharOrArrow::Direction(Direction::Up)), Key::Alt(CharOrArrow::Direction(Direction::Right)), ]; let palette = get_palette(); 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![ Key::Alt(CharOrArrow::Char('a')), Key::Ctrl('b'), Key::Char('c'), ]; let palette = get_palette(); 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_unprintables() { let keyvec = vec![ Key::Backspace, Key::Char('\n'), Key::Char(' '), Key::Char('\t'), Key::PageDown, Key::Delete, Key::Home, Key::End, Key::Insert, Key::BackTab, Key::Esc, ]; let palette = get_palette(); 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_unprintables_with_common_ctrl_modifier() { let keyvec = vec![Key::Ctrl('\n'), Key::Ctrl(' '), Key::Ctrl('\t')]; let palette = get_palette(); let ret = style_key_with_modifier(&keyvec, &palette, None); let ret = unstyle(&ANSIStrings(&ret)); assert_eq!(ret, "Ctrl + ".to_string()) } #[test] fn style_key_with_modifier_unprintables_with_common_alt_modifier() { let keyvec = vec![ Key::Alt(CharOrArrow::Char('\n')), Key::Alt(CharOrArrow::Char(' ')), Key::Alt(CharOrArrow::Char('\t')), ]; let palette = get_palette(); let ret = style_key_with_modifier(&keyvec, &palette, None); let ret = unstyle(&ANSIStrings(&ret)); assert_eq!(ret, "Alt + ".to_string()) } }