noctua/src/active_state.rs

166 lines
4.7 KiB
Rust
Raw Normal View History

// 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<Uuid>,
active_item: Option<usize>,
}
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<Uuid> {
self.active_session
}
#[must_use]
pub fn active_item_index(&self) -> Option<usize> {
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<Uuid> {
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())
}
}