1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
|
use crate::config::{Keybinds, SoundConfig};
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;
/// 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 {
Navigate,
Terminal,
CreateSession,
RenameSession,
Resize,
ConfirmClose,
ContextMenu,
}
/// Active mouse drag on a split border.
pub(crate) struct DragState {
pub path: Vec<bool>,
pub direction: Direction,
pub area: Rect,
}
/// Right-click context menu state.
pub struct ContextMenuState {
pub ws_idx: usize,
pub x: u16,
pub y: u16,
pub selected: usize,
}
pub const CONTEXT_MENU_ITEMS: &[&str] = &["Rename", "Close"];
/// 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 name_input: String,
// 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>,
// Update notification
pub update_available: Option<String>,
pub update_dismissed: bool,
// Config
pub prefix_code: KeyCode,
pub prefix_mods: KeyModifiers,
pub prefix_label: String,
pub sidebar_width: u16,
pub sidebar_collapsed: bool,
pub confirm_close: bool,
pub accent: Color,
pub sound: SoundConfig,
pub keybinds: Keybinds,
}
impl AppState {
pub fn is_prefix(&self, key: &crossterm::event::KeyEvent) -> bool {
key.code == self.prefix_code && key.modifiers.contains(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)
}
}
}
// ---------------------------------------------------------------------------
// 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,
name_input: String::new(),
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,
prefix_code: KeyCode::Char('s'),
prefix_mods: KeyModifiers::CONTROL,
prefix_label: "ctrl+s".into(),
sidebar_width: 26,
sidebar_collapsed: false,
confirm_close: true,
accent: Color::Cyan,
sound: SoundConfig {
enabled: false,
..SoundConfig::default()
},
keybinds: Keybinds {
split_vertical: (KeyCode::Char('v'), KeyModifiers::empty()),
split_horizontal: (KeyCode::Char('-'), KeyModifiers::empty()),
close_pane: (KeyCode::Char('x'), KeyModifiers::empty()),
fullscreen: (KeyCode::Char('f'), KeyModifiers::empty()),
},
}
}
}
|