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]
chrono = "0.4.42"
colog = { version = "1.4.0", optional = true }
log = { version = "0.4.29", features = ["serde"] }
serde = "1.0.219"
slint = "1.15.1"
toml = "1.1.2"
@ -16,5 +18,7 @@ slint-build = "1.15.1"
opt-level = 3
[features]
default = ["stdout_logger"]
light = []
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/))
### Feature flags
By default Aliveline compiles with theme autodetection, provided by Slint, which sometimes does not work on Linux.
You can use these flags to compile Aliveline with selected theme:
By default Aliveline compiles with theme autodetection, provided by Slint.
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`
- `dark`
### Instructions
Run `cargo build --release`
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]`
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
# Set stdout logging level from 0 to 5, higher is more verbose
loglevel = 3
# Paths may be relative to config directory or absolute
[paths]
logs = "logs"

View file

@ -1,4 +1,5 @@
use std::{collections::HashMap, hash::{DefaultHasher, Hash, Hasher}};
use log::warn;
use serde::{Serialize, Deserialize};
use slint::Color;
@ -92,7 +93,13 @@ impl Colors {
/// Get either the color or fallback color
#[inline(always)]
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
@ -112,7 +119,8 @@ impl Colors {
chosen = new_count;
},
None => {
return &self.events[id];
let out = &self.events[id];
return out;
},
}
}

View file

@ -1,4 +1,5 @@
use std::path::PathBuf;
use log::LevelFilter;
use serde::{Deserialize, Serialize};
use color::Colors;
@ -22,7 +23,7 @@ impl Default for Paths {
}
/// Configuration struct
#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Eq, Debug)]
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
#[serde(default)]
pub struct Config {
/// Directory, where config is located
@ -32,6 +33,19 @@ pub struct Config {
pub colors: Colors,
/// Config 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 {

View file

@ -1,4 +1,5 @@
use config::Config;
use ::log::LevelFilter;
use std::path::PathBuf;
/// Configuration module
@ -39,11 +40,17 @@ pub fn load_config() -> Config {
];
for (place_var, place) in places {
if let Some(conf) = try_config_path(place_var, place.as_slice()) {
println!("Found config at ${place_var} / {place:?}");
return conf;
}
println!("Config not found at ${place_var} / {place:?}");
}
println!("Using config path ./config.toml");
Config::new(PathBuf::from("./config.toml"))
let default_path = 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.
#![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 chrono::{Datelike, Timelike};
use log::{error, info};
use slint::{Color, Model, ModelRc, SharedString, ToSharedString, VecModel, Weak};
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>) {
let ui = ui_weak.unwrap();
let log_guard = log.lock().expect("Log shouldn't be used twice");
let events: Vec<TimelineEvent> = (*log_guard)
.events
.iter()
.map(|event| TimelineEvent::from_event(event.clone(), &config.colors))
.collect();
let events: Vec<TimelineEvent> = {
let log_guard = lock_mutex(&log);
info!("Loading log {}", log_guard.date);
(*log_guard)
.events
.iter()
.map(|event| TimelineEvent::from_event(event.clone(), &config.colors))
.collect()
};
let in_progress = events.iter().any(|event| !event.finished);
let model: ModelRc<TimelineEvent> = Rc::new(VecModel::from(events)).into();
let mut state = ui.get_record_state();
@ -97,7 +107,12 @@ fn main() -> Result<(), Box<dyn Error>> {
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 ui_weak = ui.as_weak();
@ -137,9 +152,9 @@ fn main() -> Result<(), Box<dyn Error>> {
let config = config.clone();
let log = writing_log.clone();
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) {
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 config = config.clone();
move |event_name: SharedString| {
info!("Starting event \"{event_name}\"");
let ui = ui_weak.unwrap();
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()));
}
@ -212,8 +229,12 @@ fn main() -> Result<(), Box<dyn Error>> {
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 Some(event) = events.row_data(event_id) else {
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());
@ -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()));
let index = log_guard.events.iter().position(|data| !data.finished).unwrap();
log_guard.events.swap_remove(index);
@ -254,13 +275,10 @@ fn main() -> Result<(), Box<dyn Error>> {
move || {
let ui = ui_weak.unwrap();
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) {
Some(prev_index) => {
let prev_event = log_guard.events
.get(prev_index)
.expect("Index is already checked")
.name
let prev_event = log_guard.events[prev_index].name
.clone();
Some(prev_event)
},
@ -277,9 +295,9 @@ fn main() -> Result<(), Box<dyn Error>> {
let config = config.clone();
move || {
let ui = ui_weak.unwrap();
info!("Starting new day");
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);
match maybe_unfinished_event {
@ -287,11 +305,13 @@ fn main() -> Result<(), Box<dyn Error>> {
_ => 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();
let now = chrono::Local::now();