Skip to main content

kimun_notes/cli/
helpers.rs

1// tui/src/cli/helpers.rs
2//
3// Common helper functions for CLI operations to reduce code duplication.
4
5use crate::settings::AppSettings;
6use color_eyre::eyre::Result;
7use kimun_core::nfs::{PATH_SEPARATOR, VaultPath};
8use kimun_core::{NoteVault, SystemPath, VaultConfig};
9use std::path::PathBuf;
10
11/// Load settings from either a specific config file path or the default location.
12pub fn load_settings(config_path: Option<PathBuf>) -> Result<AppSettings> {
13    Ok(match config_path {
14        Some(path) => AppSettings::load_from_file(path)?,
15        None => AppSettings::load_from_disk()?,
16    })
17}
18
19/// Resolve workspace configuration from settings, returning the workspace path and name.
20///
21/// Returns an error if no workspace is configured.
22pub fn resolve_workspace_config(settings: &AppSettings) -> Result<(SystemPath, String)> {
23    let path = settings.resolve_workspace_path();
24    let name = settings
25        .workspace_config
26        .as_ref()
27        .map(|wc| wc.global.current_workspace.clone())
28        .unwrap_or_else(|| "default".to_string());
29
30    match path {
31        Some(p) => Ok((p, name)),
32        None => Err(color_eyre::eyre::eyre!(
33            "No workspace configured. Run 'kimun' to set up a workspace."
34        )),
35    }
36}
37
38/// Load settings and resolve workspace configuration in one operation.
39///
40/// This is a convenience function that combines loading settings and resolving
41/// the workspace configuration, which is a common pattern in CLI commands.
42pub fn load_and_resolve_workspace(
43    config_path: Option<PathBuf>,
44) -> Result<(AppSettings, SystemPath, String)> {
45    let settings = load_settings(config_path)?;
46    let (workspace_path, workspace_name) = resolve_workspace_config(&settings)?;
47    Ok((settings, workspace_path, workspace_name))
48}
49
50/// Returns the configured quick_note_path for the active workspace, falling
51/// back to `VaultPath::root()` when there is none.
52pub fn resolve_quick_note_path(settings: &AppSettings) -> String {
53    let root = kimun_core::nfs::VaultPath::root().to_string();
54    if let Some(ref ws_config) = settings.workspace_config
55        && let Some(entry) = ws_config.get_current_workspace()
56    {
57        return entry.effective_quick_note_path();
58    }
59    root
60}
61
62/// Returns the configured inbox_path for the active workspace.
63pub fn resolve_inbox_path(settings: &AppSettings) -> String {
64    if let Some(ref wc) = settings.workspace_config
65        && let Some(entry) = wc.get_current_workspace()
66    {
67        return entry.effective_inbox_path();
68    }
69    kimun_core::DEFAULT_INBOX_PATH.to_string()
70}
71
72/// Resolve a user-provided note path string into a VaultPath.
73///
74/// Rules:
75/// - Empty or whitespace-only input → error
76/// - Starts with PATH_SEPARATOR → absolute from vault root (quick_note_path ignored)
77/// - Otherwise → relative, joined with quick_note_path using PATH_SEPARATOR
78/// - VaultPath::note_path_from normalizes path and ensures .md extension
79pub fn resolve_note_path(input: &str, quick_note_path: &str) -> Result<VaultPath> {
80    let trimmed = input.trim();
81    if trimmed.is_empty() {
82        return Err(color_eyre::eyre::eyre!(
83            "Note path cannot be empty or whitespace-only"
84        ));
85    }
86    if trimmed.len() == 1 && trimmed.starts_with(PATH_SEPARATOR) {
87        return Err(color_eyre::eyre::eyre!(
88            "Note path cannot be the root separator alone"
89        ));
90    }
91    let raw = if trimmed.starts_with(PATH_SEPARATOR) {
92        trimmed.to_string()
93    } else {
94        let base = if quick_note_path.trim().is_empty() {
95            VaultPath::root().to_string()
96        } else {
97            quick_note_path.trim_end_matches(PATH_SEPARATOR).to_string()
98        };
99        format!("{}{}{}", base, PATH_SEPARATOR, trimmed)
100    };
101    Ok(VaultPath::note_path_from(&raw))
102}
103
104/// Returns content from the Option, or reads from stdin if not a TTY.
105/// Returns an empty string if content is None and stdin is a TTY.
106/// Propagates I/O errors from stdin.
107pub fn resolve_content(content: Option<String>) -> color_eyre::eyre::Result<String> {
108    use std::io::IsTerminal;
109    match content {
110        Some(c) => Ok(c),
111        None => {
112            if std::io::stdin().is_terminal() {
113                Ok(String::new())
114            } else {
115                use std::io::Read;
116                let mut buf = String::new();
117                std::io::stdin()
118                    .read_to_string(&mut buf)
119                    .map_err(|e| color_eyre::eyre::eyre!("Failed to read stdin: {}", e))?;
120                Ok(buf.trim_end_matches(['\n', '\r']).to_string())
121            }
122        }
123    }
124}
125
126/// Create and initialize a vault from workspace configuration.
127///
128/// This handles the common pattern of creating a NoteVault from workspace settings
129/// and initializing/validating its database.
130pub async fn create_and_init_vault(config_path: Option<PathBuf>) -> Result<(NoteVault, String)> {
131    let (settings, workspace_path, workspace_name) = load_and_resolve_workspace(config_path)?;
132
133    let cache_path = settings.index_for(&workspace_name);
134    // Backups on: every command built through this helper (search/notes/labels
135    // are read-only no-ops, journal writes do get backed up) and the MCP server.
136    let mut vault = NoteVault::new(
137        VaultConfig::new(workspace_path)
138            .with_index(cache_path)
139            .with_backup(true),
140    )
141    .await?;
142    let inbox = resolve_inbox_path(&settings);
143    vault.set_inbox_path(kimun_core::nfs::VaultPath::new(&inbox));
144    vault.validate_and_init().await?;
145
146    Ok((vault, workspace_name))
147}