kimun_notes/cli/commands/
journal.rs1use clap::Subcommand;
6use color_eyre::eyre::Result;
7use kimun_core::{NoteVault, nfs::VaultPath};
8
9use crate::cli::output::OutputFormat;
10
11#[derive(clap::Args, Debug)]
12pub struct JournalArgs {
13 #[arg(long)]
15 pub date: Option<String>,
16 pub content: Option<String>,
18 #[command(subcommand)]
19 pub subcommand: Option<JournalSubcommand>,
20}
21
22#[derive(Subcommand, Debug)]
23pub enum JournalSubcommand {
24 Show {
26 #[arg(long)]
28 date: Option<String>,
29 #[arg(long, value_enum, default_value = "text")]
31 format: OutputFormat,
32 },
33}
34
35pub async fn run(args: JournalArgs, vault: &NoteVault, workspace_name: &str) -> Result<()> {
36 match args.subcommand {
37 Some(JournalSubcommand::Show { date, format }) => {
38 run_show(vault, date.as_deref(), format, workspace_name).await
39 }
40 None => run_append(vault, args.date.as_deref(), args.content).await,
41 }
42}
43
44fn resolve_date(date: Option<&str>) -> Result<String> {
46 match date {
47 None => Ok(chrono::Utc::now().format("%Y-%m-%d").to_string()),
48 Some(d) => {
49 chrono::NaiveDate::parse_from_str(d, "%Y-%m-%d").map_err(|_| {
50 color_eyre::eyre::eyre!("Invalid date '{}' — expected format YYYY-MM-DD", d)
51 })?;
52 Ok(d.to_string())
53 }
54 }
55}
56
57fn journal_entry_path(vault: &NoteVault, date_str: &str) -> VaultPath {
59 vault
60 .journal_path()
61 .append(&VaultPath::note_path_from(date_str))
62 .absolute()
63}
64
65async fn run_append(vault: &NoteVault, date: Option<&str>, content: Option<String>) -> Result<()> {
66 use crate::cli::helpers::resolve_content;
67
68 let text = resolve_content(content)?;
69 if text.is_empty() {
70 return Ok(());
71 }
72
73 let (vault_path, existing) = match date {
74 None => {
75 let (details, existing) = vault
77 .journal_entry()
78 .await
79 .map_err(|e| color_eyre::eyre::eyre!("{}", e))?;
80 (details.path, existing)
81 }
82 Some(d) => {
83 let date_str = resolve_date(Some(d))?;
84 let vault_path = journal_entry_path(vault, &date_str);
85 let existing = vault
86 .load_or_create_note(&vault_path, Some(format!("# {}\n\n", date_str)))
87 .await
88 .map_err(|e| color_eyre::eyre::eyre!("{}", e))?;
89 (vault_path, existing)
90 }
91 };
92
93 let combined = format!("{}\n{}", existing, text);
94 vault
95 .save_note(&vault_path, &combined)
96 .await
97 .map_err(|e| color_eyre::eyre::eyre!("{}", e))?;
98
99 println!("Note saved: {}", vault_path);
100 Ok(())
101}
102
103async fn run_show(
104 vault: &NoteVault,
105 date: Option<&str>,
106 format: OutputFormat,
107 workspace_name: &str,
108) -> Result<()> {
109 use crate::cli::commands::note_ops::format_note_show_text;
110 use crate::cli::json_output::{
111 JsonNoteEntry, JsonNoteMetadata, JsonOutput, JsonOutputMetadata,
112 };
113 use crate::cli::metadata_extractor::{extract_headers, extract_links, extract_tags};
114 use chrono::Utc;
115 use kimun_core::error::{FSError, VaultError};
116 use std::time::UNIX_EPOCH;
117
118 if matches!(format, OutputFormat::Paths) {
119 return Err(color_eyre::eyre::eyre!(
120 "--format paths is not valid for journal show; use 'text' or 'json'"
121 ));
122 }
123
124 let date_str = resolve_date(date)?;
125 let vault_path = journal_entry_path(vault, &date_str);
126
127 let note_details = vault.load_note(&vault_path).await.map_err(|e| match e {
128 VaultError::FSError(FSError::VaultPathNotFound { .. }) => {
129 color_eyre::eyre::eyre!("No journal entry found for {}", date_str)
130 }
131 _ => color_eyre::eyre::eyre!("{}", e),
132 })?;
133
134 let content = ¬e_details.raw_text;
135 let content_data = note_details.get_content_data();
136
137 let backlinks = vault
138 .get_backlinks(&vault_path)
139 .await
140 .map_err(|e| color_eyre::eyre::eyre!("{}", e))?;
141 let backlink_paths: Vec<String> = backlinks.iter().map(|(e, _)| e.path.to_string()).collect();
142
143 match format {
144 OutputFormat::Text => {
145 let tags = extract_tags(content);
146 let links = extract_links(content);
147 print!(
148 "{}",
149 format_note_show_text(
150 &vault_path,
151 content,
152 &content_data.title,
153 &tags,
154 &links,
155 &backlink_paths,
156 )
157 );
158 }
159 OutputFormat::Json => {
160 let meta = tokio::fs::metadata(vault.path_to_pathbuf(&vault_path))
161 .await
162 .map_err(|e| color_eyre::eyre::eyre!("{}", e))?;
163 let modified_secs = meta
164 .modified()
165 .map(|t| t.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs())
166 .unwrap_or(0);
167 let tags = extract_tags(content);
168 let links = extract_links(content);
169 let headers = extract_headers(content);
170 let journal_date = vault
171 .journal_date(&vault_path)
172 .map(|d| d.format("%Y-%m-%d").to_string());
173 let entry = JsonNoteEntry {
174 path: vault_path.to_string_with_ext(),
175 title: content_data.title.clone(),
176 content: content.clone(),
177 size: meta.len(),
178 modified: modified_secs,
179 created: modified_secs,
180 hash: format!("{:x}", content_data.hash),
181 journal_date,
182 metadata: JsonNoteMetadata {
183 tags,
184 links,
185 headers,
186 },
187 backlinks: if backlink_paths.is_empty() {
188 None
189 } else {
190 Some(backlink_paths)
191 },
192 };
193 let output = JsonOutput {
194 metadata: JsonOutputMetadata {
195 workspace: workspace_name.to_string(),
196 workspace_path: vault.workspace_path().to_string_lossy().to_string(),
197 total_results: 1,
198 query: None,
199 is_listing: false,
200 generated_at: Utc::now().to_rfc3339(),
201 },
202 notes: vec![entry],
203 };
204 print!(
205 "{}",
206 serde_json::to_string(&output).map_err(|e| color_eyre::eyre::eyre!("{}", e))?
207 );
208 }
209 OutputFormat::Paths => unreachable!("guarded above"),
210 }
211
212 Ok(())
213}