Compare commits

...

4 commits

Author SHA1 Message Date
ba976d9e12 Made every thing in Config optional internally 2025-09-19 00:10:20 +03:00
1b6f8ef282 Configurable colors 2025-09-18 17:33:34 +03:00
ca3c171698 Improved color picker algorithm 2025-09-18 16:35:30 +03:00
218ee49a8b Pseudo pseudorandom color picker 2025-09-18 16:30:37 +03:00
8 changed files with 220 additions and 21 deletions

View file

@ -15,4 +15,4 @@ toml = "0.9.5"
slint-build = "1.12.1"
[profile.release]
opt-level = "s"
opt-level = 3

View file

@ -2,23 +2,138 @@ use std::path::PathBuf;
use serde::Deserialize;
#[derive(Deserialize)]
struct RawColors {
pub background: Option<u32>,
pub timeline: Option<u32>,
pub background_text: Option<u32>
}
#[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<RawColors> 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<PathBuf>,
pub colors: Option<RawColors>,
pub event_colors: Option<Vec<u32>>,
pub text_colors: Option<Vec<u32>>
}
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<u32>,
pub text_colors: Vec<u32>
}
impl Default for Config {
fn default() -> Self {
let conf_path = PathBuf::new();
let colors: Colors = Default::default();
let event_colors: Vec<u32> = 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<u32> = vec![
0xff000000,
0xff000000,
0xff000000,
0xff000000,
0xff000000,
0xffffffff,
0xff000000,
0xff000000,
0xff000000,
0xffffffff,
0xff000000,
0xff000000,
0xff000000,
0xff000000,
0xff000000,
0xff000000
];
Config {
conf_path,
log_path: PathBuf::from("./logs"),
colors,
event_colors,
text_colors
}
}
}
impl From<RawConfig> 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, log_path: PathBuf::from("./logs") }
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::<Self>(&toml_string);
if let Ok(mut conf) = conf {
let conf = toml::from_str::<RawConfig>(&toml_string);
if let Ok(raw_conf) = conf {
let mut conf: Config = raw_conf.into();
conf.conf_path = path.parent().unwrap().into();
return conf;
}

View file

@ -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,10 @@ 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 % 16) as i32 }

View file

@ -3,9 +3,9 @@
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 slint::{Color, Model, ModelRc, SharedString, ToSharedString, VecModel, Weak};
use toml::value::{Date as TomlDate, Time};
slint::include_modules!();
@ -22,7 +22,8 @@ impl From<Event> 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)
}
}
}
@ -60,6 +61,26 @@ fn load_log(ui_weak: Weak<AppWindow>, log: Arc<Mutex<Log>>) {
ui.set_in_progress(in_progress);
}
fn load_colors(ui_weak: Weak<AppWindow>, config: Arc<Config>) {
let ui = ui_weak.unwrap();
let pal = ui.global::<Palette>();
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<Color> = Rc::new(VecModel::from(
config.event_colors.iter()
.map(|value| Color::from_argb_encoded(*value)).collect::<Vec<Color>>()
)).into();
pal.set_event_colors(event_colors_rc);
let event_text_rc: ModelRc<Color> = Rc::new(VecModel::from(
config.text_colors.iter()
.map(|value| Color::from_argb_encoded(*value)).collect::<Vec<Color>>()
)).into();
pal.set_event_text(event_text_rc);
}
fn main() -> Result<(), Box<dyn Error>> {
let ui = AppWindow::new()?;
@ -79,6 +100,10 @@ fn main() -> Result<(), Box<dyn Error>> {
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({
@ -143,8 +168,9 @@ fn main() -> Result<(), Box<dyn Error>> {
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 +203,9 @@ fn main() -> Result<(), Box<dyn Error>> {
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 +242,7 @@ fn main() -> Result<(), Box<dyn Error>> {
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
}
};

View file

@ -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;
@ -31,7 +32,6 @@ export component AppWindow inherits Window {
property<[string]> combo-spans: ["1 Hour", "4 Hours", "8 Hours", "24 Hours"];
title: "Aliveline";
TabWidget {
Tab {
title: "Record";

View file

@ -16,7 +16,6 @@ export component RecordWidget inherits VerticalBox {
property<string> event-name: "";
property<bool> minimized: false;
property<int> combo-index: 0;
tl := Timeline {
preferred-height: 100%;
updating: true;

43
ui/theme.slint Normal file
View file

@ -0,0 +1,43 @@
export global Palette {
in-out property<color> background: gray;
in-out property<color> timeline: darkgray;
in-out property<color> 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
];
}

View file

@ -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<bool> 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