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::NoteVault;
8use kimun_core::nfs::{PATH_SEPARATOR, VaultPath};
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    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<(PathBuf, String)> {
23    // Check legacy workspace_dir first (Phase 1 compatibility)
24    if let Some(dir) = &settings.workspace_dir {
25        return Ok((dir.clone(), "default".to_string()));
26    }
27
28    // Check Phase 2 workspace configuration
29    if let Some(ref ws_config) = settings.workspace_config
30        && let Some(entry) = ws_config.get_current_workspace()
31    {
32        let name = ws_config.global.current_workspace.clone();
33        return Ok((entry.path.clone(), name));
34    }
35
36    Err(color_eyre::eyre::eyre!(
37        "No workspace configured. Run 'kimun' to set up a workspace."
38    ))
39}
40
41/// Load settings and resolve workspace configuration in one operation.
42///
43/// This is a convenience function that combines loading settings and resolving
44/// the workspace configuration, which is a common pattern in CLI commands.
45pub fn load_and_resolve_workspace(
46    config_path: Option<PathBuf>,
47) -> Result<(AppSettings, PathBuf, String)> {
48    let settings = load_settings(config_path)?;
49    let (workspace_path, workspace_name) = resolve_workspace_config(&settings)?;
50    Ok((settings, workspace_path, workspace_name))
51}
52
53/// Returns the configured quick_note_path for the active workspace.
54/// Falls back to VaultPath::root() for Phase 1 workspaces (no WorkspaceEntry) or if not configured.
55pub fn resolve_quick_note_path(settings: &AppSettings) -> String {
56    let root = kimun_core::nfs::VaultPath::root().to_string();
57    // Phase 1 legacy: workspace_dir only, no WorkspaceEntry
58    if settings.workspace_dir.is_some() {
59        return root;
60    }
61    // Phase 2: workspace_config
62    if let Some(ref ws_config) = settings.workspace_config
63        && let Some(entry) = ws_config.get_current_workspace()
64    {
65        return entry.effective_quick_note_path();
66    }
67    root
68}
69
70/// Resolve a user-provided note path string into a VaultPath.
71///
72/// Rules:
73/// - Empty or whitespace-only input → error
74/// - Starts with PATH_SEPARATOR → absolute from vault root (quick_note_path ignored)
75/// - Otherwise → relative, joined with quick_note_path using PATH_SEPARATOR
76/// - VaultPath::note_path_from normalizes path and ensures .md extension
77pub fn resolve_note_path(input: &str, quick_note_path: &str) -> Result<VaultPath> {
78    let trimmed = input.trim();
79    if trimmed.is_empty() {
80        return Err(color_eyre::eyre::eyre!(
81            "Note path cannot be empty or whitespace-only"
82        ));
83    }
84    if trimmed.len() == 1 && trimmed.starts_with(PATH_SEPARATOR) {
85        return Err(color_eyre::eyre::eyre!(
86            "Note path cannot be the root separator alone"
87        ));
88    }
89    let raw = if trimmed.starts_with(PATH_SEPARATOR) {
90        trimmed.to_string()
91    } else {
92        let base = if quick_note_path.trim().is_empty() {
93            VaultPath::root().to_string()
94        } else {
95            quick_note_path.trim_end_matches(PATH_SEPARATOR).to_string()
96        };
97        format!("{}{}{}", base, PATH_SEPARATOR, trimmed)
98    };
99    Ok(VaultPath::note_path_from(&raw))
100}
101
102/// Returns content from the Option, or reads from stdin if not a TTY.
103/// Returns an empty string if content is None and stdin is a TTY.
104/// Propagates I/O errors from stdin.
105pub fn resolve_content(content: Option<String>) -> color_eyre::eyre::Result<String> {
106    use std::io::IsTerminal;
107    match content {
108        Some(c) => Ok(c),
109        None => {
110            if std::io::stdin().is_terminal() {
111                Ok(String::new())
112            } else {
113                use std::io::Read;
114                let mut buf = String::new();
115                std::io::stdin()
116                    .read_to_string(&mut buf)
117                    .map_err(|e| color_eyre::eyre::eyre!("Failed to read stdin: {}", e))?;
118                Ok(buf.trim_end_matches(['\n', '\r']).to_string())
119            }
120        }
121    }
122}
123
124/// Create and initialize a vault from workspace configuration.
125///
126/// This handles the common pattern of creating a NoteVault from workspace settings
127/// and initializing/validating its database.
128pub async fn create_and_init_vault(config_path: Option<PathBuf>) -> Result<(NoteVault, String)> {
129    let (_settings, workspace_path, workspace_name) = load_and_resolve_workspace(config_path)?;
130
131    let vault = NoteVault::new(&workspace_path).await?;
132    vault.validate_and_init().await?;
133
134    Ok((vault, workspace_name))
135}