* relayout working with hard coded layout * work * refactor(layout): PaneLayout => TiledPaneLayout * tests passing * tests passing * tests passing * stacked panes and passing tests * tests for stacked panes * refactor(panes): stacked panes * fix: focusing into stacked panes from the left/right * fix(layouts): handle stacked layouts in the middle of the screen * fix(pane-stack): focus correctly when coming to stack from above/below * fix(stacked-panes): resize stack * fix(stacked-panes): focus with mouse * fix(stacked-panes): focus next pane * fix(layout-applier): sane focus order * fix(stacked-panes): better titles for one-liners * fix(stacked-panes): handle moving pane location in stack * fix(relayout): properly calculate display area * fix(relayout): properly calculate rounding errors * fix(stacked-panes): properly handle closing a pane near a stack * fix(swap-layouts): adjust swap layout sort order * feat(swap-layouts): ui + ux * fix(swap-layouts): include base layout * refactor(layout): remove unused method * fix(swap-layouts): respect pane contents and focus * work * fix(swap-layouts): load swap layouts from external file * fix(swap-layouts): properly truncate layout children * fix(stacked-panes): allow stacked panes to become fullscreen * fix(swap-layouts): work with multiple tabs * fix(swap-layouts): embed/eject panes properly with auto-layout * fix(stacked-panes): close last pane in stack * fix(stacked-panes): move focus for all clients in stack * fix(floating-panes): set layout damaged when moving panes * fix(relayout): move out of unfitting layout when resizing whole tab * fix(ui): background color for swap layout indicator * fix(keybinds): add switch next layout in tmux * fix(ui): swap layout indication in compact layout * fix(compact): correct swap constraint * fix(tests): tmux swap config shortcut * fix(resizes): cache resizes so as not to confuse panes (eg. vim) with multiple resizes that it debounces weirdly * feat(cli): dump swap layouts * fix(ui): stacked panes without pane frames * fix(ux): move pane forward/backwards also with floating panes * refactor(lint): remove unused stuff * refactor(tab): move swap layouts to separate file * style(fmt): rustfmt * style(fmt): rustfmt * refactor(panes): various cleanups * chore(deps): upgrade termwiz to get alt left-bracket * fix(assets): merge conflicts of binary files * style(fmt): rustfmt * style(clippy): no thank you! * chore(repo): remove garbage file
758 lines
30 KiB
Rust
758 lines
30 KiB
Rust
use ansi_term::{
|
|
unstyled_len, ANSIString, ANSIStrings,
|
|
Color::{Fixed, RGB},
|
|
Style,
|
|
};
|
|
use zellij_tile::prelude::actions::Action;
|
|
use zellij_tile::prelude::*;
|
|
use zellij_tile_utils::palette_match;
|
|
|
|
use crate::{
|
|
action_key, action_key_group, style_key_with_modifier,
|
|
tip::{data::TIPS, TipFn},
|
|
LinePart, MORE_MSG, TO_NORMAL,
|
|
};
|
|
|
|
fn full_length_shortcut(
|
|
is_first_shortcut: bool,
|
|
key: Vec<Key>,
|
|
action: &str,
|
|
palette: Palette,
|
|
) -> LinePart {
|
|
if key.is_empty() {
|
|
return LinePart::default();
|
|
}
|
|
|
|
let text_color = palette_match!(match palette.theme_hue {
|
|
ThemeHue::Dark => palette.white,
|
|
ThemeHue::Light => palette.black,
|
|
});
|
|
|
|
let separator = if is_first_shortcut { " " } else { " / " };
|
|
let mut bits: Vec<ANSIString> = vec![Style::new().fg(text_color).paint(separator)];
|
|
bits.extend(style_key_with_modifier(&key, &palette, None));
|
|
bits.push(
|
|
Style::new()
|
|
.fg(text_color)
|
|
.bold()
|
|
.paint(format!(" {}", action)),
|
|
);
|
|
let part = ANSIStrings(&bits);
|
|
|
|
LinePart {
|
|
part: part.to_string(),
|
|
len: unstyled_len(&part),
|
|
}
|
|
}
|
|
|
|
fn locked_interface_indication(palette: Palette) -> LinePart {
|
|
let locked_text = " -- INTERFACE LOCKED -- ";
|
|
let locked_text_len = locked_text.chars().count();
|
|
let text_color = palette_match!(match palette.theme_hue {
|
|
ThemeHue::Dark => palette.white,
|
|
ThemeHue::Light => palette.black,
|
|
});
|
|
let locked_styled_text = Style::new().fg(text_color).bold().paint(locked_text);
|
|
LinePart {
|
|
part: locked_styled_text.to_string(),
|
|
len: locked_text_len,
|
|
}
|
|
}
|
|
|
|
fn add_shortcut(help: &ModeInfo, linepart: &LinePart, text: &str, keys: Vec<Key>) -> LinePart {
|
|
let shortcut = if linepart.len == 0 {
|
|
full_length_shortcut(true, keys, text, help.style.colors)
|
|
} else {
|
|
full_length_shortcut(false, keys, text, help.style.colors)
|
|
};
|
|
|
|
let mut new_linepart = LinePart::default();
|
|
new_linepart.len += linepart.len + shortcut.len;
|
|
new_linepart.part = format!("{}{}", linepart.part, shortcut);
|
|
new_linepart
|
|
}
|
|
|
|
fn full_shortcut_list_nonstandard_mode(help: &ModeInfo) -> LinePart {
|
|
let mut line_part = LinePart::default();
|
|
let keys_and_hints = get_keys_and_hints(help);
|
|
|
|
for (long, _short, keys) in keys_and_hints.into_iter() {
|
|
line_part = add_shortcut(help, &line_part, &long, keys.to_vec());
|
|
}
|
|
line_part
|
|
}
|
|
|
|
/// Collect all relevant keybindings and hints to display.
|
|
///
|
|
/// Creates a vector with tuples containing the following entries:
|
|
///
|
|
/// - A String to display for this keybinding when there are no size restrictions,
|
|
/// - A shortened String (where sensible) to display if the whole second line becomes too long,
|
|
/// - A `Vec<Key>` of the keys that map to this keyhint
|
|
///
|
|
/// This vector is created by iterating over the keybindings for the current [`InputMode`] and
|
|
/// storing all Keybindings that match pre-defined patterns of `Action`s. For example, the
|
|
/// `InputMode::Pane` input mode determines which keys to display for the "Move focus" hint by
|
|
/// searching the keybindings for anything that matches the `Action::MoveFocus(_)` action. Since by
|
|
/// default multiple keybindings map to some action patterns (e.g. `Action::MoveFocus(_)` is bound
|
|
/// to "hjkl", the arrow keys and "Alt + <hjkl>"), we deduplicate the vector of all keybindings
|
|
/// before processing it.
|
|
///
|
|
/// Therefore we sort it by the [`Key`]s of the current keymap and deduplicate the resulting sorted
|
|
/// vector by the `Vec<Action>` action vectors bound to the keys. As such, when multiple keys map
|
|
/// to the same sequence of actions, the keys that appear first in the [`Key`] structure will be
|
|
/// displayed.
|
|
// Please don't let rustfmt play with the formatting. It will stretch out the function to about
|
|
// three times the length and all the keybinding vectors we generate become virtually unreadable
|
|
// for humans.
|
|
#[rustfmt::skip]
|
|
fn get_keys_and_hints(mi: &ModeInfo) -> Vec<(String, String, Vec<Key>)> {
|
|
use Action as A;
|
|
use InputMode as IM;
|
|
use Direction as Dir;
|
|
use actions::SearchDirection as SDir;
|
|
use actions::SearchOption as SOpt;
|
|
|
|
let mut old_keymap = mi.get_mode_keybinds();
|
|
let s = |string: &str| string.to_string();
|
|
|
|
// Find a keybinding to get back to "Normal" input mode. In this case we prefer '\n' over other
|
|
// choices. Do it here before we dedupe the keymap below!
|
|
let to_normal_keys = action_key(&old_keymap, &[TO_NORMAL]);
|
|
let to_normal_key = if to_normal_keys.contains(&Key::Char('\n')) {
|
|
vec![Key::Char('\n')]
|
|
} else {
|
|
// Yield `vec![key]` if `to_normal_keys` has at least one key, or an empty vec otherwise.
|
|
to_normal_keys.into_iter().take(1).collect()
|
|
};
|
|
|
|
// Sort and deduplicate the keybindings first. We sort after the `Key`s, and deduplicate by
|
|
// their `Action` vectors. An unstable sort is fine here because if the user maps anything to
|
|
// the same key again, anything will happen...
|
|
old_keymap.sort_unstable_by(|(keya, _), (keyb, _)| keya.partial_cmp(keyb).unwrap());
|
|
|
|
let mut known_actions: Vec<Vec<Action>> = vec![];
|
|
let mut km = vec![];
|
|
for (key, acvec) in old_keymap {
|
|
if known_actions.contains(&acvec) {
|
|
// This action is known already
|
|
continue;
|
|
} else {
|
|
known_actions.push(acvec.to_vec());
|
|
km.push((key, acvec));
|
|
}
|
|
}
|
|
|
|
if mi.mode == IM::Pane { vec![
|
|
(s("Move focus"), s("Move"),
|
|
action_key_group(&km, &[&[A::MoveFocus(Dir::Left)], &[A::MoveFocus(Dir::Down)],
|
|
&[A::MoveFocus(Dir::Up)], &[A::MoveFocus(Dir::Right)]])),
|
|
(s("New"), s("New"), action_key(&km, &[A::NewPane(None, None), TO_NORMAL])),
|
|
(s("Close"), s("Close"), action_key(&km, &[A::CloseFocus, TO_NORMAL])),
|
|
(s("Rename"), s("Rename"),
|
|
action_key(&km, &[A::SwitchToMode(IM::RenamePane), A::PaneNameInput(vec![0])])),
|
|
(s("Split down"), s("Down"), action_key(&km, &[A::NewPane(Some(Dir::Down), None), TO_NORMAL])),
|
|
(s("Split right"), s("Right"), action_key(&km, &[A::NewPane(Some(Dir::Right), None), TO_NORMAL])),
|
|
(s("Fullscreen"), s("Fullscreen"), action_key(&km, &[A::ToggleFocusFullscreen, TO_NORMAL])),
|
|
(s("Frames"), s("Frames"), action_key(&km, &[A::TogglePaneFrames, TO_NORMAL])),
|
|
(s("Floating toggle"), s("Floating"),
|
|
action_key(&km, &[A::ToggleFloatingPanes, TO_NORMAL])),
|
|
(s("Embed pane"), s("Embed"), action_key(&km, &[A::TogglePaneEmbedOrFloating, TO_NORMAL])),
|
|
(s("Next"), s("Next"), action_key(&km, &[A::SwitchFocus])),
|
|
(s("Select pane"), s("Select"), to_normal_key),
|
|
]} else if mi.mode == IM::Tab {
|
|
// With the default bindings, "Move focus" for tabs is tricky: It binds all the arrow keys
|
|
// to moving tabs focus (left/up go left, right/down go right). Since we sort the keys
|
|
// above and then dedpulicate based on the actions, we will end up with LeftArrow for
|
|
// "left" and DownArrow for "right". What we really expect is to see LeftArrow and
|
|
// RightArrow.
|
|
// FIXME: So for lack of a better idea we just check this case manually here.
|
|
let old_keymap = mi.get_mode_keybinds();
|
|
let focus_keys_full: Vec<Key> = action_key_group(&old_keymap,
|
|
&[&[A::GoToPreviousTab], &[A::GoToNextTab]]);
|
|
let focus_keys = if focus_keys_full.contains(&Key::Left)
|
|
&& focus_keys_full.contains(&Key::Right) {
|
|
vec![Key::Left, Key::Right]
|
|
} else {
|
|
action_key_group(&km, &[&[A::GoToPreviousTab], &[A::GoToNextTab]])
|
|
};
|
|
|
|
vec![
|
|
(s("Move focus"), s("Move"), focus_keys),
|
|
(s("New"), s("New"), action_key(&km, &[A::NewTab(None, vec![], None, None, None), TO_NORMAL])),
|
|
(s("Close"), s("Close"), action_key(&km, &[A::CloseTab, TO_NORMAL])),
|
|
(s("Rename"), s("Rename"),
|
|
action_key(&km, &[A::SwitchToMode(IM::RenameTab), A::TabNameInput(vec![0])])),
|
|
(s("Sync"), s("Sync"), action_key(&km, &[A::ToggleActiveSyncTab, TO_NORMAL])),
|
|
(s("Toggle"), s("Toggle"), action_key(&km, &[A::ToggleTab])),
|
|
(s("Select pane"), s("Select"), to_normal_key),
|
|
]} else if mi.mode == IM::Resize { vec![
|
|
(s("Increase to"), s("Increase"), action_key_group(&km, &[
|
|
&[A::Resize(Resize::Increase, Some(Dir::Left))],
|
|
&[A::Resize(Resize::Increase, Some(Dir::Down))],
|
|
&[A::Resize(Resize::Increase, Some(Dir::Up))],
|
|
&[A::Resize(Resize::Increase, Some(Dir::Right))]
|
|
])),
|
|
(s("Decrease from"), s("Decrease"), action_key_group(&km, &[
|
|
&[A::Resize(Resize::Decrease, Some(Dir::Left))],
|
|
&[A::Resize(Resize::Decrease, Some(Dir::Down))],
|
|
&[A::Resize(Resize::Decrease, Some(Dir::Up))],
|
|
&[A::Resize(Resize::Decrease, Some(Dir::Right))]
|
|
])),
|
|
(s("Increase/Decrease size"), s("Increase/Decrease"),
|
|
action_key_group(&km, &[
|
|
&[A::Resize(Resize::Increase, None)],
|
|
&[A::Resize(Resize::Decrease, None)]
|
|
])),
|
|
(s("Select pane"), s("Select"), to_normal_key),
|
|
]} else if mi.mode == IM::Move { vec![
|
|
(s("Move"), s("Move"), action_key_group(&km, &[
|
|
&[Action::MovePane(Some(Dir::Left))], &[Action::MovePane(Some(Dir::Down))],
|
|
&[Action::MovePane(Some(Dir::Up))], &[Action::MovePane(Some(Dir::Right))]])),
|
|
(s("Next pane"), s("Next"), action_key(&km, &[Action::MovePane(None)])),
|
|
(s("Previous pane"), s("Previous"), action_key(&km, &[Action::MovePaneBackwards])),
|
|
]} else if mi.mode == IM::Scroll { vec![
|
|
(s("Scroll"), s("Scroll"),
|
|
action_key_group(&km, &[&[Action::ScrollDown], &[Action::ScrollUp]])),
|
|
(s("Scroll page"), s("Scroll"),
|
|
action_key_group(&km, &[&[Action::PageScrollDown], &[Action::PageScrollUp]])),
|
|
(s("Scroll half page"), s("Scroll"),
|
|
action_key_group(&km, &[&[Action::HalfPageScrollDown], &[Action::HalfPageScrollUp]])),
|
|
(s("Edit scrollback in default editor"), s("Edit"),
|
|
action_key(&km, &[Action::EditScrollback, TO_NORMAL])),
|
|
(s("Enter search term"), s("Search"),
|
|
action_key(&km, &[A::SwitchToMode(IM::EnterSearch), A::SearchInput(vec![0])])),
|
|
(s("Select pane"), s("Select"), to_normal_key),
|
|
]} else if mi.mode == IM::EnterSearch { vec![
|
|
(s("When done"), s("Done"), action_key(&km, &[A::SwitchToMode(IM::Search)])),
|
|
(s("Cancel"), s("Cancel"),
|
|
action_key(&km, &[A::SearchInput(vec![27]), A::SwitchToMode(IM::Scroll)])),
|
|
]} else if mi.mode == IM::Search { vec![
|
|
(s("Scroll"), s("Scroll"),
|
|
action_key_group(&km, &[&[Action::ScrollDown], &[Action::ScrollUp]])),
|
|
(s("Scroll page"), s("Scroll"),
|
|
action_key_group(&km, &[&[Action::PageScrollDown], &[Action::PageScrollUp]])),
|
|
(s("Scroll half page"), s("Scroll"),
|
|
action_key_group(&km, &[&[Action::HalfPageScrollDown], &[Action::HalfPageScrollUp]])),
|
|
(s("Enter term"), s("Search"),
|
|
action_key(&km, &[A::SwitchToMode(IM::EnterSearch), A::SearchInput(vec![0])])),
|
|
(s("Search down"), s("Down"), action_key(&km, &[A::Search(SDir::Down)])),
|
|
(s("Search up"), s("Up"), action_key(&km, &[A::Search(SDir::Up)])),
|
|
(s("Case sensitive"), s("Case"),
|
|
action_key(&km, &[A::SearchToggleOption(SOpt::CaseSensitivity)])),
|
|
(s("Wrap"), s("Wrap"),
|
|
action_key(&km, &[A::SearchToggleOption(SOpt::Wrap)])),
|
|
(s("Whole words"), s("Whole"),
|
|
action_key(&km, &[A::SearchToggleOption(SOpt::WholeWord)])),
|
|
]} else if mi.mode == IM::Session { vec![
|
|
(s("Detach"), s("Detach"), action_key(&km, &[Action::Detach])),
|
|
(s("Select pane"), s("Select"), to_normal_key),
|
|
]} else if mi.mode == IM::Tmux { vec![
|
|
(s("Move focus"), s("Move"), action_key_group(&km, &[
|
|
&[A::MoveFocus(Dir::Left)], &[A::MoveFocus(Dir::Down)],
|
|
&[A::MoveFocus(Dir::Up)], &[A::MoveFocus(Dir::Right)]])),
|
|
(s("Split down"), s("Down"), action_key(&km, &[A::NewPane(Some(Dir::Down), None), TO_NORMAL])),
|
|
(s("Split right"), s("Right"), action_key(&km, &[A::NewPane(Some(Dir::Right), None), TO_NORMAL])),
|
|
(s("Fullscreen"), s("Fullscreen"), action_key(&km, &[A::ToggleFocusFullscreen, TO_NORMAL])),
|
|
(s("New tab"), s("New"), action_key(&km, &[A::NewTab(None, vec![], None, None, None), TO_NORMAL])),
|
|
(s("Rename tab"), s("Rename"),
|
|
action_key(&km, &[A::SwitchToMode(IM::RenameTab), A::TabNameInput(vec![0])])),
|
|
(s("Previous Tab"), s("Previous"), action_key(&km, &[A::GoToPreviousTab, TO_NORMAL])),
|
|
(s("Next Tab"), s("Next"), action_key(&km, &[A::GoToNextTab, TO_NORMAL])),
|
|
(s("Select pane"), s("Select"), to_normal_key),
|
|
]} else if matches!(mi.mode, IM::RenamePane | IM::RenameTab) { vec![
|
|
(s("When done"), s("Done"), to_normal_key),
|
|
(s("Select pane"), s("Select"), action_key_group(&km, &[
|
|
&[A::MoveFocus(Dir::Left)], &[A::MoveFocus(Dir::Down)],
|
|
&[A::MoveFocus(Dir::Up)], &[A::MoveFocus(Dir::Right)]])),
|
|
]} else { vec![] }
|
|
}
|
|
|
|
fn full_shortcut_list(help: &ModeInfo, tip: TipFn) -> LinePart {
|
|
match help.mode {
|
|
InputMode::Normal => tip(help),
|
|
InputMode::Locked => locked_interface_indication(help.style.colors),
|
|
_ => full_shortcut_list_nonstandard_mode(help),
|
|
}
|
|
}
|
|
|
|
fn shortened_shortcut_list_nonstandard_mode(help: &ModeInfo) -> LinePart {
|
|
let mut line_part = LinePart::default();
|
|
let keys_and_hints = get_keys_and_hints(help);
|
|
|
|
for (_, short, keys) in keys_and_hints.into_iter() {
|
|
line_part = add_shortcut(help, &line_part, &short, keys.to_vec());
|
|
}
|
|
line_part
|
|
}
|
|
|
|
fn shortened_shortcut_list(help: &ModeInfo, tip: TipFn) -> LinePart {
|
|
match help.mode {
|
|
InputMode::Normal => tip(help),
|
|
InputMode::Locked => locked_interface_indication(help.style.colors),
|
|
_ => shortened_shortcut_list_nonstandard_mode(help),
|
|
}
|
|
}
|
|
|
|
fn best_effort_shortcut_list_nonstandard_mode(help: &ModeInfo, max_len: usize) -> LinePart {
|
|
let mut line_part = LinePart::default();
|
|
let keys_and_hints = get_keys_and_hints(help);
|
|
|
|
for (_, short, keys) in keys_and_hints.into_iter() {
|
|
let new_line_part = add_shortcut(help, &line_part, &short, keys.to_vec());
|
|
if new_line_part.len + MORE_MSG.chars().count() > max_len {
|
|
line_part.part = format!("{}{}", line_part.part, MORE_MSG);
|
|
line_part.len += MORE_MSG.chars().count();
|
|
break;
|
|
}
|
|
line_part = new_line_part;
|
|
}
|
|
line_part
|
|
}
|
|
|
|
fn best_effort_shortcut_list(help: &ModeInfo, tip: TipFn, max_len: usize) -> LinePart {
|
|
match help.mode {
|
|
InputMode::Normal => {
|
|
let line_part = tip(help);
|
|
if line_part.len <= max_len {
|
|
line_part
|
|
} else {
|
|
LinePart::default()
|
|
}
|
|
},
|
|
InputMode::Locked => {
|
|
let line_part = locked_interface_indication(help.style.colors);
|
|
if line_part.len <= max_len {
|
|
line_part
|
|
} else {
|
|
LinePart::default()
|
|
}
|
|
},
|
|
_ => best_effort_shortcut_list_nonstandard_mode(help, max_len),
|
|
}
|
|
}
|
|
|
|
pub fn keybinds(help: &ModeInfo, tip_name: &str, max_width: usize) -> LinePart {
|
|
// It is assumed that there is at least one TIP data in the TIPS HasMap.
|
|
let tip_body = TIPS
|
|
.get(tip_name)
|
|
.unwrap_or_else(|| TIPS.get("quicknav").unwrap());
|
|
|
|
let full_shortcut_list = full_shortcut_list(help, tip_body.full);
|
|
if full_shortcut_list.len <= max_width {
|
|
return full_shortcut_list;
|
|
}
|
|
let shortened_shortcut_list = shortened_shortcut_list(help, tip_body.medium);
|
|
if shortened_shortcut_list.len <= max_width {
|
|
return shortened_shortcut_list;
|
|
}
|
|
best_effort_shortcut_list(help, tip_body.short, max_width)
|
|
}
|
|
|
|
pub fn text_copied_hint(palette: &Palette, copy_destination: CopyDestination) -> LinePart {
|
|
let green_color = palette_match!(palette.green);
|
|
let hint = match copy_destination {
|
|
CopyDestination::Command => "Text piped to external command",
|
|
#[cfg(not(target_os = "macos"))]
|
|
CopyDestination::Primary => "Text copied to system primary selection",
|
|
#[cfg(target_os = "macos")] // primary selection does not exist on macos
|
|
CopyDestination::Primary => "Text copied to system clipboard",
|
|
CopyDestination::System => "Text copied to system clipboard",
|
|
};
|
|
LinePart {
|
|
part: Style::new().fg(green_color).bold().paint(hint).to_string(),
|
|
len: hint.len(),
|
|
}
|
|
}
|
|
|
|
pub fn system_clipboard_error(palette: &Palette) -> LinePart {
|
|
let hint = " Error using the system clipboard.";
|
|
let red_color = palette_match!(palette.red);
|
|
LinePart {
|
|
part: Style::new().fg(red_color).bold().paint(hint).to_string(),
|
|
len: hint.len(),
|
|
}
|
|
}
|
|
|
|
pub fn fullscreen_panes_to_hide(palette: &Palette, panes_to_hide: usize) -> LinePart {
|
|
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 shortcut_left_separator = Style::new().fg(text_color).bold().paint(" (");
|
|
let shortcut_right_separator = Style::new().fg(text_color).bold().paint("): ");
|
|
let fullscreen = "FULLSCREEN";
|
|
let puls = "+ ";
|
|
let panes = panes_to_hide.to_string();
|
|
let hide = " hidden panes";
|
|
let len = fullscreen.chars().count()
|
|
+ puls.chars().count()
|
|
+ panes.chars().count()
|
|
+ hide.chars().count()
|
|
+ 5; // 3 for ():'s around shortcut, 2 for the space
|
|
LinePart {
|
|
part: format!(
|
|
"{}{}{}{}{}{}",
|
|
shortcut_left_separator,
|
|
Style::new().fg(orange_color).bold().paint(fullscreen),
|
|
shortcut_right_separator,
|
|
Style::new().fg(text_color).bold().paint(puls),
|
|
Style::new().fg(green_color).bold().paint(panes),
|
|
Style::new().fg(text_color).bold().paint(hide)
|
|
),
|
|
len,
|
|
}
|
|
}
|
|
|
|
pub fn floating_panes_are_visible(mode_info: &ModeInfo) -> LinePart {
|
|
let palette = mode_info.style.colors;
|
|
let km = &mode_info.get_mode_keybinds();
|
|
let white_color = match palette.white {
|
|
PaletteColor::Rgb((r, g, b)) => RGB(r, g, b),
|
|
PaletteColor::EightBit(color) => Fixed(color),
|
|
};
|
|
let green_color = match palette.green {
|
|
PaletteColor::Rgb((r, g, b)) => RGB(r, g, b),
|
|
PaletteColor::EightBit(color) => Fixed(color),
|
|
};
|
|
let orange_color = match palette.orange {
|
|
PaletteColor::Rgb((r, g, b)) => RGB(r, g, b),
|
|
PaletteColor::EightBit(color) => Fixed(color),
|
|
};
|
|
let shortcut_left_separator = Style::new().fg(white_color).bold().paint(" (");
|
|
let shortcut_right_separator = Style::new().fg(white_color).bold().paint("): ");
|
|
let floating_panes = "FLOATING PANES VISIBLE";
|
|
let press = "Press ";
|
|
let pane_mode = format!(
|
|
"{}",
|
|
action_key(km, &[Action::SwitchToMode(InputMode::Pane)])
|
|
.first()
|
|
.unwrap_or(&Key::Char('?'))
|
|
);
|
|
let plus = ", ";
|
|
let p_left_separator = "<";
|
|
let p = format!(
|
|
"{}",
|
|
action_key(
|
|
&mode_info.get_keybinds_for_mode(InputMode::Pane),
|
|
&[Action::ToggleFloatingPanes, TO_NORMAL]
|
|
)
|
|
.first()
|
|
.unwrap_or(&Key::Char('?'))
|
|
);
|
|
let p_right_separator = "> ";
|
|
let to_hide = "to hide.";
|
|
|
|
let len = floating_panes.chars().count()
|
|
+ press.chars().count()
|
|
+ pane_mode.chars().count()
|
|
+ plus.chars().count()
|
|
+ p_left_separator.chars().count()
|
|
+ p.chars().count()
|
|
+ p_right_separator.chars().count()
|
|
+ to_hide.chars().count()
|
|
+ 5; // 3 for ():'s around floating_panes, 2 for the space
|
|
LinePart {
|
|
part: format!(
|
|
"{}{}{}{}{}{}{}{}{}{}",
|
|
shortcut_left_separator,
|
|
Style::new().fg(orange_color).bold().paint(floating_panes),
|
|
shortcut_right_separator,
|
|
Style::new().fg(white_color).bold().paint(press),
|
|
Style::new().fg(green_color).bold().paint(pane_mode),
|
|
Style::new().fg(white_color).bold().paint(plus),
|
|
Style::new().fg(white_color).bold().paint(p_left_separator),
|
|
Style::new().fg(green_color).bold().paint(p),
|
|
Style::new().fg(white_color).bold().paint(p_right_separator),
|
|
Style::new().fg(white_color).bold().paint(to_hide),
|
|
),
|
|
len,
|
|
}
|
|
}
|
|
|
|
pub fn locked_fullscreen_panes_to_hide(palette: &Palette, panes_to_hide: usize) -> LinePart {
|
|
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 locked_text = " -- INTERFACE LOCKED -- ";
|
|
let shortcut_left_separator = Style::new().fg(text_color).bold().paint(" (");
|
|
let shortcut_right_separator = Style::new().fg(text_color).bold().paint("): ");
|
|
let fullscreen = "FULLSCREEN";
|
|
let puls = "+ ";
|
|
let panes = panes_to_hide.to_string();
|
|
let hide = " hidden panes";
|
|
let len = locked_text.chars().count()
|
|
+ fullscreen.chars().count()
|
|
+ puls.chars().count()
|
|
+ panes.chars().count()
|
|
+ hide.chars().count()
|
|
+ 5; // 3 for ():'s around shortcut, 2 for the space
|
|
LinePart {
|
|
part: format!(
|
|
"{}{}{}{}{}{}{}",
|
|
Style::new().fg(text_color).bold().paint(locked_text),
|
|
shortcut_left_separator,
|
|
Style::new().fg(orange_color).bold().paint(fullscreen),
|
|
shortcut_right_separator,
|
|
Style::new().fg(text_color).bold().paint(puls),
|
|
Style::new().fg(green_color).bold().paint(panes),
|
|
Style::new().fg(text_color).bold().paint(hide)
|
|
),
|
|
len,
|
|
}
|
|
}
|
|
|
|
pub fn locked_floating_panes_are_visible(palette: &Palette) -> LinePart {
|
|
let white_color = match palette.white {
|
|
PaletteColor::Rgb((r, g, b)) => RGB(r, g, b),
|
|
PaletteColor::EightBit(color) => Fixed(color),
|
|
};
|
|
let orange_color = match palette.orange {
|
|
PaletteColor::Rgb((r, g, b)) => RGB(r, g, b),
|
|
PaletteColor::EightBit(color) => Fixed(color),
|
|
};
|
|
let shortcut_left_separator = Style::new().fg(white_color).bold().paint(" (");
|
|
let shortcut_right_separator = Style::new().fg(white_color).bold().paint(")");
|
|
let locked_text = " -- INTERFACE LOCKED -- ";
|
|
let floating_panes = "FLOATING PANES VISIBLE";
|
|
|
|
let len = locked_text.chars().count() + floating_panes.chars().count();
|
|
LinePart {
|
|
part: format!(
|
|
"{}{}{}{}",
|
|
Style::new().fg(white_color).bold().paint(locked_text),
|
|
shortcut_left_separator,
|
|
Style::new().fg(orange_color).bold().paint(floating_panes),
|
|
shortcut_right_separator,
|
|
),
|
|
len,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
/// Unit tests.
|
|
///
|
|
/// Note that we cheat a little here, because the number of things one may want to test is endless,
|
|
/// and creating a Mockup of [`ModeInfo`] by hand for all these testcases is nothing less than
|
|
/// torture. Hence, we test the most atomic unit thoroughly ([`full_length_shortcut`] and then test
|
|
/// the public API ([`keybinds`]) to ensure correct operation.
|
|
mod tests {
|
|
use super::*;
|
|
|
|
// Strip style information from `LinePart` and return a raw String instead
|
|
fn unstyle(line_part: LinePart) -> String {
|
|
let string = line_part.to_string();
|
|
|
|
let re = regex::Regex::new(r"\x1b\[[0-9;]*m").unwrap();
|
|
let string = re.replace_all(&string, "".to_string());
|
|
|
|
string.to_string()
|
|
}
|
|
|
|
fn get_palette() -> Palette {
|
|
Palette::default()
|
|
}
|
|
|
|
#[test]
|
|
fn full_length_shortcut_with_key() {
|
|
let keyvec = vec![Key::Char('a')];
|
|
let palette = get_palette();
|
|
|
|
let ret = full_length_shortcut(false, keyvec, "Foobar", palette);
|
|
let ret = unstyle(ret);
|
|
|
|
assert_eq!(ret, " / <a> Foobar");
|
|
}
|
|
|
|
#[test]
|
|
fn full_length_shortcut_with_key_first_element() {
|
|
let keyvec = vec![Key::Char('a')];
|
|
let palette = get_palette();
|
|
|
|
let ret = full_length_shortcut(true, keyvec, "Foobar", palette);
|
|
let ret = unstyle(ret);
|
|
|
|
assert_eq!(ret, " <a> Foobar");
|
|
}
|
|
|
|
#[test]
|
|
// When there is no binding, we print no shortcut either
|
|
fn full_length_shortcut_without_key() {
|
|
let keyvec = vec![];
|
|
let palette = get_palette();
|
|
|
|
let ret = full_length_shortcut(false, keyvec, "Foobar", palette);
|
|
let ret = unstyle(ret);
|
|
|
|
assert_eq!(ret, "");
|
|
}
|
|
|
|
#[test]
|
|
fn full_length_shortcut_with_key_unprintable_1() {
|
|
let keyvec = vec![Key::Char('\n')];
|
|
let palette = get_palette();
|
|
|
|
let ret = full_length_shortcut(false, keyvec, "Foobar", palette);
|
|
let ret = unstyle(ret);
|
|
|
|
assert_eq!(ret, " / <ENTER> Foobar");
|
|
}
|
|
|
|
#[test]
|
|
fn full_length_shortcut_with_key_unprintable_2() {
|
|
let keyvec = vec![Key::Backspace];
|
|
let palette = get_palette();
|
|
|
|
let ret = full_length_shortcut(false, keyvec, "Foobar", palette);
|
|
let ret = unstyle(ret);
|
|
|
|
assert_eq!(ret, " / <BACKSPACE> Foobar");
|
|
}
|
|
|
|
#[test]
|
|
fn full_length_shortcut_with_ctrl_key() {
|
|
let keyvec = vec![Key::Ctrl('a')];
|
|
let palette = get_palette();
|
|
|
|
let ret = full_length_shortcut(false, keyvec, "Foobar", palette);
|
|
let ret = unstyle(ret);
|
|
|
|
assert_eq!(ret, " / Ctrl + <a> Foobar");
|
|
}
|
|
|
|
#[test]
|
|
fn full_length_shortcut_with_alt_key() {
|
|
let keyvec = vec![Key::Alt(CharOrArrow::Char('a'))];
|
|
let palette = get_palette();
|
|
|
|
let ret = full_length_shortcut(false, keyvec, "Foobar", palette);
|
|
let ret = unstyle(ret);
|
|
|
|
assert_eq!(ret, " / Alt + <a> Foobar");
|
|
}
|
|
|
|
#[test]
|
|
fn full_length_shortcut_with_homogenous_key_group() {
|
|
let keyvec = vec![Key::Char('a'), Key::Char('b'), Key::Char('c')];
|
|
let palette = get_palette();
|
|
|
|
let ret = full_length_shortcut(false, keyvec, "Foobar", palette);
|
|
let ret = unstyle(ret);
|
|
|
|
assert_eq!(ret, " / <a|b|c> Foobar");
|
|
}
|
|
|
|
#[test]
|
|
fn full_length_shortcut_with_heterogenous_key_group() {
|
|
let keyvec = vec![Key::Char('a'), Key::Ctrl('b'), Key::Char('\n')];
|
|
let palette = get_palette();
|
|
|
|
let ret = full_length_shortcut(false, keyvec, "Foobar", palette);
|
|
let ret = unstyle(ret);
|
|
|
|
assert_eq!(ret, " / <a|Ctrl+b|ENTER> Foobar");
|
|
}
|
|
|
|
#[test]
|
|
fn full_length_shortcut_with_key_group_shared_ctrl_modifier() {
|
|
let keyvec = vec![Key::Ctrl('a'), Key::Ctrl('b'), Key::Ctrl('c')];
|
|
let palette = get_palette();
|
|
|
|
let ret = full_length_shortcut(false, keyvec, "Foobar", palette);
|
|
let ret = unstyle(ret);
|
|
|
|
assert_eq!(ret, " / Ctrl + <a|b|c> Foobar");
|
|
}
|
|
//pub fn keybinds(help: &ModeInfo, tip_name: &str, max_width: usize) -> LinePart {
|
|
|
|
#[test]
|
|
// Note how it leaves out elements that don't exist!
|
|
fn keybinds_wide() {
|
|
let mode_info = ModeInfo {
|
|
mode: InputMode::Pane,
|
|
keybinds: vec![(
|
|
InputMode::Pane,
|
|
vec![
|
|
(Key::Left, vec![Action::MoveFocus(Direction::Left)]),
|
|
(Key::Down, vec![Action::MoveFocus(Direction::Down)]),
|
|
(Key::Up, vec![Action::MoveFocus(Direction::Up)]),
|
|
(Key::Right, vec![Action::MoveFocus(Direction::Right)]),
|
|
(Key::Char('n'), vec![Action::NewPane(None, None), TO_NORMAL]),
|
|
(Key::Char('x'), vec![Action::CloseFocus, TO_NORMAL]),
|
|
(
|
|
Key::Char('f'),
|
|
vec![Action::ToggleFocusFullscreen, TO_NORMAL],
|
|
),
|
|
],
|
|
)],
|
|
..ModeInfo::default()
|
|
};
|
|
|
|
let ret = keybinds(&mode_info, "quicknav", 500);
|
|
let ret = unstyle(ret);
|
|
|
|
assert_eq!(
|
|
ret,
|
|
" <←↓↑→> Move focus / <n> New / <x> Close / <f> Fullscreen"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
// Note how "Move focus" becomes "Move"
|
|
fn keybinds_tight_width() {
|
|
let mode_info = ModeInfo {
|
|
mode: InputMode::Pane,
|
|
keybinds: vec![(
|
|
InputMode::Pane,
|
|
vec![
|
|
(Key::Left, vec![Action::MoveFocus(Direction::Left)]),
|
|
(Key::Down, vec![Action::MoveFocus(Direction::Down)]),
|
|
(Key::Up, vec![Action::MoveFocus(Direction::Up)]),
|
|
(Key::Right, vec![Action::MoveFocus(Direction::Right)]),
|
|
(Key::Char('n'), vec![Action::NewPane(None, None), TO_NORMAL]),
|
|
(Key::Char('x'), vec![Action::CloseFocus, TO_NORMAL]),
|
|
(
|
|
Key::Char('f'),
|
|
vec![Action::ToggleFocusFullscreen, TO_NORMAL],
|
|
),
|
|
],
|
|
)],
|
|
..ModeInfo::default()
|
|
};
|
|
|
|
let ret = keybinds(&mode_info, "quicknav", 35);
|
|
let ret = unstyle(ret);
|
|
|
|
assert_eq!(ret, " <←↓↑→> Move / <n> New ... ");
|
|
}
|
|
|
|
#[test]
|
|
fn keybinds_wide_weird_keys() {
|
|
let mode_info = ModeInfo {
|
|
mode: InputMode::Pane,
|
|
keybinds: vec![(
|
|
InputMode::Pane,
|
|
vec![
|
|
(Key::Ctrl('a'), vec![Action::MoveFocus(Direction::Left)]),
|
|
(Key::Ctrl('\n'), vec![Action::MoveFocus(Direction::Down)]),
|
|
(Key::Ctrl('1'), vec![Action::MoveFocus(Direction::Up)]),
|
|
(Key::Ctrl(' '), vec![Action::MoveFocus(Direction::Right)]),
|
|
(Key::Backspace, vec![Action::NewPane(None, None), TO_NORMAL]),
|
|
(Key::Esc, vec![Action::CloseFocus, TO_NORMAL]),
|
|
(Key::End, vec![Action::ToggleFocusFullscreen, TO_NORMAL]),
|
|
],
|
|
)],
|
|
..ModeInfo::default()
|
|
};
|
|
|
|
let ret = keybinds(&mode_info, "quicknav", 500);
|
|
let ret = unstyle(ret);
|
|
|
|
assert_eq!(ret, " Ctrl + <a|ENTER|1|SPACE> Move focus / <BACKSPACE> New / <ESC> Close / <END> Fullscreen");
|
|
}
|
|
}
|