1use anyhow::Result;
2use chrono::{Datelike, Local, NaiveTime, Timelike};
3use gpui::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 workspace2::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(app_state: 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.background_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 cx.spawn(|mut cx| async move {
90 let (journal_dir, entry_path) = create_entry.await?;
91 let (workspace, _) = cx
92 .update(|cx| workspace2::open_paths(&[journal_dir], &app_state, None, cx))?
93 .await?;
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 // todo!("editor")
102 // if let Some(Some(Ok(item))) = opened.first() {
103 // if let Some(editor) = item.downcast::<Editor>().map(|editor| editor.downgrade()) {
104 // editor.update(&mut cx, |editor, cx| {
105 // let len = editor.buffer().read(cx).len(cx);
106 // editor.change_selections(Some(Autoscroll::center()), cx, |s| {
107 // s.select_ranges([len..len])
108 // });
109 // if len > 0 {
110 // editor.insert("\n\n", cx);
111 // }
112 // editor.insert(&entry_heading, cx);
113 // editor.insert("\n\n", cx);
114 // })?;
115 // }
116 // }
117
118 anyhow::Ok(())
119 })
120 .detach_and_log_err(cx);
121}
122
123fn journal_dir(path: &str) -> Option<PathBuf> {
124 let expanded_journal_dir = shellexpand::full(path) //TODO handle this better
125 .ok()
126 .map(|dir| Path::new(&dir.to_string()).to_path_buf().join("journal"));
127
128 return expanded_journal_dir;
129}
130
131fn heading_entry(now: NaiveTime, hour_format: &Option<HourFormat>) -> String {
132 match hour_format {
133 Some(HourFormat::Hour24) => {
134 let hour = now.hour();
135 format!("# {}:{:02}", hour, now.minute())
136 }
137 _ => {
138 let (pm, hour) = now.hour12();
139 let am_or_pm = if pm { "PM" } else { "AM" };
140 format!("# {}:{:02} {}", hour, now.minute(), am_or_pm)
141 }
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 mod heading_entry_tests {
148 use super::super::*;
149
150 #[test]
151 fn test_heading_entry_defaults_to_hour_12() {
152 let naive_time = NaiveTime::from_hms_milli_opt(15, 0, 0, 0).unwrap();
153 let actual_heading_entry = heading_entry(naive_time, &None);
154 let expected_heading_entry = "# 3:00 PM";
155
156 assert_eq!(actual_heading_entry, expected_heading_entry);
157 }
158
159 #[test]
160 fn test_heading_entry_is_hour_12() {
161 let naive_time = NaiveTime::from_hms_milli_opt(15, 0, 0, 0).unwrap();
162 let actual_heading_entry = heading_entry(naive_time, &Some(HourFormat::Hour12));
163 let expected_heading_entry = "# 3:00 PM";
164
165 assert_eq!(actual_heading_entry, expected_heading_entry);
166 }
167
168 #[test]
169 fn test_heading_entry_is_hour_24() {
170 let naive_time = NaiveTime::from_hms_milli_opt(15, 0, 0, 0).unwrap();
171 let actual_heading_entry = heading_entry(naive_time, &Some(HourFormat::Hour24));
172 let expected_heading_entry = "# 15:00";
173
174 assert_eq!(actual_heading_entry, expected_heading_entry);
175 }
176 }
177}