// SPDX-License-Identifier: GPL-3.0-or-later // src/active_state.rs // // Pure navigation state: tracks which session and item are currently active. use anyhow::{Context, Result}; use uuid::Uuid; use crate::workspace::model::{Item, Session, Workspace}; /// Tracks the active session and item selection. /// /// `ActiveState` does not own or store a `Workspace`. All methods that need /// workspace data receive it as a parameter. This keeps navigation state /// cleanly separated from domain data. #[derive(Debug, Clone)] pub struct ActiveState { active_session: Option, active_item: Option, } impl Default for ActiveState { fn default() -> Self { Self::new() } } impl ActiveState { #[must_use] pub fn new() -> Self { Self { active_session: None, active_item: None, } } /// Clear all navigation state. pub fn reset(&mut self) { self.active_session = None; self.active_item = None; } pub fn select_session(&mut self, workspace: &Workspace, name: &str) -> Result<()> { let session = workspace.session_by_name(name)?; if self.active_session == Some(session.id) { return Ok(()); } self.active_session = Some(session.id); self.active_item = if session.items.is_empty() { None } else { Some(0) }; Ok(()) } pub fn select_session_by_id(&mut self, workspace: &Workspace, id: Uuid) -> Result<()> { if self.active_session == Some(id) { return Ok(()); } let session = workspace.session(id)?; self.active_session = Some(session.id); self.active_item = if session.items.is_empty() { None } else { Some(0) }; Ok(()) } pub fn clear_session(&mut self) { self.active_session = None; self.active_item = None; } pub fn select_item(&mut self, workspace: &Workspace, index: usize) -> Result<()> { let session = self.require_active_session(workspace)?; if index >= session.items.len() { anyhow::bail!( "Item index {index} out of range (session {:?} has {} items)", session.name, session.items.len() ); } self.active_item = Some(index); Ok(()) } pub fn next_item(&mut self, workspace: &Workspace) { let count = self.item_count(workspace); if let Some(idx) = self.active_item { if idx + 1 < count { self.active_item = Some(idx + 1); } } } pub fn previous_item(&mut self) { if let Some(idx) = self.active_item { if idx > 0 { self.active_item = Some(idx - 1); } } } /// Adjust the item cursor after an item was removed from the active session. pub fn adjust_after_remove(&mut self, workspace: &Workspace) { let count = self.item_count(workspace); self.active_item = if count == 0 { None } else { Some(self.active_item.unwrap_or(0).min(count - 1)) }; } /// Point the item cursor to the last item in the active session. pub fn select_last_item(&mut self, workspace: &Workspace) { let count = self.item_count(workspace); self.active_item = if count == 0 { None } else { Some(count - 1) }; } // -- Read-only accessors -- #[must_use] pub fn active_session_id(&self) -> Option { self.active_session } #[must_use] pub fn active_item_index(&self) -> Option { self.active_item } /// Returns the active session from the workspace. #[must_use] pub fn active_session<'a>(&self, workspace: &'a Workspace) -> Option<&'a Session> { let id = self.active_session?; workspace.session(id).ok() } /// Returns the active item from the active session. #[must_use] pub fn active_item<'a>(&self, workspace: &'a Workspace) -> Option<&'a Item> { let session = self.active_session(workspace)?; let idx = self.active_item?; session.items.get(idx) } /// Returns the UUID of the active item. #[must_use] pub fn active_item_id(&self, workspace: &Workspace) -> Option { self.active_item(workspace).map(|i| i.id) } // Private helpers fn require_active_session<'a>(&self, workspace: &'a Workspace) -> Result<&'a Session> { let id = self.active_session.context("No active session")?; workspace.session(id) } fn item_count(&self, workspace: &Workspace) -> usize { self.active_session(workspace).map_or(0, |s| s.items.len()) } }