aboutsummaryrefslogtreecommitdiff
path: root/src/app/state.rs
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-03-23 00:36:52 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-03-27 21:37:05 +0300
commita57b97285c3308fbdcc167ab3da9ae1dd78767b6 (patch)
tree25a0eb4df100ea9b09c7551fa7cc25f8d86027ef /src/app/state.rs
initial release
terminal workspace manager for AI coding agents. single rust binary, runs inside your existing terminal. workspaces, tiled panes, intelligent agent state detection. https://herdr.dev
Diffstat (limited to 'src/app/state.rs')
-rw-r--r--src/app/state.rs130
1 files changed, 130 insertions, 0 deletions
diff --git a/src/app/state.rs b/src/app/state.rs
new file mode 100644
index 00000000..b0a81165
--- /dev/null
+++ b/src/app/state.rs
@@ -0,0 +1,130 @@
+use crossterm::event::{KeyCode, KeyModifiers};
+use crate::config::Keybinds;
+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 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,
+ 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()),
+ },
+ }
+ }
+}