feat: stdout logging capabilities

- Added feature "stdout_logging" (see README)
- Added config.loglevel option (see config.toml)
- Logging some common things
- Hopefully fix on_new_day_started
This commit is contained in:
Alexey 2026-05-08 16:04:39 +03:00
commit 28b842b8c4
8 changed files with 669 additions and 550 deletions

1102
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -5,6 +5,8 @@ edition = "2021"
[dependencies] [dependencies]
chrono = "0.4.42" chrono = "0.4.42"
colog = { version = "1.4.0", optional = true }
log = { version = "0.4.29", features = ["serde"] }
serde = "1.0.219" serde = "1.0.219"
slint = "1.15.1" slint = "1.15.1"
toml = "1.1.2" toml = "1.1.2"
@ -16,5 +18,7 @@ slint-build = "1.15.1"
opt-level = 3 opt-level = 3
[features] [features]
default = ["stdout_logger"]
light = [] light = []
dark = [] dark = []
stdout_logger = ["colog"]

View file

@ -40,14 +40,17 @@ _Note: if event is not finished yet, it may have_ `end = start`.
- Slint dependencies (see [Platforms](https://docs.slint.dev/latest/docs/slint/guide/platforms/desktop/) and [Backends & Renderers](https://docs.slint.dev/latest/docs/slint/guide/backends-and-renderers/backends_and_renderers/)) - Slint dependencies (see [Platforms](https://docs.slint.dev/latest/docs/slint/guide/platforms/desktop/) and [Backends & Renderers](https://docs.slint.dev/latest/docs/slint/guide/backends-and-renderers/backends_and_renderers/))
### Feature flags ### Feature flags
By default Aliveline compiles with theme autodetection, provided by Slint, which sometimes does not work on Linux. By default Aliveline compiles with theme autodetection, provided by Slint.
You can use these flags to compile Aliveline with selected theme: On Linux, it depends on [org.freedesktop.portal.Settings](https://wiki.archlinux.org/title/XDG_Desktop_Portal).
On Windows it should work out of the box
You can also use one of these flags to compile Aliveline with selected theme:
- `light` - `light`
- `dark` - `dark`
### Instructions ### Instructions
Run `cargo build --release` Run `cargo build --release`
Pass features in build command with `-F foo` or `--feature foo` Pass features in build command with `-F foo` or `--feature foo`
Pass `--no-default-features` to disable stdout logging
Resulting binary will be located at `target/release/aliveline[.exe]` Resulting binary will be located at `target/release/aliveline[.exe]`
If compilation fails, double check that you have all required dependencies. If it still fails, file an issue on [Codeberg](https://codeberg.org/2ndbeam/aliveline/issues), including logs and system info. If compilation fails, double check that you have all required dependencies. If it still fails, file an issue on [Codeberg](https://codeberg.org/2ndbeam/aliveline/issues), including logs and system info.

View file

@ -1,5 +1,8 @@
# Default Aliveline config # Default Aliveline config
# Set stdout logging level from 0 to 5, higher is more verbose
loglevel = 3
# Paths may be relative to config directory or absolute # Paths may be relative to config directory or absolute
[paths] [paths]
logs = "logs" logs = "logs"

View file

@ -1,4 +1,5 @@
use std::{collections::HashMap, hash::{DefaultHasher, Hash, Hasher}}; use std::{collections::HashMap, hash::{DefaultHasher, Hash, Hasher}};
use log::warn;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use slint::Color; use slint::Color;
@ -92,7 +93,13 @@ impl Colors {
/// Get either the color or fallback color /// Get either the color or fallback color
#[inline(always)] #[inline(always)]
pub fn get_color(&self, color: &ConfigColor) -> Color { pub fn get_color(&self, color: &ConfigColor) -> Color {
self.try_get_color(color).unwrap_or_else(|| self.fallback_color()) self.try_get_color(color)
.unwrap_or_else(|| {
// raw color can't return none so it's an alias
let ConfigColor::Alias(alias) = color else { unreachable!() };
warn!("Color alias \"{alias}\" is not defined, using fallback color");
self.fallback_color()
})
} }
/// Compute hash and choose a color for the given event string /// Compute hash and choose a color for the given event string
@ -112,7 +119,8 @@ impl Colors {
chosen = new_count; chosen = new_count;
}, },
None => { None => {
return &self.events[id]; let out = &self.events[id];
return out;
}, },
} }
} }

View file

