From 218ee49a8be677e395603619a60e39f9597f86f9 Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Thu, 18 Sep 2025 16:30:37 +0300 Subject: [PATCH 1/4] Pseudo pseudorandom color picker --- Cargo.toml | 2 +- src/lib.rs | 10 +++++++++- src/main.rs | 17 ++++++++++------- ui/app-window.slint | 1 - ui/record.slint | 1 - ui/theme.slint | 43 +++++++++++++++++++++++++++++++++++++++++++ ui/timeline.slint | 16 ++++++++++++---- 7 files changed, 75 insertions(+), 15 deletions(-) create mode 100644 ui/theme.slint diff --git a/Cargo.toml b/Cargo.toml index 49e34c7..7675ffc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,4 +15,4 @@ toml = "0.9.5" slint-build = "1.12.1" [profile.release] -opt-level = "s" +opt-level = 3 diff --git a/src/lib.rs b/src/lib.rs index bdf3eb3..f63008d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ use config::Config; -use std::path::PathBuf; +use std::{hash::{DefaultHasher, Hash, Hasher}, path::PathBuf}; pub mod config; pub mod log; @@ -13,3 +13,11 @@ pub fn load_config() -> Config { } Config::new(PathBuf::from("./config.toml")) } + +/// Get random-like color id in range 0..16 by computing string hash +pub fn color_id_from_name(name: String) -> i32 { + let mut s = DefaultHasher::new(); + name.hash(&mut s); + let hash = s.finish(); + (hash.count_ones() / 4) as i32 +} diff --git a/src/main.rs b/src/main.rs index 3285d0f..19cb68d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ use std::{error::Error, rc::Rc, sync::{Arc, Mutex}}; -use aliveline::{config::Config, load_config, log::{Event, Log}}; +use aliveline::{color_id_from_name, config::Config, load_config, log::{Event, Log}}; use chrono::{Datelike, Timelike}; use slint::{Model, ModelRc, SharedString, ToSharedString, VecModel, Weak}; use toml::value::{Date as TomlDate, Time}; @@ -22,7 +22,8 @@ impl From for TimelineEvent { start, duration: end - start, label: event.name.to_shared_string(), - finished: event.finished + finished: event.finished, + color_id: color_id_from_name(event.name) } } } @@ -143,8 +144,9 @@ fn main() -> Result<(), Box> { let event = TimelineEvent { duration: 0, finished: false, - label: event_name, - start: offset + label: event_name.clone(), + start: offset, + color_id: color_id_from_name(event_name.to_string()) }; { @@ -177,8 +179,9 @@ fn main() -> Result<(), Box> { let new_event = TimelineEvent { duration: offset - event.start, finished: true, - label: event.label, - start: event.start + label: event.label.clone(), + start: event.start, + color_id: color_id_from_name(event.label.to_string()) }; { @@ -215,7 +218,7 @@ fn main() -> Result<(), Box> { let maybe_unfinished_event = log_guard.events.iter().find(|event| !event.finished); match maybe_unfinished_event { Some(unfinished_event) => Some(Event::new(unfinished_event.name.clone(), 0, 0, false)), - None => None + _ => None } }; diff --git a/ui/app-window.slint b/ui/app-window.slint index 373e060..7137014 100644 --- a/ui/app-window.slint +++ b/ui/app-window.slint @@ -31,7 +31,6 @@ export component AppWindow inherits Window { property<[string]> combo-spans: ["1 Hour", "4 Hours", "8 Hours", "24 Hours"]; title: "Aliveline"; - TabWidget { Tab { title: "Record"; diff --git a/ui/record.slint b/ui/record.slint index 1dff7b4..4d7090c 100644 --- a/ui/record.slint +++ b/ui/record.slint @@ -16,7 +16,6 @@ export component RecordWidget inherits VerticalBox { property event-name: ""; property minimized: false; property combo-index: 0; - tl := Timeline { preferred-height: 100%; updating: true; diff --git a/ui/theme.slint b/ui/theme.slint new file mode 100644 index 0000000..30b211a --- /dev/null +++ b/ui/theme.slint @@ -0,0 +1,43 @@ +export global Palette { + in-out property background: gray; + in-out property timeline: darkgray; + in-out property background-text: black; + // Note: these colors were almost randomly picked + in-out property<[color]> event-colors: [ + #97f9f9, + #a4def9, + #c1e0f7, + #cfbae1, + #c59fc9, + #4e3d42, + #c9d5b5, + #2d82b7, + #556f44, + #772e25, + #c44536, + #7c6a0a, + #babd8d, + #ffdac6, + #fa9500, + #eb6424 + ]; + + in-out property <[color]> event-text: [ + #000000, + #000000, + #000000, + #000000, + #000000, + #ffffff, + #000000, + #000000, + #000000, + #ffffff, + #000000, + #000000, + #000000, + #000000, + #000000, + #000000 + ]; +} diff --git a/ui/timeline.slint b/ui/timeline.slint index c9fef81..2e9db7f 100644 --- a/ui/timeline.slint +++ b/ui/timeline.slint @@ -1,8 +1,11 @@ +import { Palette } from "theme.slint"; + export struct TimelineEvent { start: int, duration: int, finished: bool, - label: string + label: string, + color-id: int } global TimeString { @@ -26,6 +29,7 @@ global TimeString { export component Timeline inherits Rectangle { callback new-day-started; callback clicked <=> ta.clicked; + background: Palette.background; in-out property updating: true; in-out property<[TimelineEvent]> events: []; @@ -52,7 +56,6 @@ export component Timeline inherits Rectangle { preferred-height: 100%; } - background: gray; border-width: 1px; border-color: black; Rectangle { @@ -63,19 +66,21 @@ export component Timeline inherits Rectangle { height: parent.height / 2; border-color: black; border-width: 1px; - background: purple; + background: Palette.timeline; } Text { x: 0; y: parent.height - self.height; text: TimeString.from(visible-offset - visible-time); + color: Palette.background-text; } Text { x: parent.width - self.width; y: parent.height - self.height; text: TimeString.from(visible-offset); + color: Palette.background-text; } for event in events: timeline-event := Rectangle { @@ -91,13 +96,14 @@ export component Timeline inherits Rectangle { visible: self.width > 0 && self.real-x < parent.width; border-color: black; border-width: 1px; - background: red; + background: Palette.event-colors[event.color-id]; Text { x: 0; y: -self.height; text: event.label; visible: timeline-event.visible; + color: Palette.background-text; } start-txt := Text { x: 0; @@ -108,6 +114,7 @@ export component Timeline inherits Rectangle { visible: timeline-event.visible && (self.width * 2 < timeline-event.width || (!end-txt.visible && self.width < timeline-event.width)); + color: Palette.event-text[event.color-id]; } end-txt := Text { x: timeline-event.width - self.width; @@ -116,6 +123,7 @@ export component Timeline inherits Rectangle { TimeString.from(event.start + event.duration) : TimeString.from(visible-offset); visible: timeline-event.visible && timeline-event.width - self.width * 2 > 0; + color: Palette.event-text[event.color-id]; } } @children From ca3c1716987b080be8c2ef50d04193c7c06b4ff6 Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Thu, 18 Sep 2025 16:35:30 +0300 Subject: [PATCH 2/4] Improved color picker algorithm --- src/lib.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f63008d..7eb4df8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,5 +19,4 @@ pub fn color_id_from_name(name: String) -> i32 { let mut s = DefaultHasher::new(); name.hash(&mut s); let hash = s.finish(); - (hash.count_ones() / 4) as i32 -} + (hash % 16) as i32 } From 1b6f8ef2821ecde02e061877627e5e959906756c Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Thu, 18 Sep 2025 17:33:34 +0300 Subject: [PATCH 3/4] Configurable colors --- src/config.rs | 61 +++++++++++++++++++++++++++++++++++++++++++-- src/main.rs | 26 ++++++++++++++++++- ui/app-window.slint | 1 + 3 files changed, 85 insertions(+), 3 deletions(-) diff --git a/src/config.rs b/src/config.rs index 68afdf0..bc190ef 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,18 +1,75 @@ use std::path::PathBuf; use serde::Deserialize; +#[derive(Deserialize)] +pub struct Colors { + pub background: u32, + pub timeline: u32, + pub background_text: u32 +} + #[derive(Deserialize)] pub struct Config { /// directory, where config is located #[serde(skip)] pub conf_path: PathBuf, - pub log_path: PathBuf + pub log_path: PathBuf, + pub colors: Colors, + pub event_colors: Vec, + pub text_colors: Vec } impl Config { 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") } + let colors = Colors { + background: 0xff_808080, + timeline: 0xff_a9a9a9, + background_text: 0xff_000000 + }; + let event_colors: Vec = vec![ + 0xff_97f9f9, + 0xff_a4def9, + 0xff_c1e0f7, + 0xff_cfbae1, + 0xff_c59fc9, + 0xff_4e3d42, + 0xff_c9d5b5, + 0xff_2d82b7, + 0xff_556f44, + 0xff_772e25, + 0xff_c44536, + 0xff_7c6a0a, + 0xff_babd8d, + 0xff_ffdac6, + 0xff_fa9500, + 0xff_eb6424 + ]; + let text_colors: Vec = vec![ + 0xff000000, + 0xff000000, + 0xff000000, + 0xff000000, + 0xff000000, + 0xffffffff, + 0xff000000, + 0xff000000, + 0xff000000, + 0xffffffff, + 0xff000000, + 0xff000000, + 0xff000000, + 0xff000000, + 0xff000000, + 0xff000000 + ]; + Config { + conf_path: conf_dir, + log_path: PathBuf::from("./logs"), + colors, + event_colors, + text_colors + } } pub fn load(path: PathBuf) -> Self { diff --git a/src/main.rs b/src/main.rs index 19cb68d..9b1c5a0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ use std::{error::Error, rc::Rc, sync::{Arc, Mutex}}; use aliveline::{color_id_from_name, config::Config, load_config, log::{Event, Log}}; use chrono::{Datelike, Timelike}; -use slint::{Model, ModelRc, SharedString, ToSharedString, VecModel, Weak}; +use slint::{Color, Model, ModelRc, SharedString, ToSharedString, VecModel, Weak}; use toml::value::{Date as TomlDate, Time}; slint::include_modules!(); @@ -61,6 +61,26 @@ fn load_log(ui_weak: Weak, log: Arc>) { ui.set_in_progress(in_progress); } +fn load_colors(ui_weak: Weak, config: Arc) { + let ui = ui_weak.unwrap(); + let pal = ui.global::(); + pal.set_background(Color::from_argb_encoded(config.colors.background)); + pal.set_timeline(Color::from_argb_encoded(config.colors.timeline)); + pal.set_background_text(Color::from_argb_encoded(config.colors.background_text)); + + // This looks like war crime + let event_colors_rc: ModelRc = Rc::new(VecModel::from( + config.event_colors.iter() + .map(|value| Color::from_argb_encoded(*value)).collect::>() + )).into(); + pal.set_event_colors(event_colors_rc); + let event_text_rc: ModelRc = Rc::new(VecModel::from( + config.text_colors.iter() + .map(|value| Color::from_argb_encoded(*value)).collect::>() + )).into(); + pal.set_event_text(event_text_rc); +} + fn main() -> Result<(), Box> { let ui = AppWindow::new()?; @@ -80,6 +100,10 @@ fn main() -> Result<(), Box> { let log = writing_log.clone(); load_log(ui_weak, log); + let ui_weak = ui.as_weak(); + let config_arc = config.clone(); + load_colors(ui_weak, config_arc); + ui.invoke_update_record_offset(offset as i32); ui.on_fetch_log({ diff --git a/ui/app-window.slint b/ui/app-window.slint index 7137014..7535149 100644 --- a/ui/app-window.slint +++ b/ui/app-window.slint @@ -2,6 +2,7 @@ import { TabWidget } from "std-widgets.slint"; import { RecordWidget } from "record.slint"; import { ReviewWidget } from "review.slint"; import { TimelineEvent } from "timeline.slint"; +export { Palette } from "theme.slint"; export component AppWindow inherits Window { callback start-new-event <=> record.start-new-event; From ba976d9e124079fe27ac43d8f4346131e1a24e0d Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Fri, 19 Sep 2025 00:10:20 +0300 Subject: [PATCH 4/4] Made every thing in Config optional internally --- src/config.rs | 82 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 70 insertions(+), 12 deletions(-) diff --git a/src/config.rs b/src/config.rs index bc190ef..e1afe8d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,16 +2,50 @@ use std::path::PathBuf; use serde::Deserialize; #[derive(Deserialize)] +struct RawColors { + pub background: Option, + pub timeline: Option, + pub background_text: Option +} + +#[derive(Clone)] pub struct Colors { pub background: u32, pub timeline: u32, pub background_text: u32 } +impl Default for Colors { + fn default() -> Self { + Colors { + background: 0xff_808080, + timeline: 0xff_a9a9a9, + background_text: 0xff_000000 + } + } +} + +impl From for Colors { + fn from(value: RawColors) -> Self { + let default_colors: Colors = Default::default(); + Colors { + background: value.background.unwrap_or(default_colors.background), + timeline: value.timeline.unwrap_or(default_colors.timeline), + background_text: value.background_text.unwrap_or(default_colors.background_text), + } + } +} + #[derive(Deserialize)] +struct RawConfig { + pub log_path: Option, + pub colors: Option, + pub event_colors: Option>, + pub text_colors: Option> +} + pub struct Config { /// directory, where config is located - #[serde(skip)] pub conf_path: PathBuf, pub log_path: PathBuf, pub colors: Colors, @@ -19,14 +53,10 @@ pub struct Config { pub text_colors: Vec } -impl Config { - pub fn new(conf_path: PathBuf) -> Self { - let conf_dir: PathBuf = conf_path.parent().unwrap().into(); - let colors = Colors { - background: 0xff_808080, - timeline: 0xff_a9a9a9, - background_text: 0xff_000000 - }; +impl Default for Config { + fn default() -> Self { + let conf_path = PathBuf::new(); + let colors: Colors = Default::default(); let event_colors: Vec = vec![ 0xff_97f9f9, 0xff_a4def9, @@ -64,18 +94,46 @@ impl Config { 0xff000000 ]; Config { - conf_path: conf_dir, + conf_path, log_path: PathBuf::from("./logs"), colors, event_colors, text_colors } } +} + +impl From for Config { + fn from(value: RawConfig) -> Self { + let default_config: Config = Default::default(); + let colors: Colors = match value.colors { + Some(raw_colors) => raw_colors.into(), + None => default_config.colors.clone() + }; + Config { + conf_path: default_config.conf_path, + log_path: value.log_path.unwrap_or(default_config.log_path), + colors, + event_colors: value.event_colors.unwrap_or(default_config.event_colors.clone()), + text_colors: value.text_colors.unwrap_or(default_config.text_colors.clone()) + } + } +} + +impl Config { + pub fn new(conf_path: PathBuf) -> Self { + let conf_dir: PathBuf = conf_path.parent().unwrap().into(); + Config { + conf_path: conf_dir, + ..Default::default() + } + } pub fn load(path: PathBuf) -> Self { 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 { + let conf = toml::from_str::(&toml_string); + if let Ok(raw_conf) = conf { + let mut conf: Config = raw_conf.into(); conf.conf_path = path.parent().unwrap().into(); return conf; }