166 lines
4.7 KiB
Rust
166 lines
4.7 KiB
Rust
|
|
// 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())
|
||
|
|
}
|
||
|
|
}
|