@ -1,4 +1,5 @@
use std::path::PathBuf; use std::path::PathBuf;
use log::LevelFilter;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use color::Colors; use color::Colors;
@ -22,7 +23,7 @@ impl Default for Paths {
} }
/// Configuration struct /// Configuration struct
#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Eq, Debug)] #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
#[serde(default)] #[serde(default)]
pub struct Config { pub struct Config {
/// Directory, where config is located /// Directory, where config is located
@ -32,6 +33,19 @@ pub struct Config {
pub colors: Colors, pub colors: Colors,
/// Config paths /// Config paths
pub paths: Paths, pub paths: Paths,
/// Logging level (0 to 5, greater is more verbose)
pub loglevel: LevelFilter,
}
impl Default for Config {
fn default() -> Self {
Self {
conf_path: PathBuf::default(),
colors: Colors::default(),
paths: Paths::default(),
loglevel: LevelFilter::Info,
}
}
} }
impl Config { impl Config {

View file

@ -1,4 +1,5 @@
use config::Config; use config::Config;
use ::log::LevelFilter;
use std::path::PathBuf; use std::path::PathBuf;
/// Configuration module /// Configuration module
@ -39,11 +40,17 @@ pub fn load_config() -> Config {
]; ];
for (place_var, place) in places { for (place_var, place) in places {
if let Some(conf) = try_config_path(place_var, place.as_slice()) { if let Some(conf) = try_config_path(place_var, place.as_slice()) {
println!("Found config at ${place_var} / {place:?}");
return conf; return conf;
} }
println!("Config not found at ${place_var} / {place:?}");
} }
println!("Using config path ./config.toml"); let default_path = PathBuf::from("./config.toml");
Config::new(PathBuf::from("./config.toml")) Config::load(default_path.clone()).unwrap_or_else(|| { Config::new(default_path) })
}
/// [colog::init] with given log level filter
#[cfg(feature = "stdout_logger")]
pub fn init_stdout_logger(filter: LevelFilter) {
colog::default_builder()
.filter(None, filter)
.init();
} }

View file

