Load basic config

This commit is contained in:
Candifloss 2026-02-28 13:11:14 +05:30
parent 7cb750cfa7
commit 0fe4c45b31
5 changed files with 254 additions and 21 deletions

View File

@ -5,6 +5,7 @@ edition = "2024"
[dependencies]
alarm_widg_config = {path = "../alarm_widg_config"}
chrono = "0.4.44"
dirs = "6.0.0"
i-slint-backend-winit = "1.15.1"
slint = "1.15.1"

View File

@ -1,6 +1,78 @@
mod show_popup;
fn main() -> Result<(), Box<dyn std::error::Error>> {
show_popup::show_popup()?;
use alarm_widg_config::Config;
use chrono::{Datelike, Local, Timelike};
use show_popup::{PopupData, show_popup};
use std::{collections::HashSet, thread, time::Duration};
use alarm_widg_config::Alarm;
use chrono::NaiveDate;
fn alarm_matches_today(alarm: &Alarm, today: NaiveDate) -> bool {
if let Some(days) = &alarm.days {
let weekday = today.weekday().num_days_from_sunday() as u8;
return days.contains(&weekday);
}
if let Some(dates) = &alarm.dates {
return dates.contains(&today);
}
false
}
fn trigger_alarm(alarm: &Alarm) -> Result<(), Box<dyn std::error::Error>> {
println!("Triggering alarm: {}", alarm.title);
let popup = PopupData {
title: alarm.title.clone(),
time: alarm.time.format("%I:%M%p").to_string(),
color: alarm.color.unwrap_or(0xff232323),
};
// UI must run on main thread in many backends.
// For now we block until closed.
show_popup(popup)?;
Ok(())
}
fn run_scheduler(config: Config) -> Result<(), Box<dyn std::error::Error>> {
println!("alarm_widg scheduler started");
// prevents duplicate triggers within same minute
let mut fired: HashSet<(String, i64)> = HashSet::new();
loop {
let now = Local::now();
for alarm in &config.alarms {
if !alarm.enabled {
continue;
}
if !alarm_matches_today(alarm, now.date_naive()) {
continue;
}
if alarm.time.hour() == now.hour() && alarm.time.minute() == now.minute() {
let key = (alarm.title.clone(), now.timestamp() / 60);
if fired.insert(key) {
trigger_alarm(alarm)?;
}
}
}
// keep memory bounded
fired.retain(|(_, minute)| now.timestamp() / 60 - *minute < 5);
thread::sleep(Duration::from_secs(1));
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = Config::load()?;
run_scheduler(config)
}

View File

@ -10,24 +10,27 @@ use i_slint_backend_winit::{
slint::include_modules!();
fn set_ui_props(ui: &MainWindow) {
// Window placement
ui.window().set_position(LogicalPosition::new(200.0, 35.0));
pub struct PopupData {
pub title: String,
pub time: String,
pub color: u32,
}
// Window size
fn set_ui_props(ui: &MainWindow, data: &PopupData) {
ui.window().set_position(LogicalPosition::new(200.0, 35.0));
ui.window().set_size(LogicalSize::new(300.0, 124.0));
// Alarm properties
ui.set_alarm_name(SharedString::from("Sample alarm"));
ui.set_alarm_time(SharedString::from("11:45AM"));
ui.set_alarm_color(Color::from_argb_encoded(0xff232323));
ui.set_alarm_name(SharedString::from(data.title.clone()));
ui.set_alarm_time(SharedString::from(data.time.clone()));
ui.set_alarm_color(Color::from_argb_encoded(data.color));
ui.set_popup_width(300);
ui.set_popup_height(124);
ui.set_default_font(SharedString::from("IosevkaTermSlab Nerd Font Mono"));
ui.set_alarm_icon(SharedString::from("🕓"));
}
pub fn show_popup() -> Result<(), Box<dyn std::error::Error>> {
pub fn show_popup(data: PopupData) -> Result<(), Box<dyn std::error::Error>> {
// Closure to configure X11 window attributes before Slint creates the window.
let window_attrs = |attrs: WindowAttributes| {
attrs
@ -49,7 +52,7 @@ pub fn show_popup() -> Result<(), Box<dyn std::error::Error>> {
let ui = MainWindow::new()?;
// Send the alarm data to the UI as properties
set_ui_props(&ui);
set_ui_props(&ui, &data);
// Run the UI
ui.run()?;

View File

@ -4,6 +4,8 @@ version = "0.1.0"
edition = "2024"
[dependencies]
chrono = { version = "0.4.44", features = ["serde"] }
dirs = "6.0.0"
serde = { version = "1.0.228", features = ["derive"] }
thiserror = "2.0.18"
toml = "1.0.3"

View File

@ -1,14 +1,169 @@
pub fn add(left: u64, right: u64) -> u64 {
left + right
use chrono::{NaiveDate, NaiveTime};
use dirs::config_dir;
use serde::Deserialize;
use std::{fs, path::PathBuf};
use thiserror::Error;
const CONFIG_PATH: &str = "candywidgets/alarm-app/config.toml";
// Errors
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("config directory not found")]
NoConfigDir,
#[error("config file not found: {0}")]
MissingFile(PathBuf),
#[error("failed to read config: {0}")]
Io(#[from] std::io::Error),
#[error("toml parse error: {0}")]
Toml(#[from] toml::de::Error),
#[error("invalid alarm: {0}")]
Invalid(String),
}
#[cfg(test)]
mod tests {
use super::*;
// Public API Types
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
#[derive(Debug, Clone)]
pub struct Config {
pub alarms: Vec<Alarm>,
}
#[derive(Debug, Clone)]
pub struct Alarm {
pub title: String,
pub time: NaiveTime,
pub days: Option<Vec<u8>>,
pub dates: Option<Vec<NaiveDate>>,
pub color: Option<u32>,
pub enabled: bool,
}
// TOML Deserialization Layer
// (separate from public types intentionally)
#[derive(Debug, Deserialize)]
struct RawConfig {
#[serde(default)]
alarm: Vec<RawAlarm>,
}
#[derive(Debug, Deserialize)]
struct RawAlarm {
title: String,
time: String,
days: Option<Vec<u8>>,
dates: Option<Vec<String>>,
color: Option<String>,
#[serde(default = "default_enabled")]
enabled: bool,
}
fn default_enabled() -> bool {
true
}
// Loader
impl Config {
pub fn load() -> Result<Self, ConfigError> {
let path = config_path()?;
if !path.exists() {
return Err(ConfigError::MissingFile(path));
}
let content = fs::read_to_string(&path)?;
let raw: RawConfig = toml::from_str(&content)?;
let alarms = raw
.alarm
.into_iter()
.map(Alarm::from_raw)
.collect::<Result<Vec<_>, _>>()?;
Ok(Self { alarms })
}
}
// Conversion + Validation
impl Alarm {
fn from_raw(raw: RawAlarm) -> Result<Self, ConfigError> {
// validate schedule
if raw.days.is_none() && raw.dates.is_none() {
return Err(ConfigError::Invalid(format!(
"alarm '{}' must specify 'days' or 'dates'",
raw.title
)));
}
// parse time
let time = NaiveTime::parse_from_str(&raw.time, "%H:%M").map_err(|_| {
ConfigError::Invalid(format!("invalid time '{}' in '{}'", raw.time, raw.title))
})?;
// validate days
if let Some(ref days) = raw.days {
for d in days {
if *d > 6 {
return Err(ConfigError::Invalid(format!(
"day '{}' out of range in '{}'",
d, raw.title
)));
}
}
}
// parse dates
let dates = match raw.dates {
Some(list) => {
let mut out = Vec::new();
for d in list {
let parsed = NaiveDate::parse_from_str(&d, "%Y-%m-%d").map_err(|_| {
ConfigError::Invalid(format!("invalid date '{}' in '{}'", d, raw.title))
})?;
out.push(parsed);
}
Some(out)
}
None => None,
};
// parse color (ARGB hex)
let color = match raw.color {
Some(c) => Some(parse_argb(&c)?),
None => None,
};
Ok(Self {
title: raw.title,
time,
days: raw.days,
dates,
color,
enabled: raw.enabled,
})
}
}
// Helpers
fn config_path() -> Result<PathBuf, ConfigError> {
let base = config_dir().ok_or(ConfigError::NoConfigDir)?;
Ok(base.join(CONFIG_PATH))
}
fn parse_argb(input: &str) -> Result<u32, ConfigError> {
let cleaned = input.trim_start_matches('#');
u32::from_str_radix(cleaned, 16)
.map_err(|_| ConfigError::Invalid(format!("invalid ARGB color '{input}'")))
}