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 = qt.clone();
104 std::thread::spawn(move || {
105 settle(&Zoned::now());
106 apply();
107 if crate::dnd::take_pause_pressed() {
108 let _ = qt.queue(|sioul| press(sioul));
109 }
110 });
111}
112
113pub(crate) fn moment(config: &sioul_core::config::Config, overrides: &Overrides, now: &Zoned) -> serde_json::Value {
115 let usual = usual_end(config, now);
116 let moved = overrides.moved_end(now.date());
117 let free = overrides.free_from().is_some();
118 let can_keep = config.free_time.moves && usual.is_some() && overrides.keep_end_on != Some(now.date()) && (free || moved.is_some());
120 serde_json::json!({
121 "nothing": pause::nothing_now(overrides, &config.free_time),
122 "usual_end": usual.map(|u| hm(u, now)).unwrap_or_default(),
123 "moved_end": moved.map(|m| hm(m, now)).unwrap_or_default(),
124 "can_keep": can_keep,
125 })
126}
127
128pub(crate) fn set_free(mut sioul: Pin<&mut Sioul>, on: bool) -> QString {
133 let now = Zoned::now();
134 let stamp = now.timestamp().as_second();
135 settle(&now);
137 let mut overrides = load();
138 if on {
139 if overrides.paused_from().is_some() || overrides.free_from().is_some() {
140 return QString::from("false");
141 }
142 pause::start_free(&mut overrides, stamp);
143 if !save(&mut sioul, &overrides) {
144 return QString::from("false");
145 }
146 let (qt, shared) = (sioul.qt_thread(), sioul.shared());
147 let stopped = sioul_core::timelog::running().is_some();
148 if stopped {
149 crate::work::focus_stop(&qt, &shared, false, "");
150 }
151 sioul.as_mut().set_status(QString::default());
152 shown_again(sioul);
153 return QString::from(if stopped { "true" } else { "false" });
154 }
155 let config = load_config();
156 let day = today(&now);
157 let moved = pause::end_free(&mut overrides, &day.evening(), &now, config.free_time.moves);
158 if !save(&mut sioul, &overrides) {
159 return QString::default();
160 }
161 let line = moved.map(|end| say("free-back-moved", &[("time", hm(end, &now))])).unwrap_or_default();
163 sioul.as_mut().set_status(QString::from(&line));
164 shown_again(sioul);
165 QString::from(&line)
166}
167
168pub(crate) fn keep_usual_end(mut sioul: Pin<&mut Sioul>) {
171 let now = Zoned::now();
172 let mut overrides = load();
173 pause::keep_usual_end(&mut overrides, now.date());
174 if !save(&mut sioul, &overrides) {
175 return;
176 }
177 let line = usual_end(&load_config(), &now).map(|end| say("free-kept", &[("time", hm(end, &now))])).unwrap_or_default();
178 sioul.as_mut().set_status(QString::from(&line));
179 shown_again(sioul);
180}
181
182pub(crate) fn set_free_nothing(mut sioul: Pin<&mut Sioul>, on: bool) {
184 let mut overrides = load();
185 overrides.free_nothing = Some(on);
186 if save(&mut sioul, &overrides) {
187 shown_again(sioul);
188 }
189}
190
191pub(crate) fn press(mut sioul: Pin<&mut Sioul>) {
196 let now = Zoned::now();
197 let stamp = now.timestamp().as_second();
198 let mut overrides = load();
199 if overrides.paused_from().is_none() {
200 let before = overrides.clone();
201 if overrides.free_from().is_some() {
202 overrides.free_ended = Some(stamp);
203 }
204 pause::keep_usual_end(&mut overrides, now.date());
205 pause::start_pause(&mut overrides, stamp);
206 if !save(&mut sioul, &overrides) {
207 return;
208 }
209 if let Ok(mut kept) = BEFORE_PRESS.lock() {
210 *kept = Some((stamp, before));
211 }
212 let (qt, shared) = (sioul.qt_thread(), sioul.shared());
213 if sioul_core::timelog::running().is_some() {
214 crate::work::focus_stop(&qt, &shared, false, "");
215 }
216 }
217 sioul.as_mut().set_status(QString::default());
218 shown_again(sioul);
219}
220
221fn porch_back(config: &sioul_core::config::Config, now: &Zoned) -> Option<i64> {
224 let admin: Vec<_> = config.week_hours().into_iter().filter(|w| w.kind() == "admin").collect();
225 let hours = if admin.is_empty() { config.week_hours() } else { admin };
226 let morning = i8::try_from(config.agenda.day_start.unwrap_or(7)).unwrap_or(7);
227 let next = sioul_core::quiet::next_work(&hours, &config.time_off, now).unwrap_or_else(|| sioul_core::quiet::after_today(&[], &[], now, morning));
228 Some(next.timestamp().as_second())
229}
230
231pub(crate) fn come_back(mut sioul: Pin<&mut Sioul>) -> QString {
236 let config = load_config();
237 let now = Zoned::now();
238 let stamp = now.timestamp().as_second();
239 let before = load();
240 let Some(since) = before.paused_from() else { return QString::default() };
241 let mut overrides = before.clone();
242 let after = config.pause.after();
243 let morning = i8::try_from(config.agenda.day_start.unwrap_or(7)).unwrap_or(7);
244 let back = sioul_core::quiet::after_today(&config.working_hours(), &config.time_off, &now, morning);
245 let rest = (after == After::Rest).then(|| back.timestamp().as_second());
246 pause::end_pause(&mut overrides, &now, after, rest, porch_back(&config, &now));
247 if !save(&mut sioul, &overrides) {
248 return QString::default();
249 }
250 let mut undone = BEFORE_PRESS.lock().ok().and_then(|kept| kept.as_ref().filter(|(at, _)| *at == since).map(|(_, o)| o.clone())).unwrap_or(before);
252 pause::forget(&mut undone);
253 let (qt, shared) = (sioul.qt_thread(), sioul.shared());
254 let title = tr().text("pause-back-title", None);
255 crate::mail::rest(&qt, &shared, undone, overrides.clone(), title.clone());
256 let work_left = usual_end(&config, &now).is_some_and(|end| end > stamp);
258 let day = match after {
259 After::Lighter if work_left => tr().text("pause-back-lighter", None),
260 After::Rest if work_left => say("pause-back-rest", &[("back", sioul_core::quiet::until_text(tr(), &back, &now))]),
261 _ => String::new(),
262 };
263 let tomorrow = now.date().tomorrow().ok().and_then(|d| d.to_zoned(now.time_zone().clone()).ok());
264 let working_tomorrow = tomorrow.is_some_and(|t| config.week_hours().is_empty() || usual_end(&config, &t).is_some());
265 shown_again(sioul);
266 QString::from(
267 &serde_json::json!({
268 "title": title,
269 "day": day,
270 "tomorrow": if working_tomorrow { tr().text("pause-back-tomorrow", None) } else { String::new() },
271 "lighten": tr().text("pause-lighten-tomorrow", None),
272 "go": tr().text("pause-back-go", None),
273 })
274 .to_string(),
275 )
276}
277
278pub(crate) fn lighten_tomorrow(mut sioul: Pin<&mut Sioul>) {
280 let now = Zoned::now();
281 let Ok(tomorrow) = now.date().tomorrow() else { return };
282 let mut overrides = load();
283 pause::lighten(&mut overrides, tomorrow);
284 if save(&mut sioul, &overrides) {
285 sioul.as_mut().set_status(QString::from(&tr().text("pause-tomorrow-lighter", None)));
286 shown_again(sioul);
287 }
288}
289
290pub(crate) fn forget(mut sioul: Pin<&mut Sioul>) {
292 let mut overrides = load();
293 if overrides.paused_from().is_some() {
294 return;
295 }
296 pause::forget(&mut overrides);
297 if save(&mut sioul, &overrides) {
298 sioul.as_mut().set_status(QString::from(&tr().text("pause-setup-forgotten", None)));
299 }
300}
301
302pub(crate) fn screen() -> String {
306 let config = load_config();
307 let mut screen = pause::screen(&config.pause, config.contacts.region.as_deref(), tr());
308 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;
309 serde_json::to_string(&screen).unwrap_or_default()
310}
311
312pub(crate) fn setup() -> String {
316 let mut report = crate::dnd::can().json();
317 report["gnome"] = serde_json::Value::Bool(load_config().pause.gnome);
318 report["forgettable"] = serde_json::Value::Bool({
319 let overrides = load();
320 overrides.paused_since.is_some() && overrides.paused_from().is_none()
321 });
322 report.to_string()
323}
324
325pub(crate) fn porch_rests(porch: &mut sioul_core::view::PorchView, opened_anyway: bool, now: &Zoned) {
328 let overrides = load();
329 let stamp = now.timestamp().as_second();
330 if opened_anyway || !overrides.porch_rests(stamp) {
331 return;
332 }
333 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 };
334 porch.open = false;
335 porch.closed = Some(say("porch-rests", &[("when", tr().when(&until))]));
336 porch.summary.clear();
337 porch.status.clear();
338 porch.lanes.clear();
339}
340
341pub(crate) fn offers(tasks: &[sioul_core::tasks::Task], quiet: Option<&sioul_core::quiet::QuietTasks>, free: bool) -> Vec<serde_json::Value> {
345 let Some(quiet) = quiet.filter(|_| free) else { return Vec::new() };
346 let mut open: Vec<&sioul_core::tasks::Task> = tasks.iter().filter(|t| t.status.is_open() && quiet.keeps(t)).collect();
347 open.sort_by_key(|t| t.created);
348 open.into_iter().take(8).map(|t| serde_json::json!({ "uid": t.uid, "title": t.title })).collect()
349}
350
351#[cfg(test)]
352mod tests {
353 #[test]
354 fn porch_back_without_hours_is_tomorrow_morning() {
355 let now: jiff::Zoned = "2026-10-05T15:00[Europe/Paris]".parse().unwrap();
356 let config = sioul_core::config::Config::default();
357 let back = super::porch_back(&config, &now).unwrap();
358 assert_eq!(super::hm(back, &now), "07:00");
359 }
360}