mirror of
https://github.com/herdrdev/herdr.git
synced 2026-09-22 00:01:06 +00:00
644 lines
21 KiB
Rust
644 lines
21 KiB
Rust
use crate::config::{Keybinds, SoundConfig, ToastConfig};
|
|
use crossterm::event::{KeyCode, KeyModifiers};
|
|
use ratatui::layout::{Direction, Rect};
|
|
use ratatui::style::Color;
|
|
|
|
use crate::layout::{PaneInfo, SplitBorder};
|
|
use crate::selection::Selection;
|
|
use crate::workspace::Workspace;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Theme palette — all UI colors in one place, ready for theming
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// All colors used by the UI. Derived from a base accent color for now,
|
|
/// but structured so a full theme system can replace it later.
|
|
#[derive(Clone)]
|
|
#[allow(dead_code)] // all fields defined for theming — some used later
|
|
pub struct Palette {
|
|
/// Primary accent (highlight, active borders).
|
|
pub accent: Color,
|
|
/// Background for floating panels, overlays, and modals.
|
|
pub panel_bg: Color,
|
|
/// Subtle surface background for selected/focused items.
|
|
pub surface0: Color,
|
|
/// Slightly lighter surface for hover/active states.
|
|
pub surface1: Color,
|
|
/// Very dim surface for separators.
|
|
pub surface_dim: Color,
|
|
/// Muted text (secondary info, numbers).
|
|
pub overlay0: Color,
|
|
/// Slightly brighter overlay text.
|
|
pub overlay1: Color,
|
|
/// Main text color — soft white.
|
|
pub text: Color,
|
|
/// Subdued text (workspace numbers, dim labels).
|
|
pub subtext0: Color,
|
|
/// Branch name / special label color.
|
|
pub mauve: Color,
|
|
/// Done / idle states.
|
|
pub green: Color,
|
|
/// Working / running states.
|
|
pub yellow: Color,
|
|
/// Needs attention / blocked states.
|
|
pub red: Color,
|
|
/// Unseen / done notification accent.
|
|
pub blue: Color,
|
|
/// Notification accent / unseen markers.
|
|
pub teal: Color,
|
|
/// Interrupted / warning states.
|
|
pub peach: Color,
|
|
}
|
|
|
|
impl Palette {
|
|
/// Catppuccin Mocha — the default.
|
|
pub fn catppuccin() -> Self {
|
|
Self {
|
|
accent: Color::Rgb(137, 180, 250), // blue
|
|
panel_bg: Color::Rgb(24, 24, 37),
|
|
surface0: Color::Rgb(49, 50, 68),
|
|
surface1: Color::Rgb(69, 71, 90),
|
|
surface_dim: Color::Rgb(30, 30, 46),
|
|
overlay0: Color::Rgb(108, 112, 134),
|
|
overlay1: Color::Rgb(127, 132, 156),
|
|
text: Color::Rgb(205, 214, 244),
|
|
subtext0: Color::Rgb(166, 173, 200),
|
|
mauve: Color::Rgb(203, 166, 247),
|
|
green: Color::Rgb(166, 227, 161),
|
|
yellow: Color::Rgb(249, 226, 175),
|
|
red: Color::Rgb(243, 139, 168),
|
|
blue: Color::Rgb(137, 180, 250),
|
|
teal: Color::Rgb(148, 226, 213),
|
|
peach: Color::Rgb(250, 179, 135),
|
|
}
|
|
}
|
|
|
|
/// Tokyo Night — blue-purple aesthetic.
|
|
pub fn tokyo_night() -> Self {
|
|
Self {
|
|
accent: Color::Rgb(122, 162, 247), // blue
|
|
panel_bg: Color::Rgb(26, 27, 38),
|
|
surface0: Color::Rgb(36, 40, 59),
|
|
surface1: Color::Rgb(65, 72, 104),
|
|
surface_dim: Color::Rgb(26, 27, 38),
|
|
overlay0: Color::Rgb(86, 95, 137),
|
|
overlay1: Color::Rgb(105, 113, 150),
|
|
text: Color::Rgb(192, 202, 245),
|
|
subtext0: Color::Rgb(169, 177, 214),
|
|
mauve: Color::Rgb(187, 154, 247),
|
|
green: Color::Rgb(158, 206, 106),
|
|
yellow: Color::Rgb(224, 175, 104),
|
|
red: Color::Rgb(247, 118, 142),
|
|
blue: Color::Rgb(122, 162, 247),
|
|
teal: Color::Rgb(125, 207, 255),
|
|
peach: Color::Rgb(255, 158, 100),
|
|
}
|
|
}
|
|
|
|
/// Dracula — purple/pink/green.
|
|
pub fn dracula() -> Self {
|
|
Self {
|
|
accent: Color::Rgb(189, 147, 249), // purple
|
|
panel_bg: Color::Rgb(40, 42, 54),
|
|
surface0: Color::Rgb(68, 71, 90),
|
|
surface1: Color::Rgb(98, 114, 164),
|
|
surface_dim: Color::Rgb(40, 42, 54),
|
|
overlay0: Color::Rgb(98, 114, 164),
|
|
overlay1: Color::Rgb(130, 140, 180),
|
|
text: Color::Rgb(248, 248, 242),
|
|
subtext0: Color::Rgb(210, 210, 220),
|
|
mauve: Color::Rgb(255, 121, 198), // pink
|
|
green: Color::Rgb(80, 250, 123),
|
|
yellow: Color::Rgb(241, 250, 140),
|
|
red: Color::Rgb(255, 85, 85),
|
|
blue: Color::Rgb(139, 233, 253), // cyan-ish
|
|
teal: Color::Rgb(139, 233, 253),
|
|
peach: Color::Rgb(255, 184, 108),
|
|
}
|
|
}
|
|
|
|
/// Nord — frosty blue palette.
|
|
pub fn nord() -> Self {
|
|
Self {
|
|
accent: Color::Rgb(136, 192, 208), // frost
|
|
panel_bg: Color::Rgb(46, 52, 64),
|
|
surface0: Color::Rgb(59, 66, 82),
|
|
surface1: Color::Rgb(67, 76, 94),
|
|
surface_dim: Color::Rgb(46, 52, 64),
|
|
overlay0: Color::Rgb(76, 86, 106),
|
|
overlay1: Color::Rgb(100, 110, 130),
|
|
text: Color::Rgb(236, 239, 244),
|
|
subtext0: Color::Rgb(216, 222, 233),
|
|
mauve: Color::Rgb(180, 142, 173),
|
|
green: Color::Rgb(163, 190, 140),
|
|
yellow: Color::Rgb(235, 203, 139),
|
|
red: Color::Rgb(191, 97, 106),
|
|
blue: Color::Rgb(129, 161, 193),
|
|
teal: Color::Rgb(143, 188, 187),
|
|
peach: Color::Rgb(208, 135, 112),
|
|
}
|
|
}
|
|
|
|
/// Gruvbox Dark — warm retro palette.
|
|
pub fn gruvbox() -> Self {
|
|
Self {
|
|
accent: Color::Rgb(215, 153, 33), // yellow
|
|
panel_bg: Color::Rgb(40, 40, 40),
|
|
surface0: Color::Rgb(60, 56, 54),
|
|
surface1: Color::Rgb(80, 73, 69),
|
|
surface_dim: Color::Rgb(40, 40, 40),
|
|
overlay0: Color::Rgb(146, 131, 116),
|
|
overlay1: Color::Rgb(168, 153, 132),
|
|
text: Color::Rgb(235, 219, 178),
|
|
subtext0: Color::Rgb(213, 196, 161),
|
|
mauve: Color::Rgb(211, 134, 155),
|
|
green: Color::Rgb(184, 187, 38),
|
|
yellow: Color::Rgb(250, 189, 47),
|
|
red: Color::Rgb(251, 73, 52),
|
|
blue: Color::Rgb(131, 165, 152),
|
|
teal: Color::Rgb(142, 192, 124),
|
|
peach: Color::Rgb(254, 128, 25),
|
|
}
|
|
}
|
|
|
|
/// One Dark — Atom's classic dark theme.
|
|
pub fn one_dark() -> Self {
|
|
Self {
|
|
accent: Color::Rgb(97, 175, 239), // blue
|
|
panel_bg: Color::Rgb(40, 44, 52),
|
|
surface0: Color::Rgb(44, 49, 58),
|
|
surface1: Color::Rgb(62, 68, 81),
|
|
surface_dim: Color::Rgb(40, 44, 52),
|
|
overlay0: Color::Rgb(92, 99, 112),
|
|
overlay1: Color::Rgb(115, 122, 135),
|
|
text: Color::Rgb(171, 178, 191),
|
|
subtext0: Color::Rgb(150, 156, 168),
|
|
mauve: Color::Rgb(198, 120, 221),
|
|
green: Color::Rgb(152, 195, 121),
|
|
yellow: Color::Rgb(229, 192, 123),
|
|
red: Color::Rgb(224, 108, 117),
|
|
blue: Color::Rgb(97, 175, 239),
|
|
teal: Color::Rgb(86, 182, 194),
|
|
peach: Color::Rgb(209, 154, 102),
|
|
}
|
|
}
|
|
|
|
/// Solarized Dark — Ethan Schoonover's classic.
|
|
pub fn solarized() -> Self {
|
|
Self {
|
|
accent: Color::Rgb(38, 139, 210), // blue
|
|
panel_bg: Color::Rgb(0, 43, 54),
|
|
surface0: Color::Rgb(7, 54, 66),
|
|
surface1: Color::Rgb(88, 110, 117),
|
|
surface_dim: Color::Rgb(0, 43, 54),
|
|
overlay0: Color::Rgb(88, 110, 117),
|
|
overlay1: Color::Rgb(101, 123, 131),
|
|
text: Color::Rgb(147, 161, 161),
|
|
subtext0: Color::Rgb(131, 148, 150),
|
|
mauve: Color::Rgb(211, 54, 130),
|
|
green: Color::Rgb(133, 153, 0),
|
|
yellow: Color::Rgb(181, 137, 0),
|
|
red: Color::Rgb(220, 50, 47),
|
|
blue: Color::Rgb(38, 139, 210),
|
|
teal: Color::Rgb(42, 161, 152),
|
|
peach: Color::Rgb(203, 75, 22),
|
|
}
|
|
}
|
|
|
|
/// Kanagawa — inspired by Katsushika Hokusai.
|
|
pub fn kanagawa() -> Self {
|
|
Self {
|
|
accent: Color::Rgb(126, 156, 216), // blue
|
|
panel_bg: Color::Rgb(31, 31, 40),
|
|
surface0: Color::Rgb(42, 42, 55),
|
|
surface1: Color::Rgb(54, 54, 70),
|
|
surface_dim: Color::Rgb(31, 31, 40),
|
|
overlay0: Color::Rgb(114, 113, 105),
|
|
overlay1: Color::Rgb(135, 134, 125),
|
|
text: Color::Rgb(220, 215, 186),
|
|
subtext0: Color::Rgb(200, 195, 170),
|
|
mauve: Color::Rgb(149, 127, 184),
|
|
green: Color::Rgb(118, 148, 106),
|
|
yellow: Color::Rgb(192, 163, 110),
|
|
red: Color::Rgb(195, 64, 67),
|
|
blue: Color::Rgb(126, 156, 216),
|
|
teal: Color::Rgb(127, 180, 202),
|
|
peach: Color::Rgb(255, 160, 102),
|
|
}
|
|
}
|
|
|
|
/// Rosé Pine — muted, elegant.
|
|
pub fn rose_pine() -> Self {
|
|
Self {
|
|
accent: Color::Rgb(196, 167, 231), // iris
|
|
panel_bg: Color::Rgb(25, 23, 36),
|
|
surface0: Color::Rgb(31, 29, 46),
|
|
surface1: Color::Rgb(38, 35, 58),
|
|
surface_dim: Color::Rgb(25, 23, 36),
|
|
overlay0: Color::Rgb(110, 106, 134),
|
|
overlay1: Color::Rgb(144, 140, 170),
|
|
text: Color::Rgb(224, 222, 244),
|
|
subtext0: Color::Rgb(200, 197, 220),
|
|
mauve: Color::Rgb(196, 167, 231), // iris
|
|
green: Color::Rgb(49, 116, 143), // pine
|
|
yellow: Color::Rgb(246, 193, 119), // gold
|
|
red: Color::Rgb(235, 111, 146), // love
|
|
blue: Color::Rgb(49, 116, 143), // pine
|
|
teal: Color::Rgb(156, 207, 216), // foam
|
|
peach: Color::Rgb(234, 154, 151), // rose
|
|
}
|
|
}
|
|
|
|
/// Resolve a theme by name. Returns None for unknown names.
|
|
pub fn from_name(name: &str) -> Option<Self> {
|
|
match name.to_lowercase().replace([' ', '_'], "-").as_str() {
|
|
"catppuccin" | "catppuccin-mocha" => Some(Self::catppuccin()),
|
|
"tokyo-night" | "tokyonight" => Some(Self::tokyo_night()),
|
|
"dracula" => Some(Self::dracula()),
|
|
"nord" => Some(Self::nord()),
|
|
"gruvbox" | "gruvbox-dark" => Some(Self::gruvbox()),
|
|
"one-dark" | "onedark" => Some(Self::one_dark()),
|
|
"solarized" | "solarized-dark" => Some(Self::solarized()),
|
|
"kanagawa" => Some(Self::kanagawa()),
|
|
"rose-pine" | "rosepine" => Some(Self::rose_pine()),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Apply custom color overrides on top of this palette.
|
|
pub fn with_overrides(mut self, custom: &crate::config::CustomThemeColors) -> Self {
|
|
use crate::config::parse_color;
|
|
if let Some(c) = &custom.accent {
|
|
self.accent = parse_color(c);
|
|
}
|
|
if let Some(c) = &custom.panel_bg {
|
|
self.panel_bg = parse_color(c);
|
|
}
|
|
if let Some(c) = &custom.surface0 {
|
|
self.surface0 = parse_color(c);
|
|
}
|
|
if let Some(c) = &custom.surface1 {
|
|
self.surface1 = parse_color(c);
|
|
}
|
|
if let Some(c) = &custom.surface_dim {
|
|
self.surface_dim = parse_color(c);
|
|
}
|
|
if let Some(c) = &custom.overlay0 {
|
|
self.overlay0 = parse_color(c);
|
|
}
|
|
if let Some(c) = &custom.overlay1 {
|
|
self.overlay1 = parse_color(c);
|
|
}
|
|
if let Some(c) = &custom.text {
|
|
self.text = parse_color(c);
|
|
}
|
|
if let Some(c) = &custom.subtext0 {
|
|
self.subtext0 = parse_color(c);
|
|
}
|
|
if let Some(c) = &custom.mauve {
|
|
self.mauve = parse_color(c);
|
|
}
|
|
if let Some(c) = &custom.green {
|
|
self.green = parse_color(c);
|
|
}
|
|
if let Some(c) = &custom.yellow {
|
|
self.yellow = parse_color(c);
|
|
}
|
|
if let Some(c) = &custom.red {
|
|
self.red = parse_color(c);
|
|
}
|
|
if let Some(c) = &custom.blue {
|
|
self.blue = parse_color(c);
|
|
}
|
|
if let Some(c) = &custom.teal {
|
|
self.teal = parse_color(c);
|
|
}
|
|
if let Some(c) = &custom.peach {
|
|
self.peach = parse_color(c);
|
|
}
|
|
self
|
|
}
|
|
}
|
|
|
|
/// Computed view geometry — derived from AppState + terminal size.
|
|
/// Updated before each render, consumed by render and mouse handling.
|
|
pub struct ViewState {
|
|
pub sidebar_rect: Rect,
|
|
pub terminal_area: Rect,
|
|
pub pane_infos: Vec<PaneInfo>,
|
|
pub split_borders: Vec<SplitBorder>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Mode {
|
|
Onboarding,
|
|
Navigate,
|
|
Terminal,
|
|
RenameSession,
|
|
Resize,
|
|
ConfirmClose,
|
|
ContextMenu,
|
|
Settings,
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Settings UI state
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Which section of the settings panel is focused.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum SettingsSection {
|
|
Theme,
|
|
Sound,
|
|
Toast,
|
|
}
|
|
|
|
impl SettingsSection {
|
|
pub const ALL: &[Self] = &[Self::Theme, Self::Sound, Self::Toast];
|
|
|
|
pub fn label(self) -> &'static str {
|
|
match self {
|
|
Self::Theme => "theme",
|
|
Self::Sound => "sound",
|
|
Self::Toast => "toasts",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// All built-in theme names in display order.
|
|
pub const THEME_NAMES: &[&str] = &[
|
|
"catppuccin",
|
|
"tokyo-night",
|
|
"dracula",
|
|
"nord",
|
|
"gruvbox",
|
|
"one-dark",
|
|
"solarized",
|
|
"kanagawa",
|
|
"rose-pine",
|
|
];
|
|
|
|
pub struct SettingsState {
|
|
/// Which section tab is active.
|
|
pub section: SettingsSection,
|
|
/// Selected item index within the current section.
|
|
pub selected: usize,
|
|
/// The palette before opening settings (for cancel/restore).
|
|
pub original_palette: Option<Palette>,
|
|
/// The theme name before opening settings.
|
|
pub original_theme: Option<String>,
|
|
}
|
|
|
|
pub(crate) enum DragTarget {
|
|
PaneSplit {
|
|
path: Vec<bool>,
|
|
direction: Direction,
|
|
area: Rect,
|
|
},
|
|
PaneScrollbar {
|
|
pane_id: crate::layout::PaneId,
|
|
},
|
|
SidebarDivider,
|
|
}
|
|
|
|
/// Active mouse drag on a split border or sidebar divider.
|
|
pub(crate) struct DragState {
|
|
pub target: DragTarget,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum ContextMenuKind {
|
|
Workspace { ws_idx: usize },
|
|
Pane,
|
|
}
|
|
|
|
/// Right-click context menu state.
|
|
pub struct ContextMenuState {
|
|
pub kind: ContextMenuKind,
|
|
pub x: u16,
|
|
pub y: u16,
|
|
pub selected: usize,
|
|
}
|
|
|
|
impl ContextMenuState {
|
|
pub fn items(&self) -> &'static [&'static str] {
|
|
match self.kind {
|
|
ContextMenuKind::Workspace { .. } => &["Rename", "Close"],
|
|
ContextMenuKind::Pane => &[
|
|
"Split vertical",
|
|
"Split horizontal",
|
|
"Fullscreen",
|
|
"Close pane",
|
|
],
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum ToastKind {
|
|
NeedsAttention,
|
|
Finished,
|
|
UpdateInstalled,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct ToastNotification {
|
|
pub kind: ToastKind,
|
|
pub title: String,
|
|
pub context: String,
|
|
}
|
|
|
|
/// All application state — pure data, no channels or async runtime.
|
|
/// Testable without PTYs or a tokio runtime.
|
|
pub struct AppState {
|
|
pub workspaces: Vec<Workspace>,
|
|
pub active: Option<usize>,
|
|
pub selected: usize,
|
|
pub mode: Mode,
|
|
pub should_quit: bool,
|
|
pub request_new_workspace: bool,
|
|
pub request_complete_onboarding: bool,
|
|
pub name_input: String,
|
|
pub onboarding_step: usize,
|
|
pub onboarding_selected: usize,
|
|
// View geometry (computed before render, consumed by render + mouse)
|
|
pub view: ViewState,
|
|
pub(crate) drag: Option<DragState>,
|
|
pub selection: Option<Selection>,
|
|
pub context_menu: Option<ContextMenuState>,
|
|
// Notifications
|
|
pub update_available: Option<String>,
|
|
pub update_dismissed: bool,
|
|
pub config_diagnostic: Option<String>,
|
|
pub toast: Option<ToastNotification>,
|
|
// Config
|
|
pub prefix_code: KeyCode,
|
|
pub prefix_mods: KeyModifiers,
|
|
pub sidebar_width: u16,
|
|
pub sidebar_width_auto: bool,
|
|
pub sidebar_collapsed: bool,
|
|
pub confirm_close: bool,
|
|
pub confirm_close_selected_confirm: bool,
|
|
#[allow(dead_code)] // kept for backward compat; palette.accent is the source of truth
|
|
pub accent: Color,
|
|
pub sound: SoundConfig,
|
|
pub toast_config: ToastConfig,
|
|
pub keybinds: Keybinds,
|
|
/// Frame counter for spinner animations (wraps around).
|
|
pub spinner_tick: u32,
|
|
/// UI color palette — all sidebar/UI colors centralized for theming.
|
|
pub palette: Palette,
|
|
/// Currently applied theme name (for settings UI).
|
|
pub theme_name: String,
|
|
/// Settings panel state.
|
|
pub settings: SettingsState,
|
|
}
|
|
|
|
impl AppState {
|
|
pub fn sound_enabled(&self) -> bool {
|
|
self.sound.enabled
|
|
}
|
|
|
|
pub fn is_prefix(&self, key: &crossterm::event::KeyEvent) -> bool {
|
|
key_matches(key, self.prefix_code, self.prefix_mods)
|
|
}
|
|
|
|
pub fn estimate_pane_size(&self) -> (u16, u16) {
|
|
if let Some(info) = self.view.pane_infos.first() {
|
|
(info.rect.height, info.rect.width)
|
|
} else {
|
|
(24, 80)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn key_matches(
|
|
key: &crossterm::event::KeyEvent,
|
|
expected_code: KeyCode,
|
|
expected_mods: KeyModifiers,
|
|
) -> bool {
|
|
if key.modifiers != expected_mods {
|
|
return false;
|
|
}
|
|
|
|
match (key.code, expected_code) {
|
|
(KeyCode::Char(actual), KeyCode::Char(expected))
|
|
if actual.is_ascii_alphabetic() && expected.is_ascii_alphabetic() =>
|
|
{
|
|
actual.eq_ignore_ascii_case(&expected)
|
|
}
|
|
(actual, expected) => actual == expected,
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[cfg(test)]
|
|
impl AppState {
|
|
/// Create an AppState for testing — no channels, no PTYs.
|
|
pub fn test_new() -> Self {
|
|
Self {
|
|
workspaces: Vec::new(),
|
|
active: None,
|
|
selected: 0,
|
|
mode: Mode::Navigate,
|
|
should_quit: false,
|
|
request_new_workspace: false,
|
|
request_complete_onboarding: false,
|
|
name_input: String::new(),
|
|
onboarding_step: 0,
|
|
onboarding_selected: 1,
|
|
view: ViewState {
|
|
sidebar_rect: Rect::default(),
|
|
terminal_area: Rect::default(),
|
|
pane_infos: Vec::new(),
|
|
split_borders: Vec::new(),
|
|
},
|
|
drag: None,
|
|
selection: None,
|
|
context_menu: None,
|
|
update_available: None,
|
|
update_dismissed: false,
|
|
config_diagnostic: None,
|
|
toast: None,
|
|
prefix_code: KeyCode::Char('b'),
|
|
prefix_mods: KeyModifiers::CONTROL,
|
|
sidebar_width: 26,
|
|
sidebar_width_auto: true,
|
|
sidebar_collapsed: false,
|
|
confirm_close: true,
|
|
confirm_close_selected_confirm: true,
|
|
accent: Color::Cyan,
|
|
sound: SoundConfig {
|
|
enabled: false,
|
|
..SoundConfig::default()
|
|
},
|
|
toast_config: ToastConfig::default(),
|
|
keybinds: Keybinds {
|
|
new_workspace: (KeyCode::Char('n'), KeyModifiers::empty()),
|
|
new_workspace_label: "n".into(),
|
|
rename_workspace: (KeyCode::Char('n'), KeyModifiers::SHIFT),
|
|
rename_workspace_label: "shift+n".into(),
|
|
close_workspace: (KeyCode::Char('d'), KeyModifiers::empty()),
|
|
close_workspace_label: "d".into(),
|
|
split_vertical: (KeyCode::Char('v'), KeyModifiers::empty()),
|
|
split_vertical_label: "v".into(),
|
|
split_horizontal: (KeyCode::Char('-'), KeyModifiers::empty()),
|
|
split_horizontal_label: "-".into(),
|
|
close_pane: (KeyCode::Char('x'), KeyModifiers::empty()),
|
|
close_pane_label: "x".into(),
|
|
fullscreen: (KeyCode::Char('f'), KeyModifiers::empty()),
|
|
fullscreen_label: "f".into(),
|
|
resize_mode: (KeyCode::Char('r'), KeyModifiers::empty()),
|
|
resize_mode_label: "r".into(),
|
|
toggle_sidebar: (KeyCode::Char('b'), KeyModifiers::empty()),
|
|
toggle_sidebar_label: "b".into(),
|
|
},
|
|
spinner_tick: 0,
|
|
palette: Palette::catppuccin(),
|
|
theme_name: "catppuccin".to_string(),
|
|
settings: SettingsState {
|
|
section: SettingsSection::Theme,
|
|
selected: 0,
|
|
original_palette: None,
|
|
original_theme: None,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crossterm::event::KeyEvent;
|
|
|
|
#[test]
|
|
fn key_matches_requires_exact_modifiers() {
|
|
assert!(key_matches(
|
|
&KeyEvent::new(KeyCode::Char('b'), KeyModifiers::CONTROL),
|
|
KeyCode::Char('b'),
|
|
KeyModifiers::CONTROL,
|
|
));
|
|
|
|
assert!(!key_matches(
|
|
&KeyEvent::new(
|
|
KeyCode::Char('b'),
|
|
KeyModifiers::CONTROL | KeyModifiers::SHIFT,
|
|
),
|
|
KeyCode::Char('b'),
|
|
KeyModifiers::CONTROL,
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn key_matches_letters_case_insensitively() {
|
|
assert!(key_matches(
|
|
&KeyEvent::new(KeyCode::Char('B'), KeyModifiers::SHIFT),
|
|
KeyCode::Char('b'),
|
|
KeyModifiers::SHIFT,
|
|
));
|
|
}
|
|
}
|