1use crate::backend::qobject::Sioul;
13use crate::backend::{QtThread, Shared, load_config, mode_json, say, show, tr};
14use cxx_qt::Threading;
15use cxx_qt_lib::QString;
16use jiff::Zoned;
17use sioul_core::pause::{self, After, Day};
18use sioul_core::quiet::Overrides;
19use std::pin::Pin;
20use std::sync::{Arc, Mutex};
21
22static BEFORE_PRESS: Mutex<Option<(i64, Overrides)>> = Mutex::new(None);
26
27fn load() -> Overrides {
28 Overrides::load(&Overrides::default_path())
29}
30
31fn save(sioul: &mut Pin<&mut Sioul>, overrides: &Overrides) -> bool {
32 match overrides.save(&Overrides::default_path()) {
33 Ok(()) => true,
34 Err(e) => {
35 sioul.as_mut().set_status(QString::from(&e));
36 false
37 }
38 }
39}
40
41fn today(now: &Zoned) -> Day {
43 let midnight = now.date().to_zoned(now.time_zone().clone()).map_or(0, |z| z.timestamp().as_second());
44 Day::of(&load_config(), crate::hours::blocks(now), &sioul_core::agenda::occurrences(midnight, midnight + 26 * 3600))
45}
46
47fn hm(stamp: i64, now: &Zoned) -> String {
49 jiff::Timestamp::from_second(stamp).map(|t| t.to_zoned(now.time_zone().clone()).strftime("%H:%M").to_string()).unwrap_or_default()
50}
51
52fn usual_end(config: &sioul_core::config::Config, now: &Zoned) -> Option<i64> {
54 if sioul_core::quiet::time_off_on(&config.time_off, now.date()).is_some() {
55 return None;
56 }
57 sioul_core::window::hours_on(&config.working_hours(), now.date(), now.time_zone()).map(|(_, closing)| closing.timestamp().as_second())
58}
59
60fn shown_again(mut sioul: Pin<&mut Sioul>) {
62 sioul.as_mut().set_mode(QString::from(&mode_json()));
63 let (qt, shared) = (sioul.qt_thread(), sioul.shared());
64 show(&qt, &shared);
65 crate::work::show_work(&qt, &shared);
66 crate::share::exchange(&qt, &shared);
67 std::thread::spawn(apply);
68}
69
70pub(crate) fn apply() {
75 crate::everywhere::apply();
76}
77
78fn settle(now: &Zoned) {
81 let path = Overrides::default_path();
82 let mut overrides = Overrides::load(&path);
83 let Some(since) = overrides.free_from() else { return };
84 let blocks = crate::hours::blocks(now);
85 let end = pause::free_until(since, &blocks, now.time_zone());
86 if now.timestamp().as_second() < end {
87 return;
88 }
89 let Ok(at) = jiff::Timestamp::from_second(end).map(|t| t.to_zoned(now.time_zone().clone())) else { return };
90 let day = today(&at);
91 pause::end_free(&mut overrides, &day.evening(), &at, load_config().free_time.moves);
92 if let Err(e) = overrides.save(&path) {
93 eprintln!("{e}");
94 }
95}
96
97pub(crate) fn tick(qt: &QtThread, shared: &Arc<Shared>) {
101 crate::everywhere::started(qt, shared);
103 let (qt, shared) = (qt.clone(), Arc::clone(shared));
104 std::thread::spawn(move || {
105 settle(&Zoned::now());
106 apply();
107 crate::backend::follow_reachable(&qt, &shared);
109 if crate::dnd::take_pause_pressed() {
110 let _ = qt.queue(|sioul| press(sioul));
111 }
112 });
113}
114
115pub(crate) fn moment(config: &sioul_core::config::Config, overrides: &Overrides, now: &Zoned) -> serde_json::Value {
117 let usual = usual_end(config, now);
118 let moved = overrides.moved_end(now.date());
119 let free = overrides.free_from().is_some();
120 let can_keep = config.free_time.moves && usual.is_some() && overrides.keep_end_on != Some(now.date()) && (free || moved.is_some());
122 serde_json::json!({
123 "nothing": pause::nothing_now(overrides, &config.free_time),
124 "usual_end": usual.map(|u| hm(u, now)).unwrap_or_default(),
125 "moved_end": moved.map(|m| hm(m, now)).unwrap_or_default(),
126 "can_keep": can_keep,
127 })
128}
129
130pub(crate) fn set_free(mut sioul: Pin<&mut Sioul>, on: bool) -> QString {
135 let now = Zoned::now();
136 let stamp = now.timestamp().as_second();
137 settle(&now);
139 let mut overrides = load();
140 if on {
141 if overrides.paused_from().is_some() || overrides.free_from().is_some() {
142 return QString::from("false");
143 }
144 pause::start_free(&mut overrides, stamp);
145 if !save(&mut sioul, &overrides) {
146 return QString::from("false");
147 }
148 let (qt, shared) = (sioul.qt_thread(), sioul.shared());
149 let stopped = sioul_core::timelog::running().is_some();
150 if stopped {
151 crate::work::focus_stop(&qt, &shared, false, "");
152 }
153 sioul.as_mut().set_status(QString::default());
154 shown_again(sioul);
155 return QString::from(if stopped { "true" } else { "false" });
156 }
157 let config = load_config();
158 let day = today(&now);
159 let moved = pause::end_free(&mut overrides, &day.evening(), &now, config.free_time.moves);
160 if !save(&mut sioul, &overrides) {
161 return QString::default();
162 }
163 let line = moved.map(|end| say("free-back-moved", &[("time", hm(end, &now))])).unwrap_or_default();
165 sioul.as_mut().set_status(QString::from(&line));
166 shown_again(sioul);
167 QString::from(&line)
168}
169
170pub(crate) fn keep_usual_end(mut sioul: Pin<&mut Sioul>) {
173 let now = Zoned::now();
174 let mut overrides = load();
175 pause::keep_usual_end(&mut overrides, now.date());
176 if !save(&mut sioul, &overrides) {
177 return;
178 }
179 let line = usual_end(&load_config(), &now).map(|end| say("free-kept", &[("time", hm(end, &now))])).unwrap_or_default();
180 sioul.as_mut().set_status(QString::from(&line));
181 shown_again(sioul);
182}
183
184pub(crate) fn set_free_nothing(mut sioul: Pin<&mut Sioul>, on: bool) {
186 let mut overrides = load();
187 overrides.free_nothing = Some(on);
188 if save(&mut sioul, &overrides) {
189 shown_again(sioul);
190 }
191}
192
193pub(crate) fn press(mut sioul: Pin<&mut Sioul>) {
198 let now = Zoned::now();
199 let stamp = now.timestamp().as_second();
200 let mut overrides = load();
201 if overrides.paused_from().is_none() {
202 let before = overrides.clone();
203 if overrides.free_from().is_some() {
204 overrides.free_ended = Some(stamp);
205 }
206 pause::keep_usual_end(&mut overrides, now.date());
207 pause::start_pause(&mut overrides, stamp, crate::everywhere::latest_press());
208 if !save(&mut sioul, &overrides) {
209 return;
210 }
211 if let Ok(mut kept) = BEFORE_PRESS.lock() {
212 *kept = Some((stamp, before));
213 }
214 let (qt, shared) = (sioul.qt_thread(), sioul.shared());
215 if sioul_core::timelog::running().is_some() {
216 crate::work::focus_stop(&qt, &shared, false, "");
217 }
218 }
219 sioul.as_mut().set_status(QString::default());
220 shown_again(sioul);
221}
222
223fn porch_back(config: &sioul_core::config::Config, now: &Zoned) -> Option<i64> {
226 let admin: Vec<_> = config.week_hours().into_iter().filter(|w| w.kind() == "admin").collect();
227 let hours = if admin.is_empty() { config.week_hours() } else { admin };
228 let morning = i8::try_from(config.agenda.day_start.unwrap_or(7)).unwrap_or(7);
229 let next = sioul_core::quiet::next_work(&hours, &config.time_off, now).unwrap_or_else(|| sioul_core::quiet::after_today(&[], &[], now, morning));
230 Some(next.timestamp().as_second())
231}
232
233pub(crate) fn come_back(mut sioul: Pin<&mut Sioul>) -> QString {
238 let config = load_config();
239 let now = Zoned::now();
240 let stamp = now.timestamp().as_second();
241 let before = load();
242 let Some(since) = before.paused_from() else { return QString::default() };
243 let mut overrides = before.clone();
244 let after = config.pause.after();
245 let morning = i8::try_from(config.agenda.day_start.unwrap_or(7)).unwrap_or(7);
246 let back = sioul_core::quiet::after_today(&config.working_hours(), &config.time_off, &now, morning);
247 let rest = (after == After::Rest).then(|| back.timestamp().as_second());
248 pause::end_pause(&mut overrides, &now, after, rest, porch_back(&config, &now));
249 if !save(&mut sioul, &overrides) {
250 return QString::default();
251 }
252 let mut undone = BEFORE_PRESS.lock().ok().and_then(|kept| kept.as_ref().filter(|(at, _)| *at == since).map(|(_, o)| o.clone())).unwrap_or(before);
254 pause::forget(&mut undone);
255 let (qt, shared) = (sioul.qt_thread(), sioul.shared());
256 let title = tr().text("pause-back-title", None);
257 crate::mail::rest(&qt, &shared, undone, overrides.clone(), title.clone());
258 let work_left = usual_end(&config, &now).is_some_and(|end| end > stamp);
260 let day = match after {
261 After::Lighter if work_left => tr().text("pause-back-lighter", None),
262 After::Rest if work_left => say("pause-back-rest", &[("back", sioul_core::quiet::until_text(tr(), &back, &now))]),
263 _ => String::new(),
264 };
265 let tomorrow = now.date().tomorrow().ok().and_then(|d| d.to_zoned(now.time_zone().clone()).ok());
266 let working_tomorrow = tomorrow.is_some_and(|t| config.week_hours().is_empty() || usual_end(&config, &t).is_some());
267 shown_again(sioul);
268 QString::from(
269 &serde_json::json!({
270 "title": title,
271 "day": day,
272 "tomorrow": if working_tomorrow { tr().text("pause-back-tomorrow", None) } else { String::new() },
273 "lighten": tr().text("pause-lighten-tomorrow", None),
274 "go": tr().text("pause-back-go", None),
275 })
276 .to_string(),
277 )
278}
279
280pub(crate) fn lighten_tomorrow(mut sioul: Pin<&mut Sioul>) {
282 let now = Zoned::now();
283 let Ok(tomorrow) = now.date().tomorrow() else { return };
284 let mut overrides = load();
285 pause::lighten(&mut overrides, tomorrow);
286 if save(&mut sioul, &overrides) {
287 sioul.as_mut().set_status(QString::from(&tr().text("pause-tomorrow-lighter", None)));
288 shown_again(sioul);
289 }
290}
291
292pub(crate) fn forget(mut sioul: Pin<&mut Sioul>) {
294 let mut overrides = load();
295 if overrides.paused_from().is_some() {
296 return;
297 }
298 pause::forget(&mut overrides);
299 if save(&mut sioul, &overrides) {
300 sioul.as_mut().set_status(QString::from(&tr().text("pause-setup-forgotten", None)));
301 }
302}
303
304pub(crate) fn screen() -> String {
308 let config = load_config();
309 let mut screen = pause::screen(&config.pause, config.contacts.region.as_deref(), tr());
310 screen.doses = sioul_core::attention::Attention::of(&config).cell(sioul_core::attention::Row::Own(sioul_core::attention::Kind::Doses), sioul_core::attention::Column::Pause) == sioul_core::attention::Level::Now;
311 serde_json::to_string(&screen).unwrap_or_default()
312}
313
314pub(crate) fn setup() -> String {
318 let mut report = crate::dnd::can().json();
319 report["gnome"] = serde_json::Value::Bool(load_config().pause.gnome);
320 report["forgettable"] = serde_json::Value::Bool({
321 let overrides = load();
322 overrides.paused_since.is_some() && overrides.paused_from().is_none()
323 });
324 report.to_string()
325}
326
327pub(crate) fn porch_rests(porch: &mut sioul_core::view::PorchView, opened_anyway: bool, now: &Zoned) {
330 let overrides = load();
331 let stamp = now.timestamp().as_second();
332 if opened_anyway || !overrides.porch_rests(stamp) {
333 return;
334 }
335 let Some(until) = overrides.porch_rests_until.and_then(|u| jiff::Timestamp::from_second(u).ok()).map(|t| t.to_zoned(now.time_zone().clone())) else { return };
336 porch.open = false;
337 porch.closed = Some(say("porch-rests", &[("when", tr().when(&until))]));
338 porch.summary.clear();
339 porch.status.clear();
340 porch.lanes.clear();
341}
342
343pub(crate) fn offers(tasks: &[sioul_core::tasks::Task], quiet: Option<&sioul_core::quiet::QuietTasks>, free: bool) -> Vec<serde_json::Value> {
347 let Some(quiet) = quiet.filter(|_| free) else { return Vec::new() };
348 let mut open: Vec<&sioul_core::tasks::Task> = tasks.iter().filter(|t| t.status.is_open() && quiet.keeps(t)).collect();
349 open.sort_by_key(|t| t.created);
350 open.into_iter().take(8).map(|t| serde_json::json!({ "uid": t.uid, "title": t.title })).collect()
351}
352
353#[cfg(test)]
354mod tests {
355 #[test]
356 fn porch_back_without_hours_is_tomorrow_morning() {
357 let now: jiff::Zoned = "2026-10-05T15:00[Europe/Paris]".parse().unwrap();
358 let config = sioul_core::config::Config::default();
359 let back = super::porch_back(&config, &now).unwrap();
360 assert_eq!(super::hm(back, &now), "07:00");
361 }
362}