|
1 | 1 | use crate::gui::LauncherApp; |
2 | 2 | use crate::plugins::note::{load_notes, save_notes, Note}; |
3 | 3 | use crate::plugins::todo::{load_todos, TODO_FILE}; |
| 4 | +use chrono::{DateTime, Local}; |
4 | 5 | use eframe::egui; |
5 | 6 |
|
| 7 | +fn format_note_timestamp(dt: DateTime<Local>) -> String { |
| 8 | + dt.format("%Y-%m-%d %H:%M:%S").to_string() |
| 9 | +} |
| 10 | + |
| 11 | +fn format_note_timestamp_now() -> String { |
| 12 | + format_note_timestamp(Local::now()) |
| 13 | +} |
| 14 | + |
| 15 | +fn insert_at_char_boundary(text: &str, idx: usize, insert: &str) -> String { |
| 16 | + let char_count = text.chars().count(); |
| 17 | + let char_idx = idx.min(char_count); |
| 18 | + let byte_idx = text |
| 19 | + .char_indices() |
| 20 | + .nth(char_idx) |
| 21 | + .map(|(byte_idx, _)| byte_idx) |
| 22 | + .unwrap_or(text.len()); |
| 23 | + |
| 24 | + let mut out = String::with_capacity(text.len() + insert.len()); |
| 25 | + out.push_str(&text[..byte_idx]); |
| 26 | + out.push_str(insert); |
| 27 | + out.push_str(&text[byte_idx..]); |
| 28 | + out |
| 29 | +} |
| 30 | + |
6 | 31 | #[derive(Default)] |
7 | 32 | pub struct NotesDialog { |
8 | 33 | pub open: bool, |
@@ -78,11 +103,30 @@ impl NotesDialog { |
78 | 103 | egui::ScrollArea::vertical() |
79 | 104 | .max_height(ui.available_height()) |
80 | 105 | .show(ui, |ui| { |
81 | | - let resp = ui.add( |
82 | | - egui::TextEdit::multiline(&mut self.text) |
83 | | - .desired_width(f32::INFINITY) |
84 | | - .desired_rows(10), |
85 | | - ); |
| 106 | + let output = egui::TextEdit::multiline(&mut self.text) |
| 107 | + .desired_width(f32::INFINITY) |
| 108 | + .desired_rows(10) |
| 109 | + .show(ui); |
| 110 | + let resp = output.response.clone(); |
| 111 | + let caret_char_idx = |
| 112 | + output.cursor_range.map(|range| range.primary.ccursor.index); |
| 113 | + |
| 114 | + let mut insert_timestamp = false; |
| 115 | + let mut insert_idx = None; |
| 116 | + resp.context_menu(|ui| { |
| 117 | + if ui.button("Insert timestamp").clicked() { |
| 118 | + insert_timestamp = true; |
| 119 | + insert_idx = caret_char_idx; |
| 120 | + ui.close_menu(); |
| 121 | + } |
| 122 | + }); |
| 123 | + |
| 124 | + if insert_timestamp { |
| 125 | + let ts = format_note_timestamp_now(); |
| 126 | + let idx = insert_idx.unwrap_or(usize::MAX); |
| 127 | + self.text = insert_at_char_boundary(&self.text, idx, &ts); |
| 128 | + } |
| 129 | + |
86 | 130 | if resp.has_focus() && ctx.input(|i| i.key_pressed(egui::Key::Enter)) { |
87 | 131 | let modifiers = ctx.input(|i| i.modifiers); |
88 | 132 | ctx.input_mut(|i| i.consume_key(modifiers, egui::Key::Enter)); |
@@ -218,3 +262,43 @@ impl NotesDialog { |
218 | 262 | } |
219 | 263 | } |
220 | 264 | } |
| 265 | + |
| 266 | +#[cfg(test)] |
| 267 | +mod tests { |
| 268 | + use super::{format_note_timestamp, insert_at_char_boundary}; |
| 269 | + use chrono::{Local, TimeZone}; |
| 270 | + |
| 271 | + #[test] |
| 272 | + fn insert_in_middle() { |
| 273 | + assert_eq!( |
| 274 | + insert_at_char_boundary("hello world", 5, ","), |
| 275 | + "hello, world" |
| 276 | + ); |
| 277 | + } |
| 278 | + |
| 279 | + #[test] |
| 280 | + fn insert_at_start_and_end() { |
| 281 | + assert_eq!(insert_at_char_boundary("world", 0, "hello "), "hello world"); |
| 282 | + assert_eq!(insert_at_char_boundary("hello", 5, " world"), "hello world"); |
| 283 | + } |
| 284 | + |
| 285 | + #[test] |
| 286 | + fn insert_out_of_range_falls_back_to_end() { |
| 287 | + assert_eq!(insert_at_char_boundary("hello", 999, "!"), "hello!"); |
| 288 | + } |
| 289 | + |
| 290 | + #[test] |
| 291 | + fn unicode_safe_char_boundary_handling() { |
| 292 | + assert_eq!(insert_at_char_boundary("a😀b", 2, "-"), "a😀-b"); |
| 293 | + assert_eq!(insert_at_char_boundary("éß", 1, "-"), "é-ß"); |
| 294 | + } |
| 295 | + |
| 296 | + #[test] |
| 297 | + fn timestamp_format_is_deterministic() { |
| 298 | + let dt = Local |
| 299 | + .with_ymd_and_hms(2024, 1, 2, 3, 4, 5) |
| 300 | + .single() |
| 301 | + .expect("valid local datetime"); |
| 302 | + assert_eq!(format_note_timestamp(dt), "2024-01-02 03:04:05"); |
| 303 | + } |
| 304 | +} |
0 commit comments