@ -1,10 +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. // 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")] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use std::{error::Error, rc::Rc, sync::{Arc, Mutex}}; use std::{error::Error, rc::Rc, sync::{Arc, Mutex, MutexGuard}};
use aliveline::{config::{Config, color::Colors}, load_config, log::{Event, Log}}; use aliveline::{config::{Config, color::Colors}, load_config, log::{Event, Log}};
use chrono::{Datelike, Timelike}; use chrono::{Datelike, Timelike};
use log::{error, info};
use slint::{Color, Model, ModelRc, SharedString, ToSharedString, VecModel, Weak}; use slint::{Color, Model, ModelRc, SharedString, ToSharedString, VecModel, Weak};
use toml::value::{Date as TomlDate, Time}; use toml::value::{Date as TomlDate, Time};
@ -60,14 +61,23 @@ impl From<TimelineEvent> for Event {
} }
} }
fn lock_mutex<'a, T: Send + Sync>(mutex: &'a Arc<Mutex<T>>) -> MutexGuard<'a, T> {
mutex.lock().unwrap_or_else(|e| {
panic!("Couldn't acquire mutex lock: {e}");
})
}
fn load_log(ui_weak: Weak<AppWindow>, log: Arc<Mutex<Log>>, config: Arc<Config>) { fn load_log(ui_weak: Weak<AppWindow>, log: Arc<Mutex<Log>>, config: Arc<Config>) {
let ui = ui_weak.unwrap(); let ui = ui_weak.unwrap();
let log_guard = log.lock().expect("Log shouldn't be used twice"); let events: Vec<TimelineEvent> = {
let events: Vec<TimelineEvent> = (*log_guard) let log_guard = lock_mutex(&log);
.events info!("Loading log {}", log_guard.date);
.iter() (*log_guard)
.map(|event| TimelineEvent::from_event(event.clone(), &config.colors)) .events
.collect(); .iter()
.map(|event| TimelineEvent::from_event(event.clone(), &config.colors))
.collect()
};
let in_progress = events.iter().any(|event| !event.finished); let in_progress = events.iter().any(|event| !event.finished);
let model: ModelRc<TimelineEvent> = Rc::new(VecModel::from(events)).into(); let model: ModelRc<TimelineEvent> = Rc::new(VecModel::from(events)).into();
let mut state = ui.get_record_state(); let mut state = ui.get_record_state();
@ -97,7 +107,12 @@ fn main() -> Result<(), Box<dyn Error>> {
year: now.year() as u16 year: now.year() as u16
}; };
let config: Arc<Config> = Arc::new(load_config()); let config = load_config();
#[cfg(feature = "stdout_logger")]
aliveline::init_stdout_logger(config.loglevel);
let config: Arc<Config> = Arc::new(config);
let writing_log: Arc<Mutex<Log>> = Arc::new(Mutex::new(Log::load_from(&config, date))); let writing_log: Arc<Mutex<Log>> = Arc::new(Mutex::new(Log::load_from(&config, date)));
let ui_weak = ui.as_weak(); let ui_weak = ui.as_weak();
@ -137,9 +152,9 @@ fn main() -> Result<(), Box<dyn Error>> {
let config = config.clone(); let config = config.clone();
let log = writing_log.clone(); let log = writing_log.clone();
move || { move || {
let log_guard = log.lock().expect("Log shouldn't be used twice"); let log_guard = lock_mutex(&log);
if let Err(error) = (*log_guard).save(&config) { if let Err(error) = (*log_guard).save(&config) {
eprintln!("Error occured while saving log: {error}"); error!("Couldn't save log: {error}");
} }
} }
}); });
@ -169,6 +184,8 @@ fn main() -> Result<(), Box<dyn Error>> {
let log = writing_log.clone(); let log = writing_log.clone();
let config = config.clone(); let config = config.clone();
move |event_name: SharedString| { move |event_name: SharedString| {
info!("Starting event \"{event_name}\"");
let ui = ui_weak.unwrap(); let ui = ui_weak.unwrap();
let state = ui.get_record_state(); let state = ui.get_record_state();
@ -188,7 +205,7 @@ fn main() -> Result<(), Box<dyn Error>> {
}; };
{ {
let mut log_guard = log.lock().expect("Log shouldn't be used twice"); let mut log_guard = lock_mutex(&log);
(*log_guard).events.push(Event::from(event.clone())); (*log_guard).events.push(Event::from(event.clone()));
} }
@ -212,8 +229,12 @@ fn main() -> Result<(), Box<dyn Error>> {
let event_id = events.iter() let event_id = events.iter()
.position(|data| !data.finished) .position(|data| !data.finished)
.unwrap(); .unwrap();
let event = events.row_data(event_id) let Some(event) = events.row_data(event_id) else {
.expect("stop-event called without unfinished events"); error!("Couldn't find event to stop");
return;
};
info!("Stopping event \"{}\"", event.label);
let (background_color, text_color) = TimelineEvent::colors(&config.colors, event.label.as_str()); let (background_color, text_color) = TimelineEvent::colors(&config.colors, event.label.as_str());
@ -227,7 +248,7 @@ fn main() -> Result<(), Box<dyn Error>> {
}; };
{ {
let mut log_guard = log.lock().expect("Log shouldn't be used twice"); let mut log_guard = lock_mutex(&log);
log_guard.events.push(Event::from(new_event.clone())); log_guard.events.push(Event::from(new_event.clone()));
let index = log_guard.events.iter().position(|data| !data.finished).unwrap(); let index = log_guard.events.iter().position(|data| !data.finished).unwrap();
log_guard.events.swap_remove(index); log_guard.events.swap_remove(index);
@ -254,13 +275,10 @@ fn main() -> Result<(), Box<dyn Error>> {
move || { move || {
let ui = ui_weak.unwrap(); let ui = ui_weak.unwrap();
let prev_event_name = { let prev_event_name = {
let log_guard = log.lock().expect("Log shouldn't be used twice"); let log_guard = lock_mutex(&log);
match log_guard.events.len().checked_sub(2) { match log_guard.events.len().checked_sub(2) {
Some(prev_index) => { Some(prev_index) => {
let prev_event = log_guard.events let prev_event = log_guard.events[prev_index].name
.get(prev_index)
.expect("Index is already checked")
.name
.clone(); .clone();
Some(prev_event) Some(prev_event)
}, },
@ -277,9 +295,9 @@ fn main() -> Result<(), Box<dyn Error>> {
let config = config.clone(); let config = config.clone();
move || { move || {
let ui = ui_weak.unwrap(); let ui = ui_weak.unwrap();
info!("Starting new day");
let new_event: Option<Event> = { let new_event: Option<Event> = {
let log_guard = log.lock().expect("Log shouldn't be used twice"); let log_guard = lock_mutex(&log);
let maybe_unfinished_event = log_guard.events.iter().find(|event| !event.finished); let maybe_unfinished_event = log_guard.events.iter().find(|event| !event.finished);
match maybe_unfinished_event { match maybe_unfinished_event {
@ -287,11 +305,13 @@ fn main() -> Result<(), Box<dyn Error>> {
_ => None _ => None
} }
}; };
ui.invoke_stop_event(); if new_event.is_some() {
ui.invoke_stop_event();
}
{ {
let mut log_guard = log.lock().expect("Log shouldn't be used twice"); let mut log_guard = lock_mutex(&log);
log_guard.events.clear(); log_guard.events.clear();
let now = chrono::Local::now(); let now = chrono::Local::now();