From 239484c3bcf4ebaa9339207f9d3fae40a1b32609 Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Tue, 9 Sep 2025 15:02:29 +0300 Subject: [PATCH 1/3] Event logging --- src/config.rs | 18 ++++++--- src/lib.rs | 3 +- src/log.rs | 91 +++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 91 +++++++++++++++++++++++++++++++++++++++------ ui/app-window.slint | 1 + 5 files changed, 187 insertions(+), 17 deletions(-) create mode 100644 src/log.rs diff --git a/src/config.rs b/src/config.rs index debad15..68afdf0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,18 +3,26 @@ use serde::Deserialize; #[derive(Deserialize)] pub struct Config { + /// directory, where config is located + #[serde(skip)] + pub conf_path: PathBuf, pub log_path: PathBuf } impl Config { - pub fn new() -> Self { - Config { log_path: PathBuf::from("./logs") } + pub fn new(conf_path: PathBuf) -> Self { + let conf_dir: PathBuf = conf_path.parent().unwrap().into(); + Config { conf_path: conf_dir, log_path: PathBuf::from("./logs") } } pub fn load(path: PathBuf) -> Self { - if let Ok(toml_string) = std::fs::read_to_string(path) { - return toml::from_str(&toml_string).unwrap_or(Config::new()); + if let Ok(toml_string) = std::fs::read_to_string(path.clone()) { + let conf = toml::from_str::(&toml_string); + if let Ok(mut conf) = conf { + conf.conf_path = path.parent().unwrap().into(); + return conf; + } } - Config::new() + Config::new(path) } } diff --git a/src/lib.rs b/src/lib.rs index 6d93152..bdf3eb3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ use config::Config; use std::path::PathBuf; pub mod config; +pub mod log; pub fn load_config() -> Config { if let Ok(path_str) = std::env::var("XDG_CONFIG_HOME") { @@ -10,5 +11,5 @@ pub fn load_config() -> Config { path.push("config.toml"); return Config::load(path); } - Config::new() + Config::new(PathBuf::from("./config.toml")) } diff --git a/src/log.rs b/src/log.rs new file mode 100644 index 0000000..5b98a67 --- /dev/null +++ b/src/log.rs @@ -0,0 +1,91 @@ +use std::{ + io::{Error, ErrorKind}, + path::PathBuf +}; +use serde::{Deserialize, Serialize}; +use toml::value::{Date, Time}; +use crate::config::Config; + +#[derive(Serialize, Deserialize)] +pub struct Log { + pub date: Date, + pub events: Vec +} + +impl Log { + pub fn new(date: Date) -> Self { + Log { date, events: Vec::new() } + } + + pub fn load_from(config: &Config, date: Date) -> Self { + let path = Log::get_filepath(&date, config); + if let Ok(log_string) = std::fs::read_to_string(path) { + return toml::from_str(&log_string).unwrap_or(Log::new(date)); + } else { + Log::new(date) + } + } + + pub fn save(&self, config: &Config) -> std::io::Result<()> { + Log::try_create_log_dir(config)?; + let path = Log::get_filepath(&self.date, config); + match toml::to_string_pretty(self) { + Ok(toml_string) => std::fs::write(&path, toml_string), + Err(error) => { + return Err(Error::new(ErrorKind::Other, format!("{error}"))); + } + } + } + + fn get_filepath(date: &Date, config: &Config) -> PathBuf { + let mut path = Log::get_log_dir(&config); + let filename = format!("{}-{}-{}", date.day, date.month, date.year); + path.push(filename); + path.set_extension("toml"); + path + } + + fn get_log_dir(config: &Config) -> PathBuf { + if config.log_path.is_relative() { + let mut path = config.conf_path.clone(); + path.push(&config.log_path); + return path; + } else { + return config.log_path.clone(); + } + } + + fn try_create_log_dir(config: &Config) -> std::io::Result<()> { + let path = Log::get_log_dir(config); + if !std::fs::exists(&path)? { + std::fs::create_dir_all(path)? + } + Ok(()) + } +} + +#[derive(Serialize, Deserialize)] +pub struct Event { + pub name: String, + pub start: Time, + pub end: Time, + pub finished: bool +} + +impl Event { + pub fn new(name: String, start: i32, end: i32, finished: bool) -> Self { + let start = Time { + hour: (start / 3600) as u8, + minute: ((start / 3600) / 60) as u8, + second: (start % 60) as u8, + nanosecond: 0 + }; + let end = Time { + hour: (end / 3600) as u8, + minute: ((end % 3600) / 60) as u8, + second: (end % 60) as u8, + nanosecond: 0 + }; + Event { name, start, end, finished } + } +} diff --git a/src/main.rs b/src/main.rs index 2a747ef..eeed74f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,21 +1,65 @@ // Prevent console window in addition to Slint window in Windows release builds when, e.g., starting the app via file manager. Ignored on other platforms. #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -use std::error::Error; +use std::{error::Error, sync::{Arc, Mutex}}; -use aliveline::{config::Config, load_config}; -use chrono::Timelike; -use slint::{Model, SharedString, VecModel}; +use aliveline::{config::Config, load_config, log::{Event, Log}}; +use chrono::{Datelike, Timelike}; +use slint::{Model, SharedString, ToSharedString, VecModel}; +use toml::value::{Date as TomlDate, Time}; slint::include_modules!(); +impl From for TimelineEvent { + fn from(event: Event) -> Self { + let start = (event.start.hour as i32) * 3600 + (event.start.minute as i32) * 60 + (event.start.second as i32); + let end = (event.end.hour as i32) * 3600 + (event.end.minute as i32) * 60 + (event.end.second as i32); + TimelineEvent { start, duration: end - start, label: event.name.to_shared_string(), finished: event.finished } + } +} + +impl From for Event { + fn from(event: TimelineEvent) -> Self { + let start = Time { + hour: (event.start / 3600) as u8, + minute: ((event.start % 3600) / 60) as u8, + second: (event.start % 60) as u8, + nanosecond: 0 + }; + let end = Time { + hour: start.hour + (event.duration / 3600) as u8, + minute: start.minute + ((event.duration % 3600) / 60) as u8, + second: start.second + (event.duration % 60) as u8, + nanosecond: 0 + }; + Event { start, end, name: event.label.to_string(), finished: event.finished } + } +} + fn main() -> Result<(), Box> { let ui = AppWindow::new()?; let now = chrono::Local::now(); let offset = now.hour() * 3600 + now.minute() * 60 + now.second(); + + let date: TomlDate = TomlDate { day: now.day() as u8, month: now.month() as u8, year: now.year() as u16 }; + + let config: Arc = Arc::new(load_config()); + let writing_log: Arc> = Arc::new(Mutex::new(Log::load_from(&config, date))); + ui.invoke_update_record_offset(offset as i32); + ui.on_save_log({ + let config = config.clone(); + let log = writing_log.clone(); + move || { + let log_guard = log.lock().expect("Log shouldn't be used twice"); + if let Err(error) = (*log_guard).save(&config) { + eprintln!("Error occured while saving log: {error}"); + } + } + }); + ui.on_update_record_visible_time({ let ui_weak = ui.as_weak(); move |hours_string: SharedString| { @@ -30,32 +74,50 @@ fn main() -> Result<(), Box> { ui.on_start_new_event({ let ui_weak = ui.as_weak(); + let log = writing_log.clone(); move |event_name: SharedString| { let ui = ui_weak.unwrap(); + + let events_rc = ui.get_record_events(); let events = events_rc.as_any() .downcast_ref::>() .unwrap(); let offset = ui.get_record_offset(); - events.push(TimelineEvent { + + let event = TimelineEvent { duration: 0, finished: false, label: event_name, start: offset - }); + }; + + { + let mut log_guard = log.lock().expect("Log shouldn't be used twice"); + (*log_guard).events.push(Event::from(event.clone())); + } + + ui.invoke_save_log(); + + events.push(event); } }); ui.on_stop_event({ let ui_weak = ui.as_weak(); + let log = writing_log.clone(); move || { let ui = ui_weak.unwrap(); let events_rc = ui.get_record_events(); - let events = events_rc.as_any().downcast_ref::>().unwrap(); + let events = events_rc.as_any() + .downcast_ref::>().unwrap(); let offset = ui.get_record_offset(); - let event_id = events.iter().position(|data| !data.finished).unwrap(); - let event = events.row_data(event_id).expect("stop-event called without unfinished events"); + let event_id = events.iter() + .position(|data| !data.finished) + .unwrap(); + let event = events.row_data(event_id) + .expect("stop-event called without unfinished events"); let new_event = TimelineEvent { duration: offset - event.start, finished: true, @@ -63,6 +125,15 @@ fn main() -> Result<(), Box> { start: event.start }; + { + let mut log_guard = log.lock().expect("Log shouldn't be used twice"); + (*log_guard).events.push(Event::from(new_event.clone())); + let index = (*log_guard).events.iter().position(|data| !data.finished).unwrap(); + (*log_guard).events.swap_remove(index); + } + + ui.invoke_save_log(); + events.set_row_data(event_id, new_event); } }); @@ -76,8 +147,6 @@ fn main() -> Result<(), Box> { } }); - let config: Config = load_config(); - println!("logs path: {}", config.log_path.to_str().unwrap()); ui.run()?; diff --git a/ui/app-window.slint b/ui/app-window.slint index a2f44ee..ccd2ad2 100644 --- a/ui/app-window.slint +++ b/ui/app-window.slint @@ -9,6 +9,7 @@ export component AppWindow inherits Window { callback stop-event <=> record.stop-event; callback chain-event <=> record.chain-event; callback update-record-offset(int); + callback save-log; update-record-offset(new-offset) => { record.offset = new-offset; From 99af3eb2b8be528ff7333efe2bcb1e588f4b464f Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Wed, 10 Sep 2025 10:00:18 +0300 Subject: [PATCH 2/3] Log loading --- src/log.rs | 4 ++-- src/main.rs | 38 +++++++++++++++++++++++++++++++++----- ui/app-window.slint | 3 +++ ui/record.slint | 2 +- 4 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/log.rs b/src/log.rs index 5b98a67..695f1a4 100644 --- a/src/log.rs +++ b/src/log.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use toml::value::{Date, Time}; use crate::config::Config; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] pub struct Log { pub date: Date, pub events: Vec @@ -64,7 +64,7 @@ impl Log { } } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone, Debug)] pub struct Event { pub name: String, pub start: Time, diff --git a/src/main.rs b/src/main.rs index eeed74f..7da69fb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,11 @@ // Prevent console window in addition to Slint window in Windows release builds when, e.g., starting the app via file manager. Ignored on other platforms. #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -use std::{error::Error, sync::{Arc, Mutex}}; +use std::{error::Error, rc::Rc, sync::{Arc, Mutex}}; use aliveline::{config::Config, load_config, log::{Event, Log}}; use chrono::{Datelike, Timelike}; -use slint::{Model, SharedString, ToSharedString, VecModel}; +use slint::{Model, ModelRc, SharedString, ToSharedString, VecModel}; use toml::value::{Date as TomlDate, Time}; slint::include_modules!(); @@ -26,10 +26,11 @@ impl From for Event { second: (event.start % 60) as u8, nanosecond: 0 }; + let endsecs = event.start + event.duration; let end = Time { - hour: start.hour + (event.duration / 3600) as u8, - minute: start.minute + ((event.duration % 3600) / 60) as u8, - second: start.second + (event.duration % 60) as u8, + hour: (endsecs / 3600) as u8, + minute: ((endsecs % 3600) / 60) as u8, + second: (endsecs % 60) as u8, nanosecond: 0 }; Event { start, end, name: event.label.to_string(), finished: event.finished } @@ -47,7 +48,26 @@ fn main() -> Result<(), Box> { let config: Arc = Arc::new(load_config()); let writing_log: Arc> = Arc::new(Mutex::new(Log::load_from(&config, date))); + { + println!("Log: {:?}", writing_log.lock().unwrap().events); + let ui_weak = ui.as_weak(); + let log = writing_log.clone(); + (move || { + println!("c"); + let ui = ui_weak.unwrap(); + let log_guard = log.lock().expect("Log shouldn't be used twice"); + let events: Vec = (*log_guard).events.iter().map(|event| TimelineEvent::from((*event).clone())).collect(); + let in_progress = events.iter().any(|event| !event.finished); + let model: ModelRc = Rc::new(VecModel::from(events)).into(); + println!("get: {:?}", model); + ui.set_record_events(model); + ui.set_in_progress(in_progress); + })() + } + ui.invoke_update_record_offset(offset as i32); + ui.invoke_load_log(); + ui.invoke_another_call(); ui.on_save_log({ let config = config.clone(); @@ -60,6 +80,14 @@ fn main() -> Result<(), Box> { } }); + ui.on_another_call({ + println!("outside move"); + move || { + println!("inside move"); + } + }); + + ui.on_update_record_visible_time({ let ui_weak = ui.as_weak(); move |hours_string: SharedString| { diff --git a/ui/app-window.slint b/ui/app-window.slint index ccd2ad2..f91b143 100644 --- a/ui/app-window.slint +++ b/ui/app-window.slint @@ -10,6 +10,8 @@ export component AppWindow inherits Window { callback chain-event <=> record.chain-event; callback update-record-offset(int); callback save-log; + callback another-call; + callback load-log; update-record-offset(new-offset) => { record.offset = new-offset; @@ -18,6 +20,7 @@ export component AppWindow inherits Window { in-out property record-events <=> record.events; in-out property record-offset <=> record.offset; in-out property record-visible-time <=> record.visible-time; + in-out property in-progress <=> record.in-progress; property<[string]> combo-spans: ["1 Hour", "4 Hours", "8 Hours", "24 Hours"]; title: "Aliveline"; diff --git a/ui/record.slint b/ui/record.slint index 0556e37..cf195af 100644 --- a/ui/record.slint +++ b/ui/record.slint @@ -11,7 +11,7 @@ export component RecordWidget inherits VerticalBox { in-out property offset <=> tl.offset; in-out property events <=> tl.events; in property<[string]> combo-spans: []; - property in-progress: false; + in-out property in-progress: false; property event-name <=> le.text; tl := Timeline { From abc9d59810294ec9be4a03f523aab4ad7de3e895 Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Wed, 10 Sep 2025 14:50:05 +0300 Subject: [PATCH 3/3] Prettified code and added review scrolling --- src/main.rs | 38 ++++++++++++++++++-------------------- ui/app-window.slint | 26 ++++++++++++++++++++------ ui/review.slint | 43 ++++++++++++++++++++++++++++++++++++++++--- ui/timeline.slint | 1 + 4 files changed, 79 insertions(+), 29 deletions(-) diff --git a/src/main.rs b/src/main.rs index 7da69fb..3e34f4c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -43,31 +43,34 @@ fn main() -> Result<(), Box> { let now = chrono::Local::now(); let offset = now.hour() * 3600 + now.minute() * 60 + now.second(); - let date: TomlDate = TomlDate { day: now.day() as u8, month: now.month() as u8, year: now.year() as u16 }; + let date: TomlDate = TomlDate { + day: now.day() as u8, + month: now.month() as u8, + year: now.year() as u16 + }; let config: Arc = Arc::new(load_config()); let writing_log: Arc> = Arc::new(Mutex::new(Log::load_from(&config, date))); { - println!("Log: {:?}", writing_log.lock().unwrap().events); let ui_weak = ui.as_weak(); let log = writing_log.clone(); (move || { - println!("c"); let ui = ui_weak.unwrap(); let log_guard = log.lock().expect("Log shouldn't be used twice"); - let events: Vec = (*log_guard).events.iter().map(|event| TimelineEvent::from((*event).clone())).collect(); + let events: Vec = (*log_guard) + .events + .iter() + .map(|event| TimelineEvent::from((*event).clone())) + .collect(); let in_progress = events.iter().any(|event| !event.finished); let model: ModelRc = Rc::new(VecModel::from(events)).into(); - println!("get: {:?}", model); ui.set_record_events(model); ui.set_in_progress(in_progress); })() } ui.invoke_update_record_offset(offset as i32); - ui.invoke_load_log(); - ui.invoke_another_call(); ui.on_save_log({ let config = config.clone(); @@ -79,24 +82,20 @@ fn main() -> Result<(), Box> { } } }); - - ui.on_another_call({ - println!("outside move"); - move || { - println!("inside move"); - } - }); - - - ui.on_update_record_visible_time({ + + ui.on_update_visible_time({ let ui_weak = ui.as_weak(); - move |hours_string: SharedString| { + move |is_record: bool, hours_string: SharedString| { let ui = ui_weak.unwrap(); let hours = hours_string.split(' ') .next() .map(|h| h.parse::().unwrap()) .unwrap(); - ui.set_record_visible_time(hours * 3600); + if is_record { + ui.set_record_visible_time(hours * 3600); + } else { + ui.set_review_visible_time(hours * 3600); + } } }); @@ -106,7 +105,6 @@ fn main() -> Result<(), Box> { move |event_name: SharedString| { let ui = ui_weak.unwrap(); - let events_rc = ui.get_record_events(); let events = events_rc.as_any() .downcast_ref::>() diff --git a/ui/app-window.slint b/ui/app-window.slint index f91b143..89a10b6 100644 --- a/ui/app-window.slint +++ b/ui/app-window.slint @@ -4,14 +4,15 @@ import { ReviewWidget } from "review.slint"; import { TimelineEvent } from "timeline.slint"; export component AppWindow inherits Window { - callback update-record-visible-time <=> record.update-visible-time; callback start-new-event <=> record.start-new-event; callback stop-event <=> record.stop-event; callback chain-event <=> record.chain-event; callback update-record-offset(int); callback save-log; - callback another-call; - callback load-log; + + callback fetch-log <=> review.fetch-log; + + callback update-visible-time(bool, string); update-record-offset(new-offset) => { record.offset = new-offset; @@ -19,8 +20,13 @@ export component AppWindow inherits Window { in-out property record-events <=> record.events; in-out property record-offset <=> record.offset; - in-out property record-visible-time <=> record.visible-time; - in-out property in-progress <=> record.in-progress; + in-out property record-visible-time <=> record.visible-time; + in-out property in-progress <=> record.in-progress; + + in-out property review-events <=> review.events; + in-out property review-offset <=> review.offset; + in-out property review-visible-time <=> review.visible-time; + property<[string]> combo-spans: ["1 Hour", "4 Hours", "8 Hours", "24 Hours"]; title: "Aliveline"; @@ -30,11 +36,19 @@ export component AppWindow inherits Window { title: "Record"; record := RecordWidget { combo-spans: combo-spans; + update-visible-time(time) => { + root.update-visible-time(true, time); + } } } Tab { title: "Review"; - review := ReviewWidget {} + review := ReviewWidget { + combo-spans: combo-spans; + update-visible-time(time) => { + root.update-visible-time(false, time) + } + } } } } diff --git a/ui/review.slint b/ui/review.slint index 6d3c7d0..229658b 100644 --- a/ui/review.slint +++ b/ui/review.slint @@ -1,13 +1,31 @@ -import { VerticalBox, LineEdit, Button, DatePickerPopup } from "std-widgets.slint"; +import { VerticalBox, LineEdit, Button, DatePickerPopup, ComboBox } from "std-widgets.slint"; import { Timeline } from "timeline.slint"; export component ReviewWidget inherits VerticalBox { + callback update-visible-time(string); + callback fetch-log(int, int, int); + + property max-offset: 24 * 3600; property current-year; property current-month; property current-day; - - Timeline { + in property<[string]> combo-spans: []; + in-out property visible-time <=> tl.visible-time; + in-out property offset <=> tl.offset; + in-out property events <=> tl.events; + + tl := Timeline { updating: false; + TouchArea { + preferred-width: 100%; + preferred-height: 100%; + moved => { + if self.pressed { + root.offset -= (self.mouse-x - self.pressed-x) / 1px; + root.offset = clamp(root.offset, visible-time, max-offset); + } + } + } } GridLayout { spacing-vertical: 8px; @@ -16,12 +34,30 @@ export component ReviewWidget inherits VerticalBox { text: "Day: \{current-day}/\{current-month}/\{current-year}"; font-size: 32px; horizontal-alignment: right; + row: 0; } Button { text: "Select"; clicked => { date-picker.show() } + row: 0; + col: 1; + } + Text { + text: "Span: "; + font-size: 24px; + row: 1; + horizontal-alignment: right; + } + ComboBox { + model: combo-spans; + current-index: 0; + row: 1; + col: 1; + selected(current-value) => { + root.update-visible-time(current-value); + } } } date-picker := DatePickerPopup { @@ -32,6 +68,7 @@ export component ReviewWidget inherits VerticalBox { current-year = date.year; current-month = date.month; current-day = date.day; + root.fetch-log(current-year, current-month, current-day); } } } diff --git a/ui/timeline.slint b/ui/timeline.slint index de530d9..c6069a0 100644 --- a/ui/timeline.slint +++ b/ui/timeline.slint @@ -85,4 +85,5 @@ export component Timeline inherits Rectangle { visible: timeline-event.visible; } } + @children }