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