From b5e9a4115a66ffd1f3804a3a3a4dab5a95249924 Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Fri, 12 Sep 2025 13:59:12 +0300 Subject: [PATCH 01/12] Several bugfixes - Fixed event invisibility when offset is after event's half width - Fixed event being shown outside timeline component - When timer reaches 24:00:00, new day is automatically started --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/main.rs | 86 +++++++++++++++++++++++++++++++++------------ ui/app-window.slint | 1 + ui/record.slint | 1 + ui/review.slint | 2 +- ui/timeline.slint | 12 +++++-- 7 files changed, 79 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5aa4671..7877dc2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -147,7 +147,7 @@ dependencies = [ [[package]] name = "aliveline" -version = "0.1.0" +version = "0.1.1" dependencies = [ "chrono", "serde", diff --git a/Cargo.toml b/Cargo.toml index b7d15fc..49e34c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aliveline" -version = "0.1.0" +version = "0.1.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/main.rs b/src/main.rs index 19baaa8..3285d0f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ 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, ModelRc, SharedString, ToSharedString, VecModel}; +use slint::{Model, ModelRc, SharedString, ToSharedString, VecModel, Weak}; use toml::value::{Date as TomlDate, Time}; slint::include_modules!(); @@ -46,6 +46,20 @@ impl From for Event { } } +fn load_log(ui_weak: Weak, log: Arc>) { + 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(); + ui.set_record_events(model); + ui.set_in_progress(in_progress); +} + fn main() -> Result<(), Box> { let ui = AppWindow::new()?; @@ -61,23 +75,9 @@ fn main() -> Result<(), Box> { let config: Arc = Arc::new(load_config()); let writing_log: Arc> = Arc::new(Mutex::new(Log::load_from(&config, date))); - { - let ui_weak = ui.as_weak(); - let log = writing_log.clone(); - (move || { - 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(); - ui.set_record_events(model); - ui.set_in_progress(in_progress); - })() - } + let ui_weak = ui.as_weak(); + let log = writing_log.clone(); + load_log(ui_weak, log); ui.invoke_update_record_offset(offset as i32); @@ -165,7 +165,8 @@ fn main() -> Result<(), Box> { let ui = ui_weak.unwrap(); let events_rc = ui.get_record_events(); let events = events_rc.as_any() - .downcast_ref::>().unwrap(); + .downcast_ref::>() + .unwrap(); let offset = ui.get_record_offset(); let event_id = events.iter() @@ -182,9 +183,9 @@ fn main() -> Result<(), Box> { { 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); + 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(); @@ -202,6 +203,47 @@ fn main() -> Result<(), Box> { } }); + ui.on_new_day_started({ + let ui_weak = ui.as_weak(); + let log = writing_log.clone(); + move || { + let ui = ui_weak.unwrap(); + + let new_event: Option = { + let log_guard = log.lock().expect("Log shouldn't be used twice"); + + 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 + } + }; + + ui.invoke_stop_event(); + + { + let mut log_guard = log.lock().expect("Log shouldn't be used twice"); + log_guard.events.clear(); + let now = chrono::Local::now(); + + let date = TomlDate { + year: now.year() as u16, + month: now.month() as u8, + day: now.day() as u8 + }; + + log_guard.date = date; + log_guard.events.clear(); + + if let Some(event) = new_event { + log_guard.events.push(event); + } + } + + load_log(ui.as_weak(), log.clone()); + ui.invoke_save_log(); + } + }); ui.run()?; diff --git a/ui/app-window.slint b/ui/app-window.slint index 89a10b6..373e060 100644 --- a/ui/app-window.slint +++ b/ui/app-window.slint @@ -7,6 +7,7 @@ export component AppWindow inherits Window { callback start-new-event <=> record.start-new-event; callback stop-event <=> record.stop-event; callback chain-event <=> record.chain-event; + callback new-day-started <=> record.new-day-started; callback update-record-offset(int); callback save-log; diff --git a/ui/record.slint b/ui/record.slint index cf195af..d8a942a 100644 --- a/ui/record.slint +++ b/ui/record.slint @@ -2,6 +2,7 @@ import { VerticalBox, LineEdit, Button, ComboBox } from "std-widgets.slint"; import { Timeline } from "timeline.slint"; export component RecordWidget inherits VerticalBox { + callback new-day-started <=> tl.new-day-started; callback update-visible-time(string); callback start-new-event(string); callback chain-event(string); diff --git a/ui/review.slint b/ui/review.slint index 9727c24..14727ce 100644 --- a/ui/review.slint +++ b/ui/review.slint @@ -22,7 +22,7 @@ export component ReviewWidget inherits VerticalBox { spacing-horizontal: 16px; Slider { minimum: visible-time; - maximum: 24 * 3600; + maximum: tl.max-offset; value: offset; row: 0; colspan: 2; diff --git a/ui/timeline.slint b/ui/timeline.slint index c6069a0..66d571a 100644 --- a/ui/timeline.slint +++ b/ui/timeline.slint @@ -24,16 +24,24 @@ global TimeString { } export component Timeline inherits Rectangle { + callback new-day-started; + in-out property updating: true; in-out property<[TimelineEvent]> events: []; in-out property visible-time: 3600; property visible-offset: max(offset, visible-time); in-out property offset: 0; + out property max-offset: 24 * 3600 - 1; timer := Timer { interval: 1s; running: updating; triggered => { + if (offset >= max-offset) { + root.new-day-started(); + offset = 0; + return; + } offset += 1; } } @@ -70,10 +78,10 @@ export component Timeline inherits Rectangle { y: parent.height / 4; z: 1; width: event.finished ? - (event.duration) / visible-time * parent.width + min(real-x, 0): + min(parent.width - self.x, event.duration / visible-time * parent.width + min(real-x, 0)): parent.width - self.x; height: parent.height / 2; - visible: self.real-x + self.width > 0 && self.real-x < parent.width; + visible: self.width > 0 && self.real-x < parent.width; border-color: black; border-width: 1px; background: red; From 6a1f371f1de545a8ec453795a41aa6626ac3396a Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Fri, 12 Sep 2025 15:18:36 +0300 Subject: [PATCH 02/12] Updated Readme --- README.md | 66 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 3a77de6..cba76e1 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,46 @@ -# Slint Rust Template - -A template for a Rust application that's using [Slint](https://slint.rs/) for the user interface. +# Aliveline ## About -This template helps you get started developing a Rust application with Slint as toolkit -for the user interface. It demonstrates the integration between the `.slint` UI markup and -Rust code, how to react to callbacks, get and set properties, and use basic widgets. +Aliveline is a small app made with Rust + Slint to track daily activity on a timeline. +All activity is saved into TOML logs, which are human readable/editable. + +Aliveline currently supports Linux. + +## Features + +### Events +Events are main timeline building blocks. They have name, start and end. +Example of event in TOML format shown below: +```toml +[[events]] +name = "test" +start = 12:05:45 +end = 13:00:11 +finished = true +``` +_Note: if event is not finished yet, it may have `end = 00:00:00`._ + +## Building +Requirements: +- Rust toolchain +Instructions: +Run `cargo build --release` ## Usage -1. Install Rust by following its [getting-started guide](https://www.rust-lang.org/learn/get-started). - Once this is done, you should have the `rustc` compiler and the `cargo` build system installed in your `PATH`. -2. Download and extract the [ZIP archive of this repository](https://github.com/slint-ui/slint-rust-template/archive/refs/heads/main.zip). -3. Rename the extracted directory and change into it: - ``` - mv slint-rust-template-main my-project - cd my-project - ``` -4. Build with `cargo`: - ``` - cargo build - ``` -5. Run the application binary: - ``` - cargo run - ``` +Just run `aliveline` by any preferred way. -We recommend using an IDE for development, along with our [LSP-based IDE integration for `.slint` files](https://github.com/slint-ui/slint/blob/master/tools/lsp/README.md). You can also load this project directly in [Visual Studio Code](https://code.visualstudio.com) and install our [Slint extension](https://marketplace.visualstudio.com/items?itemName=Slint.slint). +## Configuration +Aliveline tries to find config at `$XDG_CONFIG_DIR/aliveline/config.toml`. +If config isn't found, or `$XDG_CONFIG_DIR` is not set, Aliveline uses default values. -## Next Steps +You can configure these values: +- `log_path` + Type: `String` + Default: `logs` + Path to directory where logs are located. + Path may be absolute or relative to config directory. -We hope that this template helps you get started, and that you enjoy exploring making user interfaces with Slint. To learn more -about the Slint APIs and the `.slint` markup language, check out our [online documentation](https://slint.dev/docs). - -Don't forget to edit this readme to replace it by yours, and edit the `name =` field in `Cargo.toml` to match the name of your -project. +## Contribution +You can contribute to Aliveline by creating issue on this repository, then we'll discuss it. From 0c2a3d7e95962f20eaae51a209dfb5f6142ba69a Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Fri, 12 Sep 2025 15:21:41 +0300 Subject: [PATCH 03/12] Updated Readme again --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index cba76e1..7aaf937 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## About -Aliveline is a small app made with Rust + Slint to track daily activity on a timeline. +Aliveline is a small app made with Rust + Slint to track daily activity on a timeline. All activity is saved into TOML logs, which are human readable/editable. Aliveline currently supports Linux. @@ -10,7 +10,7 @@ Aliveline currently supports Linux. ## Features ### Events -Events are main timeline building blocks. They have name, start and end. +Events are main timeline building blocks. They have name, start and end. Example of event in TOML format shown below: ```toml [[events]] @@ -19,7 +19,7 @@ start = 12:05:45 end = 13:00:11 finished = true ``` -_Note: if event is not finished yet, it may have `end = 00:00:00`._ +_Note: if event is not finished yet, it may have_ `end = 00:00:00`. ## Building Requirements: @@ -32,14 +32,14 @@ Run `cargo build --release` Just run `aliveline` by any preferred way. ## Configuration -Aliveline tries to find config at `$XDG_CONFIG_DIR/aliveline/config.toml`. +Aliveline tries to find config at `$XDG_CONFIG_DIR/aliveline/config.toml`. If config isn't found, or `$XDG_CONFIG_DIR` is not set, Aliveline uses default values. You can configure these values: -- `log_path` - Type: `String` - Default: `logs` - Path to directory where logs are located. +- `log_path` + - Type: `String` + - Default: `logs` + Path to directory where logs are located. Path may be absolute or relative to config directory. ## Contribution From 4650fde88431239a730e3391d7e6ac169ac20765 Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Fri, 12 Sep 2025 15:22:43 +0300 Subject: [PATCH 04/12] Updated Readme again again... --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7aaf937..be32e0b 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,10 @@ finished = true _Note: if event is not finished yet, it may have_ `end = 00:00:00`. ## Building -Requirements: +Requirements: - Rust toolchain -Instructions: + +Instructions: Run `cargo build --release` ## Usage From 31281295bbb0076fa821dde1ae5d95fe5884c1ee Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Mon, 15 Sep 2025 16:54:05 +0300 Subject: [PATCH 05/12] Added event start/end timestamps --- ui/timeline.slint | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/ui/timeline.slint b/ui/timeline.slint index 66d571a..4b94d03 100644 --- a/ui/timeline.slint +++ b/ui/timeline.slint @@ -74,24 +74,43 @@ export component Timeline inherits Rectangle { for event in events: timeline-event := Rectangle { property real-x: ((visible-time - (visible-offset - event.start)) / visible-time) * parent.width; + property real-width: event.duration / visible-time * parent.width + min(real-x, 0); x: max(real-x, 0); y: parent.height / 4; z: 1; width: event.finished ? - min(parent.width - self.x, event.duration / visible-time * parent.width + min(real-x, 0)): + min(parent.width - self.x, real-width) : parent.width - self.x; height: parent.height / 2; visible: self.width > 0 && self.real-x < parent.width; border-color: black; border-width: 1px; background: red; - + Text { x: 0; y: -self.height; text: event.label; visible: timeline-event.visible; } + start-txt := Text { + x: 0; + y: root.height - self.height - timeline-event.height; + text: timeline-event.x == timeline-event.real-x ? + TimeString.from(event.start) : + TimeString.from(visible-offset - visible-time); + visible: timeline-event.visible && + (self.width * 2 < timeline-event.width || + (!end-txt.visible && self.width < timeline-event.width)); + } + end-txt := Text { + x: timeline-event.width - self.width; + y: root.height - self.height - timeline-event.height; + text: timeline-event.x + timeline-event.real-width <= root.width ? + TimeString.from(event.start + event.duration) : + TimeString.from(visible-offset); + visible: timeline-event.visible && timeline-event.width - self.width * 2 > 0; + } } @children } From 8df3893baa57d3b1817c3f78e27b0182ce8ddf6e Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Mon, 15 Sep 2025 17:26:04 +0300 Subject: [PATCH 06/12] Minimizable record options --- ui/record.slint | 18 ++++++++++++++---- ui/timeline.slint | 6 ++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/ui/record.slint b/ui/record.slint index d8a942a..1dff7b4 100644 --- a/ui/record.slint +++ b/ui/record.slint @@ -13,21 +13,30 @@ export component RecordWidget inherits VerticalBox { in-out property events <=> tl.events; in property<[string]> combo-spans: []; in-out property in-progress: false; - property event-name <=> le.text; + property event-name: ""; + property minimized: false; + property combo-index: 0; tl := Timeline { + preferred-height: 100%; updating: true; + clicked => { + minimized = !minimized; + } } - GridLayout { + if !minimized: GridLayout { spacing-vertical: 8px; spacing-horizontal: 16px; le := LineEdit { placeholder-text: "Event name"; - text: "Event name"; + text: event-name; font-size: 24px; horizontal-alignment: center; colspan: 2; row: 0; + edited(text) => { + event-name = text; + } } Button { text: in-progress ? "Stop" : "Start"; @@ -58,11 +67,12 @@ export component RecordWidget inherits VerticalBox { } ComboBox { model: combo-spans; - current-index: 0; + current-index: combo-index; row: 2; col: 1; selected(current-value) => { root.update-visible-time(current-value); + combo-index = self.current-index; } } } diff --git a/ui/timeline.slint b/ui/timeline.slint index 4b94d03..c9fef81 100644 --- a/ui/timeline.slint +++ b/ui/timeline.slint @@ -25,6 +25,7 @@ global TimeString { export component Timeline inherits Rectangle { callback new-day-started; + callback clicked <=> ta.clicked; in-out property updating: true; in-out property<[TimelineEvent]> events: []; @@ -46,6 +47,11 @@ export component Timeline inherits Rectangle { } } + ta := TouchArea { + preferred-width: 100%; + preferred-height: 100%; + } + background: gray; border-width: 1px; border-color: black; 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 07/12] 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 08/12] 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 09/12] 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 10/12] 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; } From 190ed0639af56edfbea01e8937ac8f99743c7033 Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Fri, 19 Sep 2025 00:30:55 +0300 Subject: [PATCH 11/12] Added config example --- README.md | 12 +++++------- config.toml | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 config.toml diff --git a/README.md b/README.md index be32e0b..563edcf 100644 --- a/README.md +++ b/README.md @@ -30,18 +30,16 @@ Run `cargo build --release` ## Usage -Just run `aliveline` by any preferred way. +Just run `aliveline` by any preferred way, for example: +``` +$ ./aliveline +``` ## Configuration Aliveline tries to find config at `$XDG_CONFIG_DIR/aliveline/config.toml`. If config isn't found, or `$XDG_CONFIG_DIR` is not set, Aliveline uses default values. -You can configure these values: -- `log_path` - - Type: `String` - - Default: `logs` - Path to directory where logs are located. - Path may be absolute or relative to config directory. +See the example [config.toml](http://2ndbeam.ru/git/2ndbeam/aliveline/src/branch/master/config.toml) for default values. ## Contribution You can contribute to Aliveline by creating issue on this repository, then we'll discuss it. diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..1bfb6b9 --- /dev/null +++ b/config.toml @@ -0,0 +1,53 @@ +# This is the default config for Aliveline. +# Note: All colors are of format 0xAARRGGBB + +# Path where logs are saved. May be relative to config dir or absolute. +log_path = "logs" + +# Colors used for events. For now Aliveline expects to have exactly 16 colors, but this is subject to change in future. +event_colors = [ + 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 +] + +# Colors used for event colors. Aliveline expects it to have same size as events. +text_colors = [ + 0xff_000000, + 0xff_000000, + 0xff_000000, + 0xff_000000, + 0xff_000000, + 0xff_ffffff, + 0xff_000000, + 0xff_000000, + 0xff_000000, + 0xff_ffffff, + 0xff_000000, + 0xff_000000, + 0xff_000000, + 0xff_000000, + 0xff_000000, + 0xff_000000 +] + +[colors] +# Color behind the timeline +background = 0xFF_808080 +# Color of the base timeline +timeline = 0xFF_a9a9a9 +# Color of background text (timestamps, event names, etc.) +background_text = 0xFF_000000 From dc10194971955dc4882fff78a57f0803075829f1 Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Fri, 19 Sep 2025 00:33:23 +0300 Subject: [PATCH 12/12] Bump version to 0.2.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7877dc2..283cabe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -147,7 +147,7 @@ dependencies = [ [[package]] name = "aliveline" -version = "0.1.1" +version = "0.2.0" dependencies = [ "chrono", "serde", diff --git a/Cargo.toml b/Cargo.toml index 7675ffc..4780d86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aliveline" -version = "0.1.1" +version = "0.2.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html