Skip to main content

mdvault_core/context/
manager.rs

1//! Context manager for persistent focus state.
2
3use std::fs;
4use std::path::{Path, PathBuf};
5
6use crate::context::types::{ContextState, FocusContext};
7
8/// Error type for context operations.
9#[derive(Debug, thiserror::Error)]
10pub enum ContextError {
11    #[error("Failed to read context state: {0}")]
12    Read(#[from] std::io::Error),
13
14    #[error("Failed to parse context state: {0}")]
15    Parse(#[from] toml::de::Error),
16
17    #[error("Failed to serialize context state: {0}")]
18    Serialize(#[from] toml::ser::Error),
19}
20
21type Result<T> = std::result::Result<T, ContextError>;
22
23/// Manages persistent focus context state.
24///
25/// State is stored in `.mdvault/state/context.toml` within the vault.
26#[derive(Debug)]
27pub struct ContextManager {
28    /// Path to the context state file.
29    state_path: PathBuf,
30
31    /// Current context state.
32    state: ContextState,
33}
34
35impl ContextManager {
36    /// State file location relative to vault root.
37    const STATE_DIR: &'static str = ".mdvault/state";
38    const STATE_FILE: &'static str = "context.toml";
39
40    /// Load context manager for a vault.
41    ///
42    /// Creates the state file if it doesn't exist.
43    pub fn load(vault_root: &Path) -> Result<Self> {
44        let state_dir = vault_root.join(Self::STATE_DIR);
45        let state_path = state_dir.join(Self::STATE_FILE);
46
47        let state = if state_path.exists() {
48            let content = fs::read_to_string(&state_path)?;
49            toml::from_str(&content)?
50        } else {
51            ContextState::default()
52        };
53
54        Ok(Self { state_path, state })
55    }
56
57    /// Save current state to disk.
58    pub fn save(&self) -> Result<()> {
59        // Ensure state directory exists
60        if let Some(parent) = self.state_path.parent() {
61            fs::create_dir_all(parent)?;
62        }
63
64        let content = toml::to_string_pretty(&self.state)?;
65        fs::write(&self.state_path, content)?;
66        Ok(())
67    }
68
69    /// Set focus to a project.
70    ///
71    /// Replaces any existing focus.
72    pub fn set_focus(&mut self, project: &str) -> Result<()> {
73        self.state.focus = Some(FocusContext::new(project));
74        self.save()
75    }
76
77    /// Set focus with an optional note.
78    pub fn set_focus_with_note(&mut self, project: &str, note: &str) -> Result<()> {
79        self.state.focus = Some(FocusContext::with_note(project, note));
80        self.save()
81    }
82
83    /// Clear the current focus.
84    pub fn clear_focus(&mut self) -> Result<()> {
85        self.state.focus = None;
86        self.save()
87    }
88
89    /// Get the active project ID, if any.
90    pub fn active_project(&self) -> Option<&str> {
91        self.state.focus.as_ref().map(|f| f.project.as_str())
92    }
93
94    /// Get the full focus context, if any.
95    pub fn focus(&self) -> Option<&FocusContext> {
96        self.state.focus.as_ref()
97    }
98
99    /// Get the current state (for serialization to MCP).
100    pub fn state(&self) -> &ContextState {
101        &self.state
102    }
103
104    /// Check if there is an active focus.
105    pub fn has_focus(&self) -> bool {
106        self.state.focus.is_some()
107    }
108}