a lightweight, interval-based utility to combat digital strain through "Ma" (intentional pauses) for the eyes and body.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: setup config types and load/save

+256
+46
src/config/mod.rs
··· 1 + pub mod types; 2 + pub use types::*; 3 + 4 + use anyhow::{Context, Result}; 5 + use std::path::PathBuf; 6 + 7 + pub fn config_path() -> PathBuf { 8 + #[cfg(target_os = "windows")] 9 + { 10 + dirs::config_dir() 11 + .unwrap_or_else(|| PathBuf::from(".")) 12 + .join("ioma") 13 + .join("config.toml") 14 + } 15 + #[cfg(not(target_os = "windows"))] 16 + { 17 + dirs::config_dir() 18 + .unwrap_or_else(|| dirs::home_dir().unwrap_or_default().join(".config")) 19 + .join("ioma") 20 + .join("config.toml") 21 + } 22 + } 23 + 24 + pub fn load() -> Result<AppConfig> { 25 + let path = config_path(); 26 + if !path.exists() { 27 + return Ok(AppConfig::default()); 28 + } 29 + let text = std::fs::read_to_string(&path) 30 + .with_context(|| format!("reading config from {}", path.display()))?; 31 + let cfg: AppConfig = toml::from_str(&text) 32 + .with_context(|| format!("parsing config from {}", path.display()))?; 33 + Ok(cfg) 34 + } 35 + 36 + pub fn save(cfg: &AppConfig) -> Result<()> { 37 + let path = config_path(); 38 + if let Some(parent) = path.parent() { 39 + std::fs::create_dir_all(parent) 40 + .with_context(|| format!("creating config dir {}", parent.display()))?; 41 + } 42 + let text = toml::to_string_pretty(cfg).context("serializing config")?; 43 + std::fs::write(&path, text) 44 + .with_context(|| format!("writing config to {}", path.display()))?; 45 + Ok(()) 46 + }
+210
src/config/types.rs
··· 1 + use serde::{Deserialize, Serialize}; 2 + use std::collections::HashMap; 3 + 4 + #[derive(Debug, Clone, Serialize, Deserialize)] 5 + #[serde(default)] 6 + pub struct AppConfig { 7 + pub app: AppSettings, 8 + pub profiles: HashMap<String, ProfileConfig>, 9 + pub appearance: AppearanceConfig, 10 + pub enforced: EnforcedConfig, 11 + } 12 + 13 + #[derive(Debug, Clone, Serialize, Deserialize)] 14 + #[serde(default)] 15 + pub struct AppSettings { 16 + pub active_profile: String, 17 + pub autostart: bool, 18 + } 19 + 20 + #[derive(Debug, Clone, Serialize, Deserialize)] 21 + #[serde(default)] 22 + pub struct AppearanceConfig { 23 + pub overlay_theme: OverlayTheme, 24 + pub sound_enabled: bool, 25 + pub sound_volume: f32, 26 + pub font_size: u32, 27 + } 28 + 29 + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 30 + #[serde(rename_all = "lowercase")] 31 + pub enum OverlayTheme { 32 + Dark, 33 + Light, 34 + System, 35 + } 36 + 37 + #[derive(Debug, Clone, Serialize, Deserialize)] 38 + #[serde(default)] 39 + pub struct EnforcedConfig { 40 + /// Argon2 hash of the emergency unlock password. Empty = not set. 41 + pub password_hash: String, 42 + } 43 + 44 + /// Serializable form of a break profile (stored in config file). 45 + #[derive(Debug, Clone, Serialize, Deserialize)] 46 + pub struct ProfileConfig { 47 + pub name: String, 48 + pub mode: BreakModeConfig, 49 + pub snooze_secs: u64, 50 + pub idle_threshold_secs: u64, 51 + pub idle_detection_enabled: bool, 52 + pub levels: Vec<BreakLevelConfig>, 53 + pub long_break: Option<LongBreakConfig>, 54 + } 55 + 56 + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 57 + #[serde(rename_all = "lowercase")] 58 + pub enum BreakModeConfig { 59 + Reminder, 60 + Enforced, 61 + } 62 + 63 + #[derive(Debug, Clone, Serialize, Deserialize)] 64 + pub struct BreakLevelConfig { 65 + pub work_secs: u64, 66 + pub break_secs: u64, 67 + pub label: String, 68 + } 69 + 70 + #[derive(Debug, Clone, Serialize, Deserialize)] 71 + pub struct LongBreakConfig { 72 + pub after_cycles: u32, 73 + pub max_cycle_gap_secs: u64, 74 + pub break_secs: u64, 75 + pub label: String, 76 + } 77 + 78 + // --- Defaults --- 79 + 80 + impl Default for AppConfig { 81 + fn default() -> Self { 82 + let mut profiles = HashMap::new(); 83 + profiles.insert("pomodoro".to_string(), ProfileConfig::pomodoro()); 84 + profiles.insert("52_17".to_string(), ProfileConfig::fifty_two_seventeen()); 85 + profiles.insert("20_20_20".to_string(), ProfileConfig::twenty_twenty_twenty()); 86 + profiles.insert("custom".to_string(), ProfileConfig::custom_hierarchical()); 87 + 88 + Self { 89 + app: AppSettings::default(), 90 + profiles, 91 + appearance: AppearanceConfig::default(), 92 + enforced: EnforcedConfig::default(), 93 + } 94 + } 95 + } 96 + 97 + impl Default for AppSettings { 98 + fn default() -> Self { 99 + Self { 100 + active_profile: "pomodoro".to_string(), 101 + autostart: false, 102 + } 103 + } 104 + } 105 + 106 + impl Default for AppearanceConfig { 107 + fn default() -> Self { 108 + Self { 109 + overlay_theme: OverlayTheme::Dark, 110 + sound_enabled: true, 111 + sound_volume: 0.7, 112 + font_size: 48, 113 + } 114 + } 115 + } 116 + 117 + impl Default for OverlayTheme { 118 + fn default() -> Self { 119 + OverlayTheme::Dark 120 + } 121 + } 122 + 123 + impl Default for EnforcedConfig { 124 + fn default() -> Self { 125 + Self { password_hash: String::new() } 126 + } 127 + } 128 + 129 + impl ProfileConfig { 130 + pub fn pomodoro() -> Self { 131 + Self { 132 + name: "Pomodoro".to_string(), 133 + mode: BreakModeConfig::Reminder, 134 + snooze_secs: 5 * 60, 135 + idle_threshold_secs: 5 * 60, 136 + idle_detection_enabled: true, 137 + levels: vec![BreakLevelConfig { 138 + work_secs: 25 * 60, 139 + break_secs: 5 * 60, 140 + label: "Short break".to_string(), 141 + }], 142 + long_break: Some(LongBreakConfig { 143 + after_cycles: 4, 144 + max_cycle_gap_secs: 30 * 60, 145 + break_secs: 15 * 60, 146 + label: "Long break".to_string(), 147 + }), 148 + } 149 + } 150 + 151 + pub fn fifty_two_seventeen() -> Self { 152 + Self { 153 + name: "52/17".to_string(), 154 + mode: BreakModeConfig::Reminder, 155 + snooze_secs: 5 * 60, 156 + idle_threshold_secs: 5 * 60, 157 + idle_detection_enabled: true, 158 + levels: vec![BreakLevelConfig { 159 + work_secs: 52 * 60, 160 + break_secs: 17 * 60, 161 + label: "Recharge break".to_string(), 162 + }], 163 + long_break: None, 164 + } 165 + } 166 + 167 + pub fn twenty_twenty_twenty() -> Self { 168 + Self { 169 + name: "20-20-20".to_string(), 170 + mode: BreakModeConfig::Reminder, 171 + snooze_secs: 2 * 60, 172 + idle_threshold_secs: 3 * 60, 173 + idle_detection_enabled: true, 174 + levels: vec![BreakLevelConfig { 175 + work_secs: 20 * 60, 176 + break_secs: 20, 177 + label: "Eye break — look 20 ft away".to_string(), 178 + }], 179 + long_break: None, 180 + } 181 + } 182 + 183 + pub fn custom_hierarchical() -> Self { 184 + Self { 185 + name: "Focus+Micro".to_string(), 186 + mode: BreakModeConfig::Reminder, 187 + snooze_secs: 5 * 60, 188 + idle_threshold_secs: 5 * 60, 189 + idle_detection_enabled: true, 190 + levels: vec![ 191 + BreakLevelConfig { 192 + work_secs: 10 * 60, 193 + break_secs: 15, 194 + label: "Micro-break".to_string(), 195 + }, 196 + BreakLevelConfig { 197 + work_secs: 60 * 60, 198 + break_secs: 10 * 60, 199 + label: "Stretch break".to_string(), 200 + }, 201 + ], 202 + long_break: Some(LongBreakConfig { 203 + after_cycles: 3, 204 + max_cycle_gap_secs: 30 * 60, 205 + break_secs: 30 * 60, 206 + label: "Long rest".to_string(), 207 + }), 208 + } 209 + } 210 + }