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] 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;