1use anyhow::Result;
2use chrono::{Datelike, Local, NaiveTime, Timelike};
3use gpui2::AppContext;
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6use settings2::Settings;
7use std::{
8 fs::OpenOptions,
9 path::{Path, PathBuf},
10 sync::Arc,
11};
12use workspace::AppState;
13// use zed::AppState;
14
15// todo!();
16// actions!(journal, [NewJournalEntry]);
17
18#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
19pub struct JournalSettings {
20 pub path: Option<String>,
21 pub hour_format: Option<HourFormat>,
22}
23
24impl Default for JournalSettings {
25 fn default() -> Self {
26 Self {
27 path: Some("~".into()),
28 hour_format: Some(Default::default()),
29 }
30 }
31}
32
33#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
34#[serde(rename_all = "snake_case")]
35pub enum HourFormat {
36 #[default]
37 Hour12,
38 Hour24,
39}
40
41impl settings2::Settings for JournalSettings {
42 const KEY: Option<&'static str> = Some("journal");
43
44 type FileContent = Self;
45
46 fn load(
47 defaults: &Self::FileContent,
48 user_values: &[&Self::FileContent],
49 _: &mut AppContext,
50 ) -> Result<Self> {
51 Self::load_via_json_merge(defaults, user_values)
52 }
53}
54
55pub fn init(_: Arc<AppState>, cx: &mut AppContext) {
56 JournalSettings::register(cx);
57
58 // todo!()
59 // cx.add_global_action(move |_: &NewJournalEntry, cx| new_journal_entry(app_state.clone(), cx));
60}
61
62pub fn new_journal_entry(_: Arc<AppState>, cx: &mut AppContext) {
63 let settings = JournalSettings::get_global(cx);
64 let journal_dir = match journal_dir(settings.path.as_ref().unwrap()) {
65 Some(journal_dir) => journal_dir,
66 None => {
67 log::error!("Can't determine journal directory");
68 return;
69 }
70 };
71
72 let now = Local::now();
73 let month_dir = journal_dir
74 .join(format!("{:02}", now.year()))
75 .join(format!("{:02}", now.month()));
76 let entry_path = month_dir.join(format!("{:02}.md", now.day()));
77 let now = now.time();
78 let _entry_heading = heading_entry(now, &settings.hour_format);
79
80 let _create_entry = cx.executor().spawn(async move {
81 std::fs::create_dir_all(month_dir)?;
82 OpenOptions::new()
83 .create(true)
84 .write(true)
85 .open(&entry_path)?;
86 Ok::<_, std::io::Error>((journal_dir, entry_path))
87 });
88
89 // todo!("workspace")
90 // cx.spawn(|cx| async move {
91 // let (journal_dir, entry_path) = create_entry.await?;
92 // let (workspace, _) =
93 // cx.update(|cx| workspace::open_paths(&[journal_dir], &app_state, None, cx))?;
94
95 // let opened = workspace
96 // .update(&mut cx, |workspace, cx| {
97 // workspace.open_paths(vec![entry_path], true, cx)
98 // })?
99 // .await;
100
101 // if let Some(Some(Ok(item))) = opened.first() {
102 // if let Some(editor) = item.downcast::<Editor>().map(|editor| editor.downgrade()) {
103 // editor.update(&mut cx, |editor, cx| {
104 // let len = editor.buffer().read(cx).len(cx);
105 // editor.change_selections(Some(Autoscroll::center()), cx, |s| {
106 // s.select_ranges([len..len])
107 // });
108 // if len > 0 {
109 // editor.insert("\n\n", cx);
110 // }
111 // editor.insert(&entry_heading, cx);
112 // editor.insert("\n\n", cx);
113 // })?;
114 // }
115 // }
116
117 // anyhow::Ok(())
118 // })
119 // .detach_and_log_err(cx);
120}
121
122fn journal_dir(path: &str) -> Option<PathBuf> {
123 let expanded_journal_dir = shellexpand::full(path) //TODO handle this better
124 .ok()
125 .map(|dir| Path::new(&dir.to_string()).to_path_buf().join("journal"));
126
127 return expanded_journal_dir;
128}
129
130fn heading_entry(now: NaiveTime, hour_format: &Option<HourFormat>) -> String {
131 match hour_format {
132 Some(HourFormat::Hour24) => {
133 let hour = now.hour();
134 format!("# {}:{:02}", hour, now.minute())
135 }
136 _ => {
137 let (pm, hour) = now.hour12();
138 let am_or_pm = if pm { "PM" } else { "AM" };
139 format!("# {}:{:02} {}", hour, now.minute(), am_or_pm)
140 }
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 mod heading_entry_tests {
147 use super::super::*;
148
149 #[test]
150 fn test_heading_entry_defaults_to_hour_12() {
151 let naive_time = NaiveTime::from_hms_milli_opt(15, 0, 0, 0).unwrap();
152 let actual_heading_entry = heading_entry(naive_time, &None);
153 let expected_heading_entry = "# 3:00 PM";
154
155 assert_eq!(actual_heading_entry, expected_heading_entry);
156 }
157
158 #[test]
159 fn test_heading_entry_is_hour_12() {
160 let naive_time = NaiveTime::from_hms_milli_opt(15, 0, 0, 0).unwrap();
161 let actual_heading_entry = heading_entry(naive_time, &Some(HourFormat::Hour12));
162 let expected_heading_entry = "# 3:00 PM";
163
164 assert_eq!(actual_heading_entry, expected_heading_entry);
165 }
166
167 #[test]
168 fn test_heading_entry_is_hour_24() {
169 let naive_time = NaiveTime::from_hms_milli_opt(15, 0, 0, 0).unwrap();
170 let actual_heading_entry = heading_entry(naive_time, &Some(HourFormat::Hour24));
171 let expected_heading_entry = "# 15:00";
172
173 assert_eq!(actual_heading_entry, expected_heading_entry);
174 }
175 }
176}