use std::collections::BTreeMap; use unicode_width::UnicodeWidthStr; use zellij_utils::data::StyledText; pub enum HintLevel { Plain, Emphasis, Emphasis2, Error, Success, } pub struct HintSegment { pub text: String, pub level: HintLevel, } impl HintSegment { fn plain(text: &str) -> Self { HintSegment { text: text.to_string(), level: HintLevel::Plain, } } fn emphasis(text: &str) -> Self { HintSegment { text: text.to_string(), level: HintLevel::Emphasis, } } fn emphasis_2(text: &str) -> Self { HintSegment { text: text.to_string(), level: HintLevel::Emphasis2, } } fn error(text: &str) -> Self { HintSegment { text: text.to_string(), level: HintLevel::Error, } } fn success(text: &str) -> Self { HintSegment { text: text.to_string(), level: HintLevel::Success, } } } #[derive(Copy, Clone)] pub enum HintExitStatus { Code(i32), Exited, } #[derive(Copy, Clone)] pub enum HintTier { Full, Medium, Minimal, } const ALL_TIERS: [HintTier; 3] = [HintTier::Full, HintTier::Medium, HintTier::Minimal]; fn level_index(level: &HintLevel) -> Option { match level { HintLevel::Plain => None, HintLevel::Emphasis => Some(0), HintLevel::Emphasis2 => Some(2), HintLevel::Error => Some(6), HintLevel::Success => Some(7), } } pub fn segments_to_styled_text(segments: &[HintSegment]) -> StyledText { let mut text = String::new(); let mut indices: Vec> = vec![]; let mut offset = 0; for segment in segments { let char_count = segment.text.chars().count(); if let Some(level) = level_index(&segment.level) { if indices.len() <= level { indices.resize(level + 1, vec![]); } for position in offset..offset + char_count { indices[level].push(position); } } text.push_str(&segment.text); offset += char_count; } StyledText { text, indices } } pub fn rerun_segments(is_first_run: bool, tier: HintTier) -> Vec { match tier { HintTier::Full => vec![ HintSegment::plain(if is_first_run { " <" } else { "<" }), HintSegment::emphasis("ENTER"), HintSegment::plain(">"), HintSegment::plain(if is_first_run { " run, " } else { " re-run, " }), HintSegment::plain("<"), HintSegment::emphasis("ESC"), HintSegment::plain(">"), HintSegment::plain(" drop to shell, "), HintSegment::plain("<"), HintSegment::emphasis("Ctrl-c"), HintSegment::plain(">"), HintSegment::plain(" exit "), ], HintTier::Medium => vec![ HintSegment::plain("<"), HintSegment::emphasis("ENTER"), HintSegment::plain(">"), HintSegment::plain(if is_first_run { " run " } else { " re-run " }), HintSegment::plain("<"), HintSegment::emphasis("ESC"), HintSegment::plain(">"), HintSegment::plain(" shell "), HintSegment::plain("<"), HintSegment::emphasis("Ctrl-c"), HintSegment::plain(">"), HintSegment::plain(" exit"), ], HintTier::Minimal => vec![ HintSegment::plain("<"), HintSegment::emphasis("ENTER"), HintSegment::plain(">"), HintSegment::plain(if is_first_run { " run" } else { " re-run" }), ], } } pub fn exit_code_segments(exit_status: HintExitStatus) -> Vec { match exit_status { HintExitStatus::Code(exit_code) => { let exit_code_text = format!("{}", exit_code); let exit_code_segment = if exit_code == 0 { HintSegment::success(&exit_code_text) } else { HintSegment::error(&exit_code_text) }; vec![ HintSegment::plain(" [ "), HintSegment::plain("EXIT CODE: "), exit_code_segment, HintSegment::plain(" ] "), ] }, HintExitStatus::Exited => vec![ HintSegment::plain(" [ "), HintSegment::error("EXITED"), HintSegment::plain(" ] "), ], } } pub fn hover_segments(tier: HintTier) -> Vec { match tier { HintTier::Full => vec![ HintSegment::plain(" "), HintSegment::emphasis("Alt"), HintSegment::plain(" <"), HintSegment::emphasis("Click"), HintSegment::plain(">"), HintSegment::plain(" - group,"), HintSegment::plain(" "), HintSegment::emphasis("Alt"), HintSegment::plain(" <"), HintSegment::emphasis("Right-Click"), HintSegment::plain(">"), HintSegment::plain(" - ungroup all "), ], HintTier::Medium => vec![ HintSegment::emphasis("Alt"), HintSegment::plain(" <"), HintSegment::emphasis("Click"), HintSegment::plain("> group, "), HintSegment::emphasis("Alt"), HintSegment::plain(" <"), HintSegment::emphasis("Right-Click"), HintSegment::plain("> ungroup"), ], HintTier::Minimal => vec![ HintSegment::emphasis("Alt"), HintSegment::plain(" <"), HintSegment::emphasis("Click"), HintSegment::plain("> group"), ], } } pub fn hover_hint_variants() -> BTreeMap { let mut variants = BTreeMap::new(); for tier in ALL_TIERS { let styled_text = segments_to_styled_text(&hover_segments(tier)); variants.insert(styled_text.text.width(), styled_text); } variants } pub fn resize_segments( is_floating: bool, mouse_scroll_resize: bool, tier: HintTier, ) -> Vec { if !mouse_scroll_resize { return match (is_floating, tier) { (true, HintTier::Full) => vec![ HintSegment::plain(" "), HintSegment::emphasis_2("Ctrl"), HintSegment::plain(" <"), HintSegment::emphasis_2("drag borders"), HintSegment::plain("> to resize "), ], (false, HintTier::Full) => vec![ HintSegment::plain(" <"), HintSegment::emphasis_2("drag borders"), HintSegment::plain("> to resize "), ], (true, HintTier::Medium) => vec![ HintSegment::plain(" <"), HintSegment::emphasis_2("Ctrl"), HintSegment::plain(" "), HintSegment::emphasis_2("drag borders"), HintSegment::plain("> resize "), ], (false, HintTier::Medium) => vec![ HintSegment::plain(" <"), HintSegment::emphasis_2("drag borders"), HintSegment::plain("> resize "), ], (true, HintTier::Minimal) => vec![ HintSegment::plain(" <"), HintSegment::emphasis_2("Ctrl"), HintSegment::plain(" "), HintSegment::emphasis_2("drag borders"), HintSegment::plain("> "), ], (false, HintTier::Minimal) => vec![ HintSegment::plain(" <"), HintSegment::emphasis_2("drag borders"), HintSegment::plain("> "), ], }; } match tier { HintTier::Full => { if is_floating { vec![ HintSegment::plain(" "), HintSegment::emphasis_2("Ctrl"), HintSegment::plain(" <"), HintSegment::emphasis_2("MouseScroll"), HintSegment::plain("> or "), HintSegment::emphasis_2("Ctrl"), HintSegment::plain(" <"), HintSegment::emphasis_2("drag borders"), HintSegment::plain("> to resize "), ] } else { vec![ HintSegment::plain(" "), HintSegment::emphasis_2("Ctrl"), HintSegment::plain(" <"), HintSegment::emphasis_2("MouseScroll"), HintSegment::plain("> or <"), HintSegment::emphasis_2("drag borders"), HintSegment::plain("> to resize "), ] } }, HintTier::Medium => { if is_floating { vec![ HintSegment::plain(" <"), HintSegment::emphasis_2("Ctrl"), HintSegment::plain(" "), HintSegment::emphasis_2("MouseScroll"), HintSegment::plain("/"), HintSegment::emphasis_2("drag borders"), HintSegment::plain("> resize "), ] } else { vec![ HintSegment::plain(" <"), HintSegment::emphasis_2("Ctrl"), HintSegment::plain(" "), HintSegment::emphasis_2("MouseScroll"), HintSegment::plain(">/<"), HintSegment::emphasis_2("drag borders"), HintSegment::plain("> resize "), ] } }, HintTier::Minimal => { if is_floating { vec![ HintSegment::plain(" <"), HintSegment::emphasis_2("Ctrl"), HintSegment::plain(" "), HintSegment::emphasis_2("MouseScroll"), HintSegment::plain("/"), HintSegment::emphasis_2("drag borders"), HintSegment::plain("> "), ] } else { vec![ HintSegment::plain(" <"), HintSegment::emphasis_2("Ctrl"), HintSegment::plain(" "), HintSegment::emphasis_2("MouseScroll"), HintSegment::plain(">/<"), HintSegment::emphasis_2("drag borders"), HintSegment::plain("> "), ] } }, } } pub fn resize_hint_variants( is_floating: bool, mouse_scroll_resize: bool, ) -> BTreeMap { let mut variants = BTreeMap::new(); for tier in ALL_TIERS { let styled_text = segments_to_styled_text(&resize_segments(is_floating, mouse_scroll_resize, tier)); variants.insert(styled_text.text.width(), styled_text); } variants } pub fn held_hint_variants( is_first_run: bool, exit_status: Option, ) -> BTreeMap { let mut variants = BTreeMap::new(); for tier in ALL_TIERS { let mut segments = vec![]; if let Some(exit_status) = exit_status { segments.extend(exit_code_segments(exit_status)); } segments.extend(rerun_segments(is_first_run, tier)); let styled_text = segments_to_styled_text(&segments); variants.insert(styled_text.text.width(), styled_text); } variants } #[cfg(test)] mod tests { use super::*; #[test] fn disabled_resize_hint_variants_omit_mouse_scroll_at_every_tier() { for is_floating in [false, true] { for hint in resize_hint_variants(is_floating, false).into_values() { assert!(!hint.text.contains("MouseScroll")); assert!(hint.text.contains("drag borders")); } } } #[test] fn enabled_resize_hint_variants_include_mouse_scroll_at_every_tier() { for is_floating in [false, true] { for hint in resize_hint_variants(is_floating, true).into_values() { assert!(hint.text.contains("MouseScroll")); assert!(hint.text.contains("drag borders")); } } } }