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.

fix: merge tabs, widen spinboxes

+144 -157
+1 -1
src/main.rs
··· 100 100 if settings_mgr.is_none() { 101 101 // First open: create window, wire callbacks, then show. 102 102 let cfg_snap = cfg_arc.lock().unwrap().clone(); 103 - match SettingsManager::new(&cfg_snap) { 103 + match SettingsManager::new(&cfg_snap, cfg_arc.clone()) { 104 104 Ok(mgr) => { 105 105 // Cancel 106 106 {
+4 -3
src/settings/mod.rs
··· 1 1 use std::rc::Rc; 2 + use std::sync::{Arc, Mutex}; 2 3 3 4 use slint::{ComponentHandle, Model, ModelRc, SharedString, VecModel}; 4 5 ··· 12 13 } 13 14 14 15 impl SettingsManager { 15 - pub fn new(cfg: &AppConfig) -> anyhow::Result<Self> { 16 + pub fn new(cfg: &AppConfig, cfg_arc: Arc<Mutex<AppConfig>>) -> anyhow::Result<Self> { 16 17 let window = SettingsWindow::new()?; 17 18 let levels_model: Rc<VecModel<LevelEntry>> = Rc::new(VecModel::default()); 18 19 window.set_levels(ModelRc::new(Rc::clone(&levels_model))); ··· 46 47 }); 47 48 } 48 49 { 49 - let cfg_clone = cfg.clone(); 50 50 let model = Rc::clone(&levels_model); 51 51 let win2 = window.clone_strong(); 52 52 window.on_profile_changed(move |name| { 53 - populate_for_profile(&win2, &model, &cfg_clone, name.as_str()); 53 + let cfg = cfg_arc.lock().unwrap().clone(); 54 + populate_for_profile(&win2, &model, &cfg, name.as_str()); 54 55 }); 55 56 } 56 57
+139 -153
ui/settings.slint
··· 9 9 10 10 export component SettingsWindow inherits Window { 11 11 title: "ioma — Settings"; 12 - width: 600px; 13 - height: 520px; 12 + width: 620px; 13 + height: 540px; 14 14 15 - // Populated from Rust. 16 15 in-out property <bool> enforced-mode: false; 17 16 in-out property <bool> sound-enabled: true; 18 17 in-out property <float> sound-volume: 0.7; ··· 22 21 in-out property <string> active-profile: "Pomodoro"; 23 22 in-out property <[string]> profile-names: ["Pomodoro", "52/17", "20-20-20", "Focus+Micro"]; 24 23 25 - // Profile level editor 26 24 in-out property <[LevelEntry]> levels: []; 27 25 in-out property <bool> long-break-enabled: false; 28 26 in-out property <int> long-break-after-cycles: 4; ··· 44 42 spacing: 12px; 45 43 46 44 TabWidget { 45 + 46 + // ── Merged general settings tab ────────────────────────────── 47 47 Tab { 48 - title: "Mode"; 48 + title: "Settings"; 49 49 VerticalLayout { 50 50 alignment: start; 51 51 padding: 16px; 52 - spacing: 12px; 52 + spacing: 10px; 53 + 54 + // Active profile selector 55 + HorizontalLayout { 56 + alignment: start; 57 + spacing: 10px; 58 + Text { text: "Active profile:"; vertical-alignment: center; } 59 + ComboBox { 60 + model: profile-names; 61 + current-value <=> active-profile; 62 + selected(name) => { root.profile-changed(name); } 63 + } 64 + } 65 + 66 + Rectangle { height: 1px; background: #555555; } 53 67 68 + // Mode 54 69 CheckBox { 55 70 text: "Enforced mode (full-screen, requires password to unlock early)"; 56 71 checked <=> enforced-mode; 57 72 } 58 - 59 73 if enforced-mode : Button { 60 74 text: "Set emergency unlock password…"; 61 75 clicked => { root.set-password-clicked(); } 62 76 } 63 - 64 77 Text { 65 78 text: "In reminder mode a single snooze is allowed per break interval."; 66 79 wrap: word-wrap; 67 80 color: #888888; 68 81 font-size: 13px; 69 82 } 70 - } 71 - } 72 83 73 - Tab { 74 - title: "Profile"; 75 - VerticalLayout { 76 - alignment: start; 77 - padding: 16px; 78 - spacing: 10px; 79 - 80 - HorizontalLayout { 81 - spacing: 8px; 82 - Text { text: "Active profile:"; vertical-alignment: center; } 83 - ComboBox { 84 - model: profile-names; 85 - current-value <=> active-profile; 86 - selected(name) => { root.profile-changed(name); } 87 - } 88 - } 89 - 90 - Rectangle { height: 1px; background: #cccccc; } 91 - Text { text: "Break levels:"; font-weight: 600; } 92 - 93 - for level[i] in levels : HorizontalLayout { 94 - spacing: 6px; 95 - alignment: start; 96 - SpinBox { 97 - width: 68px; 98 - value: level.work-mins; 99 - minimum: 1; 100 - maximum: 240; 101 - edited(v) => { root.level-changed(i, { work-mins: v, break-mins: level.break-mins, break-extra-secs: level.break-extra-secs, label: level.label }); } 102 - } 103 - Text { text: "min →"; vertical-alignment: center; } 104 - SpinBox { 105 - width: 58px; 106 - value: level.break-mins; 107 - minimum: 0; 108 - maximum: 120; 109 - edited(v) => { root.level-changed(i, { work-mins: level.work-mins, break-mins: v, break-extra-secs: level.break-extra-secs, label: level.label }); } 110 - } 111 - Text { text: "m"; vertical-alignment: center; } 112 - SpinBox { 113 - width: 58px; 114 - value: level.break-extra-secs; 115 - minimum: 0; 116 - maximum: 59; 117 - edited(v) => { root.level-changed(i, { work-mins: level.work-mins, break-mins: level.break-mins, break-extra-secs: v, label: level.label }); } 118 - } 119 - Text { text: "s"; vertical-alignment: center; } 120 - LineEdit { 121 - width: 130px; 122 - text: level.label; 123 - edited(v) => { root.level-changed(i, { work-mins: level.work-mins, break-mins: level.break-mins, break-extra-secs: level.break-extra-secs, label: v }); } 124 - } 125 - Button { 126 - width: 32px; 127 - text: "✕"; 128 - clicked => { root.level-removed(i); } 129 - } 130 - } 131 - 132 - Button { 133 - text: "+ Add level"; 134 - clicked => { root.level-added(); } 135 - } 136 - 137 - Rectangle { height: 1px; background: #cccccc; } 138 - 139 - CheckBox { 140 - text: "Long break"; 141 - checked <=> long-break-enabled; 142 - } 143 - 144 - if long-break-enabled : VerticalLayout { 145 - spacing: 8px; 146 - padding-left: 16px; 147 - 148 - HorizontalLayout { 149 - spacing: 6px; 150 - Text { text: "After"; vertical-alignment: center; } 151 - SpinBox { 152 - width: 58px; 153 - value <=> long-break-after-cycles; 154 - minimum: 1; 155 - maximum: 20; 156 - } 157 - Text { text: "cycles, take a"; vertical-alignment: center; } 158 - SpinBox { 159 - width: 58px; 160 - value <=> long-break-duration-mins; 161 - minimum: 1; 162 - maximum: 120; 163 - } 164 - Text { text: "min break"; vertical-alignment: center; } 165 - } 166 - 167 - HorizontalLayout { 168 - spacing: 6px; 169 - Text { text: "Reset cycle count if gap >"; vertical-alignment: center; } 170 - SpinBox { 171 - width: 58px; 172 - value <=> long-break-gap-mins; 173 - minimum: 5; 174 - maximum: 240; 175 - } 176 - Text { text: "min"; vertical-alignment: center; } 177 - } 178 - 179 - HorizontalLayout { 180 - spacing: 6px; 181 - Text { text: "Label:"; vertical-alignment: center; } 182 - LineEdit { 183 - width: 200px; 184 - text <=> long-break-label; 185 - } 186 - } 187 - } 188 - } 189 - } 84 + Rectangle { height: 1px; background: #555555; } 190 85 191 - Tab { 192 - title: "Idle"; 193 - VerticalLayout { 194 - alignment: start; 195 - padding: 16px; 196 - spacing: 12px; 197 - 86 + // Idle 198 87 CheckBox { 199 88 text: "Pause timer when idle"; 200 89 checked <=> idle-detection-enabled; 201 90 } 202 - 203 91 HorizontalLayout { 204 92 alignment: start; 205 93 spacing: 8px; 206 - Text { text: "Idle threshold (minutes):"; vertical-alignment: center; } 94 + Text { text: "Idle threshold:"; vertical-alignment: center; } 207 95 SpinBox { 208 - width: 80px; 96 + width: 82px; 209 97 value <=> idle-threshold-mins; 210 98 minimum: 1; 211 99 maximum: 60; 212 100 } 101 + Text { text: "min"; vertical-alignment: center; } 213 102 } 214 - } 215 - } 216 103 217 - Tab { 218 - title: "Sound"; 219 - VerticalLayout { 220 - alignment: start; 221 - padding: 16px; 222 - spacing: 12px; 104 + Rectangle { height: 1px; background: #555555; } 223 105 106 + // Sound 224 107 CheckBox { 225 108 text: "Play sound on break start"; 226 109 checked <=> sound-enabled; 227 110 } 228 - 229 111 HorizontalLayout { 230 112 alignment: start; 231 113 spacing: 8px; ··· 237 119 maximum: 1.0; 238 120 } 239 121 } 122 + 123 + Rectangle { height: 1px; background: #555555; } 124 + 125 + // System 126 + CheckBox { 127 + text: "Start ioma automatically at login"; 128 + checked <=> autostart; 129 + } 130 + Button { 131 + text: "Open config file location"; 132 + clicked => { root.open-config-dir(); } 133 + } 240 134 } 241 135 } 242 136 137 + // ── Profile level editor ───────────────────────────────────── 243 138 Tab { 244 - title: "System"; 139 + title: "Profile"; 245 140 VerticalLayout { 246 141 alignment: start; 247 142 padding: 16px; 248 - spacing: 12px; 143 + spacing: 10px; 249 144 250 - CheckBox { 251 - text: "Start ioma automatically at login"; 252 - checked <=> autostart; 145 + Text { text: "Break levels:"; font-weight: 600; } 146 + 147 + for level[i] in levels : HorizontalLayout { 148 + spacing: 8px; 149 + alignment: start; 150 + SpinBox { 151 + width: 86px; 152 + value: level.work-mins; 153 + minimum: 1; 154 + maximum: 240; 155 + edited(v) => { root.level-changed(i, { work-mins: v, break-mins: level.break-mins, break-extra-secs: level.break-extra-secs, label: level.label }); } 156 + } 157 + Text { text: "min →"; vertical-alignment: center; } 158 + SpinBox { 159 + width: 74px; 160 + value: level.break-mins; 161 + minimum: 0; 162 + maximum: 120; 163 + edited(v) => { root.level-changed(i, { work-mins: level.work-mins, break-mins: v, break-extra-secs: level.break-extra-secs, label: level.label }); } 164 + } 165 + Text { text: "m"; vertical-alignment: center; } 166 + SpinBox { 167 + width: 74px; 168 + value: level.break-extra-secs; 169 + minimum: 0; 170 + maximum: 59; 171 + edited(v) => { root.level-changed(i, { work-mins: level.work-mins, break-mins: level.break-mins, break-extra-secs: v, label: level.label }); } 172 + } 173 + Text { text: "s break"; vertical-alignment: center; } 174 + LineEdit { 175 + width: 130px; 176 + text: level.label; 177 + edited(v) => { root.level-changed(i, { work-mins: level.work-mins, break-mins: level.break-mins, break-extra-secs: level.break-extra-secs, label: v }); } 178 + } 179 + Button { 180 + width: 32px; 181 + text: "✕"; 182 + clicked => { root.level-removed(i); } 183 + } 253 184 } 254 185 255 186 Button { 256 - text: "Open config file location"; 257 - clicked => { root.open-config-dir(); } 187 + text: "+ Add level"; 188 + clicked => { root.level-added(); } 189 + } 190 + 191 + Rectangle { height: 1px; background: #555555; } 192 + 193 + CheckBox { 194 + text: "Long break (after multiple cycles)"; 195 + checked <=> long-break-enabled; 196 + } 197 + 198 + if long-break-enabled : VerticalLayout { 199 + spacing: 8px; 200 + padding-left: 16px; 201 + 202 + HorizontalLayout { 203 + alignment: start; 204 + spacing: 6px; 205 + Text { text: "After"; vertical-alignment: center; } 206 + SpinBox { 207 + width: 74px; 208 + value <=> long-break-after-cycles; 209 + minimum: 2; 210 + maximum: 20; 211 + } 212 + Text { text: "cycles, take a"; vertical-alignment: center; } 213 + SpinBox { 214 + width: 74px; 215 + value <=> long-break-duration-mins; 216 + minimum: 1; 217 + maximum: 120; 218 + } 219 + Text { text: "min break"; vertical-alignment: center; } 220 + } 221 + 222 + HorizontalLayout { 223 + alignment: start; 224 + spacing: 6px; 225 + Text { text: "Reset cycle count if gap >"; vertical-alignment: center; } 226 + SpinBox { 227 + width: 74px; 228 + value <=> long-break-gap-mins; 229 + minimum: 5; 230 + maximum: 240; 231 + } 232 + Text { text: "min"; vertical-alignment: center; } 233 + } 234 + 235 + HorizontalLayout { 236 + alignment: start; 237 + spacing: 6px; 238 + Text { text: "Label:"; vertical-alignment: center; } 239 + LineEdit { 240 + width: 200px; 241 + text <=> long-break-label; 242 + } 243 + } 258 244 } 259 245 } 260 246 }