Skip to main content

sioul_app/
backend.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2// Copyright © 2026 Aurélien Pierre
3
4//! The object behind the window.
5//!
6//! It hands QML the core's views as JSON (`sioul_core::view`), already in your
7//! language, and runs everything slow on background threads: fetching mail,
8//! finding servers, testing logins. Results come back to Qt's thread through
9//! `qt_thread().queue`. Each mail account has a watcher that keeps its inbox
10//! open with IDLE, so a verified code becomes a notification within seconds.
11
12use cxx_qt::{CxxQtType, Threading};
13use cxx_qt_lib::QString;
14use jiff::Zoned;
15use sioul_core::budget::{self, Ledger, MailLine};
16use sioul_core::card::ImapOrigin;
17use sioul_core::projects::ProjectStore;
18use sioul_core::config::{self, Account, Config, Priority, Security};
19use sioul_core::i18n::{self, Translator};
20use sioul_core::porch::{self, KnownSenders, SenderList, Triaged};
21use sioul_core::state::{MoneyState, PorchState};
22use sioul_core::{maildir, reading, view};
23use crate::{crypto, gmail, mail, outlook, pim, securitykey, work};
24use sioul_sync::antivirus::{self, Verdict};
25use sioul_sync::{Control, Learned, Report, SyncError, notify, secret};
26use std::collections::{BTreeMap, BTreeSet};
27use std::hash::{Hash, Hasher};
28use std::path::{Path, PathBuf};
29use std::pin::Pin;
30use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
31use std::sync::{Arc, Mutex, OnceLock};
32
33#[cxx_qt::bridge]
34pub mod qobject {
35    // SAFETY: cxx checks these declarations against the headers they include, when it builds; the types are cxx-qt-lib's.
36    unsafe extern "C++" {
37        include!("cxx-qt-lib/qstring.h");
38        type QString = cxx_qt_lib::QString;
39    }
40
41    #[auto_cxx_name]
42    extern "RustQt" {
43        #[qobject]
44        #[qml_element]
45        #[qproperty(QString, porch)]
46        #[qproperty(QString, budgets)]
47        #[qproperty(QString, accounts)]
48        #[qproperty(QString, blocked)]
49        #[qproperty(QString, status)]
50        #[qproperty(bool, busy)]
51        #[qproperty(QString, found)]
52        #[qproperty(QString, scouted)]
53        #[qproperty(bool, scouting)]
54        #[qproperty(QString, form_error)]
55        #[qproperty(bool, form_busy)]
56        #[qproperty(QString, mail_accounts)]
57        #[qproperty(QString, mail_folder)]
58        #[qproperty(QString, drafts)]
59        // The Mail page's search by conditions: its sentence, what the servers said, the rows (`mailsearch`).
60        #[qproperty(QString, mail_search)]
61        #[qproperty(QString, undo_line)]
62        #[qproperty(QString, contacts)]
63        // The Contacts page's duplicates (`duplicates::find`), looked for off the window's thread when asked.
64        #[qproperty(QString, duplicates)]
65        #[qproperty(QString, agenda)]
66        // Two events at once in the next two weeks (`pim::overlaps`), made with the agenda, off the window's thread.
67        #[qproperty(QString, overlaps)]
68        // The Health page, the Porch's doses (today's not marked yet, and those due
69        // while Sioul was closed: `health::missed`), today's meals and naps
70        // (`health::show_health`), made off the window's thread: they read the
71        // doses' record and your other devices' claims.
72        #[qproperty(QString, health_view)]
73        #[qproperty(QString, missed_view)]
74        #[qproperty(QString, needs_view)]
75        #[qproperty(QString, tasks)]
76        #[qproperty(QString, notes)]
77        #[qproperty(QString, focus_session)]
78        #[qproperty(QString, reading)]
79        #[qproperty(bool, realtime)]
80        // Put away on a phone (in the background): the pages' clocks and reloads stop until it is back.
81        #[qproperty(bool, away)]
82        #[qproperty(QString, mode)]
83        #[qproperty(QString, forecast)]
84        #[qproperty(QString, places_found)]
85        type Sioul = super::SioulRust;
86
87        /// A sentence of the interface, in your language.
88        #[qinvokable]
89        fn text(self: &Sioul, id: &QString) -> QString;
90
91        /// The same, with one named argument.
92        #[qinvokable]
93        fn text_with(self: &Sioul, id: &QString, name: &QString, value: &QString) -> QString;
94
95        /// A counted message, its plural chosen by `n` (`$n`, `$count`, `$countf`…: `Translator::counted`).
96        #[qinvokable]
97        fn text_counted(self: &Sioul, id: &QString, n: i32) -> QString;
98
99        /// A sentence with several arguments, given as a JSON object of names and values.
100        #[qinvokable]
101        fn text_args(self: &Sioul, id: &QString, args: &QString) -> QString;
102
103        /// Shows what is on disk, then starts a watcher per mail account.
104        #[qinvokable]
105        fn start(self: Pin<&mut Sioul>);
106
107        /// Computes the views again: windows open and close, codes expire.
108        #[qinvokable]
109        fn refresh(self: Pin<&mut Sioul>);
110
111        /// Fetches mail now, and restarts watchers that stopped.
112        #[qinvokable]
113        fn sync_now(self: Pin<&mut Sioul>);
114
115        /// Opens the Porch outside an admin window, until "Done for now".
116        #[qinvokable]
117        fn open_anyway(self: Pin<&mut Sioul>);
118
119        /// Closes the Porch on what it shows.
120        #[qinvokable]
121        fn done(self: Pin<&mut Sioul>);
122
123        /// Reads a message for the reading pane off the window's thread, so
124        /// that a large or hostile one never holds the window (and the
125        /// doses' reminders with it): `message_read` brings it.
126        #[qinvokable]
127        fn read_message(self: Pin<&mut Sioul>, key: &QString);
128
129        /// Has the antivirus check an attachment, then opens it with the desktop's application.
130        #[qinvokable]
131        fn open_attachment(self: Pin<&mut Sioul>, key: &QString, index: i32);
132
133        /// Has the antivirus check an attachment, then copies it into the downloads folder.
134        #[qinvokable]
135        fn save_attachment(self: Pin<&mut Sioul>, key: &QString, index: i32);
136
137        /// Opens, saves (1) or keeps as a paper (2) an attachment without a check: there is no antivirus,
138        /// and you said to go on.
139        #[qinvokable]
140        fn attachment_unchecked(self: Pin<&mut Sioul>, key: &QString, index: i32, what: i32);
141
142        /// Has the antivirus check an attachment, then keeps it in the papers wallet; `paper_kept` follows.
143        #[qinvokable]
144        fn keep_attachment_as_paper(self: Pin<&mut Sioul>, key: &QString, index: i32);
145
146        /// Whether attachments open here without an antivirus check: a phone has
147        /// none Sioul can call (`attachments`); the Reader says so.
148        #[qinvokable]
149        fn attachments_unscanned(self: &Sioul) -> bool;
150
151        /// The papers wallet, as JSON (`papers::view`).
152        #[qinvokable]
153        fn papers(self: &Sioul) -> QString;
154
155        /// A paper saved (new when `id` is empty) from the form's JSON; returns what went wrong, else "".
156        #[qinvokable]
157        fn save_paper(self: Pin<&mut Sioul>, id: &QString, edit: &QString) -> QString;
158
159        /// A paper taken out of the wallet, its file left where it is; returns what went wrong, else "".
160        #[qinvokable]
161        fn remove_paper(self: Pin<&mut Sioul>, id: &QString) -> QString;
162
163        /// A paper's renewal planned as a task; returns what went wrong, else "".
164        #[qinvokable]
165        fn plan_renewal(self: Pin<&mut Sioul>, id: &QString) -> QString;
166
167        /// Lets a sender in: they leave the screener.
168        #[qinvokable]
169        fn let_in(self: Pin<&mut Sioul>, address: &QString);
170
171        /// Blocks an address, or `@domain`: their mail is set aside for good.
172        #[qinvokable]
173        fn block(self: Pin<&mut Sioul>, entry: &QString);
174
175        /// Blocks an address, or `@domain`, from one of their messages (its
176        /// file, `key`): as `block`, and the message goes into the spam filter's label log.
177        #[qinvokable]
178        fn block_from(self: Pin<&mut Sioul>, entry: &QString, key: &QString);
179
180        /// Out of the blocked list; written neutral when a category or a domain would still block it.
181        #[qinvokable]
182        fn unblock(self: Pin<&mut Sioul>, entry: &QString);
183
184        /// An address or a pattern marked "safe", "neutral", "restricted" or
185        /// "blocked", out of the three other lists; "": out of every list, its
186        /// categories, else its domain, deciding (docs/porch.md).
187        #[qinvokable]
188        fn set_standing(self: Pin<&mut Sioul>, entry: &QString, standing: &QString);
189
190        /// Who an address is: "safe", "neutral", "restricted", "blocked" or "stranger".
191        #[qinvokable]
192        fn standing(self: &Sioul, address: &QString) -> QString;
193
194        /// "Their mail" for a person (`addresses`: a JSON array of theirs, or
195        /// one): where they stand and why, the choices with their times (`senders`).
196        #[qinvokable]
197        fn standing_of(self: &Sioul, addresses: &QString) -> QString;
198
199        /// "Their mail" chosen for every address given (a JSON array, or one):
200        /// a list's name, or "" for "As their categories say".
201        #[qinvokable]
202        fn set_standing_of(self: Pin<&mut Sioul>, addresses: &QString, standing: &QString);
203
204        /// "Their list" for a contact's card (`key`: its file): who they are,
205        /// why, the choices, their addresses and numbers with lists of their own (`senders`).
206        #[qinvokable]
207        fn person_of(self: &Sioul, key: &QString) -> QString;
208
209        /// "Their list" chosen for a contact's card, all their addresses and
210        /// numbers: a list's name, or "" for "As their categories say".
211        #[qinvokable]
212        fn set_person(self: Pin<&mut Sioul>, key: &QString, standing: &QString);
213
214        /// Writes the line a message about money stands for into a budget.
215        #[qinvokable]
216        fn add_mail_line(self: Pin<&mut Sioul>, key: &QString, budget: &QString);
217
218        /// Leaves a message about money out of the budgets.
219        #[qinvokable]
220        fn ignore_mail_line(self: Pin<&mut Sioul>, key: &QString);
221
222        /// Finds the IMAP server of an address; the answer comes in `found`.
223        #[qinvokable]
224        fn discover(self: Pin<&mut Sioul>, address: &QString);
225
226        /// The same, for an address you say is at Microsoft (Microsoft 365)
227        /// when discovery found nothing: its mail first goes through another
228        /// service, so its domain's MX does not say it (docs/microsoft.md).
229        #[qinvokable]
230        fn discover_at_microsoft(self: Pin<&mut Sioul>, address: &QString);
231
232        /// An account switched on or off: off, it keeps its settings and is
233        /// neither synced nor shown. Returns what went wrong, if anything.
234        #[qinvokable]
235        fn set_account_enabled(self: Pin<&mut Sioul>, id: &QString, on: bool) -> QString;
236
237        /// What the server of an address offers (mail, calendars and contacts,
238        /// a Nextcloud's apps), into `scouted`, as JSON.
239        #[qinvokable]
240        fn scout_account(self: Pin<&mut Sioul>, address: &QString);
241
242        /// Mail added to an address that has calendars and contacts already, with their password.
243        #[qinvokable]
244        fn add_mail_like(self: Pin<&mut Sioul>, address: &QString, host: &QString, port: i32, security: &QString, login: &QString);
245
246        /// Tests the login, keeps the password in the keyring, adds the account.
247        #[qinvokable]
248        fn add_account(self: Pin<&mut Sioul>, address: &QString, host: &QString, port: i32, security: &QString, login: &QString, password: &QString);
249
250        /// Adds a web-only mailbox.
251        #[qinvokable]
252        fn add_portal(self: Pin<&mut Sioul>, name: &QString, url: &QString);
253
254        /// Removes an account and its password; its mail stays on disk. A
255        /// Microsoft account's sign-in is forgotten, and its mail on this
256        /// device too when `delete_mail` (docs/microsoft.md).
257        #[qinvokable]
258        fn remove_account(self: Pin<&mut Sioul>, id: &QString, delete_mail: bool);
259
260        /// An account's password given on this device (one come from another
261        /// device arrives without it): tested with its server, then kept;
262        /// `account_password_done` says how it went.
263        #[qinvokable]
264        fn set_account_password(self: Pin<&mut Sioul>, id: &QString, password: &QString);
265
266        /// Ranks an account's mail: "above", "average" or "below".
267        #[qinvokable]
268        fn set_priority(self: Pin<&mut Sioul>, id: &QString, priority: &QString);
269
270        /// Shows a folder of an account: the last two weeks, or `all`; `query` searches it.
271        #[qinvokable]
272        fn open_folder(self: Pin<&mut Sioul>, account: &QString, folder: &QString, all: bool, query: &QString);
273
274        /// A message was opened: it is marked read.
275        #[qinvokable]
276        fn opened(self: Pin<&mut Sioul>, key: &QString);
277
278        /// Does something to a message: "read", "unread", "flag", "unflag" at once;
279        /// "archive", "trash", "junk", "not-junk", "not-spam" (mail set aside as
280        /// spam, back in its lane), "move" (to `target`) after ten seconds to undo.
281        #[qinvokable]
282        fn act(self: Pin<&mut Sioul>, key: &QString, action: &QString, target: &QString);
283
284        /// The same act on several messages (a JSON list of keys), under one "Undo".
285        #[qinvokable]
286        fn act_many(self: Pin<&mut Sioul>, keys: &QString, action: &QString);
287
288        /// Messages (a JSON list of keys) into a folder of an account, theirs or another.
289        #[qinvokable]
290        fn move_messages(self: Pin<&mut Sioul>, keys: &QString, account: &QString, folder: &QString);
291
292        /// Searches the mail by conditions (`mailsearch::Search`, as JSON): the
293        /// mail here at once, the servers after a pause in typing, or at once
294        /// (`now`); `mailSearch` follows.
295        #[qinvokable]
296        fn search_mail(self: Pin<&mut Sioul>, search: &QString, now: bool);
297
298        /// Closes the search: the page shows its folder again.
299        #[qinvokable]
300        fn clear_mail_search(self: Pin<&mut Sioul>);
301
302        /// Brings a message the search found on its server only here; `mailSearch` then names its file.
303        #[qinvokable]
304        fn bring_message(self: Pin<&mut Sioul>, key: &QString);
305
306        /// What the search's column offers (fields, comparisons, your addresses and folders), as JSON.
307        #[qinvokable]
308        fn mail_search_fields(self: &Sioul) -> QString;
309
310        /// Leaves the list a message comes from (docs/client.md, "Unsubscribing"):
311        /// one click or a message after ten seconds to undo; returns the web page
312        /// to open in the browser when the list takes it only there, else "".
313        #[qinvokable]
314        fn unsubscribe(self: Pin<&mut Sioul>, key: &QString) -> QString;
315
316        /// Cancels the newest act still waiting.
317        #[qinvokable]
318        fn undo(self: Pin<&mut Sioul>);
319
320        /// The window closes: what waits is done now.
321        #[qinvokable]
322        fn flush(self: Pin<&mut Sioul>);
323
324        /// Starts a draft ("new", "reply", "reply-all", "forward"); returns its name, or "" when it cannot.
325        #[qinvokable]
326        fn compose(self: Pin<&mut Sioul>, kind: &QString, key: &QString, account: &QString) -> QString;
327
328        /// A draft as the writing window shows it, as JSON.
329        #[qinvokable]
330        fn draft(self: &Sioul, id: &QString) -> QString;
331
332        /// Saves what the writing window holds (JSON); returns "Saved at 14:02", or what went wrong.
333        #[qinvokable]
334        fn save_draft(self: Pin<&mut Sioul>, edit: &QString) -> QString;
335
336        /// Attaches a file (a `file://` URL); returns what went wrong, if anything.
337        #[qinvokable]
338        fn attach(self: Pin<&mut Sioul>, id: &QString, url: &QString) -> QString;
339
340        /// Takes an attachment off a draft: one added, or one a forward carries.
341        #[qinvokable]
342        fn detach(self: Pin<&mut Sioul>, id: &QString, index: i32, forwarded: bool);
343
344        /// What the Markdown becomes, for the preview.
345        #[qinvokable]
346        fn preview(self: &Sioul, markdown: &QString) -> QString;
347
348        /// Sends after ten seconds to undo; returns what stops it now, else "".
349        #[qinvokable]
350        fn send(self: Pin<&mut Sioul>, id: &QString) -> QString;
351
352        /// Deletes a draft after ten seconds to undo.
353        #[qinvokable]
354        fn discard(self: Pin<&mut Sioul>, id: &QString);
355
356        /// The writing window closed.
357        #[qinvokable]
358        fn draft_closed(self: Pin<&mut Sioul>, id: &QString);
359
360        /// Where a message is: its account and folder, as JSON.
361        #[qinvokable]
362        fn place(self: &Sioul, key: &QString) -> QString;
363
364        /// The message as it came, for "Show the source".
365        #[qinvokable]
366        fn source(self: &Sioul, key: &QString) -> QString;
367
368        /// Your name as recipients see it, and your signature.
369        #[qinvokable]
370        fn set_writing(self: Pin<&mut Sioul>, id: &QString, name: &QString, signature: &QString);
371
372        /// For development and documentation: the folder in `SIOUL_GRAB`, where
373        /// the window saves each page as an image before quitting; empty otherwise.
374        #[qinvokable]
375        fn grab_folder(self: &Sioul) -> QString;
376
377        /// Shows the contacts matching `query` (names, addresses, numbers, organisations).
378        #[qinvokable]
379        fn search_contacts(self: Pin<&mut Sioul>, query: &QString);
380
381        /// A contact, every field, as JSON; empty when it cannot be read.
382        #[qinvokable]
383        fn contact(self: &Sioul, key: &QString) -> QString;
384
385        /// Saves the contact form (JSON); an empty key makes a new contact.
386        /// Returns {"key": …} or {"error": …}.
387        #[qinvokable]
388        fn save_contact(self: Pin<&mut Sioul>, key: &QString, edit: &QString) -> QString;
389
390        /// Deletes a contact after ten seconds to undo.
391        #[qinvokable]
392        fn delete_contact(self: Pin<&mut Sioul>, key: &QString);
393
394        /// Shows the contacts of one category only; empty: all of them.
395        #[qinvokable]
396        fn show_contacts_category(self: Pin<&mut Sioul>, category: &QString);
397
398        /// Looks for duplicates in the contacts, off the window's thread: they come in `duplicates`.
399        #[qinvokable]
400        fn find_duplicates(self: Pin<&mut Sioul>);
401
402        /// Takes the duplicates off these cards (a JSON list of their files), one
403        /// write each; returns what went wrong, else "".
404        #[qinvokable]
405        fn clean_contacts(self: Pin<&mut Sioul>, keys: &QString) -> QString;
406
407        /// Merges two cards, `lead` keeping its name, the other deleted; returns {"key"} or {"error"}.
408        #[qinvokable]
409        fn merge_contacts(self: Pin<&mut Sioul>, lead: &QString, other: &QString) -> QString;
410
411        /// Two cards are not one person: never offered again; returns what went wrong, else "".
412        #[qinvokable]
413        fn not_same_contacts(self: Pin<&mut Sioul>, first: &QString, second: &QString) -> QString;
414
415        /// Puts the cards back as they were before what was done (`duplicates`' "done"); returns what went wrong, else "".
416        #[qinvokable]
417        fn undo_contacts(self: Pin<&mut Sioul>, id: &QString) -> QString;
418
419        /// Shows `days` days of the agenda from `from` ("2026-10-05"; empty: today).
420        #[qinvokable]
421        fn show_days(self: Pin<&mut Sioul>, from: &QString, days: i32, upcoming: bool);
422
423        /// The calendars a new event can go into, as JSON.
424        #[qinvokable]
425        fn calendars(self: &Sioul) -> QString;
426
427        /// An event as its form shows it, as JSON. Not `event`, which would hide QObject::event.
428        #[qinvokable]
429        fn event_form(self: &Sioul, key: &QString) -> QString;
430
431        /// Saves the event form (JSON); an empty key makes a new event in `calendar`.
432        /// Returns what went wrong, else "".
433        #[qinvokable]
434        fn save_event(self: Pin<&mut Sioul>, key: &QString, edit: &QString, calendar: &QString) -> QString;
435
436        /// The event form a task would become ("Make it an appointment",
437        /// `appointments`): JSON, at its block's time (`start`, `end`, Unix
438        /// seconds; 0 for none); {"error"} when it is gone.
439        #[qinvokable]
440        fn task_as_event(self: &Sioul, uid: &QString, start: f64, end: f64) -> QString;
441
442        /// A task made an appointment: the event form (JSON) saved in
443        /// `calendar`, the task and its blocks (`block`, the one opened) taken
444        /// out, never both, never neither. Returns what went wrong, else "".
445        #[qinvokable]
446        fn task_to_event(self: Pin<&mut Sioul>, uid: &QString, block: &QString, edit: &QString, calendar: &QString) -> QString;
447
448        /// What an event cannot keep as a task, in a sentence; "" when everything goes along.
449        #[qinvokable]
450        fn event_to_task_losses(self: &Sioul, key: &QString) -> QString;
451
452        /// An event made a task ("Make it a task"), its time its block's.
453        /// Returns what went wrong, else "".
454        #[qinvokable]
455        fn event_to_task(self: Pin<&mut Sioul>, key: &QString) -> QString;
456
457        /// An event's reminder before it, the occurrence starting at `start`,
458        /// as its details say it: JSON {line, remind, writable} (docs/reminders.md).
459        #[qinvokable]
460        fn event_reminder(self: &Sioul, key: &QString, start: f64) -> QString;
461
462        /// The choices of an event's reminder before it, JSON [{value, label}];
463        /// `current` among them when it is another time.
464        #[qinvokable]
465        fn reminder_choices(self: &Sioul, current: &QString) -> QString;
466
467        /// An event's own reminder changed for every time it comes ("" as
468        /// usual, "none", "15"); returns what went wrong, else "".
469        #[qinvokable]
470        fn set_event_reminder(self: Pin<&mut Sioul>, key: &QString, value: &QString) -> QString;
471
472        /// Deletes an event, or only the occurrence starting at `start`, after ten seconds to undo.
473        #[qinvokable]
474        fn delete_event(self: Pin<&mut Sioul>, key: &QString, start: f64, only_this: bool);
475
476        /// An event dragged: its occurrence starting at `start` now from
477        /// `new_start` to `new_end` (Unix seconds), that time alone or every
478        /// time; "Undo" offered. Returns what went wrong, else "".
479        #[qinvokable]
480        fn move_event(self: Pin<&mut Sioul>, key: &QString, start: f64, new_start: f64, new_end: f64, only_this: bool) -> QString;
481
482        /// Addresses from the contacts for what is typed in an address field, as JSON.
483        #[qinvokable]
484        fn completions(self: &Sioul, typed: &QString) -> QString;
485
486        /// The contact with this address, by file; empty when none.
487        #[qinvokable]
488        fn contact_for(self: &Sioul, address: &QString) -> QString;
489
490        /// Makes a contact of a sender.
491        #[qinvokable]
492        fn add_sender(self: Pin<&mut Sioul>, name: &QString, address: &QString);
493
494        /// Answers an invitation: "accepted", "tentative", "declined"; "add" or "remove" for events sent without a question.
495        #[qinvokable]
496        fn answer_invitation(self: Pin<&mut Sioul>, key: &QString, answer: &QString);
497
498        /// How far back mail and the agenda reach, in weeks; 0 for everything.
499        #[qinvokable]
500        fn history_weeks(self: &Sioul) -> i32;
501
502        /// The hour the day and week planning open on.
503        #[qinvokable]
504        fn day_start(self: &Sioul) -> i32;
505
506        /// Sets how far back mail and the agenda reach; the older mail comes at the next fetch.
507        #[qinvokable]
508        fn set_history(self: Pin<&mut Sioul>, weeks: i32);
509
510        /// Adds a contacts-and-calendars account (CardDAV, CalDAV); the server address may be empty.
511        #[qinvokable]
512        fn add_dav(self: Pin<&mut Sioul>, address: &QString, url: &QString, login: &QString, password: &QString);
513
514        /// Adds a Google account with your OAuth client: Google's page opens in
515        /// the browser, Sioul waits for its answer. `again`: an account's id, signed in again.
516        #[qinvokable]
517        fn add_google(self: Pin<&mut Sioul>, address: &QString, client_id: &QString, client_secret: &QString, again: &QString);
518
519        /// Stops waiting for Google's page.
520        #[qinvokable]
521        fn cancel_google(self: Pin<&mut Sioul>);
522
523        /// Adds an address's Google mail signed in on Google's page, with a
524        /// key of your own (given, else kept on this device), or signs its
525        /// account in again (`again`: its id). IMAP and SMTP then take an
526        /// access token (XOAUTH2): no password (docs/google.md, "Mail").
527        #[qinvokable]
528        fn add_google_mail(self: Pin<&mut Sioul>, address: &QString, client_id: &QString, client_secret: &QString, again: &QString);
529
530        /// Adds an address's Microsoft mail signed in on Microsoft's page, or
531        /// signs its account in again (`again`: its id). `kind`: what
532        /// discovery found ("personal", "work", "unknown"); `client_id` and
533        /// `tenant`: an application of your own, else the one kept here, else
534        /// Sioul's. IMAP and SMTP then take an access token: no password
535        /// (docs/microsoft.md).
536        #[qinvokable]
537        fn add_microsoft_mail(self: Pin<&mut Sioul>, address: &QString, kind: &QString, client_id: &QString, tenant: &QString, again: &QString);
538
539        /// Stops waiting for Microsoft's page.
540        #[qinvokable]
541        fn cancel_microsoft(self: Pin<&mut Sioul>);
542
543        /// Your answer to `microsoft_question`: Yes keeps the mailbox of the
544        /// login Microsoft's page signed in for the address given; No keeps nothing.
545        #[qinvokable]
546        fn answer_microsoft(self: Pin<&mut Sioul>, yes: bool);
547
548        /// Whether this build carries Sioul's own Microsoft application: then none of your own is needed.
549        #[qinvokable]
550        fn microsoft_built_in(self: &Sioul) -> bool;
551
552        /// What an organisation's administrator does, once, for Sioul to read
553        /// a work or school mailbox, with the link that approves the
554        /// application whose id is typed in the form; "" while it is not one.
555        /// (For the application kept here or Sioul's, discovery says it: FoundView.)
556        #[qinvokable]
557        fn microsoft_admin_text(self: &Sioul, address: &QString, client_id: &QString) -> QString;
558
559        /// Your OpenPGP keys and others', as JSON.
560        #[qinvokable]
561        fn pgp_keys(self: &Sioul) -> QString;
562
563        /// Makes a key for one of your addresses.
564        #[qinvokable]
565        fn pgp_generate(self: Pin<&mut Sioul>, address: &QString);
566
567        /// Imports keys from a file (`file://`); returns what went wrong, else "".
568        #[qinvokable]
569        fn pgp_import(self: Pin<&mut Sioul>, url: &QString, passphrase: &QString) -> QString;
570
571        /// Saves a public key into the downloads folder.
572        #[qinvokable]
573        fn pgp_export(self: Pin<&mut Sioul>, fingerprint: &QString);
574
575        /// Saves a copy of the revocation certificate of a key made here into the downloads folder.
576        #[qinvokable]
577        fn pgp_save_revocation(self: Pin<&mut Sioul>, fingerprint: &QString);
578
579        /// Removes a key; yours are kept aside, never deleted.
580        #[qinvokable]
581        fn pgp_remove(self: Pin<&mut Sioul>, fingerprint: &QString);
582
583        /// Looks up the keys a draft's recipients miss (their domain, then keys.openpgp.org).
584        #[qinvokable]
585        fn pgp_lookup(self: Pin<&mut Sioul>, draft: &QString);
586
587        /// The security key that signs a draft at Send, as JSON {ident, label},
588        /// or {expired} when its certificate expired; "" when it is signed
589        /// another way, or not at all.
590        #[qinvokable]
591        fn security_key_for_draft(self: &Sioul, id: &QString) -> QString;
592
593        /// Signs a draft with your security key, then sends it after ten
594        /// seconds to undo; `pin` as typed, or "" for the one held. What the
595        /// band shows comes in `security_key_changed`, for the draft's id.
596        #[qinvokable]
597        fn sign_and_send(self: Pin<&mut Sioul>, id: &QString, pin: &QString);
598
599        /// "Open with your security key": `pin` as typed, or "" for the one
600        /// held. What the band shows comes in `security_key_changed`, for the message's key.
601        #[qinvokable]
602        fn open_with_security_key(self: Pin<&mut Sioul>, key: &QString, pin: &QString);
603
604        /// "Not now": the band stops waiting for the security key.
605        #[qinvokable]
606        fn security_key_not_now(self: Pin<&mut Sioul>, context: &QString);
607
608        /// "Open NFC settings", on a phone whose NFC is off.
609        #[qinvokable]
610        fn open_nfc_settings(self: Pin<&mut Sioul>);
611
612        /// "Let GnuPG release it": GnuPG's smart card daemon stopped. JSON {line, done}.
613        #[qinvokable]
614        fn let_gnupg_release(self: Pin<&mut Sioul>) -> QString;
615
616        /// Your security keys, as JSON, for Accounts ▸ Encryption.
617        #[qinvokable]
618        fn security_keys(self: &Sioul) -> QString;
619
620        /// "Use a security key": reads the key plugged in, without its PIN.
621        #[qinvokable]
622        fn read_security_key(self: Pin<&mut Sioul>);
623
624        /// "Look for it", "Look for a newer version": the key's certificate,
625        /// looked up now that you asked; `ident` empty for the key just read.
626        #[qinvokable]
627        fn find_security_key_certificate(self: Pin<&mut Sioul>, ident: &QString);
628
629        /// "Import a file…": the key's certificate from a file (`file://`).
630        #[qinvokable]
631        fn import_security_key_certificate(self: Pin<&mut Sioul>, ident: &QString, url: &QString);
632
633        /// "Import from GnuPG": the key's certificate as your GnuPG keeps it;
634        /// `ident` empty for the key just read.
635        #[qinvokable]
636        fn import_security_key_from_gnupg(self: Pin<&mut Sioul>, ident: &QString);
637
638        /// "Renew for two years": GnuPG renews the key, its pinentry asking the PIN.
639        #[qinvokable]
640        fn renew_security_key(self: Pin<&mut Sioul>, ident: &QString);
641
642        /// "Send it to keys.openpgp.org", pressed once its sentence was read.
643        #[qinvokable]
644        fn send_security_key_to_keys_openpgp(self: Pin<&mut Sioul>, ident: &QString);
645
646        /// "Forget the PIN now".
647        #[qinvokable]
648        fn forget_security_key_pin(self: Pin<&mut Sioul>);
649
650        /// "Stop using this security key".
651        #[qinvokable]
652        fn stop_using_security_key(self: Pin<&mut Sioul>, ident: &QString);
653
654        /// The task page's choices: the list grouped "project" or "list", done tasks, a search, one project.
655        #[qinvokable]
656        fn show_tasks(self: Pin<&mut Sioul>, by: &QString, done: bool, query: &QString, project_id: &QString);
657
658        /// Tasks of one kind ("call"…) and one category only; "" for all. Kept for next time.
659        #[qinvokable]
660        fn filter_tasks(self: Pin<&mut Sioul>, kind: &QString, category: &QString);
661
662        /// Asleep, the tasks all the same ("Show anyway"), until the page is left.
663        #[qinvokable]
664        fn show_tasks_anyway(self: Pin<&mut Sioul>, on: bool);
665
666        /// Reads tasks and notes again and shows them.
667        #[qinvokable]
668        fn refresh_work(self: Pin<&mut Sioul>);
669
670        /// One task in full, with its steps and what it is tied to, as JSON.
671        #[qinvokable]
672        fn task(self: &Sioul, uid: &QString) -> QString;
673
674        /// The form of a task not made yet, as `task` gives one (without a card):
675        /// blank, or what Add ▾ makes from `from` (a message's `key`, an
676        /// occurrence's `start`); in `list`, else the usual list. Nothing is
677        /// written: `save_task` with an empty UID makes it. Or {"error"}.
678        #[qinvokable]
679        fn new_task_form(self: &Sioul, from: &QString, key: &QString, start: f64, list: &QString) -> QString;
680
681        /// Saves the task form (JSON); an empty UID makes a new task in `list`.
682        /// Returns {"uid": …} or {"error": …}.
683        #[qinvokable]
684        fn save_task(self: Pin<&mut Sioul>, uid: &QString, edit: &QString, list: &QString) -> QString;
685
686        /// What a typed line says (its chips), before it becomes a task, as JSON.
687        #[qinvokable]
688        fn capture(self: &Sioul, line: &QString) -> QString;
689
690        /// A task from a typed line; a step of `parent` when given. Returns {"uid"} or {"error"}.
691        #[qinvokable]
692        fn add_task(self: Pin<&mut Sioul>, line: &QString, parent: &QString, list: &QString) -> QString;
693
694        /// "completed", "in-process", "needs-action", "cancelled"; returns what it changed, in a sentence.
695        #[qinvokable]
696        fn set_task_status(self: Pin<&mut Sioul>, uid: &QString, status: &QString) -> QString;
697
698        /// How a task was, felt: today's rating ({cognitive, emotional, anxiety,
699        /// body, gain}, null for unsaid), written alone; returns what went wrong, else "".
700        #[qinvokable]
701        fn set_felt(self: Pin<&mut Sioul>, uid: &QString, felt: &QString) -> QString;
702
703        /// The task waits for `other`, or no longer; with `gap_minutes`, that long
704        /// after `other` is done (`work::set_waits`). Returns what went wrong, else "".
705        #[qinvokable]
706        fn set_waits(self: Pin<&mut Sioul>, uid: &QString, other: &QString, wait: bool, gap_minutes: i32) -> QString;
707
708        /// Puts a task off until tomorrow.
709        #[qinvokable]
710        fn not_now(self: Pin<&mut Sioul>, uid: &QString);
711
712        /// How today is: "clear", "haze", "fog".
713        #[qinvokable]
714        fn set_weather(self: Pin<&mut Sioul>, weather: &QString);
715
716        /// Deletes a task after ten seconds to undo.
717        #[qinvokable]
718        fn delete_task(self: Pin<&mut Sioul>, uid: &QString);
719
720        /// A task pinned to a time ("2026-10-06T14:30"), its time block (an
721        /// event) made or moved there, as long as it is, else as the plan lays
722        /// it; or left to the plan (""), its blocks to come taken away; "Undo"
723        /// offered. Returns what went wrong, else "".
724        #[qinvokable]
725        fn set_task_at(self: Pin<&mut Sioul>, uid: &QString, at: &QString) -> QString;
726
727        /// A task pinned to a time ("2026-10-06T14:30") for `minutes` (0: as
728        /// long as its block is, else as the plan lays it): "Do at…", or a step
729        /// dragged in the day view (docs/tasks.md, "Pinned to a time"); "Undo"
730        /// offered. Returns what went wrong, else "".
731        #[qinvokable]
732        fn pin_task(self: Pin<&mut Sioul>, uid: &QString, at: &QString, minutes: i32) -> QString;
733
734        /// Open tasks whose title holds `query`, `except` left out, as JSON.
735        #[qinvokable]
736        fn search_tasks(self: &Sioul, query: &QString, except: &QString) -> QString;
737
738        /// Makes a task list in an account ("local": kept here only); returns "account/id", or what went wrong.
739        #[qinvokable]
740        fn new_list(self: Pin<&mut Sioul>, account: &QString, name: &QString) -> QString;
741
742        /// The accounts a task list can be made in, as JSON.
743        #[qinvokable]
744        fn list_accounts(self: &Sioul) -> QString;
745
746        /// Starts a focus session on a task.
747        #[qinvokable]
748        fn focus_start(self: Pin<&mut Sioul>, uid: &QString, minutes: i32);
749
750        /// Pauses the focus session, or goes on.
751        #[qinvokable]
752        fn focus_pause(self: Pin<&mut Sioul>);
753
754        /// More minutes for the focus session.
755        #[qinvokable]
756        fn focus_extend(self: Pin<&mut Sioul>, minutes: i32);
757
758        /// Ends the focus session, kept with where you stopped; returns what it changed.
759        #[qinvokable]
760        fn focus_stop(self: Pin<&mut Sioul>, done: bool, note: &QString) -> QString;
761
762        /// Shows the notes matching `query`.
763        #[qinvokable]
764        fn search_notes(self: Pin<&mut Sioul>, query: &QString);
765
766        /// One note: text, HTML, tags, checkboxes, what is tied to it, as JSON.
767        #[qinvokable]
768        fn note(self: &Sioul, path: &QString) -> QString;
769
770        /// Saves a note read as `stamp` (its "stamp" in `note`); returns JSON
771        /// {problem, stamp, path, kept}: when the file changed meanwhile, this
772        /// version went beside it, at `path`, and `kept` says so.
773        #[qinvokable]
774        fn save_note(self: Pin<&mut Sioul>, path: &QString, text: &QString, stamp: &QString) -> QString;
775
776        /// A new note in the notes folder, linking `links` (a JSON list); returns its path.
777        #[qinvokable]
778        fn create_note(self: Pin<&mut Sioul>, title: &QString, links: &QString) -> QString;
779
780        /// What a thing is tied to, both ways, as JSON.
781        #[qinvokable]
782        fn related(self: &Sioul, uri: &QString) -> QString;
783
784        /// Ties two things, in the one that can hold the link, else locally; returns what the line says.
785        #[qinvokable]
786        fn link_things(self: Pin<&mut Sioul>, from: &QString, to: &QString) -> QString;
787
788        /// Undoes a tie wherever it is written; returns what the line says.
789        #[qinvokable]
790        fn unlink_things(self: Pin<&mut Sioul>, from: &QString, to: &QString) -> QString;
791
792        /// Things to tie `from` to, matching `query`, of `kind` ("" for all), as JSON.
793        #[qinvokable]
794        fn search_things(self: &Sioul, query: &QString, kind: &QString, from: &QString) -> QString;
795
796        /// The address of a thing known by its file or id: "mail" (a message's file), "note", "task", "event", "contact".
797        #[qinvokable]
798        fn uri_of(self: &Sioul, kind: &QString, id: &QString) -> QString;
799
800        /// Whether a file, by its name, path or address, is one the desktop would start: a program, a script, an installer
801        /// (on a phone, Android's installers too).
802        #[qinvokable]
803        fn is_program(self: &Sioul, name: &QString) -> bool;
804
805        /// The window's first frame drawn: said with how long the start took, when timed (`crate::timing`).
806        #[qinvokable]
807        fn first_frame(self: &Sioul);
808
809        /// A step of the start, said with how long the start took so far, when timed (`crate::timing`).
810        #[qinvokable]
811        fn mark(self: &Sioul, what: &QString);
812
813        /// Whether this system keeps accounts Sioul may be shown (Android's: Murena, Google…).
814        #[qinvokable]
815        fn phone_accounts(self: &Sioul) -> bool;
816
817        /// Android's own chooser of the phone's accounts; the one chosen comes back by `phone_account_chosen`.
818        #[qinvokable]
819        fn choose_phone_account(self: Pin<&mut Sioul>);
820
821        /// A budget opened: its ledger for the period around `anchor` ("2026-10-05"),
822        /// its balance by `step` ("day", "week", "month", "year"), as JSON.
823        #[qinvokable]
824        fn budget_detail(self: &Sioul, id: &QString, anchor: &QString, step: &QString) -> QString;
825
826        /// A folder kept here, or left on the server only (its copy here goes);
827        /// returns what went wrong, else "".
828        #[qinvokable]
829        fn keep_folder(self: Pin<&mut Sioul>, account: &QString, folder: &QString, kept: bool) -> QString;
830
831        /// A new folder on the server; the status line says how it went.
832        #[qinvokable]
833        fn create_folder(self: Pin<&mut Sioul>, account: &QString, name: &QString);
834
835        /// An empty folder taken off the server; the status line says how it went.
836        #[qinvokable]
837        fn delete_folder(self: Pin<&mut Sioul>, account: &QString, folder: &QString);
838
839        /// A noise made here ("white", "pink", "brown"), as a file URL; made the first time.
840        #[qinvokable]
841        fn noise_url(self: &Sioul, kind: &QString) -> QString;
842
843        /// Your recordings to rest by, from the notes' `sounds` folder, as JSON.
844        #[qinvokable]
845        fn calm_sounds(self: &Sioul) -> QString;
846
847        /// A task moved to another list ("account/id"), its steps with it; returns
848        /// {"uid"}, {"error"}, or {"losses"} to confirm first.
849        #[qinvokable]
850        fn move_task(self: Pin<&mut Sioul>, uid: &QString, list: &QString, confirmed: bool) -> QString;
851
852        /// A contact moved to another address book ("account/id"); returns {"key"},
853        /// {"error"}, or {"losses"} to confirm first.
854        #[qinvokable]
855        fn move_contact(self: Pin<&mut Sioul>, key: &QString, book: &QString, confirmed: bool) -> QString;
856
857        /// The address books a contact can go into, as JSON.
858        #[qinvokable]
859        fn address_books(self: &Sioul) -> QString;
860
861        /// Places by name for the weather; the answer comes in `places_found`.
862        #[qinvokable]
863        fn find_places(self: Pin<&mut Sioul>, name: &QString);
864
865        /// The weather's place; an empty name takes the weather away.
866        #[qinvokable]
867        fn set_weather_place(self: Pin<&mut Sioul>, name: &QString, latitude: f64, longitude: f64);
868
869        /// The health page: the week of the day shown, the medicines, the prescriptions, as JSON.
870        #[qinvokable]
871        fn health_page(self: &Sioul) -> QString;
872
873        /// A medicine made (`id` empty) or changed (JSON); returns {"id"} or {"error"}.
874        #[qinvokable]
875        fn save_medicine(self: Pin<&mut Sioul>, id: &QString, edit: &QString) -> QString;
876
877        /// A prescription made (`id` empty) or changed (JSON); returns {"id"} or {"error"}.
878        #[qinvokable]
879        fn save_prescription(self: Pin<&mut Sioul>, id: &QString, edit: &QString) -> QString;
880
881        /// "Show to a doctor or pharmacist": a prescription's medicines (its
882        /// id), or all those taken now (""), as JSON, read-only.
883        #[qinvokable]
884        fn health_for_professional(self: &Sioul, prescription: &QString) -> QString;
885
886        /// A medicine or a prescription taken out; returns what went wrong, else "".
887        #[qinvokable]
888        fn remove_health(self: Pin<&mut Sioul>, id: &QString) -> QString;
889
890        /// A prescription's medicines fetched today; returns what went wrong, else "".
891        #[qinvokable]
892        fn refilled(self: Pin<&mut Sioul>, id: &QString) -> QString;
893
894        /// A dose marked taken, or not, as it happens: taken late or early, it
895        /// may move the next ones (a medicine counted from its last dose).
896        #[qinvokable]
897        fn set_dose_taken(self: Pin<&mut Sioul>, key: &QString, taken: bool);
898
899        /// A dose not taken, said afterwards: not asked again.
900        #[qinvokable]
901        fn dose_not_taken(self: Pin<&mut Sioul>, key: &QString);
902
903        /// A dose taken late, at `time` ("09:30"); taken every few hours, the
904        /// next doses come that many hours after it. Returns what went wrong, else "".
905        #[qinvokable]
906        fn dose_taken_at(self: Pin<&mut Sioul>, key: &QString, time: &QString) -> QString;
907
908        /// What the late dose's question shows, as JSON.
909        #[qinvokable]
910        fn dose_info(self: &Sioul, key: &QString) -> QString;
911
912        /// Answers that differ on your devices settled as you chose: taken,
913        /// or not (docs/health.md, "Doses as records"). Returns what went wrong, else "".
914        #[qinvokable]
915        fn dose_choose(self: Pin<&mut Sioul>, key: &QString, taken: bool) -> QString;
916
917        /// A device you say is off (`off`), or counted again: not counted for
918        /// the doses until it says anything newer (docs/health.md, "Knowing").
919        /// Returns what went wrong, else "".
920        #[qinvokable]
921        fn device_off(self: Pin<&mut Sioul>, id: &QString, off: bool) -> QString;
922
923        /// A device silent for a week, forgotten (Settings ▸ Your folder and sharing).
924        #[qinvokable]
925        fn device_forget(self: Pin<&mut Sioul>, id: &QString) -> QString;
926
927        /// Meals, naps and the night, for the Health page, as JSON.
928        #[qinvokable]
929        fn needs(self: &Sioul) -> QString;
930
931        /// Meals, naps and the night saved; returns what went wrong, else "".
932        #[qinvokable]
933        fn save_needs(self: Pin<&mut Sioul>, edit: &QString, shown: &QString) -> QString;
934
935        /// A block skipped today, or not; returns what went wrong, else "".
936        #[qinvokable]
937        fn skip_need(self: Pin<&mut Sioul>, key: &QString, skip: bool) -> QString;
938
939        /// Today's meals, naps and night as they are now, as JSON.
940        #[qinvokable]
941        fn needs_today(self: &Sioul) -> QString;
942
943        /// The health views made again, off the window's thread (`healthView`, `missedView`, `needsView`).
944        #[qinvokable]
945        fn refresh_health(self: Pin<&mut Sioul>);
946
947        /// The Health page shows the week of `day` ("2026-10-07"): made again
948        /// off the window's thread, into `healthView`.
949        #[qinvokable]
950        fn show_health_week(self: Pin<&mut Sioul>, day: &QString);
951
952        /// A day's meal, nap or night changed, that day only (JSON: date, key,
953        /// action…; `health::change_need`); returns what went wrong, else "".
954        #[qinvokable]
955        fn change_need(self: Pin<&mut Sioul>, edit: &QString) -> QString;
956
957        /// The same, by a drag on the timeline: "Undo" offered (`health::drag_need`).
958        #[qinvokable]
959        fn drag_need(self: Pin<&mut Sioul>, edit: &QString) -> QString;
960
961        /// The Health page's settings (⚙): the pauses, where the errands go, the watch, as JSON.
962        #[qinvokable]
963        fn health_settings(self: &Sioul) -> QString;
964
965        /// On a phone, Android's page where the alarm at waking is allowed:
966        /// "exact" (Alarms & reminders), "screen" (full screen), "notifications" (`wake`).
967        #[qinvokable]
968        fn wake_settings(self: &Sioul, which: &QString);
969
970        /// On a phone, "Try the alarm": rung ten seconds on, as a waking, the
971        /// next waking left as it is; JSON {rings, line, fix, button} (`wake::try_now`).
972        #[qinvokable]
973        fn try_wake(self: &Sioul) -> QString;
974
975        /// An overlap set aside for good; returns what went wrong, else "".
976        #[qinvokable]
977        fn set_overlap_aside(self: Pin<&mut Sioul>, key: &QString) -> QString;
978
979        /// A block moved today only: `minutes` more (0: the minutes "Later" moves
980        /// it by), or to start at `time` ("13:30"). Returns what went wrong, else "".
981        #[qinvokable]
982        fn move_need(self: Pin<&mut Sioul>, key: &QString, minutes: i32, time: &QString) -> QString;
983
984        /// The minutes "Later" moves a block by.
985        #[qinvokable]
986        fn needs_later(self: &Sioul) -> i32;
987
988        /// The Porch's doses, as JSON: {due: today's not marked yet, closed: those
989        /// due while Sioul was closed}, each [{key, time, name, dose, late, doubt}].
990        #[qinvokable]
991        fn missed_doses(self: &Sioul) -> QString;
992
993        /// You are at this window now: what follows you (medicines' reminders) comes here.
994        #[qinvokable]
995        fn touch(self: Pin<&mut Sioul>);
996
997        /// Put away on a phone: what was marked goes out, and the other devices
998        /// learn this one marks nothing until it is back (docs/health.md, "Knowing").
999        #[qinvokable]
1000        fn going_away(self: Pin<&mut Sioul>);
1001
1002        /// Back on a phone: the claims say so at once, before the next minute.
1003        #[qinvokable]
1004        fn back_here(self: Pin<&mut Sioul>);
1005
1006        /// The pause to move, the chats' limit: "movement.minutes", "chats.enabled"…
1007        #[qinvokable]
1008        fn set_health(self: Pin<&mut Sioul>, key: &QString, value: &QString) -> QString;
1009
1010        /// Minutes of focus before the pause to move; 0 when off.
1011        #[qinvokable]
1012        fn movement_minutes(self: &Sioul) -> i32;
1013
1014        /// One more minute in a chat; whether chats are covered now.
1015        #[qinvokable]
1016        fn chat_minute(self: Pin<&mut Sioul>) -> bool;
1017
1018        /// Whether chats are covered now.
1019        #[qinvokable]
1020        fn chats_covered(self: &Sioul) -> bool;
1021
1022        /// Where a new audio memo is recorded, as a file URL; "" without a notes folder.
1023        #[qinvokable]
1024        fn memo_url(self: &Sioul) -> QString;
1025
1026        /// A memo recorded: the notes are read again; returns its path in the notes.
1027        #[qinvokable]
1028        fn memo_recorded(self: Pin<&mut Sioul>, url: &QString) -> QString;
1029
1030        /// A note (a picture, a PDF, a sound) moved to the vault's trash, "Undo"
1031        /// offered; returns what went wrong, else "".
1032        #[qinvokable]
1033        fn trash_note(self: Pin<&mut Sioul>, path: &QString) -> QString;
1034
1035        /// A note renamed, what names it following; returns {"path"} or {"error"}.
1036        #[qinvokable]
1037        fn rename_note(self: Pin<&mut Sioul>, path: &QString, name: &QString) -> QString;
1038
1039        /// A new note in a folder ("" for the notes' own); returns its path.
1040        #[qinvokable]
1041        fn create_note_in(self: Pin<&mut Sioul>, folder: &QString, title: &QString) -> QString;
1042
1043        /// A new folder of notes in `parent`; returns what went wrong, else "".
1044        #[qinvokable]
1045        fn make_folder(self: Pin<&mut Sioul>, parent: &QString, name: &QString) -> QString;
1046
1047        /// A folder renamed, its notes and their links following; returns {"path"} or {"error"}.
1048        #[qinvokable]
1049        fn rename_folder(self: Pin<&mut Sioul>, path: &QString, name: &QString) -> QString;
1050
1051        /// An empty folder of notes taken out; returns what went wrong, else "".
1052        #[qinvokable]
1053        fn remove_folder(self: Pin<&mut Sioul>, path: &QString) -> QString;
1054
1055        /// A project taken out of the list; returns what went wrong, else "".
1056        #[qinvokable]
1057        fn remove_project(self: Pin<&mut Sioul>, id: &QString) -> QString;
1058
1059        /// A budget made (`id` empty) or changed (JSON); returns {"id"} or {"error"}.
1060        #[qinvokable]
1061        fn save_budget(self: Pin<&mut Sioul>, id: &QString, edit: &QString) -> QString;
1062
1063        /// A budget taken out, by its id; returns what went wrong.
1064        #[qinvokable]
1065        fn remove_budget(self: Pin<&mut Sioul>, what: &QString) -> QString;
1066
1067        /// A line of the budgets' file taken out, by its address and its row
1068        /// as the page showed it (JSON: day, value, label), found again by
1069        /// them; returns what went wrong, else "".
1070        #[qinvokable]
1071        fn remove_line(self: Pin<&mut Sioul>, uri: &QString, shown: &QString) -> QString;
1072
1073        /// Notes as a tree of folders, or as one list.
1074        #[qinvokable]
1075        fn set_notes_tree(self: Pin<&mut Sioul>, tree: bool);
1076
1077        /// How a page was left ("sites-narrow", "notes-recent"…), kept between sessions.
1078        #[qinvokable]
1079        fn view_flag(self: &Sioul, name: &QString) -> bool;
1080
1081        #[qinvokable]
1082        fn set_view_flag(self: Pin<&mut Sioul>, name: &QString, value: bool);
1083
1084
1085        /// A movement added by hand, once or recurring (JSON); returns what went wrong, else "".
1086        #[qinvokable]
1087        fn add_movement(self: Pin<&mut Sioul>, edit: &QString) -> QString;
1088
1089        /// A project's billable time from one day to another, written as a
1090        /// spreadsheet (CSV) at a file address; returns what went wrong, else "".
1091        #[qinvokable]
1092        fn export_time_csv(self: Pin<&mut Sioul>, project: &QString, from: &QString, to: &QString, target: &QString) -> QString;
1093
1094        /// Paper letters not done with, as JSON (`letters::view`).
1095        #[qinvokable]
1096        fn letters(self: &Sioul) -> QString;
1097
1098        /// A paper letter's date as a task; returns what went wrong, else "".
1099        #[qinvokable]
1100        fn letter_task(self: Pin<&mut Sioul>, id: &QString) -> QString;
1101
1102        /// A paper letter's appointment in the agenda; returns what went wrong, else "".
1103        #[qinvokable]
1104        fn letter_event(self: Pin<&mut Sioul>, id: &QString) -> QString;
1105
1106        /// Done with a paper letter: its project set (if `project`), its scan filed; returns what went wrong, else "".
1107        #[qinvokable]
1108        fn letter_done(self: Pin<&mut Sioul>, id: &QString, project: &QString) -> QString;
1109
1110        /// The folder scans of paper letters arrive in, made if missing; "" without one (`letters::folder`).
1111        #[qinvokable]
1112        fn letters_folder(self: &Sioul) -> QString;
1113
1114        /// Scans chosen here (a JSON array of file addresses) copied into that
1115        /// folder, to be read at a coming minute: what happened, in a sentence (`letters::add`).
1116        #[qinvokable]
1117        fn letters_add(self: Pin<&mut Sioul>, files: &QString) -> QString;
1118
1119        /// The money watch, as JSON (`bank::view`).
1120        #[qinvokable]
1121        fn bank(self: &Sioul) -> QString;
1122
1123        /// A bank export taken in: {"ok", "said"}.
1124        #[qinvokable]
1125        fn import_bank(self: Pin<&mut Sioul>, file: &QString) -> QString;
1126
1127        /// An export taken in as one bank account's; returns {"ok", "said"}.
1128        #[qinvokable]
1129        fn import_bank_into(self: Pin<&mut Sioul>, file: &QString, account: &QString) -> QString;
1130
1131        /// A bank account made (`id` empty) or changed, from the dialog's JSON; returns what went wrong, if anything.
1132        #[qinvokable]
1133        fn save_bank_account(self: Pin<&mut Sioul>, id: &QString, edit: &QString) -> QString;
1134
1135        #[qinvokable]
1136        fn remove_bank_account(self: Pin<&mut Sioul>, id: &QString) -> QString;
1137
1138        /// One movement placed by hand: "budget:<id>", "none", or "" (as the rules say).
1139        #[qinvokable]
1140        fn place_movement(self: Pin<&mut Sioul>, account: &QString, movement: &QString, choice: &QString) -> QString;
1141
1142        /// A rule for movements made (`rule` "") or changed (`rule`: the rule as
1143        /// shown, JSON: named by its id, or its place while it holds the same).
1144        #[qinvokable]
1145        fn save_bank_rule(self: Pin<&mut Sioul>, rule: &QString, edit: &QString) -> QString;
1146
1147        #[qinvokable]
1148        fn remove_bank_rule(self: Pin<&mut Sioul>, rule: &QString) -> QString;
1149
1150        /// A reserve made (`id` empty) or changed: its balance, its floor, its delay.
1151        #[qinvokable]
1152        fn save_reserve(self: Pin<&mut Sioul>, id: &QString, edit: &QString) -> QString;
1153
1154        /// Contracts and subscriptions, as JSON (`contracts::view`).
1155        #[qinvokable]
1156        fn contracts(self: &Sioul) -> QString;
1157
1158        /// A contract saved (new when `id` is empty) from the form's JSON; returns what went wrong, else "".
1159        #[qinvokable]
1160        fn save_contract(self: Pin<&mut Sioul>, id: &QString, edit: &QString) -> QString;
1161
1162        /// A contract taken out; returns what went wrong, else "".
1163        #[qinvokable]
1164        fn remove_contract(self: Pin<&mut Sioul>, id: &QString) -> QString;
1165
1166        /// The letter that stops a contract, drafted: {"draft"} or {"error"}.
1167        #[qinvokable]
1168        fn contract_letter(self: Pin<&mut Sioul>, id: &QString) -> QString;
1169
1170        /// What a mail says of a contract (its subject, its sender), to start the form, as JSON.
1171        #[qinvokable]
1172        fn contract_from(self: &Sioul, subject: &QString, from: &QString) -> QString;
1173
1174        /// The routines, as JSON: the admin window's, then yours (`work::routines`).
1175        #[qinvokable]
1176        fn routines(self: &Sioul) -> QString;
1177
1178        /// A routine saved (new when `id` is empty): its title, its steps one a line, whether it moves on by itself.
1179        #[qinvokable]
1180        fn save_routine(self: Pin<&mut Sioul>, id: &QString, title: &QString, steps: &QString, by_itself: bool) -> QString;
1181
1182        /// A routine taken out.
1183        #[qinvokable]
1184        fn remove_routine(self: Pin<&mut Sioul>, id: &QString) -> QString;
1185
1186        /// Sharing with your other computers, as JSON (`share::status`); `folder` is the one being chosen, if any.
1187        #[qinvokable]
1188        fn share_status(self: &Sioul, folder: &QString) -> QString;
1189
1190        /// Starts sharing through a folder, sealed with a passphrase (typed twice the first time); returns what went wrong, else "".
1191        #[qinvokable]
1192        fn start_sharing(self: Pin<&mut Sioul>, folder: &QString, passphrase: &QString, again: &QString) -> QString;
1193
1194        /// Stops sharing on this computer; returns what went wrong, else "".
1195        #[qinvokable]
1196        fn stop_sharing(self: Pin<&mut Sioul>) -> QString;
1197
1198        /// Exchanges with the other computers now.
1199        #[qinvokable]
1200        fn share_now(self: Pin<&mut Sioul>);
1201
1202        /// Folders already shared through by your other devices (they hold a seal), as JSON: ["path", …].
1203        #[qinvokable]
1204        fn share_candidates(self: &Sioul) -> QString;
1205
1206        /// Whether Sioul may read and write your files by their path (Android: "All files access").
1207        #[qinvokable]
1208        fn files_access(self: &Sioul) -> bool;
1209
1210        /// Projects and budgets travel through the sharing too, or not; returns what went wrong, else "".
1211        #[qinvokable]
1212        fn set_share_projects(self: Pin<&mut Sioul>, on: bool) -> QString;
1213
1214        /// A part of what is shared ("settings", "notes"…) switched on or off on this device; returns what went wrong, else "".
1215        #[qinvokable]
1216        fn set_share_part(self: Pin<&mut Sioul>, part: &QString, on: bool) -> QString;
1217
1218        /// On a phone: whether a sync app carries the notes folder, as you
1219        /// say (Sioul cannot read its settings there). Returns what went wrong, else "".
1220        #[qinvokable]
1221        fn set_notes_carried(self: Pin<&mut Sioul>, carried: bool) -> QString;
1222
1223        /// The versions kept here before other devices' changes whose file's
1224        /// name holds `filter`, listed off the window's thread: `share_listed`
1225        /// brings them, as JSON [{"part", "name", "more", "files": [{"file", "shown", "count", "versions": [{"stamp", "when", "size"}]}]}].
1226        #[qinvokable]
1227        fn share_history(self: Pin<&mut Sioul>, filter: &QString);
1228
1229        /// What putting a version back would change, in a sentence, said before it is done.
1230        #[qinvokable]
1231        fn share_put_back_preview(self: &Sioul, part: &QString, file: &QString, stamp: &QString) -> QString;
1232
1233        /// A file put back as it was (`stamp`), the one there now kept too, then
1234        /// sent, off the window's thread: `share_put_back_done` says how it went.
1235        #[qinvokable]
1236        fn share_put_back(self: Pin<&mut Sioul>, part: &QString, file: &QString, stamp: &QString);
1237
1238        /// The files gone at once from a folder of notes or papers ("files/notes/")
1239        /// taken out everywhere, as you said; returns what went wrong, else "".
1240        #[qinvokable]
1241        fn share_confirm_gone(self: Pin<&mut Sioul>, store: &QString) -> QString;
1242
1243        /// The notes and papers gone many at once here and held until they
1244        /// leave by themselves, as the last exchange left them, in words, as
1245        /// JSON (`share::withheld`): for the Porch to say once.
1246        #[qinvokable]
1247        fn share_withheld(self: &Sioul) -> QString;
1248
1249        /// "Put them back": the notes or papers held after going many at
1250        /// once from a folder of the records ("files/notes/") written back
1251        /// where they were, off the window's thread (thousands take half a
1252        /// minute), then an exchange: `share_withheld_back` says how it went.
1253        #[qinvokable]
1254        fn share_put_back_withheld(self: Pin<&mut Sioul>, store: &QString);
1255
1256        /// "Take them out now": the notes or papers held after going many at
1257        /// once from a folder of the records taken out everywhere, those
1258        /// alone, then an exchange; returns what went wrong, else "".
1259        #[qinvokable]
1260        fn share_take_out_withheld(self: Pin<&mut Sioul>, store: &QString) -> QString;
1261
1262        /// What "Remove its files" would take out for another device, and
1263        /// what stays, in words, as JSON (`share::device_files`): `{ text, live }`.
1264        #[qinvokable]
1265        fn device_files(self: &Sioul, id: &QString) -> QString;
1266
1267        /// "Remove its files" (Settings ▸ Your folder and sharing): the
1268        /// device forgotten first while it counts, then everything it wrote
1269        /// in the folder taken out (`share::remove_device_files`), then an
1270        /// exchange; returns what went wrong, else "".
1271        #[qinvokable]
1272        fn device_remove_files(self: Pin<&mut Sioul>, id: &QString) -> QString;
1273
1274        /// What switching Notes or Papers on would send, counted off the
1275        /// window's thread: `share_estimated` brings it.
1276        #[qinvokable]
1277        fn share_estimate(self: Pin<&mut Sioul>, part: &QString);
1278
1279        /// Sioul's own spam filter as the mail settings show it (`spam::status`), as JSON.
1280        #[qinvokable]
1281        fn spam_status(self: &Sioul) -> QString;
1282
1283        /// "Train now", on a computer: the corpus brought up to date, then a
1284        /// training, off the window's thread; `spam_changed` says how it goes.
1285        /// `named`: the addresses its sentence named (a JSON list,
1286        /// `train_named`), kept as your consent for a Microsoft address
1287        /// among them (`sioul_learn::consent`). Returns why it did not start, else "".
1288        #[qinvokable]
1289        fn spam_train(self: Pin<&mut Sioul>, named: &QString) -> QString;
1290
1291        /// The training running stops at its next step.
1292        #[qinvokable]
1293        fn spam_stop(self: Pin<&mut Sioul>);
1294
1295        /// The mail filters as Mail ▸ ⚙ shows them (`filters::view`): each
1296        /// filter in words, the addresses, their folders, the editor's words, as JSON.
1297        #[qinvokable]
1298        fn mail_filters(self: &Sioul) -> QString;
1299
1300        /// The list of mail filters written, in its order (JSON, as
1301        /// `mail_filters` gives them), from the list the editor showed
1302        /// (`shown`): its change set over the filters as they are now. Returns
1303        /// what went wrong, else "".
1304        #[qinvokable]
1305        fn set_mail_filters(self: Pin<&mut Sioul>, filters: &QString, shown: &QString) -> QString;
1306
1307        /// One filter being edited, in words: {said, problem, only}, as JSON.
1308        #[qinvokable]
1309        fn mail_filter_said(self: &Sioul, filter: &QString) -> QString;
1310
1311        /// "Try it": what one filter being edited takes in the inboxes now,
1312        /// counted off the window's thread; `mail_filters_found` brings it.
1313        #[qinvokable]
1314        fn mail_filter_try(self: Pin<&mut Sioul>, filter: &QString);
1315
1316        /// What every filter would do in the inboxes now, read mail included,
1317        /// found off the window's thread; `mail_filters_found` brings it.
1318        #[qinvokable]
1319        fn mail_filters_preview(self: Pin<&mut Sioul>);
1320
1321        /// What the preview said, done after ten seconds to undo; returns why not, else "".
1322        #[qinvokable]
1323        fn mail_filters_run(self: Pin<&mut Sioul>) -> QString;
1324
1325        /// A new mail filter from a search's conditions ({conditions, match},
1326        /// MailSearch.qml's "Make it a filter…"), last in the list and opened
1327        /// in the editor the next time Mail ▸ ⚙ shows it; returns what went wrong, else "".
1328        #[qinvokable]
1329        fn new_mail_filter(self: Pin<&mut Sioul>, given: &QString) -> QString;
1330
1331        /// The other devices' files fetched from the server too, or not, on
1332        /// this device (docs/database.md); returns what went wrong, else "".
1333        #[qinvokable]
1334        fn set_share_backup(self: Pin<&mut Sioul>, on: bool) -> QString;
1335
1336        /// This device's own files sent to that server too, beside the sync
1337        /// app, or not, on this device (docs/database.md, "Sent to the server
1338        /// too"); returns what went wrong, else "".
1339        #[qinvokable]
1340        fn set_share_send(self: Pin<&mut Sioul>, on: bool) -> QString;
1341
1342        /// "Send everything again": this device's files sent to the server
1343        /// again, whatever was sent before, then the others' fetched; off the
1344        /// window's thread, said in the panel's status (docs/database.md).
1345        #[qinvokable]
1346        fn share_send_again(self: Pin<&mut Sioul>);
1347
1348        /// The sharing folder's place on the server, given by hand
1349        /// ("Documents/Sioul", or its address; "" to look for it by itself),
1350        /// looked at off the window's thread; returns what went wrong, else "".
1351        #[qinvokable]
1352        fn share_backup_place(self: Pin<&mut Sioul>, place: &QString) -> QString;
1353
1354        /// Starts sharing through a folder of an account's files that Sioul
1355        /// keeps in step itself, no sync app (`account`: its id; `place`:
1356        /// "Documents/Sioul"), joined or made there, off the window's thread:
1357        /// `share_started` says how it went.
1358        #[qinvokable]
1359        fn start_sharing_on_server(self: Pin<&mut Sioul>, account: &QString, place: &QString, passphrase: &QString, again: &QString);
1360
1361        /// Signs in to a Nextcloud through its own page, in the browser
1362        /// ("murena", or another's address), to join your other devices
1363        /// (`join::sign_in`), off the window's thread: `join_changed` says
1364        /// each step, `join_status` where it stands.
1365        #[qinvokable]
1366        fn join_sign_in(self: Pin<&mut Sioul>, server: &QString);
1367
1368        /// The sign-in's page opened again in the browser.
1369        #[qinvokable]
1370        fn join_open_again(self: Pin<&mut Sioul>);
1371
1372        /// "Look again": the server looked through again for the sharing
1373        /// folder, with the sign-in held, off the window's thread (`join::look_again`).
1374        #[qinvokable]
1375        fn join_look_again(self: Pin<&mut Sioul>);
1376
1377        /// Cancel: the sign-in stops where it is; an app password given
1378        /// already is given back to its server and forgotten here.
1379        #[qinvokable]
1380        fn join_cancel(self: Pin<&mut Sioul>);
1381
1382        /// Where a sign-in and its joining stand, as JSON (`join::status`):
1383        /// {step, host, signed, places, problem, lines, needs}.
1384        #[qinvokable]
1385        fn join_status(self: &Sioul) -> QString;
1386
1387        /// Joins the sharing through `place` on the server signed in to, with
1388        /// the passphrase (typed once, checked against the seal; `again` for
1389        /// a new sharing there), off the window's thread (`join::join`).
1390        #[qinvokable]
1391        fn join_folder(self: Pin<&mut Sioul>, place: &QString, passphrase: &QString, again: &QString);
1392
1393        /// The join's result read, or a failed sign-in left: nothing said any more.
1394        #[qinvokable]
1395        fn join_done(self: Pin<&mut Sioul>);
1396
1397        /// "Does this phone replace …?" answered, once (`join::replace`):
1398        /// `yes`, this phone's entry says it replaces `device`. Returns what
1399        /// went wrong, else "".
1400        #[qinvokable]
1401        fn join_replace(self: Pin<&mut Sioul>, device: &QString, yes: bool) -> QString;
1402
1403        /// A folder's own folders, for Sioul's folder browser, as JSON: {"path", "parent", "folders", "readable"}.
1404        #[qinvokable]
1405        fn folders_in(self: &Sioul, path: &QString) -> QString;
1406
1407        /// Android: its own switch for "All files access" (or the older question before Android 11).
1408        #[qinvokable]
1409        fn ask_files_access(self: Pin<&mut Sioul>);
1410
1411        /// A line of the budgets' file changed: label, amount, date; found again
1412        /// by its address and its row as the page showed it (JSON: day, value,
1413        /// label). Returns what went wrong, else "".
1414        #[qinvokable]
1415        fn change_line(self: Pin<&mut Sioul>, uri: &QString, shown: &QString, label: &QString, amount: f64, date: &QString) -> QString;
1416
1417        /// The sites kept in Sioul, as JSON.
1418        #[qinvokable]
1419        fn site_list(self: &Sioul) -> QString;
1420
1421        /// A site's notification: shown at once in real time, else kept for the Porch.
1422        #[qinvokable]
1423        fn site_notified(self: Pin<&mut Sioul>, id: &QString, title: &QString, text: &QString);
1424
1425        /// A site opened: what it notified is seen.
1426        #[qinvokable]
1427        fn site_seen(self: Pin<&mut Sioul>, id: &QString);
1428
1429        /// What the sites notified, for the Porch, as JSON.
1430        #[qinvokable]
1431        fn site_notices(self: &Sioul) -> QString;
1432
1433        /// The site a sender's mail announces: {"id", "name"}, else "".
1434        #[qinvokable]
1435        fn site_for_sender(self: &Sioul, sender: &QString) -> QString;
1436
1437        /// A site's setting: "realtime", "muted", "background" (true or false), or "site" (its kind).
1438        #[qinvokable]
1439        fn set_site(self: Pin<&mut Sioul>, id: &QString, field: &QString, value: &QString) -> QString;
1440
1441        /// The sites offered when adding one, for a country and a region ("" for yours): JSON.
1442        #[qinvokable]
1443        fn site_presets(self: &Sioul, country: &QString, region: &QString) -> QString;
1444
1445        /// A site added: {"name", "url", "category", "area", "announced_by"}; returns {"id"} or {"error"}.
1446        #[qinvokable]
1447        fn add_site(self: Pin<&mut Sioul>, edit: &QString) -> QString;
1448
1449        /// The usual sites as a menu: countries, their groups, the sites (JSON).
1450        #[qinvokable]
1451        fn site_presets_tree(self: &Sioul) -> QString;
1452
1453        /// Several usual sites pinned at once (a JSON list); returns {"added", "error"}.
1454        #[qinvokable]
1455        fn add_sites(self: Pin<&mut Sioul>, rows: &QString) -> QString;
1456
1457        /// A site moved one place up (-1) or down (1); returns what went wrong, else "".
1458        #[qinvokable]
1459        fn move_site(self: Pin<&mut Sioul>, id: &QString, delta: i32) -> QString;
1460
1461        /// A site taken out of Sioul; its sign-ins stay in its profile until cleared.
1462        #[qinvokable]
1463        fn remove_site(self: Pin<&mut Sioul>, id: &QString) -> QString;
1464
1465        /// Whether this build carries Sioul's own Google key: then no key of your own is needed.
1466        #[qinvokable]
1467        fn google_built_in(self: &Sioul) -> bool;
1468
1469        /// The camera, microphone and speaker of calls in sites, by their names: {"camera", "microphone", "speaker"}.
1470        #[qinvokable]
1471        fn call_devices(self: &Sioul) -> QString;
1472
1473        /// One of them chosen ("camera", "microphone", "speaker"); "" for the system's own.
1474        #[qinvokable]
1475        fn set_call_device(self: Pin<&mut Sioul>, which: &QString, name: &QString) -> QString;
1476
1477        /// The script every site's page runs for its calls (src/calls.js): the
1478        /// devices chosen, changed during a call; called with the names and a token.
1479        #[qinvokable]
1480        fn calls_script(self: &Sioul) -> QString;
1481
1482        /// "missing", "unauthenticated", "locked" or "unlocked".
1483        #[qinvokable]
1484        fn bitwarden_state(self: &Sioul) -> QString;
1485
1486        /// Unlocks Bitwarden, off the window's thread: a ticket at once, the
1487        /// answer ({"ok"}, {"factor", "key"}, {"new_device"} or {"error"}) with
1488        /// `bitwarden_answered` (sites.rs).
1489        #[qinvokable]
1490        fn bitwarden_unlock(self: Pin<&mut Sioul>, password: &QString, provider: i32, code: &QString) -> i32;
1491
1492        /// Locks Bitwarden again: its logins leave memory.
1493        #[qinvokable]
1494        fn bitwarden_lock(self: Pin<&mut Sioul>);
1495
1496        /// Sends the e-mail second step's code, off the window's thread: a
1497        /// ticket, then what went wrong, else "", with `bitwarden_answered`.
1498        #[qinvokable]
1499        fn bitwarden_send_code(self: Pin<&mut Sioul>, password: &QString) -> i32;
1500
1501        /// Begins a passkey login, off the window's thread: a ticket, then
1502        /// {"page", "script"} for the key's window, or {"error"}, with `bitwarden_answered`.
1503        #[qinvokable]
1504        fn bitwarden_passkey_begin(self: Pin<&mut Sioul>) -> i32;
1505
1506        /// The key's answer from that window: the vault opened with it, off the
1507        /// window's thread: a ticket, then {"ok"} or {"error"}, with `bitwarden_answered`.
1508        #[qinvokable]
1509        fn bitwarden_passkey(self: Pin<&mut Sioul>, answer: &QString) -> i32;
1510
1511        /// The vault's logins by site and by user name, for an address (its own
1512        /// first): {"found": [{"id", "name", "username", "site", "elsewhere"}], "more"} or {"error"};
1513        /// for a large vault {"later": n}, the answer coming with `bitwarden_found` (sites.rs).
1514        #[qinvokable]
1515        fn bitwarden_search(self: Pin<&mut Sioul>, url: &QString, site: &QString, user: &QString) -> QString;
1516
1517        /// What the vault holds for an address: {"domain", "chosen", "only"} or {"error"} (sites.rs).
1518        #[qinvokable]
1519        fn bitwarden_site(self: &Sioul, url: &QString) -> QString;
1520
1521        /// One login of the vault, for an address: {"username", "password", "code"} or {"error"}.
1522        #[qinvokable]
1523        fn bitwarden_login(self: &Sioul, url: &QString, id: &QString) -> QString;
1524
1525        /// Contacts on the map: {"allowed", "tiles", "pins", "waiting", "locating"}.
1526        #[qinvokable]
1527        fn map_view(self: &Sioul) -> QString;
1528
1529        /// Where one address is: {"lat", "lon"}, else "".
1530        #[qinvokable]
1531        fn place_of(self: &Sioul, address: &QString) -> QString;
1532
1533        /// Allows placing addresses (when `allow`), then places those not asked yet, one a second.
1534        #[qinvokable]
1535        fn locate_addresses(self: Pin<&mut Sioul>, allow: bool);
1536
1537        /// Work time or quiet time again: called each minute; the pages follow when it changes.
1538        #[qinvokable]
1539        fn refresh_mode(self: Pin<&mut Sioul>);
1540
1541        /// Work stays in view for `minutes` more, whatever the hours.
1542        #[qinvokable]
1543        fn work_a_while(self: Pin<&mut Sioul>, minutes: i32);
1544
1545        /// Done for today: the day is closed at once ("Undo" in the status line),
1546        /// today's tasks flow on, work rests until it comes back. Returns the
1547        /// screen to show after, as JSON.
1548        #[qinvokable]
1549        fn done_for_the_day(self: Pin<&mut Sioul>) -> QString;
1550
1551        /// The end of the work day ("work") or of the day before sleep
1552        /// ("night"), or a notice's press for a day ("work:2026-10-14"): the
1553        /// review's sheet, as JSON, "null" for a day it no longer closes (`reviews::view`).
1554        #[qinvokable]
1555        fn day_review(self: &Sioul, kind: &QString) -> QString;
1556
1557        /// The review kept for `date`, the day its sheet showed, each answer
1558        /// optional ("" for none); the work day then closed as "Done for
1559        /// today" when it is still today's: the screen after it, as JSON, else "".
1560        #[qinvokable]
1561        fn close_review(self: Pin<&mut Sioul>, kind: &QString, date: &QString, felt: &QString, mix: &QString, memo: &QString) -> QString;
1562
1563        /// A day's words on the Health page, and today the button to close it (`reviews::line`).
1564        #[qinvokable]
1565        fn day_review_line(self: &Sioul, date: &QString) -> QString;
1566
1567        /// The first step for when work comes back, in your words.
1568        #[qinvokable]
1569        fn set_first_step(self: Pin<&mut Sioul>, text: &QString);
1570
1571        /// The first step put away.
1572        #[qinvokable]
1573        fn clear_first_step(self: Pin<&mut Sioul>);
1574
1575        /// Back to the usual hours: overrides taken off.
1576        #[qinvokable]
1577        fn usual_hours(self: Pin<&mut Sioul>);
1578
1579        /// Work stays in view until tonight, the night's start (part 3 of
1580        /// "What reaches you": "Work now" gone, its work part here).
1581        #[qinvokable]
1582        fn work_until_tonight(self: Pin<&mut Sioul>);
1583
1584        /// Free time on or off (docs/pauses.md, `pauses::set_free`): on, "true"
1585        /// when a focus session stopped; off, the line said once on the end of work, else "".
1586        #[qinvokable]
1587        fn set_free_time(self: Pin<&mut Sioul>, on: bool) -> QString;
1588
1589        /// "Keep my usual end": today's end of work stays where it was, no reason asked.
1590        #[qinvokable]
1591        fn keep_usual_end(self: Pin<&mut Sioul>);
1592
1593        /// "Nothing at all" for this free time, or the safe list again.
1594        #[qinvokable]
1595        fn set_free_nothing(self: Pin<&mut Sioul>, on: bool);
1596
1597        /// The Pause: it asks nothing; everything held, on every device (`pauses::press`).
1598        #[qinvokable]
1599        fn pause_now(self: Pin<&mut Sioul>);
1600
1601        /// Back from the pause: the screen after it, as JSON (`pauses::come_back`).
1602        #[qinvokable]
1603        fn come_back(self: Pin<&mut Sioul>) -> QString;
1604
1605        /// Coming back's one offer: tomorrow lighter too.
1606        #[qinvokable]
1607        fn lighten_tomorrow(self: Pin<&mut Sioul>);
1608
1609        /// The last pause's times, forgotten.
1610        #[qinvokable]
1611        fn forget_pause(self: Pin<&mut Sioul>);
1612
1613        /// The pause's screen, as JSON (`pauses::screen`).
1614        #[qinvokable]
1615        fn pause_screen(self: &Sioul) -> QString;
1616
1617        /// The pauses' setup, its do-not-disturb part, as JSON (`pauses::setup`).
1618        #[qinvokable]
1619        fn pause_setup(self: &Sioul) -> QString;
1620
1621        /// A system page the pauses' setup offers ("access", "starred", "plasma": `dnd::open`).
1622        #[qinvokable]
1623        fn open_dnd(self: &Sioul, key: &QString);
1624
1625        /// Do-not-disturb's switch, on every device (`everywhere::toggle`): on for
1626        /// `minutes` (0: until turned off; -1: until what now is for ends), or off.
1627        #[qinvokable]
1628        fn dnd_toggle(self: Pin<&mut Sioul>, on: bool, minutes: i32);
1629
1630        /// One of the three states pressed here (part 3 of "What reaches
1631        /// you"; `everywhere::choose`): "reachable" for `length` ("30", "60",
1632        /// "120", "hours", "tonight", "off"), "usual", or "dnd" for `length`
1633        /// minutes ("0" until turned off, "-1" until what now is for ends).
1634        #[qinvokable]
1635        fn reach_choose(self: Pin<&mut Sioul>, state: &QString, length: &QString);
1636
1637        /// "Let what your phone holds through", pressed on a computer
1638        /// (`everywhere::let_through`): the phone lets through what it held
1639        /// before now, once.
1640        #[qinvokable]
1641        fn let_phone_through(self: Pin<&mut Sioul>);
1642
1643        /// Settings ▸ Do not disturb, its own part, as JSON (`everywhere::setup`).
1644        #[qinvokable]
1645        fn dnd_setup(self: &Sioul) -> QString;
1646
1647        /// An action of that tab (`everywhere::change`): the tab again, as JSON.
1648        #[qinvokable]
1649        fn dnd_change(self: Pin<&mut Sioul>, verb: &QString, json: &QString) -> QString;
1650
1651        /// Settings ▸ Other apps, on a phone, as JSON (`appnotes::setup`).
1652        #[qinvokable]
1653        fn app_notes_setup(self: &Sioul) -> QString;
1654
1655        /// A choice of that tab, or one of Android's pages (`appnotes::change`): the tab again, as JSON.
1656        #[qinvokable]
1657        fn app_notes_change(self: &Sioul, verb: &QString, json: &QString) -> QString;
1658
1659        /// Expecting a call, once "Let every call through" (docs/android.md, "Calls"; `calls::press`): on
1660        /// for `minutes` (0: until turned off), or calls screened again; on every device.
1661        #[qinvokable]
1662        fn calls_through(self: Pin<&mut Sioul>, on: bool, minutes: i32);
1663
1664        /// The Porch's list of calls declined on this phone, as JSON (`calls::view`).
1665        #[qinvokable]
1666        fn calls_view(self: &Sioul) -> QString;
1667
1668        /// An action of that list (`calls::act`): {said, url, shared}, as JSON.
1669        #[qinvokable]
1670        fn calls_action(self: Pin<&mut Sioul>, verb: &QString, json: &QString) -> QString;
1671
1672        /// Settings ▸ Calls, on a phone, as JSON (`calls::setup`).
1673        #[qinvokable]
1674        fn calls_setup(self: &Sioul) -> QString;
1675
1676        /// The phone's messages on your computers (`phonemsgs::call`): "view"
1677        /// (the Porch's section), "seen", "unseen", "block", "setup" (the
1678        /// phone's tab), "set", "part"; their JSON.
1679        #[qinvokable]
1680        fn phone_messages(self: Pin<&mut Sioul>, verb: &QString, json: &QString) -> QString;
1681
1682        /// texts: read and sent through your phone (`texts::call`): "view",
1683        /// "conversation", "send", "send-mms", "again", "parts", "media", "save-media", "close",
1684        /// "unattach", "setup", "ask", "part", "cap", "sending", "limit"; their JSON.
1685        #[qinvokable]
1686        fn texts(self: Pin<&mut Sioul>, verb: &QString, json: &QString) -> QString;
1687
1688        /// texts: the files chosen on the Texts page made fit for one
1689        /// multimedia message (`texts::attach`: {round, id, phone, sources}),
1690        /// off the window's thread: `texts_attached` brings them.
1691        #[qinvokable]
1692        fn texts_attach(self: Pin<&mut Sioul>, json: &QString);
1693
1694        /// Settings ▸ What reaches you, as JSON (`reaches::view`): the moment
1695        /// now in sentences, the presets, a card per time, the matrix's grid.
1696        #[qinvokable]
1697        fn reaches_view(self: &Sioul) -> QString;
1698
1699        /// The card "What reaches you now", as JSON (`reaches::now_card`): now in two
1700        /// sentences, what the phone holds, the call screening, Mail every minute.
1701        #[qinvokable]
1702        fn reach_card(self: &Sioul) -> QString;
1703
1704        /// A held conversation in that card, on the phone (`verb`: "now", let it
1705        /// through now; "then", let them through at this time from now on): what to say.
1706        #[qinvokable]
1707        fn reach_talk(self: Pin<&mut Sioul>, verb: &QString, key: &QString) -> QString;
1708
1709        /// A row of the matrix changed (`row`: "mail.safe", "codes"; `words`: its
1710        /// words whole, a JSON list; `shown`: its words as the row showed them,
1711        /// so that only the cells changed are set): what went wrong, else "".
1712        #[qinvokable]
1713        fn reaches_set(self: Pin<&mut Sioul>, row: &QString, words: &QString, shown: &QString) -> QString;
1714
1715        /// A preset chosen ("usual", "quieter", "reachable"): what went wrong, else "".
1716        #[qinvokable]
1717        fn reaches_preset(self: Pin<&mut Sioul>, id: &QString) -> QString;
1718
1719        /// How someone reaches you, as JSON (`reaches::person`): a card's file or
1720        /// UID, or a sender's addresses (a JSON array, or one).
1721        #[qinvokable]
1722        fn person_sheet(self: &Sioul, key: &QString, addresses: &QString) -> QString;
1723
1724        /// A change on a person's sheet (`verb`: "list" or "always"): {sheet, line}, as JSON.
1725        #[qinvokable]
1726        fn person_sheet_change(self: Pin<&mut Sioul>, key: &QString, addresses: &QString, verb: &QString, value: &QString) -> QString;
1727
1728        /// An action of that tab (`calls::setup_change`): the tab again, as JSON.
1729        #[qinvokable]
1730        fn calls_setup_change(self: Pin<&mut Sioul>, verb: &QString, json: &QString) -> QString;
1731
1732        /// The projects, for their list, as JSON.
1733        #[qinvokable]
1734        fn project_rows(self: &Sioul) -> QString;
1735
1736        /// One project's page, as JSON.
1737        #[qinvokable]
1738        fn project_page(self: &Sioul, id: &QString) -> QString;
1739
1740        /// Saves a project from its form (a new one when `id` is empty); returns {"id"} or {"error"}.
1741        #[qinvokable]
1742        fn save_project(self: Pin<&mut Sioul>, id: &QString, edit: &QString) -> QString;
1743
1744        /// The Time page: "week", "month" or "year" around `anchor` ("2026-10-05"), one project or all.
1745        #[qinvokable]
1746        fn time_page(self: &Sioul, period: &QString, anchor: &QString, project: &QString) -> QString;
1747
1748        /// Notes time by hand (JSON: day, at, minutes, project, task, note, unbilled); returns what went wrong.
1749        #[qinvokable]
1750        fn note_time(self: Pin<&mut Sioul>, edit: &QString) -> QString;
1751
1752        /// Takes out time noted by hand; returns what went wrong.
1753        #[qinvokable]
1754        fn remove_time(self: Pin<&mut Sioul>, key: &QString) -> QString;
1755
1756        /// Where you stopped, as JSON: {text, when, task, title}; "null" for none.
1757        #[qinvokable]
1758        fn stopped(self: &Sioul) -> QString;
1759
1760        /// A line left on where you stopped (an empty one: done); returns what went wrong, else "".
1761        #[qinvokable]
1762        fn set_stopped(self: Pin<&mut Sioul>, text: &QString) -> QString;
1763
1764        /// A stretch of time changed once noted (by the timer or by hand): its
1765        /// day, from, to, what, task, project. Returns what went wrong, else "".
1766        #[qinvokable]
1767        fn change_time(self: Pin<&mut Sioul>, key: &QString, edit: &QString) -> QString;
1768
1769        /// The tasks time can be given to, as JSON: the open ones, and `keep`.
1770        #[qinvokable]
1771        fn task_choices(self: &Sioul, keep: &QString) -> QString;
1772
1773        /// The invoice of a project's unbilled hours: {"number", "html", "pdf"} or {"error"}.
1774        #[qinvokable]
1775        fn make_invoice(self: Pin<&mut Sioul>, id: &QString) -> QString;
1776
1777        /// Invoices made on this computer from now on; returns what to say.
1778        #[qinvokable]
1779        fn take_invoices(self: Pin<&mut Sioul>) -> QString;
1780
1781        /// An invoice made before, to print again: {"html", "pdf"} or {"error"}.
1782        #[qinvokable]
1783        fn invoice_again(self: &Sioul, number: &QString) -> QString;
1784
1785        /// An invoice paid, or not; returns what went wrong.
1786        #[qinvokable]
1787        fn set_invoice_paid(self: Pin<&mut Sioul>, number: &QString, paid: bool) -> QString;
1788
1789        /// Something new tied to `from`: "task", "note", "mail"; returns {"uid"}, {"path"}, {"draft"} or {"error"}.
1790        #[qinvokable]
1791        fn make_linked(self: Pin<&mut Sioul>, kind: &QString, from: &QString, key: &QString, start: f64) -> QString;
1792
1793        /// A task from a message. Returns {"uid"} or {"error"}.
1794        #[qinvokable]
1795        fn task_from_mail(self: Pin<&mut Sioul>, key: &QString) -> QString;
1796
1797        /// A note from a message; returns its path.
1798        #[qinvokable]
1799        fn note_from_mail(self: Pin<&mut Sioul>, key: &QString) -> QString;
1800
1801        /// A note for an event's occurrence starting at `start`; returns its path.
1802        #[qinvokable]
1803        fn note_from_event(self: Pin<&mut Sioul>, key: &QString, start: f64) -> QString;
1804
1805        /// A task to prepare an event. Returns {"uid"} or {"error"}.
1806        #[qinvokable]
1807        fn task_from_event(self: Pin<&mut Sioul>, key: &QString, start: f64) -> QString;
1808
1809        /// A task from a note's checkbox line. Returns {"uid"} or {"error"}.
1810        #[qinvokable]
1811        fn task_from_line(self: Pin<&mut Sioul>, path: &QString, line: i32) -> QString;
1812
1813        /// A draft to the people a task involves; returns its id.
1814        #[qinvokable]
1815        fn draft_for_task(self: Pin<&mut Sioul>, uid: &QString) -> QString;
1816
1817        /// A draft sending a note to its event's guests; returns its id.
1818        #[qinvokable]
1819        fn mail_note(self: Pin<&mut Sioul>, path: &QString) -> QString;
1820
1821        /// A page's settings ("porch", "mail", "agenda", "tasks", "notes", "reading", "contacts", "time", "general"), as JSON.
1822        #[qinvokable]
1823        fn settings(self: &Sioul, view: &QString) -> QString;
1824
1825        /// Changes one setting (its value as JSON); returns what went wrong, else "".
1826        #[qinvokable]
1827        fn set_setting(self: Pin<&mut Sioul>, key: &QString, value: &QString) -> QString;
1828
1829        /// Changes one setting from its row: `shown` the value the row showed,
1830        /// `value` the new one, both as JSON. A list is the row's change set over
1831        /// the list as the files hold it now (`settings::change`): an element
1832        /// another device added meanwhile stays. Returns what went wrong, else "".
1833        #[qinvokable]
1834        fn change_setting(self: Pin<&mut Sioul>, key: &QString, shown: &QString, value: &QString) -> QString;
1835
1836        /// "dark" or "light" when `SIOUL_THEME` forces one, for the window's images; else "" (the setting decides).
1837        #[qinvokable]
1838        fn forced_theme(self: &Sioul) -> QString;
1839
1840        /// The icons in the window's colours, each time these change
1841        /// (main.qml, its Theme): `dark` as the window draws them, `chosen`
1842        /// when they are chosen in Sioul rather than the system's (desktop.rs, `follow`).
1843        #[qinvokable]
1844        fn follow_icons(self: &Sioul, dark: bool, chosen: bool);
1845
1846        /// A notice said as the system's own, its words alone, off the
1847        /// window's thread: the tray's notice (`tray_notice`) where no tray
1848        /// shows messages (main.qml).
1849        #[qinvokable]
1850        fn plain_notice(self: &Sioul, title: &QString, body: &QString);
1851
1852        /// Real time on or off: every folder of every address fetched each minute, and the Porch open.
1853        #[qinvokable]
1854        fn set_realtime_mode(self: Pin<&mut Sioul>, on: bool);
1855
1856        /// Images of a phone's screen (`SIOUL_GRAB_PHONE`): its size and its layout.
1857        #[qinvokable]
1858        fn grab_phone(self: &Sioul) -> bool;
1859
1860        /// Which steps the window takes while saving images: `SIOUL_GRAB_STEPS`,
1861        /// "pages" by default, or "actions" to act on mail and send some (test accounts only).
1862        #[qinvokable]
1863        fn grab_steps(self: &Sioul) -> QString;
1864    }
1865
1866    #[auto_cxx_name]
1867    // SAFETY: CXX-Qt makes both sides of these signals and methods from this declaration.
1868    unsafe extern "RustQt" {
1869        /// The notification's "copy" button was pressed.
1870        #[qsignal]
1871        fn copy_requested(self: Pin<&mut Sioul>, text: QString);
1872
1873        /// An account was added: the form can be emptied.
1874        #[qsignal]
1875        fn account_added(self: Pin<&mut Sioul>);
1876
1877        /// An account's password given here: kept (`problem` empty), else why not.
1878        #[qsignal]
1879        fn account_password_done(self: Pin<&mut Sioul>, id: QString, problem: QString);
1880
1881        /// Paper letters were read: the Porch shows them again.
1882        #[qsignal]
1883        fn letters_changed(self: Pin<&mut Sioul>);
1884
1885        /// A task just done, whichever way (a list, its panel, the focus window):
1886        /// the status line offers "How was it?", quietly.
1887        #[qsignal]
1888        fn task_done(self: Pin<&mut Sioul>, uid: QString);
1889
1890        /// Your other devices' changes written here by the sharing: the stores
1891        /// written, one per line ("data/time/", "state/health-state.toml"…), for
1892        /// the pages that show them to read again.
1893        #[qsignal]
1894        fn shared_in(self: Pin<&mut Sioul>, stores: QString);
1895
1896        /// The versions kept, listed (`share_history`), as JSON.
1897        #[qsignal]
1898        fn share_listed(self: Pin<&mut Sioul>, versions: QString);
1899
1900        /// A version put back (`share_put_back`): what went wrong, else "".
1901        #[qsignal]
1902        fn share_put_back_done(self: Pin<&mut Sioul>, problem: QString);
1903
1904        /// Notes or papers held put back (`share_put_back_withheld`): what went wrong, else "".
1905        #[qsignal]
1906        fn share_withheld_back(self: Pin<&mut Sioul>, problem: QString);
1907
1908        /// What switching Notes or Papers on would send (`share_estimate`), as JSON {"part", "text"}.
1909        #[qsignal]
1910        fn share_estimated(self: Pin<&mut Sioul>, estimate: QString);
1911
1912        /// The files chosen on the Texts page made fit (`texts_attach`), as
1913        /// JSON {round, media, said, limit, max}.
1914        #[qsignal]
1915        fn texts_attached(self: Pin<&mut Sioul>, attached: QString);
1916
1917        /// The spam filter's training moved on, or ended: its state, as `spam_status` gives it.
1918        #[qsignal]
1919        fn spam_changed(self: Pin<&mut Sioul>, status: QString);
1920
1921        /// What "Try it" or the mail filters' preview found, as JSON {kind, text, lines, count}.
1922        #[qsignal]
1923        fn mail_filters_found(self: Pin<&mut Sioul>, found: QString);
1924
1925        /// A message read for the reading pane (`read_message`): its key, the
1926        /// message laid out for reading as JSON (`view::MessageView`; empty
1927        /// if it cannot be read), and the invitation it carries (JSON; empty
1928        /// when none).
1929        #[qsignal]
1930        fn message_read(self: Pin<&mut Sioul>, key: QString, reading: QString, invitation: QString);
1931
1932        /// Sharing through a folder Sioul keeps in step with a server itself
1933        /// (`start_sharing_on_server`): "" when it started, else why not.
1934        #[qsignal]
1935        fn share_started(self: Pin<&mut Sioul>, problem: QString);
1936
1937        /// A sign-in or a join moved on (`join`): `join_status` says where it stands.
1938        #[qsignal]
1939        fn join_changed(self: Pin<&mut Sioul>);
1940
1941        /// A reminder's "Open" was pressed: what it is about ("task", "event", "budget"), shown.
1942        #[qsignal]
1943        fn reminder_opened(self: Pin<&mut Sioul>, kind: QString, uri: QString, key: QString);
1944
1945        /// On Windows, the end of work's or the night's notice, said through
1946        /// Sioul's icon in the tray (main.qml, Tray.qml): a click on it opens
1947        /// `kind` and `key` as `reminder_opened` does (reviews.rs, `show_notice`).
1948        #[qsignal]
1949        fn tray_notice(self: Pin<&mut Sioul>, title: QString, body: QString, kind: QString, key: QString);
1950
1951        /// An attachment passed the antivirus: the desktop opens it.
1952        #[qsignal]
1953        fn open_url(self: Pin<&mut Sioul>, url: QString);
1954
1955        /// Microsoft's page signed in another login than the address given,
1956        /// and only that login's mailbox opened: the form asks, naming both,
1957        /// before anything is kept (`answer_microsoft`).
1958        #[qsignal]
1959        fn microsoft_question(self: Pin<&mut Sioul>, question: QString);
1960
1961        /// No antivirus answered, or the file is larger than it scans (`big`):
1962        /// the window asks before opening or saving, with how to install one.
1963        #[qsignal]
1964        fn scan_unavailable(self: Pin<&mut Sioul>, key: QString, index: i32, what: i32, name: QString, hint: QString, big: bool);
1965
1966        /// An attachment was kept in the papers wallet: the form, to say what it is.
1967        #[qsignal]
1968        fn paper_kept(self: Pin<&mut Sioul>, file: QString, title: QString, kind: QString);
1969
1970        /// A draft's window opens again: "Undo" after sending or discarding.
1971        /// Also a new draft handed by another application (`outside`).
1972        #[qsignal]
1973        fn compose_requested(self: Pin<&mut Sioul>, id: QString);
1974
1975        /// Files shared from another application came for draft `id`
1976        /// (`outside`): its window reads its attachments again and says `line`,
1977        /// a problem when some did not come.
1978        #[qsignal]
1979        fn draft_files_arrived(self: Pin<&mut Sioul>, id: QString, line: QString, problem: bool);
1980
1981        /// Keys were found or made: windows showing them read them again.
1982        #[qsignal]
1983        fn keys_changed(self: Pin<&mut Sioul>);
1984
1985        /// What a security key's band shows now, as JSON {state, line, tries,
1986        /// warm, action}: for `context`, a draft's id, a message's key, or "setup".
1987        #[qsignal]
1988        fn security_key_changed(self: Pin<&mut Sioul>, context: QString, state: QString);
1989
1990        /// A tie was made or undone: what shows ties reads them again.
1991        #[qsignal]
1992        fn links_changed(self: Pin<&mut Sioul>);
1993
1994        /// Addresses were placed on the map.
1995        #[qsignal]
1996        fn places_changed(self: Pin<&mut Sioul>);
1997
1998        /// A site notified something, or was seen.
1999        #[qsignal]
2000        fn sites_changed(self: Pin<&mut Sioul>);
2001
2002        /// A large vault's search, done off the window's thread: its ticket and its answer (`bitwarden_search`).
2003        #[qsignal]
2004        fn bitwarden_found(self: Pin<&mut Sioul>, ticket: i32, found: QString);
2005
2006        /// Bitwarden's server answered, off the window's thread: the ticket an
2007        /// unlock, a passkey or a code's sending gave, and its answer.
2008        #[qsignal]
2009        fn bitwarden_answered(self: Pin<&mut Sioul>, ticket: i32, answer: QString);
2010
2011        /// An account chosen among the phone's: its name (an address, usually) and its kind
2012        /// ("com.google", "e.foundation.webdav.eelo"…); both empty when none was.
2013        #[qsignal]
2014        fn phone_account_chosen(self: Pin<&mut Sioul>, name: QString, kind: QString);
2015    }
2016
2017    impl cxx_qt::Threading for Sioul {}
2018}
2019
2020use qobject::Sioul;
2021pub(crate) type QtThread = cxx_qt::CxxQtThread<Sioul>;
2022
2023/// Android: the window waiting for the phone's account chooser to answer.
2024#[cfg(target_os = "android")]
2025static PHONE_CHOOSER: std::sync::Mutex<Option<QtThread>> = std::sync::Mutex::new(None);
2026
2027#[cfg(target_os = "android")]
2028// SAFETY: declared as android/main.cpp defines them: extern "C", the same types.
2029unsafe extern "C" {
2030    /// Opens Android's chooser of the phone's accounts (android/main.cpp).
2031    fn sioul_android_choose_account();
2032    /// Whether Sioul may reach your files by their path ("All files access").
2033    fn sioul_android_files_access() -> bool;
2034    /// Android's own switch for it.
2035    fn sioul_android_ask_files_access();
2036    /// A sync app asked to look for changes now: an explicit broadcast to its
2037    /// receiver, nothing when it is not installed (android/main.cpp).
2038    pub(crate) fn sioul_android_broadcast(package: *const std::ffi::c_char, receiver: *const std::ffi::c_char, action: *const std::ffi::c_char);
2039}
2040
2041/// Android: the account chosen in the phone's chooser, or two empty texts
2042/// when none was; called by android/main.cpp, on Android's thread.
2043///
2044/// # Safety
2045/// `name` and `kind` are null, or zero-terminated UTF-8 texts valid for the call.
2046#[cfg(target_os = "android")]
2047// SAFETY: no other symbol of the program has this name.
2048#[unsafe(no_mangle)]
2049pub unsafe extern "C" fn sioul_android_account_chosen(name: *const std::ffi::c_char, kind: *const std::ffi::c_char) {
2050    // SAFETY: as the caller promises.
2051    let read = |text: *const std::ffi::c_char| if text.is_null() { String::new() } else { unsafe { std::ffi::CStr::from_ptr(text) }.to_string_lossy().into_owned() };
2052    let (name, kind) = (read(name), read(kind));
2053    let waiting = PHONE_CHOOSER.lock().ok().and_then(|mut w| w.take());
2054    if let Some(qt) = waiting {
2055        let _ = qt.queue(move |mut sioul| sioul.as_mut().phone_account_chosen(QString::from(&name), QString::from(&kind)));
2056    }
2057}
2058
2059#[derive(Default)]
2060pub struct SioulRust {
2061    porch: QString,
2062    budgets: QString,
2063    accounts: QString,
2064    blocked: QString,
2065    status: QString,
2066    busy: bool,
2067    found: QString,
2068    scouted: QString,
2069    scouting: bool,
2070    form_error: QString,
2071    form_busy: bool,
2072    mail_accounts: QString,
2073    mail_folder: QString,
2074    drafts: QString,
2075    mail_search: QString,
2076    undo_line: QString,
2077    contacts: QString,
2078    duplicates: QString,
2079    agenda: QString,
2080    overlaps: QString,
2081    health_view: QString,
2082    missed_view: QString,
2083    needs_view: QString,
2084    tasks: QString,
2085    notes: QString,
2086    focus_session: QString,
2087    reading: QString,
2088    realtime: bool,
2089    away: bool,
2090    mode: QString,
2091    forecast: QString,
2092    places_found: QString,
2093    shared: Arc<Shared>,
2094}
2095
2096/// What the background threads and the window share.
2097#[derive(Default)]
2098pub(crate) struct Shared {
2099    /// "Show all mail now" (once "Open it anyway"), until "Done for now": a
2100    /// look on this device at every lane and address, at any hour, which
2101    /// tells nothing.
2102    opened_anyway: AtomicBool,
2103    /// Reachable at the last look (part 3 of "What reaches you"): the Porch
2104    /// open with every lane and address, new mail told, mail looked for every
2105    /// minute (`follow_reachable`).
2106    pub(crate) reachable: AtomicBool,
2107    /// Per account, the last thing its watcher said, and whether it is a problem.
2108    pub(crate) statuses: Mutex<BTreeMap<String, (String, bool)>>,
2109    /// Accounts whose password is wanted: none here yet, or refused (`SyncError::wants_password`).
2110    pub(crate) password_wanted: Mutex<BTreeSet<String>>,
2111    /// Per account, the newest message the window shows: what "Done for now" closes.
2112    shown: Mutex<BTreeMap<String, ImapOrigin>>,
2113    /// The running watchers.
2114    pub(crate) watchers: Mutex<BTreeMap<String, Arc<Control>>>,
2115    /// Accounts being removed, their mail deleted on a thread (`outlook::remove`):
2116    /// no watcher starts for them meanwhile.
2117    pub(crate) removing: Mutex<BTreeSet<String>>,
2118    /// Accounts whose provider id was looked for this session.
2119    learned: Mutex<BTreeSet<String>>,
2120    /// Views are computed on threads and may finish out of order: only the newest is shown.
2121    generation: AtomicU64,
2122    shown_generation: AtomicU64,
2123    /// Attachments the antivirus found clean this session, by content.
2124    clean: Mutex<BTreeSet<u64>>,
2125    /// The folder the mail page shows.
2126    pub(crate) open_folder: Mutex<Option<mail::OpenFolder>>,
2127    /// What waits ten seconds for "Undo".
2128    pub(crate) pending: Mutex<Vec<Arc<mail::Pending>>>,
2129    pub(crate) mail_generation: AtomicU64,
2130    pub(crate) mail_shown_generation: AtomicU64,
2131    /// When the status line last said what became of something you did: a
2132    /// routine "Mail fetched at…" does not cover it for a while.
2133    said_at: Mutex<Option<std::time::Instant>>,
2134    /// The contacts' search and the agenda's days.
2135    pub(crate) pim: Mutex<pim::PimState>,
2136    pub(crate) pim_generation: AtomicU64,
2137    pub(crate) pim_shown_generation: AtomicU64,
2138    /// The running syncs of contacts and calendars, by account.
2139    pub(crate) dav_watchers: Mutex<BTreeMap<String, Arc<Control>>>,
2140    /// The task and notes pages' choices.
2141    pub(crate) work: Mutex<work::WorkState>,
2142    pub(crate) work_generation: AtomicU64,
2143    pub(crate) work_shown_generation: AtomicU64,
2144    /// Everything links reach, read after the last change.
2145    pub(crate) loaded: Mutex<Option<Arc<sioul_core::links::Loaded>>>,
2146    /// Real time: every folder each minute, the Porch open.
2147    pub(crate) realtime: AtomicBool,
2148    /// Addresses are being placed on the map.
2149    pub(crate) locating: AtomicBool,
2150    /// Bitwarden's session, once unlocked; in memory only.
2151    pub(crate) bitwarden: Mutex<Option<crate::sites::Vault>>,
2152    /// A forecast is being fetched.
2153    pub(crate) weather_fetching: AtomicBool,
2154    /// Accounts whose older mail waits for room on the disk, already said.
2155    pub(crate) held_back: Mutex<std::collections::BTreeSet<String>>,
2156    /// Cancel on Google's or Microsoft's sign-in: its wait for the browser ends.
2157    pub(crate) oauth_stop: AtomicBool,
2158    /// Microsoft's sign-in waiting for your Yes or No (`answer_microsoft`).
2159    pub(crate) microsoft_answer: Mutex<Option<std::sync::mpsc::Sender<bool>>>,
2160    /// GitHub's issues being brought, when asked.
2161    pub(crate) github: Mutex<Option<Arc<sioul_sync::Control>>>,
2162    /// When you were last at this window (Unix seconds): reminders follow you.
2163    pub(crate) active: std::sync::atomic::AtomicI64,
2164    /// When the plan was last made again on the clock (Unix seconds), and for which day.
2165    pub(crate) planned_at: std::sync::atomic::AtomicI64,
2166    pub(crate) planned_day: std::sync::atomic::AtomicI64,
2167    /// One computation at a time for each set of pages (`coalesced`).
2168    views_job: Job,
2169    pub(crate) mail_job: Job,
2170    pub(crate) pim_job: Job,
2171    pub(crate) work_job: Job,
2172    pub(crate) health_job: Job,
2173}
2174
2175/// The plan is made again every twelve hours, and when the day changes: a task
2176/// not done by its planned day moves on, and what waits for it follows.
2177const REPLAN_EVERY: i64 = 12 * 3600;
2178
2179/// How long what you did stays in the status line before routine news.
2180const SAID_FOR: std::time::Duration = std::time::Duration::from_secs(30);
2181
2182pub(crate) fn config_path() -> PathBuf {
2183    config::default_path()
2184}
2185
2186pub(crate) fn load_config() -> Config {
2187    // Read a hundred times a minute: parsed again only when the file changed
2188    // (its size or its time), as the sharing or another window may change it.
2189    // A time younger than two seconds is not trusted: two writings of one size
2190    // within a tick of the clock look alike.
2191    type Parsed = (PathBuf, u64, std::time::SystemTime, Config);
2192    static PARSED: Mutex<Option<Parsed>> = Mutex::new(None);
2193    let path = config_path();
2194    let settled = |time: &std::time::SystemTime| time.elapsed().is_ok_and(|age| age > std::time::Duration::from_secs(2));
2195    let stamp = std::fs::metadata(&path).ok().and_then(|m| Some((m.len(), m.modified().ok()?))).filter(|(_, time)| settled(time));
2196    if let Some((size, time)) = stamp
2197        && let Ok(parsed) = PARSED.lock()
2198        && let Some((p, s, t, config)) = parsed.as_ref()
2199        && *p == path
2200        && *s == size
2201        && *t == time
2202    {
2203        return config.clone();
2204    }
2205    let config = Config::load(&path).unwrap_or_default();
2206    // The words this program looks for, said once each time the file is read (`words::current`).
2207    sioul_core::words::set_current(sioul_core::words::Words::of(&config));
2208    if let (Some((size, time)), Ok(mut parsed)) = (stamp, PARSED.lock()) {
2209        *parsed = Some((path, size, time, config.clone()));
2210    }
2211    config
2212}
2213
2214/// One translator for the session: the configuration's language, else the session's.
2215pub(crate) fn tr() -> &'static Translator {
2216    static TRANSLATOR: OnceLock<Translator> = OnceLock::new();
2217    TRANSLATOR.get_or_init(|| Translator::new(&load_config().language.unwrap_or_else(session_language)))
2218}
2219
2220/// The session's language: its variables (LC_ALL, LC_MESSAGES, LANG) when it
2221/// has them, as on Linux; else the system's own setting, as Qt reads it on
2222/// Windows and macOS, where a window started from the desktop gets none.
2223fn session_language() -> String {
2224    let said = ["LC_ALL", "LC_MESSAGES", "LANG"].iter().any(|v| std::env::var(v).is_ok_and(|l| !l.trim().is_empty()));
2225    if said {
2226        return i18n::system_language();
2227    }
2228    system_locale().unwrap_or_else(i18n::system_language)
2229}
2230
2231/// Windows' language for the user ("fr-FR"), as `QLocale::system()` reads it.
2232#[cfg(windows)]
2233fn system_locale() -> Option<String> {
2234    #[link(name = "kernel32")]
2235    // SAFETY: kernel32's own, as winnls.h declares it: a buffer of UTF-16 and its size in characters, the length written back.
2236    unsafe extern "system" {
2237        fn GetUserDefaultLocaleName(name: *mut u16, size: i32) -> i32;
2238    }
2239    // LOCALE_NAME_MAX_LENGTH characters, the final zero included.
2240    let mut name = [0u16; 85];
2241    // SAFETY: a buffer of `name.len()` UTF-16 characters, as the function asks; it writes no further.
2242    let written = unsafe { GetUserDefaultLocaleName(name.as_mut_ptr(), name.len() as i32) };
2243    let length = usize::try_from(written).ok().filter(|n| *n > 1 && *n <= name.len())?;
2244    Some(String::from_utf16_lossy(&name[..length - 1]))
2245}
2246
2247/// The first of the languages chosen in macOS's settings ("fr-FR").
2248#[cfg(target_os = "macos")]
2249fn system_locale() -> Option<String> {
2250    // `defaults` prints them as a list: ( "fr-FR", "en-FR" ).
2251    let out = std::process::Command::new("defaults").args(["read", "-g", "AppleLanguages"]).output().ok()?;
2252    String::from_utf8_lossy(&out.stdout).split('"').nth(1).map(str::to_string).filter(|l| !l.is_empty())
2253}
2254
2255/// Elsewhere the session's variables say it all.
2256#[cfg(not(any(windows, target_os = "macos")))]
2257fn system_locale() -> Option<String> {
2258    None
2259}
2260
2261pub(crate) fn say(id: &str, pairs: &[(&str, String)]) -> String {
2262    let mut args = i18n::args();
2263    for (key, value) in pairs {
2264        // A whole number goes as a number, so that "one" and "other" choose by
2265        // it ("1 message", "2 messages"); one written otherwise ("007") stays as written.
2266        match value.parse::<i32>() {
2267            Ok(n) if n.to_string() == *value => args.set(key.to_string(), n),
2268            _ => args.set(key.to_string(), value.clone()),
2269        }
2270    }
2271    tr().text(id, Some(&args))
2272}
2273
2274pub(crate) fn json(value: &impl serde::Serialize) -> String {
2275    serde_json::to_string(value).unwrap_or_default()
2276}
2277
2278/// A file's address for Qt and the desktop: "file:///home/me/A%20b.pdf", and
2279/// "file:///C:/Users/me/A%20b.pdf" on Windows; what an address cannot hold as
2280/// is ("#", "?", spaces, accents) percent-encoded, so the file itself is reached.
2281pub(crate) fn file_url(path: &Path) -> String {
2282    fn encoded(path: &str) -> String {
2283        path.bytes().map(|b| if b.is_ascii_alphanumeric() || b"/-._~!$&'()*+,;=:@".contains(&b) { char::from(b).to_string() } else { format!("%{b:02X}") }).collect()
2284    }
2285    let text = path.to_string_lossy().into_owned();
2286    #[cfg(windows)]
2287    let text = windows_slashed(&text);
2288    match text.strip_prefix("//") {
2289        // "//server/share/…": a network share, its server named (Windows).
2290        Some(share) if cfg!(windows) => format!("file://{}", encoded(share)),
2291        _ => format!("file://{}", encoded(&text)),
2292    }
2293}
2294
2295/// A Windows path as an address writes it: "C:\a" → "/C:/a", "\\server\s" →
2296/// "//server/s", without the "\\?\" `canonicalize` puts in front.
2297#[cfg(any(windows, test))]
2298fn windows_slashed(text: &str) -> String {
2299    let plain = match text.strip_prefix(r"\\?\UNC\") {
2300        Some(rest) => format!(r"\\{rest}"),
2301        None => text.strip_prefix(r"\\?\").unwrap_or(text).to_string(),
2302    };
2303    let slashed = plain.replace('\\', "/");
2304    if slashed.starts_with("//") { slashed } else { format!("/{slashed}") }
2305}
2306
2307/// Back from an address to a Windows path: "/C:/a" → "C:\a", "server/s" → "\\server\s".
2308#[cfg(any(windows, test))]
2309fn windows_path(decoded: &str) -> String {
2310    let bytes = decoded.as_bytes();
2311    let path = if bytes.len() >= 3 && bytes[0] == b'/' && bytes[1].is_ascii_alphabetic() && bytes[2] == b':' {
2312        decoded[1..].to_string()
2313    } else if decoded.starts_with('/') {
2314        decoded.to_string()
2315    } else {
2316        format!("//{decoded}")
2317    };
2318    path.replace('/', "\\")
2319}
2320
2321/// The file a `file://` address names (from a file dialog, a drop), its
2322/// percent-escapes undone; "file:///C:/…" is "C:\…" on Windows. Anything
2323/// else is a path already.
2324pub(crate) fn local_path(url: &str) -> PathBuf {
2325    let Some(rest) = url.strip_prefix("file://") else { return PathBuf::from(url) };
2326    // "file://localhost/…" is this computer too.
2327    let rest = rest.strip_prefix("localhost/").map_or_else(|| rest.to_string(), |r| format!("/{r}"));
2328    let decoded = crate::projects::percent_decoded(&rest);
2329    #[cfg(windows)]
2330    let decoded = windows_path(&decoded);
2331    PathBuf::from(decoded)
2332}
2333
2334/// The lock of something done one at a time, when nothing holds it; a panic
2335/// in an earlier run (bad data in a file) does not stop it for the session.
2336pub(crate) fn one_at_a_time(lock: &Mutex<()>) -> Option<std::sync::MutexGuard<'_, ()>> {
2337    match lock.try_lock() {
2338        Ok(guard) => Some(guard),
2339        Err(std::sync::TryLockError::Poisoned(poisoned)) => Some(poisoned.into_inner()),
2340        Err(std::sync::TryLockError::WouldBlock) => None,
2341    }
2342}
2343
2344/// A page computed on a thread, one computation at a time: asked again while
2345/// one runs, it runs once more after it, with everything changed meanwhile.
2346/// Fetches and changes ask often; a thread each would pile up on a slow disk.
2347#[derive(Default)]
2348pub(crate) struct Job {
2349    running: AtomicBool,
2350    again: AtomicBool,
2351    /// Asked while Sioul was put away: done once it is back.
2352    deferred: AtomicBool,
2353}
2354
2355/// Sioul put away on a phone: the pages are not computed for nobody (Android
2356/// stops an app that works in the background); each job asked meanwhile runs
2357/// once when it is back (`back_here`).
2358pub(crate) static AWAY: AtomicBool = AtomicBool::new(false);
2359
2360/// Runs `work` on a thread for the job `which` picks out (see `Job`).
2361pub(crate) fn coalesced(shared: &Arc<Shared>, which: fn(&Shared) -> &Job, work: impl Fn(&Arc<Shared>) + Send + 'static) {
2362    let job = which(shared);
2363    if AWAY.load(Ordering::SeqCst) {
2364        job.deferred.store(true, Ordering::SeqCst);
2365        return;
2366    }
2367    job.again.store(true, Ordering::SeqCst);
2368    if job.running.swap(true, Ordering::SeqCst) {
2369        return;
2370    }
2371    let shared = Arc::clone(shared);
2372    std::thread::spawn(move || {
2373        let job = which(&shared);
2374        loop {
2375            while job.again.swap(false, Ordering::SeqCst) {
2376                // A panic on bad data leaves the next computations free to run.
2377                let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| work(&shared)));
2378            }
2379            job.running.store(false, Ordering::SeqCst);
2380            // Asked between the two lines above: once more, unless another thread took it.
2381            if !job.again.load(Ordering::SeqCst) || job.running.swap(true, Ordering::SeqCst) {
2382                break;
2383            }
2384        }
2385    });
2386}
2387
2388/// How long text reads, for the window's theme; and whether the places show their names.
2389pub(crate) fn reading_json() -> String {
2390    let config = load_config();
2391    let reading = config.reading;
2392    // colour: and the sites' colours, as Settings ▸ Display has them (SitesPage.qml, docs/colour.md).
2393    serde_json::json!({ "family": reading.family, "size": reading.size, "spacing": reading.spacing, "theme": config.theme.unwrap_or_default(), "places_named": config.places_named, "screen_colours": config.screen_colours.unwrap_or(true), "calmer_colours": config.calmer_colours.unwrap_or_default() }).to_string()
2394}
2395
2396/// What the Porch and the budgets read: the configuration and its lists.
2397struct World {
2398    config: Config,
2399    store: Option<ProjectStore>,
2400    known: SenderList,
2401    /// Who is safe, neutral or blocked.
2402    senders: porch::Senders,
2403}
2404
2405impl World {
2406    fn load() -> World {
2407        let config = load_config();
2408        World {
2409            // With the mail tied to projects, so their conversations follow.
2410            store: config.notes_root_path().and_then(|root| ProjectStore::load(&root).ok()).map(|s| s.with_ties(&sioul_core::links::LocalLinks::load(&sioul_core::links::LocalLinks::default_path()))),
2411            known: KnownSenders::load(&config.known_senders_path()),
2412            senders: porch::Senders::load(&config),
2413            config,
2414        }
2415    }
2416
2417    /// The mail waiting in the Porch; with `state` empty, all the mail kept.
2418    fn gather(&self, state: &PorchState) -> Vec<Triaged> {
2419        let now = Zoned::now().timestamp().as_second();
2420        porch::gather(&self.config.mail_sources(), self.store.as_ref(), &self.known, &self.senders, state, now)
2421    }
2422
2423    fn ledger(&self) -> Option<Ledger> {
2424        // With the bank accounts' movements counted in their budgets.
2425        self.config.notes_root_path().and_then(|root| Ledger::load_with_bank(&root, &sioul_core::words::Words::of(&self.config)).ok())
2426    }
2427
2428    fn ledger_path(&self) -> Option<PathBuf> {
2429        self.config.notes_root_path().map(|root| root.join(budget::LEDGER))
2430    }
2431
2432    /// Every message about money, and what becomes of it.
2433    fn mail_lines(&self, ledger: &Ledger) -> Vec<MailLine> {
2434        let ignored = MoneyState::load(&MoneyState::default_path()).ignored;
2435        budget::mail_lines(ledger, &self.gather(&PorchState::default()), &ignored, &sioul_core::words::Words::of(&self.config))
2436    }
2437
2438    /// One message file judged as the Porch judges it, only inside the accounts' own mail.
2439    fn judge(&self, key: &str) -> Option<(PathBuf, Triaged)> {
2440        let path = maildir::locate(Path::new(key))?.canonicalize().ok()?;
2441        let sources = self.config.mail_sources();
2442        let inside = sources.iter().any(|s| s.folder.canonicalize().is_ok_and(|f| path.starts_with(f)));
2443        if !inside {
2444            return None;
2445        }
2446        let now = Zoned::now().timestamp().as_second();
2447        let canonical: Vec<_> = sources
2448            .into_iter()
2449            .map(|mut s| {
2450                s.folder = s.folder.canonicalize().unwrap_or(s.folder);
2451                s
2452            })
2453            .collect();
2454        let triaged = porch::judge(std::slice::from_ref(&path), &canonical, self.store.as_ref(), &self.known, &porch::Senders::default(), now);
2455        triaged.into_iter().next().map(|t| (path, t))
2456    }
2457}
2458
2459/// Everything the window shows, computed off Qt's thread.
2460struct Views {
2461    porch: String,
2462    budgets: String,
2463    accounts: String,
2464    blocked: String,
2465    shown: BTreeMap<String, ImapOrigin>,
2466}
2467
2468/// Whether Sioul stays off the network: `SIOUL_DEMO` set, for a demo profile
2469/// and its pictures. Nothing is fetched or sent by itself; what is kept on
2470/// this computer is shown.
2471pub(crate) fn offline() -> bool {
2472    std::env::var_os("SIOUL_DEMO").is_some()
2473}
2474
2475/// Where the forecast is kept between fetches (the phone's card reads it too, `homecard`).
2476pub(crate) fn weather_cache() -> PathBuf {
2477    config::state_dir().join("weather.json")
2478}
2479
2480/// A forecast is kept half an hour.
2481const WEATHER_FRESH: i64 = 30 * 60;
2482
2483/// The weather applet, from the forecast kept, fetched again when it is half
2484/// an hour old; nothing without a place.
2485pub(crate) fn update_weather(qt: &QtThread, shared: &Arc<Shared>) {
2486    let config = load_config();
2487    let (Some(place), Some(latitude), Some(longitude)) = (config.weather.place.clone(), config.weather.latitude, config.weather.longitude) else {
2488        let _ = qt.queue(|mut sioul| sioul.as_mut().set_forecast(QString::default()));
2489        return;
2490    };
2491    let now = Zoned::now();
2492    let kept: Option<sioul_core::weather::Forecast> = std::fs::read_to_string(weather_cache()).ok().and_then(|t| serde_json::from_str(&t).ok());
2493    let shown = move |forecast: &sioul_core::weather::Forecast| {
2494        let view = sioul_core::weather::view(forecast, &Zoned::now(), tr());
2495        serde_json::json!({ "place": place, "view": view, "credit": tr().text("weather-credit", None) }).to_string()
2496    };
2497    if let Some(forecast) = kept.as_ref() {
2498        let line = shown(forecast);
2499        let _ = qt.queue(move |mut sioul| sioul.as_mut().set_forecast(QString::from(&line)));
2500    }
2501    let stale = kept.as_ref().is_none_or(|f| now.timestamp().as_second() - f.fetched >= WEATHER_FRESH);
2502    if !stale || offline() || shared.weather_fetching.swap(true, Ordering::Relaxed) {
2503        return;
2504    }
2505    let (qt, shared) = (qt.clone(), Arc::clone(shared));
2506    std::thread::spawn(move || {
2507        if let Ok(forecast) = sioul_sync::weather::forecast(latitude, longitude, Zoned::now().timestamp().as_second()) {
2508            if let Ok(text) = serde_json::to_string(&forecast) {
2509                let _ = std::fs::create_dir_all(config::state_dir());
2510                let _ = std::fs::write(weather_cache(), text);
2511            }
2512            // The phone's card shows it too, hour by hour.
2513            crate::homecard::weather_seen();
2514            let line = shown(&forecast);
2515            let _ = qt.queue(move |mut sioul| sioul.as_mut().set_forecast(QString::from(&line)));
2516        }
2517        shared.weather_fetching.store(false, Ordering::Relaxed);
2518    });
2519}
2520
2521/// The time now: what it is for, and until when (docs/areas.md; `hours`).
2522pub(crate) fn mode_now() -> sioul_core::quiet::Mode {
2523    crate::hours::mode_now()
2524}
2525
2526/// Work time or quiet time, as the window shows it.
2527pub(crate) fn mode_json() -> String {
2528    let config = load_config();
2529    let now = Zoned::now();
2530    let overrides = sioul_core::quiet::Overrides::load(&sioul_core::quiet::Overrides::default_path());
2531    let mode = crate::hours::mode_at(&now);
2532    let until = mode.until.as_ref().map(|z| sioul_core::quiet::until_text(tr(), z, &now)).unwrap_or_default();
2533    let mut args = sioul_core::i18n::args();
2534    args.set("until", until.clone());
2535    args.set("label", if mode.label.trim().is_empty() { "none".to_string() } else { mode.label.clone() });
2536    let reason = serde_json::to_value(&mode.reason).ok().and_then(|v| v.as_str().map(str::to_string)).unwrap_or_default();
2537    use sioul_core::quiet::Reason;
2538    let line = match mode.reason {
2539        Reason::NoHours | Reason::Working => String::new(),
2540        Reason::TimeOff => tr().text("mode-time-off", Some(&args)),
2541        Reason::WorkingLate => tr().text("mode-working-late", Some(&args)),
2542        Reason::WorkNow => tr().text("mode-work-now-line", Some(&args)),
2543        Reason::AdminTime => tr().text("mode-admin", Some(&args)),
2544        // A day closed early: when work comes back.
2545        Reason::DoneForTheDay => tr().text("mode-quiet", Some(&args)),
2546        // Evenings, days without hours: leisure, until the next change.
2547        Reason::Evening | Reason::DayOff => tr().text("mode-leisure", Some(&args)),
2548        Reason::Meal => tr().text("mode-meal", Some(&args)),
2549        Reason::WindingDown => tr().text("mode-wind-down", Some(&args)),
2550        Reason::Nap => tr().text("mode-nap", Some(&args)),
2551        Reason::Sleep => tr().text("mode-sleep", Some(&args)),
2552        // The pauses (docs/pauses.md): which one, in its own words; the moved end was said once.
2553        Reason::FreeTime => tr().text(if sioul_core::pause::nothing_now(&overrides, &config.free_time) { "mode-free-time-nothing" } else { "mode-free-time" }, None),
2554        Reason::Paused => tr().text("mode-paused", None),
2555        Reason::Extended => String::new(),
2556    };
2557    // The day closed today can be taken back, that day only.
2558    let today = overrides.closed_today(&now);
2559    // Which hours are set, and the night: the Porch asks for them while they are not.
2560    let set = |kind: &str| config.windows.iter().any(|w| w.kind() == kind);
2561    serde_json::json!({
2562        "quiet": mode.quiet,
2563        "sleep": mode.sleeps(),
2564        "time": mode.time.id(),
2565        "reason": reason,
2566        "until": until,
2567        "line": line,
2568        "hours": !config.week_hours().is_empty(),
2569        "work_hours": set("work"),
2570        "admin_hours": set("admin"),
2571        "night": crate::hours::night_set(),
2572        // Sharing set up here: the Porch offers to join your other devices while it is not.
2573        "shares": crate::share::configured(),
2574        "work_now": mode.reason == Reason::WorkNow,
2575        "today": today,
2576        // The work day or the day can be closed now (docs/reviews.md); nothing at work or asleep.
2577        "review": crate::reviews::offer_json(&now, &mode),
2578        // Free time's choices and today's end of work (docs/pauses.md).
2579        "pauses": crate::pauses::moment(&config, &overrides, &now),
2580        // Do-not-disturb on every device (docs/do-not-disturb.md): its switch and where it holds.
2581        "dnd": crate::everywhere::moment(),
2582        // Expecting a call, while a phone of yours screens calls (docs/android.md, "Calls").
2583        "calls": crate::calls::moment()
2584    })
2585    .to_string()
2586}
2587
2588fn compute(shared: &Shared) -> Views {
2589    // One look at the mail: the Porch, then the money lines, each message parsed once (`maildir::CardsKept`).
2590    let _cards = sioul_core::maildir::CardsKept::begin();
2591    let world = World::load();
2592    let now = Zoned::now();
2593    let hidden = mail::hidden_files(shared);
2594    let mut items = world.gather(&PorchState::load(&PorchState::default_path()));
2595    items.retain(|t| t.card.path.as_ref().is_none_or(|p| !hidden.contains(p)));
2596    // The phone's home screen card: the Porch as each coming time will show it (homecard.rs).
2597    crate::homecard::porch_seen(&items, &world.senders, world.store.as_ref());
2598    // What reaches you now (docs/attention.md): codes and what you sent
2599    // yourself always; the rest as its sender's row of the matrix says now
2600    // (shown, or shown without a word), Always through whatever their row, and
2601    // as its address is for (an address you did not say: work's) but for your
2602    // safe senders. No hours set: work and admin together.
2603    let mode = crate::hours::mode_at(&now);
2604    {
2605        let area_of = |t: &sioul_core::porch::Triaged| t.card.account.as_deref().and_then(|id| world.config.account(id)).and_then(|a| a.area.as_deref()).and_then(sioul_core::areas::Area::parse).unwrap_or(sioul_core::areas::Area::WORK);
2606        let attention = sioul_core::attention::Attention::of(&world.config);
2607        let mut moment = crate::hours::attention_at(&mode, &now);
2608        let always = sioul_core::everywhere::People::load(&sioul_core::everywhere::People::default_path());
2609        // "Show all mail now": every lane and address, at any hour, on this device, as
2610        // Reachable shows them, the blocked's apart (decision 12); it tells nothing (`mailnote`).
2611        moment.reachable |= shared.opened_anyway.load(Ordering::Relaxed);
2612        items.retain(|t| attention.mail(t, &world.senders, &always, area_of(t), &moment).shown);
2613    }
2614    // Asleep no project shows: what your senders wrote about one comes among the people you know.
2615    if mode.sleeps() {
2616        for t in &mut items {
2617            if matches!(t.lane, sioul_core::porch::Lane::Project(_)) {
2618                t.lane = sioul_core::porch::Lane::People;
2619            }
2620        }
2621    }
2622    // Reachable opens the Porch at any hour, its rest after a pause lifted too (decision 7).
2623    let opened = shared.opened_anyway.load(Ordering::Relaxed) || shared.reachable.load(Ordering::Relaxed);
2624    let mut porch = view::porch(&items, &world.config, world.store.as_ref(), tr(), &now, opened || mode.quiet);
2625    porch.all = shared.opened_anyway.load(Ordering::Relaxed);
2626    // After a pause, the Porch rests until the next admin hours, unless opened (docs/pauses.md).
2627    crate::pauses::porch_rests(&mut porch, opened, &now);
2628    if mode.quiet {
2629        porch.status = serde_json::from_str::<serde_json::Value>(&mode_json()).ok().and_then(|v| v["line"].as_str().map(str::to_string)).unwrap_or_default();
2630    }
2631    // Closed, the Porch shows only codes: "Done for now" has nothing to close.
2632    let shown = if porch.open { PorchState::newest_shown(&items) } else { BTreeMap::new() };
2633    let budgets = world.ledger().map_or_else(String::new, |mut ledger| {
2634        // Budgets as their area goes: leisure money ("personal"), work's, the rest admin.
2635        if mode.time != sioul_core::areas::Time::Any {
2636            // "personal" was the word for leisure money before areas had three.
2637            let area_of = |b: &sioul_core::budget::Budget| match b.area.as_deref() {
2638                Some("personal") => sioul_core::areas::Area::LEISURE,
2639                Some(other) => sioul_core::areas::Area::parse(other).unwrap_or(sioul_core::areas::Area::ADMIN),
2640                None => sioul_core::areas::Area::ADMIN,
2641            };
2642            ledger.budgets.retain(|b| sioul_core::areas::in_view(area_of(b), mode.time, mode.week));
2643        }
2644        let mail = world.mail_lines(&ledger);
2645        json(&view::budgets(&ledger, &mail, tr(), now.date()))
2646    });
2647    let statuses = shared.statuses.lock().map(|s| s.clone()).unwrap_or_default();
2648    let wanted = shared.password_wanted.lock().map(|w| w.clone()).unwrap_or_default();
2649    Views {
2650        // On a phone, a line on other apps' notifications held and come back (docs/android.md).
2651        porch: crate::appnotes::with_line(json(&porch)),
2652        budgets,
2653        accounts: json(&view::accounts(&world.config, tr(), &statuses, &wanted)),
2654        blocked: json(&world.senders.blocked.entries()),
2655        shown,
2656    }
2657}
2658
2659/// Computes the views on a thread and shows them, unless newer ones came first.
2660pub(crate) fn show(qt: &QtThread, shared: &Arc<Shared>) {
2661    crate::mailsearch::changed(qt, shared);
2662    let qt = qt.clone();
2663    coalesced(shared, |s| &s.views_job, move |shared| {
2664        // One look at the mail for the Mail page and the Porch: the folder the
2665        // page shows (the inbox, most often) is parsed once for both, not once
2666        // each (the owner's question, whether the phone parses each message once).
2667        let _cards = sioul_core::maildir::CardsKept::begin();
2668        mail::show_mail_here(&qt, shared);
2669        let generation = shared.generation.fetch_add(1, Ordering::Relaxed) + 1;
2670        let views = compute(shared);
2671        let _ = qt.queue(move |mut sioul| {
2672            let shared = Arc::clone(&sioul.rust().shared);
2673            // Only the newest: results queued while the window was frozen (a phone
2674            // with Sioul in the back) are not applied one after the other at its return.
2675            if shared.generation.load(Ordering::Relaxed) != generation || shared.shown_generation.fetch_max(generation, Ordering::Relaxed) > generation {
2676                return;
2677            }
2678            if let Ok(mut shown) = shared.shown.lock() {
2679                *shown = views.shown;
2680            }
2681            sioul.as_mut().set_porch(QString::from(&views.porch));
2682            sioul.as_mut().set_budgets(QString::from(&views.budgets));
2683            sioul.as_mut().set_accounts(QString::from(&views.accounts));
2684            sioul.as_mut().set_blocked(QString::from(&views.blocked));
2685            // Accounts added, renamed or removed: Android's share sheet follows (docs/android.md, "Sharing").
2686            if cfg!(target_os = "android") {
2687                std::thread::spawn(|| crate::outside::publish(false));
2688            }
2689        });
2690    });
2691}
2692
2693pub(crate) fn set_status(qt: &QtThread, line: String) {
2694    let _ = qt.queue(move |mut sioul| sioul.as_mut().set_status(QString::from(&line)));
2695}
2696
2697/// Says what became of something you did; it stays a while (`SAID_FOR`).
2698pub(crate) fn tell(qt: &QtThread, shared: &Shared, line: String) {
2699    if let Ok(mut said) = shared.said_at.lock() {
2700        *said = Some(std::time::Instant::now());
2701    }
2702    set_status(qt, line);
2703}
2704
2705/// Starts a watcher for every mail account that has none running.
2706fn start_watchers(qt: &QtThread, shared: &Arc<Shared>) -> usize {
2707    if offline() {
2708        return 0;
2709    }
2710    let accounts: Vec<Account> = load_config().accounts.into_iter().filter(Account::syncs).collect();
2711    let count = accounts.len();
2712    for account in accounts {
2713        start_watcher(qt, shared, account);
2714    }
2715    count
2716}
2717
2718/// Syncs for the accounts not started yet this session: those another device's
2719/// settings just brought. One that stopped (a password refused) is not tried
2720/// again here: retries can lock an account.
2721pub(crate) fn start_new_watchers(qt: &QtThread, shared: &Arc<Shared>) {
2722    if offline() {
2723        return;
2724    }
2725    let seen = |id: &str| shared.statuses.lock().is_ok_and(|s| s.contains_key(id)) || shared.watchers.lock().is_ok_and(|w| w.contains_key(id)) || shared.dav_watchers.lock().is_ok_and(|w| w.contains_key(id));
2726    for account in load_config().accounts.into_iter().filter(|a| !seen(&a.id)) {
2727        if account.is_dav() {
2728            crate::pim::start_dav_watcher(qt, shared, account);
2729        } else if account.syncs() {
2730            start_watcher(qt, shared, account);
2731        }
2732    }
2733}
2734
2735/// An account's password given on this device: tested with its server (its
2736/// mail, or its calendars and contacts), kept in the keyring, its sync started
2737/// again; `account_password_done` says how it went.
2738fn give_password(qt: &QtThread, shared: &Arc<Shared>, id: String, password: String) {
2739    let (qt, shared) = (qt.clone(), Arc::clone(shared));
2740    std::thread::spawn(move || {
2741        let config = load_config();
2742        let outcome = match config.every_account().find(|a| a.id == id).cloned() {
2743            None => Err(say("account-unknown", &[("id", id.clone())])),
2744            Some(mut account) => {
2745                let host = account.host.clone().unwrap_or_default();
2746                let password = sioul_sync::tidy_password(&host, &password);
2747                // Google's mail signed in with Google, given an app password
2748                // instead: tried as a password, then kept as one.
2749                let switched = !account.is_dav() && account.auth.as_deref() == Some("google");
2750                if switched {
2751                    account.auth = None;
2752                }
2753                let tested = if account.is_dav() {
2754                    let address = account.address.clone().unwrap_or_default();
2755                    let login = account.login().unwrap_or(address.as_str()).to_string();
2756                    sioul_sync::dav::test(&address, &login, &password, account.url.as_deref(), None).map(|_| ())
2757                } else {
2758                    sioul_sync::test(&account, &password).map(|_| ())
2759                };
2760                tested
2761                    .and_then(|()| secret::save(&account, &password))
2762                    .map_err(|e| e.sentence(tr(), &account.id))
2763                    .and_then(|()| if switched { config::set_auth(&config_path(), &account.id, None) } else { Ok(()) })
2764                    .map(|()| {
2765                        if switched {
2766                            // Its watcher signed in with Google: it goes, another starts below.
2767                            if let Some(control) = shared.watchers.lock().ok().and_then(|mut w| w.remove(&account.id)) {
2768                                control.stop();
2769                            }
2770                            let _ = sioul_sync::google::revoke_for(sioul_sync::google::Purpose::Mail, account.address.as_deref().unwrap_or(""));
2771                        }
2772                        account
2773                    })
2774            }
2775        };
2776        let problem = match outcome {
2777            Ok(account) => {
2778                want_password(&shared, &account.id, None);
2779                if let Ok(mut statuses) = shared.statuses.lock() {
2780                    statuses.remove(&account.id);
2781                }
2782                if account.is_dav() {
2783                    crate::pim::start_dav_watcher(&qt, &shared, account);
2784                } else {
2785                    start_watcher(&qt, &shared, account);
2786                }
2787                String::new()
2788            }
2789            Err(e) => e,
2790        };
2791        let _ = qt.queue(move |mut sioul| {
2792            sioul.as_mut().set_form_busy(false);
2793            if problem.is_empty() {
2794                sioul.as_mut().set_status(QString::from(&say("account-password-kept", &[("account", id.clone())])));
2795            }
2796            sioul.as_mut().account_password_done(QString::from(&id), QString::from(&problem));
2797        });
2798        show(&qt, &shared);
2799    });
2800}
2801
2802/// A mail account added: tested with its password (given, or, `None`, the one
2803/// its address's calendars and contacts use), then kept; its watcher started.
2804#[allow(clippy::too_many_arguments)]
2805fn add_mail(qt: &QtThread, shared: &Arc<Shared>, address: String, host: String, port: u16, security: Security, login: String, password: Option<String>) {
2806    let (qt, shared) = (qt.clone(), Arc::clone(shared));
2807    std::thread::spawn(move || {
2808        let config = load_config();
2809        // The same address may have calendars and contacts already: only its mail counts.
2810        let outcome = match config.every_account().find(|a| a.address.as_deref() == Some(address.as_str()) && matches!(a.kind, config::AccountKind::Imap | config::AccountKind::Jmap)) {
2811            Some(existing) => Err(say("account-address-exists", &[("address", address.clone()), ("id", existing.id.clone())])),
2812            None => {
2813                let password = match password {
2814                    Some(p) => Ok(p),
2815                    None => config
2816                        .every_account()
2817                        .find(|a| a.address.as_deref() == Some(address.as_str()) && a.kind == config::AccountKind::Dav)
2818                        .ok_or_else(|| say("account-unknown", &[("id", address.clone())]))
2819                        .and_then(|dav| secret::password(dav).map_err(|e| e.sentence(tr(), &dav.id))),
2820                };
2821                password.and_then(|password| {
2822                    let id = config.free_id(&address);
2823                    let account = Account::imap(&id, &address, &host, port, security, Some(&login));
2824                    let password = sioul_sync::tidy_password(&host, &password);
2825                    sioul_sync::test(&account, &password)
2826                        .and_then(|_| secret::save(&account, &password))
2827                        .map_err(|e| e.sentence(tr(), &id))
2828                        .and_then(|()| config::add_imap_account(&config_path(), &account))
2829                        .map(|()| account)
2830                })
2831            }
2832        };
2833        let added = outcome.as_ref().ok().map(|a| say("account-added", &[("id", a.id.clone()), ("path", a.maildir_path().display().to_string())]));
2834        if let Ok(account) = outcome.as_ref() {
2835            start_watcher(&qt, &shared, account.clone());
2836        }
2837        let error = outcome.err();
2838        let _ = qt.queue(move |mut sioul| {
2839            sioul.as_mut().set_form_busy(false);
2840            match (added, error) {
2841                (Some(line), _) => {
2842                    sioul.as_mut().set_found(QString::default());
2843                    sioul.as_mut().set_scouted(QString::default());
2844                    sioul.as_mut().set_status(QString::from(&line));
2845                    sioul.as_mut().account_added();
2846                }
2847                (None, Some(e)) => sioul.as_mut().set_form_error(QString::from(&e)),
2848                (None, None) => {}
2849            }
2850        });
2851        show(&qt, &shared);
2852    });
2853}
2854
2855/// The pace of an address: its own, else everyone's.
2856fn pace_of(config: &Config, account: &Account) -> std::time::Duration {
2857    let minutes = account.fetch_minutes.or(config.fetch_minutes).unwrap_or(config::DEFAULT_FETCH_MINUTES);
2858    std::time::Duration::from_secs(u64::from(minutes.max(1)) * 60)
2859}
2860
2861/// Reachable begun or ended since the last look (a press here, one from
2862/// another device brought by the sharing, its end): the mail's pace and the
2863/// Porch follow. Each minute, and after a press.
2864pub(crate) fn follow_reachable(qt: &QtThread, shared: &Arc<Shared>) {
2865    let now = crate::everywhere::reachable().is_some();
2866    if shared.reachable.swap(now, Ordering::Relaxed) != now {
2867        update_paces(shared);
2868        show(qt, shared);
2869    }
2870}
2871
2872/// Gives every running watcher its pace from the configuration, and real time.
2873pub(crate) fn update_paces(shared: &Shared) {
2874    let config = load_config();
2875    // Mail every minute: the Real time box, or while Reachable lasts (decision 11).
2876    let realtime = shared.realtime.load(Ordering::Relaxed) || shared.reachable.load(Ordering::Relaxed);
2877    let Ok(watchers) = shared.watchers.lock() else { return };
2878    for account in config.accounts.iter() {
2879        if let Some(control) = watchers.get(&account.id) {
2880            control.set_pace(pace_of(&config, account));
2881            control.set_realtime(realtime);
2882        }
2883    }
2884}
2885
2886pub(crate) fn start_watcher(qt: &QtThread, shared: &Arc<Shared>, account: Account) {
2887    let control = Arc::new(Control::default());
2888    control.set_pace(pace_of(&load_config(), &account));
2889    control.set_realtime(shared.realtime.load(Ordering::Relaxed) || shared.reachable.load(Ordering::Relaxed));
2890    {
2891        let Ok(mut watchers) = shared.watchers.lock() else { return };
2892        // Being removed: its mail is deleted once its watcher has ended, and none starts again.
2893        if shared.removing.lock().is_ok_and(|removing| removing.contains(&account.id)) {
2894            return;
2895        }
2896        if let Some(running) = watchers.get(&account.id) {
2897            // Already watching: fetch now instead, a refused password tried again too.
2898            running.retry();
2899            return;
2900        }
2901        watchers.insert(account.id.clone(), Arc::clone(&control));
2902    }
2903    let (qt, shared) = (qt.clone(), Arc::clone(shared));
2904    std::thread::spawn(move || {
2905        match secret::password(&account) {
2906            Ok(password) => sioul_sync::watch(&account, &password, &control, |result| reported(&qt, &shared, &account, result)),
2907            Err(e) => reported(&qt, &shared, &account, Err(e)),
2908        }
2909        // Nothing of it writes any more: a removal waiting to delete its mail goes on (`outlook::remove`).
2910        control.end();
2911        // Gone, unless another watcher replaced this one meanwhile.
2912        if let Ok(mut watchers) = shared.watchers.lock()
2913            && watchers.get(&account.id).is_some_and(|c| Arc::ptr_eq(c, &control))
2914        {
2915            watchers.remove(&account.id);
2916        }
2917    });
2918}
2919
2920/// Notes whether an account's password is wanted after what its sync said:
2921/// none here, or refused. Its card then offers to give one.
2922pub(crate) fn want_password(shared: &Shared, id: &str, error: Option<&SyncError>) {
2923    if let Ok(mut wanted) = shared.password_wanted.lock() {
2924        if error.is_some_and(SyncError::wants_password) {
2925            wanted.insert(id.to_string());
2926        } else {
2927            wanted.remove(id);
2928        }
2929    }
2930}
2931
2932/// What a watcher said: the account's status, the provider's id to learn,
2933/// notifications for new codes, and the views again.
2934fn reported(qt: &QtThread, shared: &Arc<Shared>, account: &Account, result: Result<Report, SyncError>) {
2935    let (line, problem) = match &result {
2936        Ok(report) if report.first => {
2937            let mut args = tr().counted(report.new.len());
2938            args.set("account", account.id.clone());
2939            args.set("days", account.sync_days.unwrap_or(config::DEFAULT_SYNC_DAYS));
2940            (tr().text("sync-first", Some(&args)), false)
2941        }
2942        Ok(_) => (say("ui-synced-at", &[("time", Zoned::now().strftime("%H:%M").to_string())]), false),
2943        Err(e) => (e.sentence(tr(), &account.id), true),
2944    };
2945    if let Ok(mut statuses) = shared.statuses.lock() {
2946        statuses.insert(account.id.clone(), (line.clone(), problem));
2947    }
2948    want_password(shared, &account.id, result.as_ref().err());
2949    if let Ok(report) = &result {
2950        if !report.new.is_empty() {
2951            learn(qt, shared, &account.id);
2952            tie_to_projects(&report.new);
2953        }
2954        if !report.elsewhere.is_empty() {
2955            tie_to_projects(&report.elsewhere);
2956        }
2957        // Older mail waits for room: said once a session, calmly.
2958        if report.held_back
2959            && shared.held_back.lock().is_ok_and(|mut said| said.insert(account.id.clone()))
2960        {
2961            let free = sioul_sync::disk::space(&account.maildir_path()).map_or(0, |(free, _)| free);
2962            set_status(qt, say("sync-held-back", &[("account", account.id.clone()), ("free", tr().decimal((free as f64 / f64::from(1u32 << 30)) as f32))]));
2963        }
2964        if !report.first && !report.new.is_empty() {
2965            notify_codes(qt, &report.new);
2966            sort_on_server(qt, shared, account, report.new.clone());
2967            // New mail your lists let through now, once per batch (docs/porch.md,
2968            // "Notifications"); what your filter flagged or moves is never told; what
2969            // a mail filter would take waits for it (told after, if it could not act).
2970            let window = crate::mailnote::Window { qt: qt.clone(), active: shared.active.load(Ordering::Relaxed) };
2971            crate::mailnote::arrived_in_window(window, &account.id, &report.new, report.first);
2972        } else if !report.first && sioul_sync::filters::due(account) {
2973            // Nothing new, but a filter's failure to try again, or another device's mark to look at again.
2974            sort_on_server(qt, shared, account, Vec::new());
2975        }
2976        read_shielded(qt, &account.id);
2977    }
2978    // Routine news waits while the line says what became of something you did.
2979    let recent = shared.said_at.lock().ok().and_then(|s| *s).is_some_and(|at| at.elapsed() < SAID_FOR);
2980    let quiet = recent && !problem;
2981    let _ = qt.queue(move |mut sioul| {
2982        sioul.as_mut().set_busy(false);
2983        if !quiet {
2984            sioul.as_mut().set_status(QString::from(&line));
2985        }
2986    });
2987    show(qt, shared);
2988}
2989
2990/// After a fetch of `account` (`new`: its inbox's arrivals, maybe none), on a
2991/// thread of its own (it reaches the server; this is the watcher's): what your
2992/// own spam filter moves, as you chose, into the Junk folder; then your mail
2993/// filters (docs/client.md, "Filters"), on the arrivals still unread and what
2994/// waits for another look, never on what the spam filter reviews. What a filter
2995/// could not act on is told as new mail, and why said in the status line; the
2996/// Porch is read again when something moved.
2997fn sort_on_server(qt: &QtThread, shared: &Arc<Shared>, account: &Account, new: Vec<PathBuf>) {
2998    let (account, qt, shared) = (account.clone(), qt.clone(), Arc::clone(shared));
2999    let window = crate::mailnote::Window { qt: qt.clone(), active: shared.active.load(Ordering::Relaxed) };
3000    std::thread::spawn(move || {
3001        let moved = crate::spam::after_fetch(&account, &new, false);
3002        let filtered = crate::filters::after_fetch(&account, &new, false);
3003        if !filtered.problem.is_empty() {
3004            set_status(&qt, filtered.problem.clone());
3005        }
3006        crate::mailnote::unfiltered_in_window(window, &account.id, &filtered.tell);
3007        if moved + filtered.acted > 0 {
3008            show(&qt, &shared);
3009        }
3010    });
3011}
3012
3013/// Mail just fetched that a project takes, by its routes or its conversation:
3014/// tied to the project, so its page shows it whatever its routes need, and
3015/// the replies that come later follow it.
3016fn tie_to_projects(files: &[PathBuf]) {
3017    let config = load_config();
3018    let path = sioul_core::links::LocalLinks::default_path();
3019    let mut ties = sioul_core::links::LocalLinks::load(&path);
3020    let before = ties.links.len();
3021    // GitHub's notification mail, to its issue's task.
3022    crate::github::tie_mail(&config, files, &mut ties);
3023    if let Some(store) = config.notes_root_path().and_then(|root| ProjectStore::load(&root).ok()).map(|s| s.with_ties(&ties)) {
3024        let senders = porch::Senders::load(&config);
3025        for t in porch::judge(files, &config.mail_sources(), Some(&store), &KnownSenders::default(), &senders, Zoned::now().timestamp().as_second()) {
3026            if let (porch::Lane::Project(id), Some(mid)) = (&t.lane, t.card.message_id.as_deref()) {
3027                ties.add(&sioul_core::links::mail_uri(mid), &sioul_core::links::project_uri(id), "project");
3028            }
3029        }
3030    }
3031    // Only what this added, written under the file's lock over the ties there
3032    // now: a tie made meanwhile (the window, an agent, the sharing) stays.
3033    let _ = sioul_core::links::LocalLinks::add_all(&path, &ties.links[before..]);
3034}
3035
3036/// A project's routes changed: the mail already here that only its text or an
3037/// attachment ties to it, tied now, on a thread (routes on the sender and the
3038/// subject are matched again each time the project shows). Each message as
3039/// the Porch takes it (`porch::Gate`): never one it sets aside (a blocked
3040/// sender, forged, a borrowed name, spam a route does not protect).
3041fn retie_project(id: String) {
3042    std::thread::spawn(move || {
3043        let config = load_config();
3044        let Some(project) = config.notes_root_path().and_then(|root| ProjectStore::load(&root).ok()).and_then(|s| s.get(&id).cloned()) else { return };
3045        let routes: Vec<&sioul_core::projects::Route> = project.routes.iter().filter(|r| r.needs_body()).collect();
3046        if routes.is_empty() {
3047            return;
3048        }
3049        let path = sioul_core::links::LocalLinks::default_path();
3050        let mut ties = sioul_core::links::LocalLinks::load(&path);
3051        let before = ties.links.len();
3052        let gate = porch::Gate::load(&config, Zoned::now().timestamp().as_second());
3053        for account in config.accounts.iter().filter(|a| a.syncs()) {
3054            let files = sioul_core::mailindex::message_files(&account.maildir_path());
3055            for mid in gate.routed(&routes, files.iter().map(PathBuf::as_path)) {
3056                ties.add(&sioul_core::links::mail_uri(&mid), &sioul_core::links::project_uri(&project.id), "project");
3057            }
3058        }
3059        let _ = sioul_core::links::LocalLinks::add_all(&path, &ties.links[before..]);
3060    });
3061}
3062
3063/// A shielded address that allows the AI: what it has not read yet, read now,
3064/// before the Porch shows it again. Nothing is sent without a key.
3065fn read_shielded(qt: &QtThread, id: &str) {
3066    let config = load_config();
3067    if !config.account(id).is_some_and(|a| a.ai_reads()) {
3068        return;
3069    }
3070    if let Err(e) = sioul_sync::shield_ai::read_new(&config) {
3071        set_status(qt, e.sentence(tr(), "Anthropic"));
3072    }
3073}
3074
3075/// Learns the provider's authserv-id once per session, while it is unknown.
3076fn learn(qt: &QtThread, shared: &Shared, id: &str) {
3077    if shared.learned.lock().map_or(true, |l| l.contains(id)) {
3078        return;
3079    }
3080    let Some(account) = load_config().account(id).cloned() else { return };
3081    match sioul_sync::learn_provider(&config_path(), &account) {
3082        Ok(Learned::TooEarly) => {}
3083        Ok(learned) => {
3084            if let Ok(mut done) = shared.learned.lock() {
3085                done.insert(id.to_string());
3086            }
3087            match learned {
3088                Learned::Id(provider) => set_status(qt, say("sync-learned", &[("account", id.to_string()), ("id", provider)])),
3089                Learned::Nothing => set_status(qt, say("sync-not-learned", &[("account", id.to_string())])),
3090                _ => {}
3091            }
3092        }
3093        Err(e) => set_status(qt, e),
3094    }
3095}
3096
3097/// One quiet notification per verified code among new mail, with a copy button.
3098/// A phone has none yet: its codes show on the Porch and on the home screen's
3099/// card (docs/android.md, "Notifications"), and nothing is tried there.
3100fn notify_codes(qt: &QtThread, files: &[PathBuf]) {
3101    // As the matrix of what reaches you says (as usual, at once at any time;
3102    // "On the Porch only" a choice while you sleep): a code not told waits on the Porch.
3103    if cfg!(target_os = "android") || !crate::hours::comes(sioul_core::attention::Kind::Codes) {
3104        return;
3105    }
3106    let config = load_config();
3107    let now = Zoned::now().timestamp().as_second();
3108    let senders = porch::Senders::load(&config);
3109    // Codes are decided before projects and screening: neither is needed here.
3110    let judged = porch::judge(files, &config.mail_sources(), None, &KnownSenders::default(), &senders, now);
3111    for code in view::codes(&judged, tr()) {
3112        let copy = code.code.clone().map(|text| {
3113            let qt = qt.clone();
3114            let action: Box<dyn FnOnce() + Send> = Box::new(move || {
3115                let _ = qt.queue(move |sioul| sioul.copy_requested(QString::from(&text)));
3116            });
3117            (tr().text("notify-copy", None), action)
3118        });
3119        if let Err(e) = notify::code(&code.title, &code.body(), copy) {
3120            set_status(qt, e);
3121        }
3122    }
3123}
3124
3125/// Where attachments are written to be opened: `<cache>/sioul/attachments/<message>/`.
3126fn attachment_cache(key: &str) -> PathBuf {
3127    let mut hasher = std::collections::hash_map::DefaultHasher::new();
3128    key.hash(&mut hasher);
3129    config::cache_dir().join("attachments").join(format!("{:016x}", hasher.finish()))
3130}
3131
3132/// Attachments written to be opened in an earlier session, taken out of the
3133/// cache once a day old: a decrypted one would stay readable there otherwise.
3134fn forget_opened_attachments() {
3135    let Ok(entries) = std::fs::read_dir(config::cache_dir().join("attachments")) else { return };
3136    let day = std::time::Duration::from_secs(86_400);
3137    for entry in entries.filter_map(Result::ok) {
3138        if entry.metadata().and_then(|m| m.modified()).ok().and_then(|t| t.elapsed().ok()).is_some_and(|age| age > day) {
3139            let _ = std::fs::remove_dir_all(entry.path());
3140        }
3141    }
3142}
3143
3144/// A file come by mail marked as the system marks what comes from the
3145/// Internet: the Mark of the Web on Windows, the quarantine on macOS. Office
3146/// then opens it in Protected View, Gatekeeper checks it. A disk that cannot
3147/// keep the mark leaves the file as it is.
3148fn mark_from_mail(file: &Path) {
3149    #[cfg(windows)]
3150    {
3151        let mut stream = file.as_os_str().to_owned();
3152        stream.push(":Zone.Identifier");
3153        let _ = std::fs::write(stream, "[ZoneTransfer]\r\nZoneId=3\r\n");
3154    }
3155    #[cfg(target_os = "macos")]
3156    {
3157        let stamp = format!("0081;{:x};Sioul;", jiff::Timestamp::now().as_second());
3158        let _ = std::process::Command::new("xattr").args(["-w", "com.apple.quarantine", &stamp]).arg(file).output();
3159    }
3160    #[cfg(not(any(windows, target_os = "macos")))]
3161    let _ = file;
3162}
3163
3164/// The downloads folder the desktop names.
3165fn downloads() -> PathBuf {
3166    config::downloads_dir()
3167}
3168
3169/// `name`, else "name (2).ext", "name (3).ext"… so nothing is overwritten.
3170fn free_path(folder: &Path, name: &str) -> PathBuf {
3171    let wanted = folder.join(name);
3172    if !wanted.exists() {
3173        return wanted;
3174    }
3175    let (stem, extension) = match name.rsplit_once('.') {
3176        Some((stem, ext)) if !stem.is_empty() => (stem.to_string(), format!(".{ext}")),
3177        _ => (name.to_string(), String::new()),
3178    };
3179    (2..).map(|n| folder.join(format!("{stem} ({n}){extension}"))).find(|p| !p.exists()).unwrap_or(wanted)
3180}
3181
3182/// An attachment's name as one file in a folder. The core takes any path out
3183/// of it; Windows also reads "C:x" as a drive, "a:b" as a hidden stream and
3184/// "CON", "NUL"… as devices: those characters become "_", those names get one
3185/// in front, and the dots and spaces it would drop at the end go.
3186fn one_file_name(name: &str) -> String {
3187    let odd = |c: char| matches!(c, '/' | '\\') || (cfg!(windows) && matches!(c, ':' | '<' | '>' | '"' | '|' | '?' | '*'));
3188    let mut name: String = name.chars().map(|c| if odd(c) { '_' } else { c }).collect();
3189    if cfg!(windows) {
3190        name = name.trim_end_matches(['.', ' ']).to_string();
3191        let stem = name.split('.').next().unwrap_or("").trim_end().to_ascii_uppercase();
3192        let numbered = stem.len() == 4 && (stem.starts_with("COM") || stem.starts_with("LPT")) && stem.as_bytes()[3].is_ascii_digit();
3193        if numbered || matches!(stem.as_str(), "CON" | "PRN" | "AUX" | "NUL") {
3194            name.insert(0, '_');
3195        }
3196    }
3197    if matches!(name.as_str(), "" | "." | "..") { "attachment".into() } else { name }
3198}
3199
3200/// What happens to an attachment once the antivirus found it clean. One the
3201/// desktop would run rather than show (`links::is_program`: programs, scripts,
3202/// shortcuts, installers, disk images) is only saved.
3203#[derive(Clone, Copy, PartialEq, Eq)]
3204enum Afterwards {
3205    Open,
3206    Save,
3207    Paper,
3208}
3209
3210impl Afterwards {
3211    fn of(what: i32) -> Afterwards {
3212        match what {
3213            1 => Afterwards::Save,
3214            2 => Afterwards::Paper,
3215            _ => Afterwards::Open,
3216        }
3217    }
3218}
3219
3220/// Writes an attachment to the cache, has the antivirus check it, then opens or
3221/// saves it; a threat, or no antivirus, and the copy is deleted. On a phone,
3222/// which has no antivirus Sioul can call, nothing is scanned and nothing
3223/// claims it: the attachment opens in the app you choose, or Android asks
3224/// where to save it (`attachments`).
3225fn checked_attachment(qt: &QtThread, shared: &Arc<Shared>, key: String, index: i32, afterwards: Afterwards, unchecked: bool) {
3226    let (qt, shared) = (qt.clone(), Arc::clone(shared));
3227    std::thread::spawn(move || {
3228        let world = World::load();
3229        // Set aside, a message's attachments are not opened.
3230        let judged = world.judge(&key).filter(|(_, t)| t.lane != porch::Lane::SetAside);
3231        let subject = judged.as_ref().map(|(_, t)| t.card.subject.clone()).unwrap_or_default();
3232        let found = judged.and_then(|(path, _)| reading::attachment_from(&crypto::readable(&path)?, u32::try_from(index).ok()?));
3233        let Some((name, bytes)) = found else {
3234            set_status(&qt, tr().text("ui-attachments-closed", None));
3235            return;
3236        };
3237        let name = one_file_name(&name);
3238        let phone = crate::attachments::phone();
3239        // A program is never started from a mail, checked or not: an antivirus
3240        // may not know a new one yet. Saved, it is yours to run. On a phone,
3241        // Android's installers too.
3242        if afterwards != Afterwards::Save && crate::attachments::kept_closed(&name, phone) {
3243            set_status(&qt, say("attachment-program", &[("name", name)]));
3244            return;
3245        }
3246        let folder = attachment_cache(&key);
3247        let file = folder.join(&name);
3248        if std::fs::create_dir_all(&folder).and_then(|()| std::fs::write(&file, &bytes)).is_err() {
3249            set_status(&qt, say("scan-error", &[("name", name), ("detail", folder.display().to_string())]));
3250            return;
3251        }
3252        mark_from_mail(&file);
3253        if !phone {
3254            set_status(&qt, say("scan-running", &[("name", name.clone())]));
3255        }
3256        // Keyed anew each session: a file cannot be made to pass for one found clean.
3257        static KEYS: OnceLock<std::collections::hash_map::RandomState> = OnceLock::new();
3258        let content = std::hash::BuildHasher::hash_one(KEYS.get_or_init(Default::default), &bytes);
3259        let known_clean = shared.clean.lock().is_ok_and(|c| c.contains(&content));
3260        // Opened without a check: you said so, knowing no antivirus is here; or a phone, which has none.
3261        let verdict = match crate::attachments::check(phone, known_clean, unchecked) {
3262            crate::attachments::Check::Scan => antivirus::scan(&file),
3263            crate::attachments::Check::Skip | crate::attachments::Check::Unchecked => Verdict::Clean,
3264        };
3265        let line = match verdict {
3266            Verdict::Clean => {
3267                if !unchecked
3268                    && !phone
3269                    && let Ok(mut clean) = shared.clean.lock()
3270                {
3271                    clean.insert(content);
3272                }
3273                match afterwards {
3274                    // Android's own app for it, which you choose; the words say it was not checked.
3275                    Afterwards::Open if phone => crate::attachments::open(&file, &name),
3276                    Afterwards::Open => {
3277                        let url = file_url(&file);
3278                        let _ = qt.queue(move |sioul| sioul.open_url(QString::from(&url)));
3279                        say("scan-clean", &[("name", name)])
3280                    }
3281                    Afterwards::Paper => {
3282                        let kept = crate::papers::keep_checked(&file, &name, &subject);
3283                        let _ = std::fs::remove_file(&file);
3284                        match kept {
3285                            Ok((path, title, kind)) => {
3286                                let _ = qt.queue(move |mut sioul| sioul.as_mut().paper_kept(QString::from(&path), QString::from(&title), QString::from(kind)));
3287                                say("papers-kept", &[("name", name)])
3288                            }
3289                            Err(e) => say("scan-error", &[("name", name), ("detail", e)]),
3290                        }
3291                    }
3292                    // Android's question where: a phone's downloads are not Sioul's to write.
3293                    Afterwards::Save if phone => crate::attachments::save(&qt, &file, &name),
3294                    Afterwards::Save => {
3295                        let target = free_path(&downloads(), &name);
3296                        let saved = std::fs::create_dir_all(downloads()).and_then(|()| std::fs::copy(&file, &target));
3297                        let _ = std::fs::remove_file(&file);
3298                        match saved {
3299                            Ok(_) => {
3300                                mark_from_mail(&target);
3301                                say("ui-saved", &[("path", target.display().to_string())])
3302                            }
3303                            Err(e) => say("scan-error", &[("name", name), ("detail", e.to_string())]),
3304                        }
3305                    }
3306                }
3307            }
3308            Verdict::Infected(threat) => {
3309                let _ = std::fs::remove_file(&file);
3310                say("scan-infected", &[("name", name), ("threat", threat)])
3311            }
3312            // No antivirus here, or a file larger than it scans: asked before
3313            // anything opens, with how to install one, or why it was not scanned.
3314            Verdict::Unavailable(_) | Verdict::TooBig => {
3315                let _ = std::fs::remove_file(&file);
3316                let what = match afterwards {
3317                    Afterwards::Open => 0,
3318                    Afterwards::Save => 1,
3319                    Afterwards::Paper => 2,
3320                };
3321                let big = verdict == Verdict::TooBig;
3322                let (key, hint, shown) = (key.clone(), antivirus::install_hint(), name.clone());
3323                let _ = qt.queue(move |mut sioul| sioul.as_mut().scan_unavailable(QString::from(&key), index, what, QString::from(&shown), QString::from(&hint), big));
3324                match verdict {
3325                    Verdict::Unavailable(problem) => say("scan-unavailable-short", &[("detail", problem)]),
3326                    _ => say("scan-too-big", &[("name", name)]),
3327                }
3328            }
3329        };
3330        set_status(&qt, line);
3331    });
3332}
3333
3334/// The settings found, as the form shows them.
3335#[derive(serde::Serialize)]
3336struct FoundView {
3337    host: String,
3338    port: u16,
3339    security: &'static str,
3340    login: String,
3341    /// "Found: …, from your provider's own settings."
3342    by: String,
3343    /// Gmail's app password, or the general advice.
3344    hint: String,
3345    /// Where to make an app password, when the provider is known for wanting one.
3346    help_url: Option<&'static str>,
3347    /// Google keeps this address's mail (Gmail, Google Workspace): never its
3348    /// account password there, an app password or Google's own sign-in.
3349    google: bool,
3350    /// A Google key of your own is kept on this device for this address.
3351    google_key: bool,
3352    /// Microsoft keeps this address's mail (Outlook.com, Microsoft 365):
3353    /// never a password there, Microsoft's own sign-in.
3354    microsoft: bool,
3355    /// "personal", "work" or "unknown", as discovery found it.
3356    microsoft_kind: &'static str,
3357    /// An application of your own is kept on this device for this address.
3358    microsoft_key: bool,
3359    /// What the organisation's administrator does, for the application kept
3360    /// here or Sioul's (read here, off the window's thread: the keyring).
3361    microsoft_admin: String,
3362}
3363
3364/// A message laid out for reading, as JSON (`view::MessageView`); empty if it
3365/// cannot be read. Read on a thread of its own (`read_message`).
3366fn message_json(key: &str) -> String {
3367    let world = World::load();
3368    let own = mail::own_addresses(&world.config);
3369    let shown = world.judge(key).and_then(|(path, triaged)| {
3370        let raw = std::fs::read(&path).ok()?;
3371        crypto::learn_autocrypt(&raw, &triaged);
3372        let (opened, protection) = match crypto::open(&raw) {
3373            Some((opened, protection)) => (opened, Some(protection)),
3374            None => (None, None),
3375        };
3376        let mut shown = view::message_from(opened.as_deref().unwrap_or(&raw), &path, &triaged, &own, tr(), protection)?;
3377        shown.role = mail::locate(key).and_then(|(account, file)| sioul_sync::mailbox::folder_of(&account, &file)).map(|f| f.role);
3378        shown.unsubscribe = mail::unsubscribe_view(&world.config, &triaged);
3379        Some(shown)
3380    });
3381    shown.map_or_else(String::new, |m| json(&m))
3382}
3383
3384impl qobject::Sioul {
3385    pub(crate) fn shared(&self) -> Arc<Shared> {
3386        Arc::clone(&self.rust().shared)
3387    }
3388
3389    fn text(&self, id: &QString) -> QString {
3390        QString::from(&tr().text(&id.to_string(), None))
3391    }
3392
3393    fn text_args(&self, id: &QString, args: &QString) -> QString {
3394        let given: serde_json::Map<String, serde_json::Value> = serde_json::from_str(&args.to_string()).unwrap_or_default();
3395        let pairs: Vec<(&str, String)> = given.iter().map(|(k, v)| (k.as_str(), v.as_str().map_or_else(|| v.to_string(), str::to_string))).collect();
3396        QString::from(&say(&id.to_string(), &pairs))
3397    }
3398
3399    fn text_with(&self, id: &QString, name: &QString, value: &QString) -> QString {
3400        QString::from(&say(&id.to_string(), &[(&name.to_string(), value.to_string())]))
3401    }
3402
3403    fn text_counted(&self, id: &QString, n: i32) -> QString {
3404        QString::from(&tr().text(&id.to_string(), Some(&tr().counted(usize::try_from(n).unwrap_or(0)))))
3405    }
3406
3407    fn start(mut self: Pin<&mut Self>) {
3408        // Nothing ends when a window starts or closes (decision 10): each state has its own end.
3409        // Sites an older file kept among the accounts move to their own list, a copy kept beside, made once.
3410        if let Err(e) = config::migrate_sites_at_start(&config_path()) {
3411            eprintln!("{e}");
3412        }
3413        self.as_mut().set_reading(QString::from(&reading_json()));
3414        self.as_mut().set_mode(QString::from(&mode_json()));
3415        // What other applications handed (a share, a mailto: link), made drafts (docs/client.md).
3416        crate::outside::started(self.as_mut());
3417        // The pauses' do-not-disturb as they are, a "Pause" pressed on a phone's tile (docs/pauses.md).
3418        crate::pauses::tick(&self.qt_thread(), &self.shared());
3419        // Your sites' own icons, from the start rather than at the first minute.
3420        crate::sites::favicon_tick(&self.qt_thread());
3421        update_weather(&self.qt_thread(), &self.shared());
3422        let (qt, shared) = (self.qt_thread(), self.shared());
3423        // The task pages as you left them.
3424        if let Ok(mut state) = shared.work.lock() {
3425            *state = work::WorkState::load();
3426        }
3427        show(&qt, &shared);
3428        pim::show_pim(&qt, &shared);
3429        work::show_work(&qt, &shared);
3430        start_watchers(&qt, &shared);
3431        // Taking pictures of your own pages syncs nothing; the test build does, for its scripted runs.
3432        if std::env::var_os("SIOUL_GRAB").is_none() || cfg!(feature = "insecure-test-tls") {
3433            pim::start_dav_watchers(&qt, &shared);
3434            crate::github::start(&qt, &shared);
3435        }
3436        // Sioul's own antivirus signatures, when the system has none: kept current once a day.
3437        if !offline() {
3438            std::thread::spawn(|| {
3439                let _ = antivirus::refresh_signatures();
3440            });
3441        }
3442        // Attachments opened in an earlier session (decrypted ones too) leave the cache.
3443        forget_opened_attachments();
3444        // Mail fetched before Sioul checked senders itself is checked once,
3445        // quietly; the checks ask DNS, so not in a demo.
3446        if offline() {
3447            return;
3448        }
3449        let (qt, shared) = (qt.clone(), Arc::clone(&shared));
3450        std::thread::spawn(move || {
3451            let checked: usize = load_config().mail_sources().iter().map(|s| sioul_sync::verify::verify_folder(&s.folder, false)).sum();
3452            if checked > 0 {
3453                show(&qt, &shared);
3454            }
3455        });
3456    }
3457
3458    fn refresh(self: Pin<&mut Self>) {
3459        show(&self.qt_thread(), &self.shared());
3460    }
3461
3462    /// Everything again: what is on this computer (notes, tasks, the plan,
3463    /// the weather) read at once; mail, agenda, tasks, contacts and GitHub fetched.
3464    fn sync_now(mut self: Pin<&mut Self>) {
3465        let (qt, shared) = (self.qt_thread(), self.shared());
3466        crate::share::exchange(&qt, &shared);
3467        show(&qt, &shared);
3468        pim::show_pim(&qt, &shared);
3469        work::show_work(&qt, &shared);
3470        update_weather(&qt, &shared);
3471        let calendars = pim::start_dav_watchers(&qt, &shared);
3472        crate::github::start(&qt, &shared);
3473        if start_watchers(&qt, &shared) == 0 && calendars == 0 {
3474            self.as_mut().set_status(QString::from(&tr().text("sync-nothing", None)));
3475            return;
3476        }
3477        self.as_mut().set_busy(true);
3478        self.as_mut().set_status(QString::from(&tr().text("ui-refreshing", None)));
3479    }
3480
3481    fn open_anyway(self: Pin<&mut Self>) {
3482        let shared = self.shared();
3483        shared.opened_anyway.store(true, Ordering::Relaxed);
3484        show(&self.qt_thread(), &shared);
3485    }
3486
3487    fn done(mut self: Pin<&mut Self>) {
3488        let shared = self.shared();
3489        let newest = shared.shown.lock().map(|s| s.clone()).unwrap_or_default();
3490        let path = PorchState::default_path();
3491        let mut state = PorchState::load(&path);
3492        state.close(&newest);
3493        let line = match state.save(&path) {
3494            Ok(()) => tr().text(if newest.is_empty() { "done-nothing" } else { "done-closed" }, None),
3495            Err(e) => e,
3496        };
3497        shared.opened_anyway.store(false, Ordering::Relaxed);
3498        self.as_mut().set_status(QString::from(&line));
3499        show(&self.qt_thread(), &shared);
3500    }
3501
3502    fn read_message(self: Pin<&mut Self>, key: &QString) {
3503        let (qt, key) = (self.qt_thread(), key.to_string());
3504        std::thread::spawn(move || {
3505            // A message that makes the reading fail is one that cannot be read: the pane says so, never waits.
3506            let reading = std::panic::catch_unwind(|| message_json(&key)).unwrap_or_default();
3507            let invitation = std::panic::catch_unwind(|| pim::invitation(&key)).unwrap_or_default();
3508            let _ = qt.queue(move |mut sioul| sioul.as_mut().message_read(QString::from(&key), QString::from(&reading), QString::from(&invitation)));
3509        });
3510    }
3511
3512    fn open_attachment(mut self: Pin<&mut Self>, key: &QString, index: i32) {
3513        if !crate::attachments::phone() {
3514            self.as_mut().set_status(QString::from(&tr().text("scan-running-any", None)));
3515        }
3516        checked_attachment(&self.qt_thread(), &self.shared(), key.to_string(), index, Afterwards::Open, false);
3517    }
3518
3519    fn save_attachment(mut self: Pin<&mut Self>, key: &QString, index: i32) {
3520        if !crate::attachments::phone() {
3521            self.as_mut().set_status(QString::from(&tr().text("scan-running-any", None)));
3522        }
3523        checked_attachment(&self.qt_thread(), &self.shared(), key.to_string(), index, Afterwards::Save, false);
3524    }
3525
3526    fn attachments_unscanned(&self) -> bool {
3527        crate::attachments::phone()
3528    }
3529
3530    fn attachment_unchecked(self: Pin<&mut Self>, key: &QString, index: i32, what: i32) {
3531        checked_attachment(&self.qt_thread(), &self.shared(), key.to_string(), index, Afterwards::of(what), true);
3532    }
3533
3534    fn keep_attachment_as_paper(mut self: Pin<&mut Self>, key: &QString, index: i32) {
3535        if !crate::attachments::phone() {
3536            self.as_mut().set_status(QString::from(&tr().text("scan-running-any", None)));
3537        }
3538        checked_attachment(&self.qt_thread(), &self.shared(), key.to_string(), index, Afterwards::Paper, false);
3539    }
3540
3541    fn papers(&self) -> QString {
3542        QString::from(&crate::papers::view())
3543    }
3544
3545    fn save_paper(self: Pin<&mut Self>, id: &QString, edit: &QString) -> QString {
3546        QString::from(&crate::papers::save(&id.to_string(), &edit.to_string()))
3547    }
3548
3549    fn remove_paper(self: Pin<&mut Self>, id: &QString) -> QString {
3550        QString::from(&crate::papers::remove(&id.to_string()))
3551    }
3552
3553    fn plan_renewal(self: Pin<&mut Self>, id: &QString) -> QString {
3554        QString::from(&crate::papers::plan_renewal(&self.qt_thread(), &self.shared(), &id.to_string()))
3555    }
3556
3557    fn let_in(mut self: Pin<&mut Self>, address: &QString) {
3558        let address = address.to_string();
3559        let line = match KnownSenders::let_in(&load_config().known_senders_path(), &address) {
3560            Ok(()) => say("ui-let-in-done", &[("address", address)]),
3561            Err(e) => e,
3562        };
3563        self.as_mut().set_status(QString::from(&line));
3564        show(&self.qt_thread(), &self.shared());
3565    }
3566
3567    fn block(self: Pin<&mut Self>, entry: &QString) {
3568        self.set_standing(entry, &QString::from("blocked"));
3569    }
3570
3571    fn block_from(self: Pin<&mut Self>, entry: &QString, key: &QString) {
3572        crate::mail::label_blocked(&key.to_string());
3573        self.block(entry);
3574    }
3575
3576    fn unblock(mut self: Pin<&mut Self>, entry: &QString) {
3577        let line = crate::senders::unblock(&entry.to_string());
3578        self.as_mut().set_status(QString::from(&line));
3579        let (qt, shared) = (self.qt_thread(), self.shared());
3580        show(&qt, &shared);
3581        pim::show_pim(&qt, &shared);
3582    }
3583
3584    fn set_standing(self: Pin<&mut Self>, entry: &QString, standing: &QString) {
3585        let entry = entry.to_string().trim().to_ascii_lowercase();
3586        // One address or pattern: never read as a list.
3587        self.set_standing_of(&QString::from(&serde_json::json!([entry]).to_string()), standing);
3588    }
3589
3590    fn standing(&self, address: &QString) -> QString {
3591        QString::from(porch::Senders::load(&load_config()).who(&address.to_string()).id())
3592    }
3593
3594    fn standing_of(&self, addresses: &QString) -> QString {
3595        QString::from(&crate::senders::standing_json(&addresses.to_string()))
3596    }
3597
3598    fn set_standing_of(mut self: Pin<&mut Self>, addresses: &QString, standing: &QString) {
3599        let line = crate::senders::set_standing_of(&addresses.to_string(), &standing.to_string());
3600        if line.is_empty() {
3601            return;
3602        }
3603        self.as_mut().set_status(QString::from(&line));
3604        let (qt, shared) = (self.qt_thread(), self.shared());
3605        show(&qt, &shared);
3606        pim::show_pim(&qt, &shared);
3607    }
3608
3609    fn person_of(&self, key: &QString) -> QString {
3610        QString::from(&crate::senders::person_json(&key.to_string()))
3611    }
3612
3613    fn set_person(mut self: Pin<&mut Self>, key: &QString, standing: &QString) {
3614        let line = crate::senders::set_person(&key.to_string(), &standing.to_string());
3615        if line.is_empty() {
3616            return;
3617        }
3618        self.as_mut().set_status(QString::from(&line));
3619        let (qt, shared) = (self.qt_thread(), self.shared());
3620        show(&qt, &shared);
3621        pim::show_pim(&qt, &shared);
3622    }
3623
3624    fn add_mail_line(mut self: Pin<&mut Self>, key: &QString, budget: &QString) {
3625        let (key, budget) = (key.to_string(), budget.to_string());
3626        let world = World::load();
3627        let outcome = (|| {
3628            let ledger = world.ledger().ok_or_else(|| tr().text("ui-no-budgets", None))?;
3629            let path = world.ledger_path().ok_or_else(|| tr().text("ui-no-budgets", None))?;
3630            let mail = world.mail_lines(&ledger);
3631            let found = mail.iter().find(|l| l.key == key).ok_or_else(|| tr().text("note-no-amount", None))?;
3632            let line = found.line(&budget).ok_or_else(|| tr().text("note-no-amount", None))?;
3633            let origin = say("budget-origin-mail", &[("subject", found.subject.clone()), ("sender", found.sender.clone()), ("date", Zoned::now().date().to_string())]);
3634            budget::record_line(&path, &line, &origin)?;
3635            Ok::<String, String>(say("ui-line-added", &[("budget", ledger.budget_title(&budget).to_string())]))
3636        })();
3637        let line = outcome.unwrap_or_else(|e| e);
3638        self.as_mut().set_status(QString::from(&line));
3639        show(&self.qt_thread(), &self.shared());
3640    }
3641
3642    fn ignore_mail_line(mut self: Pin<&mut Self>, key: &QString) {
3643        let path = MoneyState::default_path();
3644        let mut state = MoneyState::load(&path);
3645        state.ignored.insert(key.to_string());
3646        let line = match state.save(&path) {
3647            Ok(()) => tr().text("ui-line-ignored", None),
3648            Err(e) => e,
3649        };
3650        self.as_mut().set_status(QString::from(&line));
3651        show(&self.qt_thread(), &self.shared());
3652    }
3653
3654    fn discover(self: Pin<&mut Self>, address: &QString) {
3655        self.find_servers(address, false);
3656    }
3657
3658    fn discover_at_microsoft(self: Pin<&mut Self>, address: &QString) {
3659        self.find_servers(address, true);
3660    }
3661
3662    /// `discover`, or, `at_microsoft`, Microsoft's servers for an address you say is there.
3663    fn find_servers(mut self: Pin<&mut Self>, address: &QString, at_microsoft: bool) {
3664        let address = address.to_string().trim().to_ascii_lowercase();
3665        let qt = self.qt_thread();
3666        self.as_mut().set_form_busy(true);
3667        self.as_mut().set_form_error(QString::default());
3668        self.as_mut().set_found(QString::default());
3669        std::thread::spawn(move || {
3670            // An account at Microsoft already (signed in again, or switched from a password):
3671            // Microsoft's, whatever discovery would find for its domain (one whose MX is a filtering service's).
3672            // Said to be at Microsoft: the same, its kind left to the sign-in.
3673            let said = at_microsoft.then_some(sioul_sync::discover::MicrosoftKind::Unknown);
3674            let known = said.or_else(|| load_config().every_account().find(|a| a.kind == config::AccountKind::Imap && a.address.as_deref() == Some(address.as_str()) && a.host.as_deref().is_some_and(sioul_sync::microsoft::is_imap_host)).map(|a| match (a.is_microsoft(), a.is_microsoft_work()) {
3675                (true, false) => sioul_sync::discover::MicrosoftKind::Personal,
3676                (true, true) => sioul_sync::discover::MicrosoftKind::Work,
3677                _ => sioul_sync::discover::MicrosoftKind::Unknown,
3678            }));
3679            let found = match known {
3680                Some(kind) => Ok(sioul_sync::discover::microsoft_found(&address, kind)),
3681                None => sioul_sync::discover(&address),
3682            };
3683            let result = found.map(|found| {
3684                let server = say(
3685                    "account-server",
3686                    &[
3687                        ("host", found.host.clone()),
3688                        ("port", found.port.to_string()),
3689                        ("security", tr().text(&format!("security-{}", found.security.as_str()), None)),
3690                    ],
3691                );
3692                // Gmail, or a domain whose mail Google keeps (its MX), or settings naming Google's server.
3693                let google = found.by == sioul_sync::FoundBy::Google || found.host.eq_ignore_ascii_case(sioul_sync::discover::GOOGLE_IMAP);
3694                // Outlook.com, Microsoft 365: by their domains, their MX, or settings naming their server.
3695                let microsoft_kind = match found.by {
3696                    sioul_sync::FoundBy::Microsoft(kind) => Some(kind),
3697                    _ if sioul_sync::microsoft::is_imap_host(&found.host) => Some(sioul_sync::discover::MicrosoftKind::Unknown),
3698                    _ => None,
3699                };
3700                json(&FoundView {
3701                    by: say("account-found", &[("server", server), ("by", tr().text(found.by.message_id(), None))]),
3702                    hint: tr().text(if google { "account-gmail-hint" } else { "account-app-password-hint" }, None),
3703                    help_url: google.then_some(sioul_sync::GMAIL_APP_PASSWORDS),
3704                    google,
3705                    google_key: google && gmail::has_own_key(&address),
3706                    microsoft: microsoft_kind.is_some(),
3707                    microsoft_kind: microsoft_kind.map_or("", |k| k.as_str()),
3708                    microsoft_key: microsoft_kind.is_some() && outlook::has_own_app(&address),
3709                    microsoft_admin: if microsoft_kind.is_some_and(|k| k != sioul_sync::discover::MicrosoftKind::Personal) { outlook::admin_text(&address) } else { String::new() },
3710                    host: found.host,
3711                    port: found.port,
3712                    security: found.security.as_str(),
3713                    login: found.username,
3714                })
3715            });
3716            let _ = qt.queue(move |mut sioul| {
3717                sioul.as_mut().set_form_busy(false);
3718                match result {
3719                    Ok(found) => sioul.as_mut().set_found(QString::from(&found)),
3720                    Err(e) => sioul.as_mut().set_form_error(QString::from(&e.sentence(tr(), &address))),
3721                }
3722            });
3723        });
3724    }
3725
3726    fn add_account(mut self: Pin<&mut Self>, address: &QString, host: &QString, port: i32, security: &QString, login: &QString, password: &QString) {
3727        let (address, host, login, password) = (address.to_string().trim().to_ascii_lowercase(), host.to_string().trim().to_string(), login.to_string().trim().to_string(), password.to_string());
3728        let security = if security.to_string() == "starttls" { Security::Starttls } else { Security::Tls };
3729        let port = u16::try_from(port).ok().filter(|p| *p > 0).unwrap_or(if security == Security::Tls { 993 } else { 143 });
3730        self.as_mut().set_form_busy(true);
3731        self.as_mut().set_form_error(QString::default());
3732        add_mail(&self.qt_thread(), &self.shared(), address, host, port, security, login, Some(password));
3733    }
3734
3735    fn add_mail_like(mut self: Pin<&mut Self>, address: &QString, host: &QString, port: i32, security: &QString, login: &QString) {
3736        let (address, host, login) = (address.to_string().trim().to_ascii_lowercase(), host.to_string().trim().to_string(), login.to_string().trim().to_string());
3737        let security = if security.to_string() == "starttls" { Security::Starttls } else { Security::Tls };
3738        let port = u16::try_from(port).ok().filter(|p| *p > 0).unwrap_or(if security == Security::Tls { 993 } else { 143 });
3739        self.as_mut().set_form_busy(true);
3740        self.as_mut().set_form_error(QString::default());
3741        add_mail(&self.qt_thread(), &self.shared(), address, host, port, security, login, None);
3742    }
3743
3744    fn set_account_enabled(mut self: Pin<&mut Self>, id: &QString, on: bool) -> QString {
3745        let id = id.to_string();
3746        let config = load_config();
3747        let Some(account) = config.every_account().find(|a| a.id == id).cloned() else { return QString::from(&say("account-unknown", &[("id", id)])) };
3748        if let Err(e) = config::set_enabled(&config_path(), &id, on) {
3749            return QString::from(&e);
3750        }
3751        let (qt, shared) = (self.qt_thread(), self.shared());
3752        if account.kind == config::AccountKind::Dav {
3753            // Its calendars and address books leave the pages while it is off.
3754            sioul_core::vdir::set_account_off(&id, !on);
3755            if on {
3756                pim::start_dav_watchers(&qt, &shared);
3757            } else {
3758                pim::stop_dav_watcher(&shared, &id);
3759            }
3760        } else if on {
3761            start_watchers(&qt, &shared);
3762        } else if let Some(control) = shared.watchers.lock().ok().and_then(|mut w| w.remove(&id)) {
3763            control.stop();
3764        }
3765        self.as_mut().set_status(QString::from(&say(if on { "account-on" } else { "account-off" }, &[("id", id)])));
3766        show(&qt, &shared);
3767        pim::show_pim(&qt, &shared);
3768        mail::show_mail(&qt, &shared);
3769        // Its task lists and calendars come or go: the plan follows.
3770        work::show_work(&qt, &shared);
3771        QString::default()
3772    }
3773
3774    fn scout_account(mut self: Pin<&mut Self>, address: &QString) {
3775        let address = address.to_string().trim().to_ascii_lowercase();
3776        let qt = self.qt_thread();
3777        self.as_mut().set_scouting(true);
3778        self.as_mut().set_scouted(QString::default());
3779        std::thread::spawn(move || {
3780            let config = load_config();
3781            let mine = |kind: config::AccountKind| config.every_account().find(|a| a.address.as_deref().map(str::to_lowercase).as_deref() == Some(address.as_str()) && a.kind == kind).cloned();
3782            let (mail_have, dav_have) = (mine(config::AccountKind::Imap), mine(config::AccountKind::Dav));
3783            // A Nextcloud's apps are asked with the password its calendars use.
3784            let password = dav_have.as_ref().and_then(|a| secret::password(a).ok());
3785            let login = dav_have.as_ref().and_then(|a| a.login().map(str::to_string)).unwrap_or_else(|| address.clone());
3786            let found = sioul_sync::scout::scout(&address, dav_have.as_ref().and_then(|a| a.host.as_deref()), password.as_deref().map(|p| (login.as_str(), p)));
3787            let mail = found.mail.as_ref().map(|m| {
3788                let server = say("account-server", &[("host", m.host.clone()), ("port", m.port.to_string()), ("security", tr().text(&format!("security-{}", m.security.as_str()), None))]);
3789                serde_json::json!({ "host": m.host, "port": m.port, "security": m.security.as_str(), "login": m.username, "line": server, "have": mail_have.as_ref().map(|a| a.id.clone()) })
3790            });
3791            let dav = found.dav.as_ref().map(|url| serde_json::json!({ "url": url, "have": dav_have.as_ref().map(|a| a.id.clone()) }));
3792            let nextcloud = found.nextcloud.as_ref().map(|n| {
3793                let apps: Vec<serde_json::Value> = n
3794                    .apps
3795                    .iter()
3796                    .filter_map(|a| sioul_sync::scout::known_app(a))
3797                    .map(|(id, in_sioul)| serde_json::json!({ "id": id, "title": tr().text(&format!("scout-app-{id}"), None), "in_sioul": in_sioul }))
3798                    .collect();
3799                serde_json::json!({ "product": n.product, "version": n.version, "server": n.server, "apps": apps, "asked": password.is_some() })
3800            });
3801            let answer = serde_json::json!({ "address": address, "mail": mail, "dav": dav, "nextcloud": nextcloud, "dav_password": password.is_some() }).to_string();
3802            let _ = qt.queue(move |mut sioul| {
3803                sioul.as_mut().set_scouting(false);
3804                sioul.as_mut().set_scouted(QString::from(&answer));
3805            });
3806        });
3807    }
3808
3809    fn add_portal(mut self: Pin<&mut Self>, name: &QString, url: &QString) {
3810        let (id, url) = (config::slug(&name.to_string()), url.to_string().trim().to_string());
3811        let config = load_config();
3812        let outcome = if config.site(&id).is_some() {
3813            Err(say("account-exists", &[("id", id.clone())]))
3814        } else if id.is_empty() || !url.starts_with("https://") {
3815            // A site now, and a site's sign-in never travels in clear.
3816            Err(tr().text("site-https-only", None))
3817        } else {
3818            config::add_site(&config_path(), &id, &name.to_string(), &url).map(|()| say("account-portal-added", &[("id", id.clone()), ("url", url.clone())]))
3819        };
3820        match outcome {
3821            Ok(line) => self.as_mut().set_status(QString::from(&line)),
3822            Err(e) => self.as_mut().set_form_error(QString::from(&e)),
3823        }
3824        show(&self.qt_thread(), &self.shared());
3825    }
3826
3827    fn set_account_password(mut self: Pin<&mut Self>, id: &QString, password: &QString) {
3828        self.as_mut().set_form_busy(true);
3829        give_password(&self.qt_thread(), &self.shared(), id.to_string(), password.to_string());
3830    }
3831
3832    fn remove_account(mut self: Pin<&mut Self>, id: &QString, delete_mail: bool) {
3833        let id = id.to_string();
3834        let shared = self.shared();
3835        let config = load_config();
3836        // A Microsoft account's mail is deleted once its watcher has ended: from now on
3837        // none starts for it, not even from another device's settings (`outlook::remove`).
3838        // Under the watchers' lock, which `start_watcher` holds while it looks.
3839        let microsoft = config.every_account().any(|a| a.id == id && a.is_microsoft());
3840        let watcher = shared.watchers.lock().ok().and_then(|mut watchers| {
3841            if microsoft && let Ok(mut removing) = shared.removing.lock() {
3842                removing.insert(id.clone());
3843            }
3844            watchers.remove(&id)
3845        });
3846        if let Some(control) = &watcher {
3847            control.stop();
3848        }
3849        if let Ok(mut statuses) = shared.statuses.lock() {
3850            statuses.remove(&id);
3851        }
3852        // Switched off, an account can be removed all the same.
3853        let line = match config.every_account().find(|a| a.id == id) {
3854            None => say("account-unknown", &[("id", id.clone())]),
3855            // Microsoft's: its sign-in forgotten, its mail too when you said so, on a
3856            // thread once its watcher has ended; the status line says it when done (outlook.rs).
3857            Some(account) if account.is_microsoft() => {
3858                outlook::remove(&self.qt_thread(), &shared, account.clone(), delete_mail, watcher);
3859                return;
3860            }
3861            Some(account) => {
3862                if account.is_dav() {
3863                    pim::stop_dav_watcher(&shared, &id);
3864                }
3865                // The keyring keeps one password per login and server: another
3866                // account on the same (a cPanel host's mail and calendars) keeps it.
3867                let shared_password = config.every_account().any(|a| a.id != id && a.host == account.host && a.login() == account.login());
3868                let forgotten = if account.auth.as_deref() == Some("google") {
3869                    // Google's access goes back to it: the calendars' or the mail's, each its own.
3870                    let purpose = if account.is_dav() { sioul_sync::google::Purpose::Pim } else { sioul_sync::google::Purpose::Mail };
3871                    sioul_sync::google::revoke_for(purpose, account.address.as_deref().unwrap_or("")).map_err(|e| e.sentence(tr(), &id))
3872                } else if (account.syncs() || account.is_dav()) && !shared_password {
3873                    secret::forget(account).map_err(|e| e.sentence(tr(), &id))
3874                } else {
3875                    Ok(())
3876                };
3877                match forgotten.and_then(|()| config::remove_account(&config_path(), &id)) {
3878                    Ok(()) => say("account-removed", &[("id", id.clone()), ("path", account.maildir_path().display().to_string())]),
3879                    Err(e) => e,
3880                }
3881            }
3882        };
3883        self.as_mut().set_status(QString::from(&line));
3884        show(&self.qt_thread(), &shared);
3885        // Its calendars and task lists gone: the agenda and the plan follow.
3886        pim::show_pim(&self.qt_thread(), &shared);
3887        work::show_work(&self.qt_thread(), &shared);
3888    }
3889
3890    fn set_priority(mut self: Pin<&mut Self>, id: &QString, priority: &QString) {
3891        let id = id.to_string();
3892        let line = match Priority::parse(&priority.to_string()) {
3893            Some(priority) => match config::set_priority(&config_path(), &id, priority) {
3894                Ok(()) => say("ui-priority-set", &[("id", id), ("priority", tr().text(&format!("priority-{}", priority.as_str()), None))]),
3895                Err(e) => e,
3896            },
3897            None => return,
3898        };
3899        self.as_mut().set_status(QString::from(&line));
3900        show(&self.qt_thread(), &self.shared());
3901    }
3902
3903    fn open_folder(self: Pin<&mut Self>, account: &QString, folder: &QString, all: bool, query: &QString) {
3904        let shared = self.shared();
3905        if let Ok(mut open) = shared.open_folder.lock() {
3906            *open = Some(mail::OpenFolder { account: account.to_string(), folder: folder.to_string(), all, query: query.to_string() });
3907        }
3908        mail::show_mail(&self.qt_thread(), &shared);
3909    }
3910
3911    fn opened(self: Pin<&mut Self>, key: &QString) {
3912        mail::opened(&self.qt_thread(), &self.shared(), &key.to_string());
3913    }
3914
3915    fn act(self: Pin<&mut Self>, key: &QString, action: &QString, target: &QString) {
3916        mail::act(&self.qt_thread(), &self.shared(), &key.to_string(), &action.to_string(), &target.to_string());
3917    }
3918
3919    fn act_many(self: Pin<&mut Self>, keys: &QString, action: &QString) {
3920        let keys: Vec<String> = serde_json::from_str(&keys.to_string()).unwrap_or_default();
3921        mail::act_many(&self.qt_thread(), &self.shared(), &keys, &action.to_string());
3922    }
3923
3924    fn move_messages(self: Pin<&mut Self>, keys: &QString, account: &QString, folder: &QString) {
3925        let keys: Vec<String> = serde_json::from_str(&keys.to_string()).unwrap_or_default();
3926        mail::move_many(&self.qt_thread(), &self.shared(), &keys, &account.to_string(), &folder.to_string());
3927    }
3928
3929    fn unsubscribe(self: Pin<&mut Self>, key: &QString) -> QString {
3930        let world = World::load();
3931        let triaged = world.judge(&key.to_string()).map(|(_, triaged)| triaged);
3932        QString::from(&mail::unsubscribe(&self.qt_thread(), &self.shared(), &world.config, triaged))
3933    }
3934
3935    fn undo(mut self: Pin<&mut Self>) {
3936        if let Some(draft) = mail::undo(&self.qt_thread(), &self.shared()) {
3937            self.as_mut().compose_requested(QString::from(&draft));
3938        }
3939    }
3940
3941    fn flush(self: Pin<&mut Self>) {
3942        mail::flush(&self.shared());
3943        securitykey::closing();
3944    }
3945
3946    fn compose(mut self: Pin<&mut Self>, kind: &QString, key: &QString, account: &QString) -> QString {
3947        match mail::compose(&kind.to_string(), &key.to_string(), &account.to_string()) {
3948            Ok(id) => {
3949                mail::show_mail(&self.qt_thread(), &self.shared());
3950                QString::from(&id)
3951            }
3952            Err(e) => {
3953                self.as_mut().set_status(QString::from(&e));
3954                QString::default()
3955            }
3956        }
3957    }
3958
3959    fn draft(&self, id: &QString) -> QString {
3960        QString::from(&mail::draft(&id.to_string()).unwrap_or_default())
3961    }
3962
3963    fn save_draft(self: Pin<&mut Self>, edit: &QString) -> QString {
3964        let line = mail::save_draft(&edit.to_string()).unwrap_or_else(|e| e);
3965        mail::show_mail(&self.qt_thread(), &self.shared());
3966        QString::from(&line)
3967    }
3968
3969    fn attach(self: Pin<&mut Self>, id: &QString, url: &QString) -> QString {
3970        QString::from(&mail::attach(&id.to_string(), &url.to_string()).err().unwrap_or_default())
3971    }
3972
3973    fn detach(mut self: Pin<&mut Self>, id: &QString, index: i32, forwarded: bool) {
3974        if let Err(e) = mail::detach(&id.to_string(), usize::try_from(index).unwrap_or(usize::MAX), forwarded) {
3975            self.as_mut().set_status(QString::from(&e));
3976        }
3977    }
3978
3979    fn preview(&self, markdown: &QString) -> QString {
3980        QString::from(&sioul_core::compose::markdown_html(&markdown.to_string()))
3981    }
3982
3983    fn send(self: Pin<&mut Self>, id: &QString) -> QString {
3984        QString::from(&mail::send(&self.qt_thread(), &self.shared(), &id.to_string()).unwrap_or_default())
3985    }
3986
3987    fn discard(self: Pin<&mut Self>, id: &QString) {
3988        mail::discard(&self.qt_thread(), &self.shared(), &id.to_string());
3989    }
3990
3991    fn draft_closed(self: Pin<&mut Self>, id: &QString) {
3992        mail::closed(&self.qt_thread(), &self.shared(), &id.to_string());
3993    }
3994
3995    fn place(&self, key: &QString) -> QString {
3996        QString::from(&mail::place(&key.to_string()))
3997    }
3998
3999    fn source(&self, key: &QString) -> QString {
4000        QString::from(&mail::source(&key.to_string()))
4001    }
4002
4003    fn set_writing(mut self: Pin<&mut Self>, id: &QString, name: &QString, signature: &QString) {
4004        let id = id.to_string();
4005        let line = match config::set_writing(&config_path(), &id, &name.to_string(), &signature.to_string()) {
4006            Ok(()) => say("ui-writing-saved", &[("id", id)]),
4007            Err(e) => e,
4008        };
4009        self.as_mut().set_status(QString::from(&line));
4010        show(&self.qt_thread(), &self.shared());
4011    }
4012
4013    fn grab_folder(&self) -> QString {
4014        QString::from(&std::env::var("SIOUL_GRAB").unwrap_or_default())
4015    }
4016
4017    fn search_contacts(self: Pin<&mut Self>, query: &QString) {
4018        let shared = self.shared();
4019        if let Ok(mut state) = shared.pim.lock() {
4020            state.query = query.to_string();
4021        }
4022        pim::show_pim(&self.qt_thread(), &shared);
4023    }
4024
4025    fn contact(&self, key: &QString) -> QString {
4026        QString::from(&pim::contact(&key.to_string()))
4027    }
4028
4029    fn save_contact(self: Pin<&mut Self>, key: &QString, edit: &QString) -> QString {
4030        let answer = match pim::save_contact(&self.qt_thread(), &self.shared(), &key.to_string(), &edit.to_string()) {
4031            Ok(key) => serde_json::json!({ "key": key }),
4032            Err(error) => serde_json::json!({ "error": error }),
4033        };
4034        QString::from(&answer.to_string())
4035    }
4036
4037    fn delete_contact(self: Pin<&mut Self>, key: &QString) {
4038        pim::delete_contact(&self.qt_thread(), &self.shared(), &key.to_string());
4039    }
4040
4041    fn show_contacts_category(self: Pin<&mut Self>, category: &QString) {
4042        let shared = self.shared();
4043        if let Ok(mut state) = shared.pim.lock() {
4044            state.category = category.to_string();
4045        }
4046        pim::show_pim(&self.qt_thread(), &shared);
4047    }
4048
4049    fn find_duplicates(self: Pin<&mut Self>) {
4050        crate::duplicates::find(&self.qt_thread(), &self.shared());
4051    }
4052
4053    fn clean_contacts(self: Pin<&mut Self>, keys: &QString) -> QString {
4054        QString::from(&crate::duplicates::clean(&self.qt_thread(), &self.shared(), &keys.to_string()))
4055    }
4056
4057    fn merge_contacts(self: Pin<&mut Self>, lead: &QString, other: &QString) -> QString {
4058        QString::from(&crate::duplicates::merge(&self.qt_thread(), &self.shared(), &lead.to_string(), &other.to_string()))
4059    }
4060
4061    fn not_same_contacts(self: Pin<&mut Self>, first: &QString, second: &QString) -> QString {
4062        QString::from(&crate::duplicates::not_same(&self.qt_thread(), &self.shared(), &first.to_string(), &second.to_string()))
4063    }
4064
4065    fn undo_contacts(self: Pin<&mut Self>, id: &QString) -> QString {
4066        QString::from(&crate::duplicates::undo(&self.qt_thread(), &self.shared(), &id.to_string()))
4067    }
4068
4069    fn show_days(self: Pin<&mut Self>, from: &QString, days: i32, upcoming: bool) {
4070        let shared = self.shared();
4071        if let Ok(mut state) = shared.pim.lock() {
4072            state.from = from.to_string().parse().unwrap_or_else(|_| Zoned::now().date());
4073            state.days = i64::from(days.clamp(1, 366));
4074            state.upcoming = upcoming;
4075        }
4076        pim::show_pim(&self.qt_thread(), &shared);
4077    }
4078
4079    fn calendars(&self) -> QString {
4080        QString::from(&pim::calendars())
4081    }
4082
4083    fn event_form(&self, key: &QString) -> QString {
4084        QString::from(&pim::event(&key.to_string()))
4085    }
4086
4087    fn save_event(self: Pin<&mut Self>, key: &QString, edit: &QString, calendar: &QString) -> QString {
4088        let result = pim::save_event(&self.qt_thread(), &self.shared(), &key.to_string(), &edit.to_string(), &calendar.to_string());
4089        QString::from(&result.err().unwrap_or_default())
4090    }
4091
4092    fn task_as_event(&self, uid: &QString, start: f64, end: f64) -> QString {
4093        QString::from(&crate::appointments::form(&self.shared(), &uid.to_string(), start as i64, end as i64))
4094    }
4095
4096    fn task_to_event(self: Pin<&mut Self>, uid: &QString, block: &QString, edit: &QString, calendar: &QString) -> QString {
4097        QString::from(&crate::appointments::task_to_event(&self.qt_thread(), &self.shared(), &uid.to_string(), &block.to_string(), &edit.to_string(), &calendar.to_string()))
4098    }
4099
4100    fn event_to_task_losses(&self, key: &QString) -> QString {
4101        QString::from(&crate::appointments::task_losses(&key.to_string()))
4102    }
4103
4104    fn event_to_task(self: Pin<&mut Self>, key: &QString) -> QString {
4105        QString::from(&crate::appointments::event_to_task(&self.qt_thread(), &self.shared(), &key.to_string()))
4106    }
4107
4108    fn event_reminder(&self, key: &QString, start: f64) -> QString {
4109        QString::from(&crate::remind::of_event(&key.to_string(), start as i64))
4110    }
4111
4112    fn reminder_choices(&self, current: &QString) -> QString {
4113        QString::from(&crate::remind::choices(&current.to_string()))
4114    }
4115
4116    fn set_event_reminder(self: Pin<&mut Self>, key: &QString, value: &QString) -> QString {
4117        let result = crate::remind::set_event(&self.qt_thread(), &self.shared(), &key.to_string(), &value.to_string());
4118        QString::from(&result.err().unwrap_or_default())
4119    }
4120
4121    fn move_event(self: Pin<&mut Self>, key: &QString, start: f64, new_start: f64, new_end: f64, only_this: bool) -> QString {
4122        QString::from(&pim::move_event(&self.qt_thread(), &self.shared(), &key.to_string(), start as i64, new_start as i64, new_end as i64, only_this))
4123    }
4124
4125    fn delete_event(self: Pin<&mut Self>, key: &QString, start: f64, only_this: bool) {
4126        pim::delete_event(&self.qt_thread(), &self.shared(), &key.to_string(), start as i64, only_this);
4127    }
4128
4129    fn completions(&self, typed: &QString) -> QString {
4130        QString::from(&pim::completions(&typed.to_string()))
4131    }
4132
4133    fn contact_for(&self, address: &QString) -> QString {
4134        QString::from(&pim::contact_for(&address.to_string()))
4135    }
4136
4137    fn add_sender(mut self: Pin<&mut Self>, name: &QString, address: &QString) {
4138        if let Err(e) = pim::add_sender(&self.qt_thread(), &self.shared(), &name.to_string(), &address.to_string()) {
4139            self.as_mut().set_status(QString::from(&e));
4140        }
4141    }
4142
4143    fn answer_invitation(mut self: Pin<&mut Self>, key: &QString, answer: &QString) {
4144        // Sending waits on the network: on a thread.
4145        let (qt, shared, key, answer) = (self.qt_thread(), self.shared(), key.to_string(), answer.to_string());
4146        self.as_mut().set_status(QString::from(&tr().text("invitation-answering", None)));
4147        std::thread::spawn(move || {
4148            if let Err(e) = pim::answer(&qt, &shared, &key, &answer) {
4149                tell(&qt, &shared, e);
4150            }
4151        });
4152    }
4153
4154    fn history_weeks(&self) -> i32 {
4155        i32::try_from(load_config().history_weeks.unwrap_or(config::DEFAULT_HISTORY_WEEKS)).unwrap_or(2)
4156    }
4157
4158    fn day_start(&self) -> i32 {
4159        i32::try_from(load_config().agenda.day_start.unwrap_or(7).min(23)).unwrap_or(7)
4160    }
4161
4162    fn set_history(mut self: Pin<&mut Self>, weeks: i32) {
4163        let weeks = u32::try_from(weeks.max(0)).unwrap_or(0);
4164        let line = match config::set_history(&config_path(), weeks) {
4165            Ok(()) if weeks == 0 => tr().text("ui-history-everything", None),
4166            Ok(()) => say("ui-history-set", &[("weeks", weeks.to_string())]),
4167            Err(e) => e,
4168        };
4169        self.as_mut().set_status(QString::from(&line));
4170        // The watchers fetch now: the older mail comes with this round.
4171        let (qt, shared) = (self.qt_thread(), self.shared());
4172        start_watchers(&qt, &shared);
4173        show(&qt, &shared);
4174        pim::show_pim(&qt, &shared);
4175    }
4176
4177    fn add_dav(mut self: Pin<&mut Self>, address: &QString, url: &QString, login: &QString, password: &QString) {
4178        self.as_mut().set_form_busy(true);
4179        self.as_mut().set_form_error(QString::default());
4180        pim::add_account(&self.qt_thread(), &self.shared(), address.to_string(), url.to_string(), login.to_string(), password.to_string());
4181    }
4182
4183    fn add_google(mut self: Pin<&mut Self>, address: &QString, client_id: &QString, client_secret: &QString, again: &QString) {
4184        self.as_mut().set_form_busy(true);
4185        self.as_mut().set_form_error(QString::default());
4186        let again = Some(again.to_string()).filter(|a| !a.is_empty());
4187        pim::add_google_account(&self.qt_thread(), &self.shared(), address.to_string(), client_id.to_string(), client_secret.to_string(), again);
4188    }
4189
4190    fn cancel_google(self: Pin<&mut Self>) {
4191        self.shared().oauth_stop.store(true, Ordering::Relaxed);
4192    }
4193
4194    fn add_google_mail(mut self: Pin<&mut Self>, address: &QString, client_id: &QString, client_secret: &QString, again: &QString) {
4195        self.as_mut().set_form_busy(true);
4196        self.as_mut().set_form_error(QString::default());
4197        let again = Some(again.to_string()).filter(|a| !a.is_empty());
4198        gmail::add_google_mail(&self.qt_thread(), &self.shared(), address.to_string(), client_id.to_string(), client_secret.to_string(), again);
4199    }
4200
4201    fn add_microsoft_mail(mut self: Pin<&mut Self>, address: &QString, kind: &QString, client_id: &QString, tenant: &QString, again: &QString) {
4202        self.as_mut().set_form_busy(true);
4203        self.as_mut().set_form_error(QString::default());
4204        let again = Some(again.to_string()).filter(|a| !a.is_empty());
4205        outlook::add_microsoft_mail(&self.qt_thread(), &self.shared(), address.to_string(), kind.to_string(), client_id.to_string(), tenant.to_string(), again);
4206    }
4207
4208    fn cancel_microsoft(self: Pin<&mut Self>) {
4209        self.shared().oauth_stop.store(true, Ordering::Relaxed);
4210    }
4211
4212    fn answer_microsoft(self: Pin<&mut Self>, yes: bool) {
4213        if let Some(answer) = self.shared().microsoft_answer.lock().ok().and_then(|mut waiting| waiting.take()) {
4214            let _ = answer.send(yes);
4215        }
4216    }
4217
4218    fn microsoft_built_in(&self) -> bool {
4219        sioul_sync::microsoft::built_in().is_some()
4220    }
4221
4222    fn microsoft_admin_text(&self, address: &QString, client_id: &QString) -> QString {
4223        QString::from(&outlook::admin_text_for(&address.to_string(), &client_id.to_string()))
4224    }
4225
4226    fn pgp_keys(&self) -> QString {
4227        QString::from(&crypto::keys_view())
4228    }
4229
4230    fn pgp_generate(mut self: Pin<&mut Self>, address: &QString) {
4231        // Making a key takes a moment (its passphrase is hashed with Argon2): on a thread.
4232        let (qt, shared, address) = (self.qt_thread(), self.shared(), address.to_string());
4233        self.as_mut().set_status(QString::from(&tr().text("pgp-making", None)));
4234        std::thread::spawn(move || {
4235            let line = crypto::generate(&address).unwrap_or_else(|e| e);
4236            tell(&qt, &shared, line);
4237            let _ = qt.queue(|mut sioul| sioul.as_mut().keys_changed());
4238        });
4239    }
4240
4241    fn pgp_import(mut self: Pin<&mut Self>, url: &QString, passphrase: &QString) -> QString {
4242        match crypto::import(&url.to_string(), &passphrase.to_string()) {
4243            Ok(line) => {
4244                self.as_mut().set_status(QString::from(&line));
4245                self.as_mut().keys_changed();
4246                QString::default()
4247            }
4248            Err(e) => QString::from(&e),
4249        }
4250    }
4251
4252    fn pgp_export(mut self: Pin<&mut Self>, fingerprint: &QString) {
4253        let line = match crypto::export(&fingerprint.to_string(), &downloads()) {
4254            Ok(path) => say("ui-saved", &[("path", path.display().to_string())]),
4255            Err(e) => e,
4256        };
4257        self.as_mut().set_status(QString::from(&line));
4258    }
4259
4260    fn pgp_save_revocation(mut self: Pin<&mut Self>, fingerprint: &QString) {
4261        let line = match sioul_core::pgp::save_revocation(&fingerprint.to_string(), &downloads()) {
4262            Ok(path) => say("pgp-revocation-saved", &[("path", path.display().to_string())]),
4263            Err(e) => e,
4264        };
4265        self.as_mut().set_status(QString::from(&line));
4266    }
4267
4268    fn pgp_remove(mut self: Pin<&mut Self>, fingerprint: &QString) {
4269        let line = match sioul_core::pgp::remove(&fingerprint.to_string()) {
4270            Ok(()) => tr().text("pgp-removed", None),
4271            Err(e) => e,
4272        };
4273        self.as_mut().set_status(QString::from(&line));
4274        self.as_mut().keys_changed();
4275    }
4276
4277    fn pgp_lookup(mut self: Pin<&mut Self>, draft: &QString) {
4278        let (qt, shared, id) = (self.qt_thread(), self.shared(), draft.to_string());
4279        self.as_mut().set_status(QString::from(&tr().text("pgp-looking", None)));
4280        std::thread::spawn(move || {
4281            let Some(draft) = sioul_core::compose::Draft::by_id(&id) else { return };
4282            tell(&qt, &shared, crypto::lookup(&draft));
4283            let _ = qt.queue(|mut sioul| sioul.as_mut().keys_changed());
4284        });
4285    }
4286
4287    fn security_key_for_draft(&self, id: &QString) -> QString {
4288        QString::from(&securitykey::for_draft(&id.to_string()))
4289    }
4290
4291    fn sign_and_send(self: Pin<&mut Self>, id: &QString, pin: &QString) {
4292        securitykey::sign_and_send(&self.qt_thread(), &self.shared(), &id.to_string(), pin.to_string());
4293    }
4294
4295    fn open_with_security_key(self: Pin<&mut Self>, key: &QString, pin: &QString) {
4296        securitykey::open_message(&self.qt_thread(), &self.shared(), &key.to_string(), pin.to_string());
4297    }
4298
4299    fn security_key_not_now(self: Pin<&mut Self>, context: &QString) {
4300        securitykey::not_now(&context.to_string());
4301    }
4302
4303    fn open_nfc_settings(self: Pin<&mut Self>) {
4304        securitykey::open_nfc_settings();
4305    }
4306
4307    fn let_gnupg_release(self: Pin<&mut Self>) -> QString {
4308        QString::from(&securitykey::release())
4309    }
4310
4311    fn security_keys(&self) -> QString {
4312        QString::from(&securitykey::known_view())
4313    }
4314
4315    fn read_security_key(self: Pin<&mut Self>) {
4316        securitykey::read_for_setup(&self.qt_thread());
4317    }
4318
4319    fn find_security_key_certificate(self: Pin<&mut Self>, ident: &QString) {
4320        securitykey::look_for_certificate(&self.qt_thread(), &ident.to_string());
4321    }
4322
4323    fn import_security_key_certificate(self: Pin<&mut Self>, ident: &QString, url: &QString) {
4324        securitykey::import_certificate(&self.qt_thread(), &ident.to_string(), &url.to_string());
4325    }
4326
4327    fn import_security_key_from_gnupg(self: Pin<&mut Self>, ident: &QString) {
4328        securitykey::import_from_gnupg(&self.qt_thread(), &ident.to_string());
4329    }
4330
4331    fn renew_security_key(self: Pin<&mut Self>, ident: &QString) {
4332        securitykey::renew(&self.qt_thread(), &ident.to_string());
4333    }
4334
4335    fn send_security_key_to_keys_openpgp(self: Pin<&mut Self>, ident: &QString) {
4336        securitykey::send_to_keys_openpgp(&self.qt_thread(), &ident.to_string());
4337    }
4338
4339    fn forget_security_key_pin(mut self: Pin<&mut Self>) {
4340        let line = securitykey::forget_pin();
4341        self.as_mut().set_status(QString::from(&line));
4342        self.as_mut().keys_changed();
4343    }
4344
4345    fn stop_using_security_key(self: Pin<&mut Self>, ident: &QString) {
4346        securitykey::stop_using(&self.qt_thread(), &self.shared(), &ident.to_string());
4347    }
4348
4349    fn show_tasks(self: Pin<&mut Self>, by: &QString, done: bool, query: &QString, project_id: &QString) {
4350        work::set_view(&self.qt_thread(), &self.shared(), &by.to_string(), done, &query.to_string(), &project_id.to_string());
4351    }
4352
4353    fn filter_tasks(self: Pin<&mut Self>, kind: &QString, category: &QString) {
4354        work::set_filter(&self.qt_thread(), &self.shared(), &kind.to_string(), &category.to_string());
4355    }
4356
4357    fn refresh_work(self: Pin<&mut Self>) {
4358        work::show_work(&self.qt_thread(), &self.shared());
4359    }
4360
4361    fn show_tasks_anyway(self: Pin<&mut Self>, on: bool) {
4362        work::show_anyway(&self.qt_thread(), &self.shared(), on);
4363    }
4364
4365    fn task(&self, uid: &QString) -> QString {
4366        QString::from(&work::task(&self.shared(), &uid.to_string()))
4367    }
4368
4369    fn new_task_form(&self, from: &QString, key: &QString, start: f64, list: &QString) -> QString {
4370        QString::from(&work::new_form(&self.shared(), &from.to_string(), &key.to_string(), start, &list.to_string()))
4371    }
4372
4373    fn save_task(self: Pin<&mut Self>, uid: &QString, edit: &QString, list: &QString) -> QString {
4374        QString::from(&work::save(&self.qt_thread(), &self.shared(), &uid.to_string(), &edit.to_string(), &list.to_string()))
4375    }
4376
4377    fn capture(&self, line: &QString) -> QString {
4378        QString::from(&work::captured(&self.shared(), &line.to_string()))
4379    }
4380
4381    fn add_task(self: Pin<&mut Self>, line: &QString, parent: &QString, list: &QString) -> QString {
4382        QString::from(&work::add(&self.qt_thread(), &self.shared(), &line.to_string(), &parent.to_string(), &list.to_string()))
4383    }
4384
4385    fn set_task_status(self: Pin<&mut Self>, uid: &QString, status: &QString) -> QString {
4386        QString::from(&work::set_status(&self.qt_thread(), &self.shared(), &uid.to_string(), &status.to_string()))
4387    }
4388
4389    fn set_felt(self: Pin<&mut Self>, uid: &QString, felt: &QString) -> QString {
4390        QString::from(&work::set_felt(&self.qt_thread(), &self.shared(), &uid.to_string(), &felt.to_string()))
4391    }
4392
4393    fn set_waits(self: Pin<&mut Self>, uid: &QString, other: &QString, wait: bool, gap_minutes: i32) -> QString {
4394        QString::from(&work::set_waits(&self.qt_thread(), &self.shared(), &uid.to_string(), &other.to_string(), wait, i64::from(gap_minutes)))
4395    }
4396
4397    fn not_now(self: Pin<&mut Self>, uid: &QString) {
4398        work::not_now(&self.qt_thread(), &self.shared(), &uid.to_string());
4399    }
4400
4401    fn set_weather(self: Pin<&mut Self>, weather: &QString) {
4402        work::set_weather(&self.qt_thread(), &self.shared(), &weather.to_string());
4403        // The morning's weather, kept with the day's reviews.
4404        crate::reviews::weather_said(sioul_core::today::Weather::parse(&weather.to_string()));
4405    }
4406
4407    fn set_task_at(self: Pin<&mut Self>, uid: &QString, at: &QString) -> QString {
4408        QString::from(&work::set_at(&self.qt_thread(), &self.shared(), &uid.to_string(), &at.to_string(), 0))
4409    }
4410
4411    fn pin_task(self: Pin<&mut Self>, uid: &QString, at: &QString, minutes: i32) -> QString {
4412        QString::from(&work::set_at(&self.qt_thread(), &self.shared(), &uid.to_string(), &at.to_string(), u32::try_from(minutes).unwrap_or(0)))
4413    }
4414
4415    fn delete_task(self: Pin<&mut Self>, uid: &QString) {
4416        work::delete(&self.qt_thread(), &self.shared(), &uid.to_string());
4417    }
4418
4419    fn search_tasks(&self, query: &QString, except: &QString) -> QString {
4420        QString::from(&work::search(&self.shared(), &query.to_string(), &except.to_string()))
4421    }
4422
4423    fn new_list(self: Pin<&mut Self>, account: &QString, name: &QString) -> QString {
4424        QString::from(&work::new_list(&self.qt_thread(), &self.shared(), &account.to_string(), &name.to_string()))
4425    }
4426
4427    fn list_accounts(&self) -> QString {
4428        QString::from(&work::list_accounts())
4429    }
4430
4431    fn focus_start(self: Pin<&mut Self>, uid: &QString, minutes: i32) {
4432        work::focus_start(&self.qt_thread(), &self.shared(), &uid.to_string(), minutes);
4433    }
4434
4435    fn focus_pause(self: Pin<&mut Self>) {
4436        work::focus_pause(&self.qt_thread(), &self.shared());
4437    }
4438
4439    fn focus_extend(self: Pin<&mut Self>, minutes: i32) {
4440        work::focus_extend(&self.qt_thread(), &self.shared(), minutes);
4441    }
4442
4443    fn focus_stop(self: Pin<&mut Self>, done: bool, note: &QString) -> QString {
4444        QString::from(&work::focus_stop(&self.qt_thread(), &self.shared(), done, &note.to_string()))
4445    }
4446
4447    fn search_notes(self: Pin<&mut Self>, query: &QString) {
4448        work::search_notes(&self.qt_thread(), &self.shared(), &query.to_string());
4449    }
4450
4451    fn note(&self, path: &QString) -> QString {
4452        QString::from(&work::note(&self.shared(), &path.to_string()))
4453    }
4454
4455    fn save_note(self: Pin<&mut Self>, path: &QString, text: &QString, stamp: &QString) -> QString {
4456        QString::from(&work::save_note(&self.qt_thread(), &self.shared(), &path.to_string(), &text.to_string(), &stamp.to_string()))
4457    }
4458
4459    fn create_note(self: Pin<&mut Self>, title: &QString, links: &QString) -> QString {
4460        QString::from(&work::create_note(&self.qt_thread(), &self.shared(), &title.to_string(), &links.to_string()))
4461    }
4462
4463    fn related(&self, uri: &QString) -> QString {
4464        QString::from(&work::related(&self.shared(), &uri.to_string()))
4465    }
4466
4467    fn link_things(mut self: Pin<&mut Self>, from: &QString, to: &QString) -> QString {
4468        let line = work::link(&self.qt_thread(), &self.shared(), &from.to_string(), &to.to_string());
4469        self.as_mut().set_status(QString::from(&line));
4470        QString::from(&line)
4471    }
4472
4473    fn unlink_things(mut self: Pin<&mut Self>, from: &QString, to: &QString) -> QString {
4474        let line = work::unlink(&self.qt_thread(), &self.shared(), &from.to_string(), &to.to_string());
4475        self.as_mut().set_status(QString::from(&line));
4476        QString::from(&line)
4477    }
4478
4479    fn search_things(&self, query: &QString, kind: &QString, from: &QString) -> QString {
4480        QString::from(&work::search_things(&self.shared(), &query.to_string(), &kind.to_string(), &from.to_string()))
4481    }
4482
4483    fn uri_of(&self, kind: &QString, id: &QString) -> QString {
4484        QString::from(&work::uri_of(&kind.to_string(), &id.to_string()))
4485    }
4486
4487    fn is_program(&self, name: &QString) -> bool {
4488        crate::attachments::kept_closed(&name.to_string(), crate::attachments::phone())
4489    }
4490
4491    fn first_frame(&self) {
4492        crate::timing("the first frame drawn");
4493    }
4494
4495    fn mark(&self, what: &QString) {
4496        crate::timing(&what.to_string());
4497    }
4498
4499    fn phone_accounts(&self) -> bool {
4500        cfg!(target_os = "android")
4501    }
4502
4503    fn choose_phone_account(self: Pin<&mut Self>) {
4504        #[cfg(target_os = "android")]
4505        {
4506            if let Ok(mut waiting) = PHONE_CHOOSER.lock() {
4507                *waiting = Some(self.qt_thread());
4508            }
4509            // SAFETY: android/main.cpp's, called on Qt's thread; it answers through
4510            // `sioul_android_account_chosen`, on whatever thread Android gives.
4511            unsafe { sioul_android_choose_account() };
4512        }
4513        #[cfg(not(target_os = "android"))]
4514        let _ = self;
4515    }
4516
4517    fn budget_detail(&self, id: &QString, anchor: &QString, step: &QString) -> QString {
4518        let world = World::load();
4519        let today = Zoned::now().date();
4520        let Some(ledger) = world.ledger() else { return QString::default() };
4521        let Some(found) = ledger.budgets.iter().find(|b| b.id == id.to_string()) else { return QString::default() };
4522        let anchor = anchor.to_string().parse().unwrap_or(today);
4523        QString::from(&json(&view::budget_detail(&ledger, found, anchor, &step.to_string(), tr(), today)))
4524    }
4525
4526    fn keep_folder(self: Pin<&mut Self>, account: &QString, folder: &QString, kept: bool) -> QString {
4527        let (id, folder) = (account.to_string(), folder.to_string());
4528        // The folder kept or set aside, over the list as the configuration holds it
4529        // now, read and written under its lock, which the sharing takes too.
4530        let path = config_path();
4531        let changed = sioul_core::filelock::with_lock(&path, || {
4532            let config = config::Config::load(&path)?;
4533            let Some(account) = config.account(&id).cloned() else { return Ok(None) };
4534            let mut skipped = account.skip_folders.clone();
4535            skipped.retain(|f| f != &folder);
4536            if !kept {
4537                skipped.push(folder.clone());
4538            }
4539            config::set_value(&path, &format!("account.{id}.skip_folders"), &config::SettingValue::Texts(skipped)).map(|()| Some(account))
4540        });
4541        let account = match changed {
4542            Ok(Some(account)) => account,
4543            Ok(None) => return QString::default(),
4544            Err(e) => return QString::from(&e),
4545        };
4546        let (qt, shared) = (self.qt_thread(), self.shared());
4547        std::thread::spawn(move || {
4548            if !kept && let Err(e) = sioul_sync::mailbox::forget_folder(&account, &folder) {
4549                set_status(&qt, e.sentence(tr(), &account.id));
4550            }
4551            if let Some(control) = shared.watchers.lock().ok().and_then(|w| w.get(&account.id).cloned()) {
4552                control.nudge();
4553            }
4554            mail::show_mail(&qt, &shared);
4555        });
4556        QString::default()
4557    }
4558
4559    fn create_folder(self: Pin<&mut Self>, account: &QString, name: &QString) {
4560        let (id, name) = (account.to_string(), name.to_string());
4561        let (qt, shared) = (self.qt_thread(), self.shared());
4562        std::thread::spawn(move || {
4563            let Some(account) = load_config().account(&id).cloned() else { return };
4564            let made = sioul_sync::secret::password(&account).and_then(|p| sioul_sync::mailbox::create_folder(&account, &p, &name));
4565            match made {
4566                Ok(()) => tell(&qt, &shared, say("folder-made", &[("name", name.clone())])),
4567                Err(e) => tell(&qt, &shared, e.sentence(tr(), &account.id)),
4568            }
4569            mail::show_mail(&qt, &shared);
4570        });
4571    }
4572
4573    fn delete_folder(self: Pin<&mut Self>, account: &QString, folder: &QString) {
4574        let (id, folder) = (account.to_string(), folder.to_string());
4575        let (qt, shared) = (self.qt_thread(), self.shared());
4576        std::thread::spawn(move || {
4577            let Some(account) = load_config().account(&id).cloned() else { return };
4578            let gone = sioul_sync::secret::password(&account).and_then(|p| sioul_sync::mailbox::delete_folder(&account, &p, &folder));
4579            match gone {
4580                Ok(()) => tell(&qt, &shared, say("folder-deleted", &[("name", folder.clone())])),
4581                Err(sioul_sync::SyncError::Message(m)) if m == sioul_sync::mailbox::NOT_EMPTY => tell(&qt, &shared, say("folder-not-empty", &[("name", folder.clone())])),
4582                Err(e) => tell(&qt, &shared, e.sentence(tr(), &account.id)),
4583            }
4584            mail::show_mail(&qt, &shared);
4585        });
4586    }
4587
4588    fn trash_note(self: Pin<&mut Self>, path: &QString) -> QString {
4589        QString::from(&work::trash_note(&self.qt_thread(), &self.shared(), &path.to_string()))
4590    }
4591
4592    fn rename_note(self: Pin<&mut Self>, path: &QString, name: &QString) -> QString {
4593        QString::from(&work::rename_note(&self.qt_thread(), &self.shared(), &path.to_string(), &name.to_string()))
4594    }
4595
4596    fn create_note_in(self: Pin<&mut Self>, folder: &QString, title: &QString) -> QString {
4597        QString::from(&work::create_note_in(&self.qt_thread(), &self.shared(), &folder.to_string(), &title.to_string()))
4598    }
4599
4600    fn make_folder(self: Pin<&mut Self>, parent: &QString, name: &QString) -> QString {
4601        QString::from(&work::make_folder(&self.qt_thread(), &self.shared(), &parent.to_string(), &name.to_string()))
4602    }
4603
4604    fn rename_folder(self: Pin<&mut Self>, path: &QString, name: &QString) -> QString {
4605        QString::from(&work::rename_folder(&self.qt_thread(), &self.shared(), &path.to_string(), &name.to_string()))
4606    }
4607
4608    fn remove_folder(self: Pin<&mut Self>, path: &QString) -> QString {
4609        QString::from(&work::remove_folder(&self.qt_thread(), &self.shared(), &path.to_string()))
4610    }
4611
4612    fn remove_project(self: Pin<&mut Self>, id: &QString) -> QString {
4613        QString::from(&crate::projects::remove(&self.qt_thread(), &self.shared(), &id.to_string()))
4614    }
4615
4616    fn save_budget(self: Pin<&mut Self>, id: &QString, edit: &QString) -> QString {
4617        QString::from(&crate::projects::save_budget(&self.qt_thread(), &self.shared(), &id.to_string(), &edit.to_string()))
4618    }
4619
4620    fn letters(&self) -> QString {
4621        QString::from(&crate::letters::view())
4622    }
4623
4624    fn letter_task(self: Pin<&mut Self>, id: &QString) -> QString {
4625        QString::from(&crate::letters::make_task(&self.qt_thread(), &self.shared(), &id.to_string()))
4626    }
4627
4628    fn letter_event(self: Pin<&mut Self>, id: &QString) -> QString {
4629        QString::from(&crate::letters::make_event(&self.qt_thread(), &self.shared(), &id.to_string()))
4630    }
4631
4632    fn letter_done(self: Pin<&mut Self>, id: &QString, project: &QString) -> QString {
4633        QString::from(&crate::letters::done(&id.to_string(), &project.to_string()))
4634    }
4635
4636    fn letters_folder(&self) -> QString {
4637        QString::from(&crate::letters::folder())
4638    }
4639
4640    fn letters_add(self: Pin<&mut Self>, files: &QString) -> QString {
4641        QString::from(&crate::letters::add(&files.to_string()))
4642    }
4643
4644    fn bank(&self) -> QString {
4645        QString::from(&crate::bank::view())
4646    }
4647
4648    fn import_bank(self: Pin<&mut Self>, file: &QString) -> QString {
4649        let (ok, said) = crate::bank::import(&file.to_string());
4650        // The budgets count the bank accounts' movements.
4651        show(&self.qt_thread(), &self.shared());
4652        QString::from(&serde_json::json!({ "ok": ok, "said": said }).to_string())
4653    }
4654
4655    fn import_bank_into(self: Pin<&mut Self>, file: &QString, account: &QString) -> QString {
4656        let (ok, said) = crate::bank::import_into(&file.to_string(), &account.to_string());
4657        show(&self.qt_thread(), &self.shared());
4658        QString::from(&serde_json::json!({ "ok": ok, "said": said }).to_string())
4659    }
4660
4661    fn save_bank_account(self: Pin<&mut Self>, id: &QString, edit: &QString) -> QString {
4662        let problem = crate::bank::save_account(&id.to_string(), &edit.to_string());
4663        show(&self.qt_thread(), &self.shared());
4664        QString::from(&problem)
4665    }
4666
4667    fn remove_bank_account(self: Pin<&mut Self>, id: &QString) -> QString {
4668        let problem = crate::bank::remove_account(&id.to_string());
4669        show(&self.qt_thread(), &self.shared());
4670        QString::from(&problem)
4671    }
4672
4673    fn place_movement(self: Pin<&mut Self>, account: &QString, movement: &QString, choice: &QString) -> QString {
4674        let problem = crate::bank::place_movement(&account.to_string(), &movement.to_string(), &choice.to_string());
4675        show(&self.qt_thread(), &self.shared());
4676        QString::from(&problem)
4677    }
4678
4679    fn save_bank_rule(self: Pin<&mut Self>, rule: &QString, edit: &QString) -> QString {
4680        let problem = crate::bank::save_rule(&rule.to_string(), &edit.to_string());
4681        show(&self.qt_thread(), &self.shared());
4682        QString::from(&problem)
4683    }
4684
4685    fn remove_bank_rule(self: Pin<&mut Self>, rule: &QString) -> QString {
4686        let problem = crate::bank::remove_rule(&rule.to_string());
4687        show(&self.qt_thread(), &self.shared());
4688        QString::from(&problem)
4689    }
4690
4691    fn save_reserve(self: Pin<&mut Self>, id: &QString, edit: &QString) -> QString {
4692        let problem = crate::bank::save_reserve(&id.to_string(), &edit.to_string());
4693        show(&self.qt_thread(), &self.shared());
4694        QString::from(&problem)
4695    }
4696
4697    fn contracts(&self) -> QString {
4698        QString::from(&crate::contracts::view())
4699    }
4700
4701    fn save_contract(self: Pin<&mut Self>, id: &QString, edit: &QString) -> QString {
4702        QString::from(&crate::contracts::save(&id.to_string(), &edit.to_string()))
4703    }
4704
4705    fn remove_contract(self: Pin<&mut Self>, id: &QString) -> QString {
4706        QString::from(&crate::contracts::remove(&id.to_string()))
4707    }
4708
4709    fn contract_letter(self: Pin<&mut Self>, id: &QString) -> QString {
4710        QString::from(&crate::contracts::letter(&id.to_string()))
4711    }
4712
4713    fn contract_from(&self, subject: &QString, from: &QString) -> QString {
4714        QString::from(&crate::contracts::from_card(&subject.to_string(), &from.to_string()))
4715    }
4716
4717    fn routines(&self) -> QString {
4718        QString::from(&work::routines(&self.shared()))
4719    }
4720
4721    fn save_routine(self: Pin<&mut Self>, id: &QString, title: &QString, steps: &QString, by_itself: bool) -> QString {
4722        QString::from(&work::save_routine(&id.to_string(), &title.to_string(), &steps.to_string(), by_itself))
4723    }
4724
4725    fn remove_routine(self: Pin<&mut Self>, id: &QString) -> QString {
4726        QString::from(&work::remove_routine(&id.to_string()))
4727    }
4728
4729    fn share_status(&self, folder: &QString) -> QString {
4730        QString::from(&crate::share::status(&folder.to_string()))
4731    }
4732
4733    fn start_sharing(self: Pin<&mut Self>, folder: &QString, passphrase: &QString, again: &QString) -> QString {
4734        let problem = crate::share::start(&folder.to_string(), &passphrase.to_string(), &again.to_string());
4735        if problem.is_empty() {
4736            crate::share::exchange(&self.qt_thread(), &self.shared());
4737        }
4738        QString::from(&problem)
4739    }
4740
4741    fn stop_sharing(self: Pin<&mut Self>) -> QString {
4742        QString::from(&crate::share::stop())
4743    }
4744
4745    fn share_candidates(&self) -> QString {
4746        QString::from(&json(&crate::share::candidates()))
4747    }
4748
4749    fn set_share_projects(self: Pin<&mut Self>, on: bool) -> QString {
4750        let problem = crate::share::set_projects(on);
4751        if problem.is_empty() {
4752            crate::share::exchange(&self.qt_thread(), &self.shared());
4753        }
4754        QString::from(&problem)
4755    }
4756
4757    fn folders_in(&self, path: &QString) -> QString {
4758        QString::from(&crate::share::folders_in(&path.to_string()))
4759    }
4760
4761    fn files_access(&self) -> bool {
4762        #[cfg(target_os = "android")]
4763        {
4764            // SAFETY: android/main.cpp's, asking Android through Java.
4765            unsafe { sioul_android_files_access() }
4766        }
4767        #[cfg(not(target_os = "android"))]
4768        true
4769    }
4770
4771    fn ask_files_access(self: Pin<&mut Self>) {
4772        // SAFETY: android/main.cpp's, on Qt's thread.
4773        #[cfg(target_os = "android")]
4774        unsafe {
4775            sioul_android_ask_files_access()
4776        };
4777        let _ = self;
4778    }
4779
4780    fn share_now(self: Pin<&mut Self>) {
4781        crate::share::exchange(&self.qt_thread(), &self.shared());
4782    }
4783
4784    fn export_time_csv(mut self: Pin<&mut Self>, project: &QString, from: &QString, to: &QString, target: &QString) -> QString {
4785        match crate::projects::export_time_csv(&self.shared(), &project.to_string(), &from.to_string(), &to.to_string(), &target.to_string()) {
4786            Ok(path) => {
4787                self.as_mut().set_status(QString::from(&say("time-exported", &[("path", path)])));
4788                QString::default()
4789            }
4790            Err(e) => QString::from(&e),
4791        }
4792    }
4793
4794    fn change_line(self: Pin<&mut Self>, uri: &QString, shown: &QString, label: &QString, amount: f64, date: &QString) -> QString {
4795        QString::from(&crate::projects::change_line(&self.qt_thread(), &self.shared(), &uri.to_string(), &shown.to_string(), &label.to_string(), amount, &date.to_string()))
4796    }
4797
4798    fn remove_budget(self: Pin<&mut Self>, what: &QString) -> QString {
4799        QString::from(&crate::projects::remove_budget(&self.qt_thread(), &self.shared(), &what.to_string()))
4800    }
4801
4802    fn remove_line(self: Pin<&mut Self>, uri: &QString, shown: &QString) -> QString {
4803        QString::from(&crate::projects::remove_line(&self.qt_thread(), &self.shared(), &uri.to_string(), &shown.to_string()))
4804    }
4805
4806    fn noise_url(&self, kind: &QString) -> QString {
4807        match sioul_core::sounds::noise_file(&config::cache_dir().join("sounds"), &kind.to_string()) {
4808            Ok(path) => QString::from(&file_url(&path)),
4809            Err(_) => QString::default(),
4810        }
4811    }
4812
4813    fn calm_sounds(&self) -> QString {
4814        QString::from(&work::calm_sounds(&self.shared()))
4815    }
4816
4817    fn move_task(self: Pin<&mut Self>, uid: &QString, list: &QString, confirmed: bool) -> QString {
4818        QString::from(&work::move_task(&self.qt_thread(), &self.shared(), &uid.to_string(), &list.to_string(), confirmed))
4819    }
4820
4821    fn move_contact(self: Pin<&mut Self>, key: &QString, book: &QString, confirmed: bool) -> QString {
4822        QString::from(&pim::move_contact(&self.qt_thread(), &self.shared(), &key.to_string(), &book.to_string(), confirmed))
4823    }
4824
4825    fn address_books(&self) -> QString {
4826        QString::from(&pim::books())
4827    }
4828
4829    fn find_places(self: Pin<&mut Self>, name: &QString) {
4830        let (qt, name) = (self.qt_thread(), name.to_string());
4831        let language = tr().text("qt-locale", None).chars().take(2).collect::<String>();
4832        std::thread::spawn(move || {
4833            let found = sioul_sync::weather::places(&name, &language).map(|p| json(&p)).unwrap_or_else(|e| serde_json::json!({ "error": e.sentence(tr(), "Open-Meteo") }).to_string());
4834            let _ = qt.queue(move |mut sioul| sioul.as_mut().set_places_found(QString::from(&found)));
4835        });
4836    }
4837
4838    fn set_weather_place(mut self: Pin<&mut Self>, name: &QString, latitude: f64, longitude: f64) {
4839        let name = name.to_string();
4840        let path = config_path();
4841        let written = if name.trim().is_empty() {
4842            ["weather.place", "weather.latitude", "weather.longitude"].iter().try_for_each(|k| config::set_value(&path, k, &config::SettingValue::Text(String::new())))
4843        } else {
4844            config::set_value(&path, "weather.place", &config::SettingValue::Text(name))
4845                .and_then(|()| config::set_value(&path, "weather.latitude", &config::SettingValue::Float(latitude)))
4846                .and_then(|()| config::set_value(&path, "weather.longitude", &config::SettingValue::Float(longitude)))
4847        };
4848        if let Err(e) = written {
4849            self.as_mut().set_status(QString::from(&e));
4850        }
4851        let _ = std::fs::remove_file(weather_cache());
4852        self.as_mut().set_forecast(QString::default());
4853        update_weather(&self.qt_thread(), &self.shared());
4854        // The phone's card: no place, where to choose one; a new place, its forecast once fetched.
4855        crate::homecard::weather_seen();
4856    }
4857
4858    fn health_page(&self) -> QString {
4859        QString::from(&crate::health::page())
4860    }
4861
4862    fn save_medicine(self: Pin<&mut Self>, id: &QString, edit: &QString) -> QString {
4863        QString::from(&crate::health::save_medicine(&id.to_string(), &edit.to_string()))
4864    }
4865
4866    fn save_prescription(self: Pin<&mut Self>, id: &QString, edit: &QString) -> QString {
4867        QString::from(&crate::health::save_prescription(&id.to_string(), &edit.to_string()))
4868    }
4869
4870    fn health_for_professional(&self, prescription: &QString) -> QString {
4871        QString::from(&crate::health::for_professional(&prescription.to_string()))
4872    }
4873
4874    fn remove_health(self: Pin<&mut Self>, id: &QString) -> QString {
4875        QString::from(&crate::health::remove(&id.to_string()))
4876    }
4877
4878    fn refilled(self: Pin<&mut Self>, id: &QString) -> QString {
4879        QString::from(&crate::health::refilled(&id.to_string()))
4880    }
4881
4882    fn set_dose_taken(self: Pin<&mut Self>, key: &QString, taken: bool) {
4883        let problem = crate::health::set_taken(&key.to_string(), taken);
4884        if !problem.is_empty() {
4885            tell(&self.qt_thread(), &self.shared(), problem);
4886        }
4887        // Your other computers know at once.
4888        crate::share::exchange(&self.qt_thread(), &self.shared());
4889    }
4890
4891    fn dose_not_taken(self: Pin<&mut Self>, key: &QString) {
4892        let problem = crate::health::not_taken(&key.to_string());
4893        if !problem.is_empty() {
4894            tell(&self.qt_thread(), &self.shared(), problem);
4895        }
4896        crate::share::exchange(&self.qt_thread(), &self.shared());
4897    }
4898
4899    fn dose_taken_at(self: Pin<&mut Self>, key: &QString, time: &QString) -> QString {
4900        let problem = crate::health::taken_late(&key.to_string(), &time.to_string());
4901        if problem.is_empty() {
4902            crate::share::exchange(&self.qt_thread(), &self.shared());
4903        }
4904        QString::from(&problem)
4905    }
4906
4907    fn dose_info(&self, key: &QString) -> QString {
4908        QString::from(&crate::health::dose_info(&key.to_string()))
4909    }
4910
4911    fn dose_choose(self: Pin<&mut Self>, key: &QString, taken: bool) -> QString {
4912        let problem = crate::health::choose(&key.to_string(), taken);
4913        // Your other devices know at once; the pages say it.
4914        crate::share::exchange(&self.qt_thread(), &self.shared());
4915        crate::health::show_health(&self.qt_thread(), &self.shared());
4916        QString::from(&problem)
4917    }
4918
4919    fn device_off(self: Pin<&mut Self>, id: &QString, off: bool) -> QString {
4920        let problem = crate::health::device_off(&id.to_string(), off);
4921        crate::health::show_health(&self.qt_thread(), &self.shared());
4922        QString::from(&problem)
4923    }
4924
4925    fn device_forget(self: Pin<&mut Self>, id: &QString) -> QString {
4926        QString::from(&crate::health::device_forget(&id.to_string()))
4927    }
4928
4929    fn needs(&self) -> QString {
4930        QString::from(&crate::health::needs_page())
4931    }
4932
4933    fn save_needs(self: Pin<&mut Self>, edit: &QString, shown: &QString) -> QString {
4934        let problem = crate::health::save_needs(&edit.to_string(), &shown.to_string());
4935        // The plan goes around them at once.
4936        crate::work::show_work(&self.qt_thread(), &self.shared());
4937        QString::from(&problem)
4938    }
4939
4940    fn skip_need(self: Pin<&mut Self>, key: &QString, skip: bool) -> QString {
4941        let problem = crate::health::skip_today(&key.to_string(), skip);
4942        crate::work::show_work(&self.qt_thread(), &self.shared());
4943        QString::from(&problem)
4944    }
4945
4946    fn needs_today(&self) -> QString {
4947        QString::from(&crate::health::needs_today())
4948    }
4949
4950    fn refresh_health(self: Pin<&mut Self>) {
4951        crate::health::show_health(&self.qt_thread(), &self.shared());
4952    }
4953
4954    fn show_health_week(self: Pin<&mut Self>, day: &QString) {
4955        crate::health::show_week(&day.to_string());
4956        crate::health::show_health(&self.qt_thread(), &self.shared());
4957    }
4958
4959    fn drag_need(self: Pin<&mut Self>, edit: &QString) -> QString {
4960        let (qt, shared) = (self.qt_thread(), self.shared());
4961        let problem = crate::health::drag_need(&qt, &shared, &edit.to_string());
4962        // The plan goes around it as that day has it now; the page shows it.
4963        work::show_work(&qt, &shared);
4964        crate::health::show_health(&qt, &shared);
4965        QString::from(&problem)
4966    }
4967
4968    fn change_need(self: Pin<&mut Self>, edit: &QString) -> QString {
4969        let problem = crate::health::change_need(&edit.to_string());
4970        // The plan goes around it as that day has it now; the page shows it.
4971        crate::work::show_work(&self.qt_thread(), &self.shared());
4972        crate::health::show_health(&self.qt_thread(), &self.shared());
4973        QString::from(&problem)
4974    }
4975
4976    fn health_settings(&self) -> QString {
4977        QString::from(&crate::health::settings_view())
4978    }
4979
4980    fn wake_settings(&self, which: &QString) {
4981        crate::wake::open_settings(&which.to_string());
4982    }
4983
4984    fn try_wake(&self) -> QString {
4985        QString::from(&crate::wake::try_now())
4986    }
4987
4988    fn set_overlap_aside(self: Pin<&mut Self>, key: &QString) -> QString {
4989        let problem = crate::pim::set_overlap_aside(&key.to_string());
4990        pim::show_pim(&self.qt_thread(), &self.shared());
4991        QString::from(&problem)
4992    }
4993
4994    fn move_need(self: Pin<&mut Self>, key: &QString, minutes: i32, time: &QString) -> QString {
4995        let problem = crate::health::move_today(&key.to_string(), i64::from(minutes), &time.to_string());
4996        // The plan goes around it where it is now.
4997        crate::work::show_work(&self.qt_thread(), &self.shared());
4998        QString::from(&problem)
4999    }
5000
5001    fn needs_later(&self) -> i32 {
5002        i32::try_from(crate::health::later_minutes()).unwrap_or(15)
5003    }
5004
5005    fn missed_doses(&self) -> QString {
5006        QString::from(&crate::health::missed())
5007    }
5008
5009    // Both off the window's thread: Android waits for the window to answer
5010    // when it puts Sioul away, and an exchange can take seconds.
5011    fn going_away(mut self: Pin<&mut Self>) {
5012        self.as_mut().set_away(true);
5013        AWAY.store(true, Ordering::SeqCst);
5014        std::thread::spawn(crate::alarms::schedule);
5015        // The events' reminders as they are, for Android's alarm clock (`eventalarms`).
5016        std::thread::spawn(crate::eventalarms::schedule);
5017        // The home screen's card as Sioul leaves it, drawn at once (homecard.rs).
5018        crate::homecard::now();
5019        crate::share::set_shown(false);
5020        let (qt, shared) = (self.qt_thread(), self.shared());
5021        std::thread::spawn(move || {
5022            crate::mail::quiet_watchers(&shared);
5023            crate::mail::finish_all(&qt, &shared);
5024            crate::share::closing();
5025            // The closed claims go up at once.
5026            crate::share::nudge(&qt, &shared, 0, false);
5027        });
5028    }
5029
5030    fn back_here(mut self: Pin<&mut Self>) {
5031        self.as_mut().set_away(false);
5032        AWAY.store(false, Ordering::SeqCst);
5033        // A dose's reminder tapped while away: its question, at once.
5034        if let Some(key) = crate::alarms::opened() {
5035            self.as_mut().reminder_opened(QString::from("dose"), QString::default(), QString::from(&key));
5036        }
5037        // An event's reminder or new mail's notification tapped while away: the event, or the Porch.
5038        if let Some((kind, key)) = crate::eventalarms::opened() {
5039            self.as_mut().reminder_opened(QString::from(&kind), QString::default(), QString::from(&key));
5040        }
5041        // The home screen's card tapped: the Porch or a message on it, Now on
5042        // its step, the Agenda or an event in it (homecard.rs).
5043        if let Some((kind, uri, key)) = crate::homecard::opened() {
5044            self.as_mut().reminder_opened(QString::from(kind), QString::from(&uri), QString::from(&key));
5045        }
5046        // What was asked while away, computed once now.
5047        let (qt, shared) = (self.qt_thread(), self.shared());
5048        if shared.views_job.deferred.swap(false, Ordering::SeqCst) | shared.mail_job.deferred.swap(false, Ordering::SeqCst) {
5049            show(&qt, &shared);
5050        }
5051        if shared.pim_job.deferred.swap(false, Ordering::SeqCst) {
5052            pim::show_pim(&qt, &shared);
5053        }
5054        if shared.work_job.deferred.swap(false, Ordering::SeqCst) {
5055            work::show_work(&qt, &shared);
5056        }
5057        if shared.health_job.deferred.swap(false, Ordering::SeqCst) {
5058            crate::health::show_health(&qt, &shared);
5059        }
5060        crate::share::set_shown(true);
5061        // Something shared to Sioul, a mailto: link opened with it: its draft (docs/android.md, "Sharing").
5062        crate::outside::take(self.as_mut());
5063        std::thread::spawn(|| crate::outside::publish(true));
5064        // A "Pause" pressed on the quick-settings tile or the home screen's shortcut (docs/pauses.md).
5065        crate::pauses::tick(&self.qt_thread(), &self.shared());
5066        let now = jiff::Timestamp::now().as_second();
5067        self.shared().active.store(now, std::sync::atomic::Ordering::Relaxed);
5068        let (qt, shared) = (self.qt_thread(), self.shared());
5069        std::thread::spawn(move || {
5070            // A session begins again: up, said to your other devices (docs/database.md, "Devices").
5071            crate::devices::window_opened();
5072            let _ = crate::share::keeper("health", sioul_sync::lease::Rule::FollowsYou, now, false);
5073            // What the other devices marked meanwhile comes down, and is read;
5074            // mail and calendars fetched now, not when their pauses end.
5075            crate::share::nudge(&qt, &shared, 0, true);
5076            crate::mail::wake_watchers(&shared);
5077        });
5078    }
5079
5080    fn touch(self: Pin<&mut Self>) {
5081        self.shared().active.store(jiff::Timestamp::now().as_second(), std::sync::atomic::Ordering::Relaxed);
5082    }
5083
5084    fn set_health(self: Pin<&mut Self>, key: &QString, value: &QString) -> QString {
5085        QString::from(&crate::health::set_setting(&key.to_string(), &value.to_string()))
5086    }
5087
5088    fn movement_minutes(&self) -> i32 {
5089        crate::health::movement_minutes()
5090    }
5091
5092    fn chat_minute(self: Pin<&mut Self>) -> bool {
5093        crate::health::chat_minute()
5094    }
5095
5096    fn chats_covered(&self) -> bool {
5097        crate::health::chats_covered()
5098    }
5099
5100    fn memo_url(&self) -> QString {
5101        QString::from(&work::memo_url())
5102    }
5103
5104    fn memo_recorded(self: Pin<&mut Self>, url: &QString) -> QString {
5105        let (qt, shared) = (self.qt_thread(), self.shared());
5106        work::show_work(&qt, &shared);
5107        QString::from(&work::note_path_of(&url.to_string()))
5108    }
5109
5110    fn set_notes_tree(self: Pin<&mut Self>, tree: bool) {
5111        work::set_notes_tree(&self.qt_thread(), &self.shared(), tree);
5112    }
5113
5114    fn view_flag(&self, name: &QString) -> bool {
5115        work::view_flag(&self.shared(), &name.to_string())
5116    }
5117
5118    fn set_view_flag(self: Pin<&mut Self>, name: &QString, value: bool) {
5119        work::set_view_flag(&self.shared(), &name.to_string(), value);
5120    }
5121
5122    fn add_movement(mut self: Pin<&mut Self>, edit: &QString) -> QString {
5123        #[derive(serde::Deserialize)]
5124        struct Movement {
5125            /// "once" or "recurring".
5126            kind: String,
5127            budget: String,
5128            label: String,
5129            amount: f64,
5130            #[serde(default)]
5131            date: String,
5132            /// Recurring: "month" or "year", the day, the month of a yearly one, from, until.
5133            #[serde(default)]
5134            every: String,
5135            #[serde(default)]
5136            day: i8,
5137            #[serde(default)]
5138            month: Option<i8>,
5139            #[serde(default)]
5140            from: String,
5141            #[serde(default)]
5142            until: String,
5143            #[serde(default)]
5144            estimate: bool,
5145        }
5146        let world = World::load();
5147        let outcome = (|| {
5148            let movement: Movement = serde_json::from_str(&edit.to_string()).map_err(|e| e.to_string())?;
5149            let ledger = world.ledger().ok_or_else(|| tr().text("ui-no-budgets", None))?;
5150            let path = world.ledger_path().ok_or_else(|| tr().text("ui-no-budgets", None))?;
5151            if movement.amount == 0.0 || !movement.amount.is_finite() {
5152                return Err(tr().text("budget-add-bad-amount", None));
5153            }
5154            let today = Zoned::now().date();
5155            let origin = say("budget-origin-hand", &[("date", today.to_string())]);
5156            if movement.kind == "recurring" {
5157                let preset = budget::PresetEdit {
5158                    budget: movement.budget.clone(),
5159                    label: movement.label.clone(),
5160                    amount: movement.amount,
5161                    every: movement.every.clone(),
5162                    day: movement.day,
5163                    month: movement.month,
5164                    from: movement.from.clone(),
5165                    until: movement.until.clone(),
5166                    estimate: movement.estimate,
5167                };
5168                budget::record_preset(&path, &preset, &origin)?;
5169            } else {
5170                let date: jiff::civil::Date = movement.date.parse().unwrap_or(today);
5171                let line = budget::Line {
5172                    budget: movement.budget.clone(),
5173                    date,
5174                    amount: sioul_core::money::Money::from_units(movement.amount),
5175                    label: movement.label.trim().to_string(),
5176                    planned: date > today,
5177                    reserve: None,
5178                    links: Vec::new(),
5179                    preset: None,
5180                };
5181                budget::record_line(&path, &line, &origin)?;
5182            }
5183            Ok::<String, String>(say("budget-added", &[("budget", ledger.budget_title(&movement.budget).to_string())]))
5184        })();
5185        let (line, problem) = match outcome {
5186            Ok(line) => (line, String::new()),
5187            Err(e) => (e.clone(), e),
5188        };
5189        self.as_mut().set_status(QString::from(&line));
5190        show(&self.qt_thread(), &self.shared());
5191        QString::from(&problem)
5192    }
5193
5194    fn site_list(&self) -> QString {
5195        QString::from(&crate::sites::list())
5196    }
5197
5198    fn site_notified(self: Pin<&mut Self>, id: &QString, title: &QString, text: &QString) {
5199        crate::sites::notified(&self.qt_thread(), &self.shared(), &id.to_string(), &title.to_string(), &text.to_string());
5200    }
5201
5202    fn site_seen(self: Pin<&mut Self>, id: &QString) {
5203        crate::sites::seen(&self.qt_thread(), &id.to_string());
5204    }
5205
5206    fn site_notices(&self) -> QString {
5207        QString::from(&crate::sites::waiting())
5208    }
5209
5210    fn site_for_sender(&self, sender: &QString) -> QString {
5211        QString::from(&crate::sites::announced_by(&sender.to_string()))
5212    }
5213
5214    fn set_site(mut self: Pin<&mut Self>, id: &QString, field: &QString, value: &QString) -> QString {
5215        let (field, value) = (field.to_string(), value.to_string());
5216        let setting = match field.as_str() {
5217            "realtime" | "muted" | "background" | "microphone" | "camera" | "screen" => config::SettingValue::Bool(value == "true"),
5218            "site" | "area" | "name" => config::SettingValue::Text(value.trim().to_string()),
5219            // Your categories, as a JSON list of words.
5220            "categories" => config::SettingValue::Texts(serde_json::from_str::<Vec<String>>(&value).unwrap_or_default().into_iter().map(|c| c.trim().to_string()).filter(|c| !c.is_empty()).collect()),
5221            // Only an encrypted address: a site's sign-in never travels in clear.
5222            "url" if value.trim().starts_with("https://") => config::SettingValue::Text(value.trim().to_string()),
5223            "url" => return QString::from(&tr().text("site-https-only", None)),
5224            _ => return QString::default(),
5225        };
5226        let result = config::set_value(&config_path(), &format!("site.{}.{field}", id), &setting);
5227        self.as_mut().sites_changed();
5228        QString::from(&result.err().unwrap_or_default())
5229    }
5230
5231    fn site_presets(&self, country: &QString, region: &QString) -> QString {
5232        QString::from(&crate::sites::presets(&country.to_string(), &region.to_string()))
5233    }
5234
5235    fn add_site(mut self: Pin<&mut Self>, edit: &QString) -> QString {
5236        let answer = crate::sites::add(&edit.to_string());
5237        self.as_mut().sites_changed();
5238        QString::from(&answer)
5239    }
5240
5241    fn site_presets_tree(&self) -> QString {
5242        QString::from(&crate::sites::presets_tree())
5243    }
5244
5245    fn add_sites(mut self: Pin<&mut Self>, rows: &QString) -> QString {
5246        let answer = crate::sites::add_many(&rows.to_string());
5247        self.as_mut().sites_changed();
5248        QString::from(&answer)
5249    }
5250
5251    fn move_site(mut self: Pin<&mut Self>, id: &QString, delta: i32) -> QString {
5252        let said = crate::sites::move_site(&id.to_string(), i64::from(delta));
5253        self.as_mut().sites_changed();
5254        QString::from(&said)
5255    }
5256
5257    fn remove_site(mut self: Pin<&mut Self>, id: &QString) -> QString {
5258        let problem = crate::sites::remove(&id.to_string());
5259        self.as_mut().sites_changed();
5260        QString::from(&problem)
5261    }
5262
5263    fn google_built_in(&self) -> bool {
5264        sioul_sync::google::built_in().is_some()
5265    }
5266
5267    fn call_devices(&self) -> QString {
5268        QString::from(&serde_json::to_string(&load_config().calls).unwrap_or_default())
5269    }
5270
5271    fn calls_script(&self) -> QString {
5272        QString::from(include_str!("calls.js"))
5273    }
5274
5275    fn set_call_device(mut self: Pin<&mut Self>, which: &QString, name: &QString) -> QString {
5276        let which = which.to_string();
5277        if !matches!(which.as_str(), "camera" | "microphone" | "speaker") {
5278            return QString::default();
5279        }
5280        let name = name.to_string().trim().to_string();
5281        let result = config::set_value(&config_path(), &format!("calls.{which}"), &config::SettingValue::Text(name));
5282        self.as_mut().sites_changed();
5283        QString::from(&result.err().unwrap_or_default())
5284    }
5285
5286    fn bitwarden_state(&self) -> QString {
5287        QString::from(&crate::sites::bitwarden_state(&self.shared()))
5288    }
5289
5290    fn bitwarden_unlock(self: Pin<&mut Self>, password: &QString, provider: i32, code: &QString) -> i32 {
5291        let (shared, password, code) = (self.shared(), password.to_string(), code.to_string());
5292        crate::sites::bitwarden_later(&self.qt_thread(), move || crate::sites::bitwarden_unlock(&shared, &password, provider, &code))
5293    }
5294
5295    fn bitwarden_lock(self: Pin<&mut Self>) {
5296        crate::sites::bitwarden_lock(&self.shared());
5297    }
5298
5299    fn bitwarden_send_code(self: Pin<&mut Self>, password: &QString) -> i32 {
5300        let password = password.to_string();
5301        crate::sites::bitwarden_later(&self.qt_thread(), move || crate::sites::bitwarden_send_code(&password))
5302    }
5303
5304    fn bitwarden_passkey_begin(self: Pin<&mut Self>) -> i32 {
5305        crate::sites::bitwarden_later(&self.qt_thread(), crate::sites::bitwarden_passkey_begin)
5306    }
5307
5308    fn bitwarden_passkey(self: Pin<&mut Self>, answer: &QString) -> i32 {
5309        let (shared, answer) = (self.shared(), answer.to_string());
5310        crate::sites::bitwarden_later(&self.qt_thread(), move || crate::sites::bitwarden_passkey(&shared, &answer))
5311    }
5312
5313    fn bitwarden_search(self: Pin<&mut Self>, url: &QString, site: &QString, user: &QString) -> QString {
5314        QString::from(&crate::sites::bitwarden_search(&self.qt_thread(), &self.shared(), &url.to_string(), &site.to_string(), &user.to_string()))
5315    }
5316
5317    fn bitwarden_site(&self, url: &QString) -> QString {
5318        QString::from(&crate::sites::bitwarden_site(&self.shared(), &url.to_string()))
5319    }
5320
5321    fn bitwarden_login(&self, url: &QString, id: &QString) -> QString {
5322        QString::from(&crate::sites::bitwarden_login(&self.shared(), &url.to_string(), &id.to_string()))
5323    }
5324
5325    fn map_view(&self) -> QString {
5326        QString::from(&crate::map::view(&self.shared()))
5327    }
5328
5329    fn place_of(&self, address: &QString) -> QString {
5330        QString::from(&crate::map::place_of(&address.to_string()))
5331    }
5332
5333    fn locate_addresses(mut self: Pin<&mut Self>, allow: bool) {
5334        if allow && !load_config().map.geocode {
5335            if let Err(e) = config::set_value(&config_path(), "map.geocode", &config::SettingValue::Bool(true)) {
5336                self.as_mut().set_status(QString::from(&e));
5337                return;
5338            }
5339        }
5340        crate::map::locate(&self.qt_thread(), &self.shared());
5341    }
5342
5343    fn refresh_mode(mut self: Pin<&mut Self>) {
5344        // Doses to remind, errands to make: off the window's thread.
5345        let (qt, shared_tick) = (self.qt_thread(), self.shared());
5346        std::thread::spawn(move || crate::health::tick(&qt, &shared_tick));
5347        // The time running's notification: what changed elsewhere (another
5348        // device, the command line), or while a phone had Sioul put away.
5349        let (qt_time, shared_time) = (self.qt_thread(), self.shared());
5350        std::thread::spawn(move || crate::timenote::follow(&qt_time, &shared_time));
5351        // The spam filter trains again by itself, plugged in and idle, on the computer that made its table.
5352        let qt_spam = self.qt_thread();
5353        std::thread::spawn(move || crate::spam::by_itself(qt_spam));
5354        // The pauses: free time ended by itself, the do-not-disturb as the pauses are (docs/pauses.md).
5355        crate::pauses::tick(&self.qt_thread(), &self.shared());
5356        // Put away on a phone: the reminders only, and an exchange every five
5357        // minutes; the rest waits until it is back (Android stops an app that
5358        // works in the background, and nobody looks at its pages then).
5359        if *self.as_ref().away() {
5360            if Zoned::now().minute() % 5 == 0 {
5361                crate::share::exchange_tick(&self.qt_thread(), &self.shared());
5362            }
5363            return;
5364        }
5365        // The home screen's card: written again when its first frame ended, or five minutes went by.
5366        crate::homecard::tick();
5367        // Something handed by another application while the window stayed shown (split screen).
5368        crate::outside::take(self.as_mut());
5369        // The plan, made again on the clock.
5370        let now = Zoned::now();
5371        let (stamp, day) = (now.timestamp().as_second(), i64::from(now.date().year()) * 1000 + i64::from(now.date().day_of_year()));
5372        let shared = self.shared();
5373        let last = shared.planned_at.load(Ordering::Relaxed);
5374        if last == 0 {
5375            shared.planned_at.store(stamp, Ordering::Relaxed);
5376            shared.planned_day.store(day, Ordering::Relaxed);
5377        } else if stamp - last >= REPLAN_EVERY || shared.planned_day.load(Ordering::Relaxed) != day {
5378            // A new day: the agenda and today's overlaps follow it too.
5379            if shared.planned_day.load(Ordering::Relaxed) != day {
5380                pim::show_pim(&self.qt_thread(), &shared);
5381            }
5382            shared.planned_at.store(stamp, Ordering::Relaxed);
5383            shared.planned_day.store(day, Ordering::Relaxed);
5384            work::show_work(&self.qt_thread(), &shared);
5385        }
5386        // What changed here goes to your other computers, theirs comes in
5387        // (left out when a save's exchange just did it: `share::exchange_tick`).
5388        crate::share::exchange_tick(&self.qt_thread(), &self.shared());
5389        crate::share::nudge_tick(&self.qt_thread(), &self.shared());
5390        // Invoices stay with the computer that makes them: its claim renewed,
5391        // off the window's thread (it reads and writes the sharing folder).
5392        let shared_invoices = self.shared();
5393        std::thread::spawn(move || crate::projects::keep_invoices(&shared_invoices));
5394        // What sites notified, gathered at the times you set.
5395        crate::sites::gather_tick(&self.qt_thread(), &self.shared());
5396        // Your sites' own icons, when missing or a week old.
5397        crate::sites::favicon_tick(&self.qt_thread());
5398        // Dates coming: told once each.
5399        crate::remind::tick(&self.qt_thread());
5400        // Mail that waited for its time, told when the time begins (docs/porch.md, "Notifications").
5401        crate::mailnote::tick_in_window(crate::mailnote::Window { qt: self.qt_thread(), active: self.shared().active.load(Ordering::Relaxed) });
5402        // The end of the work day, said once (docs/reviews.md).
5403        crate::reviews::tick(&self.qt_thread(), &self.shared());
5404        // Paper letters scanned: read, to wait for the Porch.
5405        crate::letters::tick(&self.qt_thread(), &self.shared());
5406        // The weather follows the hours; fetched again when half an hour old.
5407        update_weather(&self.qt_thread(), &self.shared());
5408        let line = mode_json();
5409        if self.as_ref().mode().to_string() != line {
5410            let was_quiet = self.as_ref().mode().to_string().contains("\"quiet\":true");
5411            self.as_mut().set_mode(QString::from(&line));
5412            // Work came or went: what the pages show follows.
5413            if was_quiet != line.contains("\"quiet\":true") {
5414                let (qt, shared) = (self.qt_thread(), self.shared());
5415                show(&qt, &shared);
5416                work::show_work(&qt, &shared);
5417            }
5418        }
5419    }
5420
5421    fn work_a_while(self: Pin<&mut Self>, minutes: i32) {
5422        let until = Zoned::now().timestamp().as_second() + i64::from(minutes.max(1)) * 60;
5423        self.change_overrides(|o| {
5424            o.rest_until = None;
5425            o.work_until = Some(until);
5426        });
5427    }
5428
5429    pub(crate) fn done_for_the_day(mut self: Pin<&mut Self>) -> QString {
5430        let (qt, shared) = (self.qt_thread(), self.shared());
5431        let closed = work::closing(&shared);
5432        let path = sioul_core::quiet::Overrides::default_path();
5433        let previous = sioul_core::quiet::Overrides::load(&path);
5434        let mut next = previous.clone();
5435        next.work_until = None;
5436        next.rest_until = Some(closed.back.timestamp().as_second());
5437        next.rest_from = Some(Zoned::now().timestamp().as_second());
5438        next.first_step = Some(closed.first.text.clone()).filter(|s| !s.is_empty());
5439        next.first_step_task = Some(closed.first.uid.clone()).filter(|s| !s.is_empty());
5440        next.first_step_day = Some(closed.back.date());
5441        if let Err(e) = next.save(&path) {
5442            self.as_mut().set_status(QString::from(&e));
5443            return QString::default();
5444        }
5445        // A session running stops here, and counts.
5446        if sioul_core::timelog::running().is_some() {
5447            work::focus_stop(&qt, &shared, false, "");
5448        }
5449        let screen = json(&closed.closing);
5450        mail::rest(&qt, &shared, previous, next, serde_json::from_str::<serde_json::Value>(&screen).ok().and_then(|v| v["put_away"].as_str().map(str::to_string)).unwrap_or_default());
5451        self.as_mut().set_mode(QString::from(&mode_json()));
5452        show(&qt, &shared);
5453        work::show_work(&qt, &shared);
5454        QString::from(&screen)
5455    }
5456
5457    fn day_review(&self, kind: &QString) -> QString {
5458        QString::from(&crate::reviews::view(&kind.to_string()))
5459    }
5460
5461    fn close_review(self: Pin<&mut Self>, kind: &QString, date: &QString, felt: &QString, mix: &QString, memo: &QString) -> QString {
5462        crate::reviews::close(self, &kind.to_string(), &date.to_string(), &felt.to_string(), &mix.to_string(), &memo.to_string())
5463    }
5464
5465    fn day_review_line(&self, date: &QString) -> QString {
5466        QString::from(&crate::reviews::line(&date.to_string()))
5467    }
5468
5469    fn set_first_step(self: Pin<&mut Self>, text: &QString) {
5470        let text = text.to_string().trim().to_string();
5471        self.change_overrides(|o| {
5472            if o.first_step.as_deref() != Some(text.as_str()) {
5473                // Your own words: the task it named may not be the one any more.
5474                o.first_step_task = None;
5475                o.first_step = Some(text).filter(|t| !t.is_empty());
5476            }
5477        });
5478    }
5479
5480    fn clear_first_step(self: Pin<&mut Self>) {
5481        self.change_overrides(|o| {
5482            o.first_step = None;
5483            o.first_step_task = None;
5484        });
5485    }
5486
5487    fn usual_hours(self: Pin<&mut Self>) {
5488        self.change_overrides(|o| {
5489            o.work_until = None;
5490            o.rest_until = None;
5491            o.work_now = None;
5492        });
5493    }
5494
5495    fn work_until_tonight(self: Pin<&mut Self>) {
5496        let Some(night) = crate::everywhere::tonight(&Zoned::now()) else { return };
5497        self.change_overrides(|o| {
5498            o.rest_until = None;
5499            o.work_until = Some(night);
5500        });
5501    }
5502
5503    fn set_free_time(self: Pin<&mut Self>, on: bool) -> QString {
5504        crate::pauses::set_free(self, on)
5505    }
5506
5507    fn keep_usual_end(self: Pin<&mut Self>) {
5508        crate::pauses::keep_usual_end(self);
5509    }
5510
5511    fn set_free_nothing(self: Pin<&mut Self>, on: bool) {
5512        crate::pauses::set_free_nothing(self, on);
5513    }
5514
5515    fn pause_now(self: Pin<&mut Self>) {
5516        crate::pauses::press(self);
5517    }
5518
5519    fn come_back(self: Pin<&mut Self>) -> QString {
5520        crate::pauses::come_back(self)
5521    }
5522
5523    fn lighten_tomorrow(self: Pin<&mut Self>) {
5524        crate::pauses::lighten_tomorrow(self);
5525    }
5526
5527    fn forget_pause(self: Pin<&mut Self>) {
5528        crate::pauses::forget(self);
5529    }
5530
5531    fn pause_screen(&self) -> QString {
5532        QString::from(&crate::pauses::screen())
5533    }
5534
5535    fn pause_setup(&self) -> QString {
5536        QString::from(&crate::pauses::setup())
5537    }
5538
5539    fn open_dnd(&self, key: &QString) {
5540        crate::dnd::open(&key.to_string());
5541    }
5542
5543    fn dnd_toggle(self: Pin<&mut Self>, on: bool, minutes: i32) {
5544        crate::everywhere::toggle(self, on, minutes);
5545    }
5546
5547    fn reach_choose(self: Pin<&mut Self>, state: &QString, length: &QString) {
5548        crate::everywhere::choose(self, &state.to_string(), &length.to_string());
5549    }
5550
5551    fn let_phone_through(self: Pin<&mut Self>) {
5552        crate::everywhere::let_through(self);
5553    }
5554
5555    fn dnd_setup(&self) -> QString {
5556        QString::from(&crate::everywhere::setup())
5557    }
5558
5559    fn dnd_change(self: Pin<&mut Self>, verb: &QString, json: &QString) -> QString {
5560        let (answer, changed) = crate::everywhere::change(&verb.to_string(), &json.to_string());
5561        // The list or a shared setting changed: your other devices told at once.
5562        if changed {
5563            crate::share::exchange(&self.qt_thread(), &self.shared());
5564        }
5565        QString::from(&answer)
5566    }
5567
5568    fn app_notes_setup(&self) -> QString {
5569        QString::from(&crate::appnotes::setup())
5570    }
5571
5572    fn app_notes_change(&self, verb: &QString, json: &QString) -> QString {
5573        QString::from(&crate::appnotes::change(&verb.to_string(), &json.to_string()))
5574    }
5575
5576    fn calls_through(mut self: Pin<&mut Self>, on: bool, minutes: i32) {
5577        if let Err(e) = crate::calls::press(on, minutes) {
5578            self.as_mut().set_status(QString::from(&e));
5579            return;
5580        }
5581        // The phone's table at once, the status line again, your other devices told.
5582        let (qt, shared) = (self.qt_thread(), self.shared());
5583        std::thread::spawn(move || {
5584            crate::calls::refresh(true);
5585            let _ = qt.queue(|mut sioul| sioul.as_mut().set_mode(QString::from(&mode_json())));
5586            crate::share::exchange(&qt, &shared);
5587        });
5588    }
5589
5590    fn calls_view(&self) -> QString {
5591        QString::from(&crate::calls::view())
5592    }
5593
5594    fn calls_action(self: Pin<&mut Self>, verb: &QString, json: &QString) -> QString {
5595        let answer = crate::calls::act(&verb.to_string(), &json.to_string());
5596        // A number blocked: the table at once, your other devices told (the lists travel).
5597        if answer.contains("\"shared\":true") {
5598            let (qt, shared) = (self.qt_thread(), self.shared());
5599            std::thread::spawn(move || {
5600                crate::calls::refresh(true);
5601                crate::share::exchange(&qt, &shared);
5602            });
5603        }
5604        QString::from(&answer)
5605    }
5606
5607    fn calls_setup(&self) -> QString {
5608        QString::from(&crate::calls::setup())
5609    }
5610
5611    fn phone_messages(self: Pin<&mut Self>, verb: &QString, json: &QString) -> QString {
5612        let (answer, shared) = crate::phonemsgs::call(&verb.to_string(), &json.to_string());
5613        // Seen, a number blocked, the part switched: your other devices told at once.
5614        if shared {
5615            crate::share::exchange(&self.qt_thread(), &self.shared());
5616        }
5617        QString::from(&answer)
5618    }
5619
5620    // texts: SMS phase (b).
5621    fn texts(self: Pin<&mut Self>, verb: &QString, json: &QString) -> QString {
5622        let (answer, shared) = crate::texts::call(&verb.to_string(), &json.to_string());
5623        // A text to send, the part switched: your phone told at once.
5624        if shared {
5625            crate::share::exchange(&self.qt_thread(), &self.shared());
5626        }
5627        QString::from(&answer)
5628    }
5629
5630    fn reaches_view(&self) -> QString {
5631        QString::from(&crate::reaches::view(self.shared().realtime.load(Ordering::Relaxed)))
5632    }
5633
5634    fn reach_talk(mut self: Pin<&mut Self>, verb: &QString, key: &QString) -> QString {
5635        let key = key.to_string();
5636        let said = match verb.to_string().as_str() {
5637            "now" => crate::appnotes::release_talk(&key),
5638            "then" => crate::appnotes::let_talk_then(&key),
5639            _ => String::new(),
5640        };
5641        if !said.is_empty() {
5642            self.as_mut().set_status(QString::from(&said));
5643        }
5644        QString::from(&said)
5645    }
5646
5647    fn reach_card(&self) -> QString {
5648        QString::from(&crate::reaches::now_card(self.shared().realtime.load(Ordering::Relaxed)))
5649    }
5650
5651    fn reaches_set(self: Pin<&mut Self>, row: &QString, words: &QString, shown: &QString) -> QString {
5652        let problem = crate::reaches::set_row(&row.to_string(), &words.to_string(), &shown.to_string());
5653        if problem.is_empty() {
5654            self.attention_changed();
5655        }
5656        QString::from(&problem)
5657    }
5658
5659    fn reaches_preset(self: Pin<&mut Self>, id: &QString) -> QString {
5660        let problem = crate::reaches::preset(&id.to_string());
5661        if problem.is_empty() {
5662            self.attention_changed();
5663        }
5664        QString::from(&problem)
5665    }
5666
5667    fn person_sheet(&self, key: &QString, addresses: &QString) -> QString {
5668        QString::from(&crate::reaches::person(&key.to_string(), &addresses.to_string()))
5669    }
5670
5671    fn person_sheet_change(mut self: Pin<&mut Self>, key: &QString, addresses: &QString, verb: &QString, value: &QString) -> QString {
5672        let answer = crate::reaches::person_change(&key.to_string(), &addresses.to_string(), &verb.to_string(), &value.to_string());
5673        let line = serde_json::from_str::<serde_json::Value>(&answer).ok().and_then(|a| a["line"].as_str().map(str::to_string)).unwrap_or_default();
5674        if !line.is_empty() {
5675            self.as_mut().set_status(QString::from(&line));
5676        }
5677        // The lists and the Always through list travel: your other devices told, the pages shown again.
5678        let (qt, shared) = (self.qt_thread(), self.shared());
5679        show(&qt, &shared);
5680        pim::show_pim(&qt, &shared);
5681        std::thread::spawn(move || {
5682            crate::calls::refresh(true);
5683            crate::everywhere::apply();
5684            crate::share::exchange(&qt, &shared);
5685        });
5686        QString::from(&answer)
5687    }
5688
5689    /// The matrix changed (Settings ▸ What reaches you): the phone's calls'
5690    /// table and its modes made again, the status line, the pages shown
5691    /// again, your other devices told.
5692    fn attention_changed(mut self: Pin<&mut Self>) {
5693        self.as_mut().set_mode(QString::from(&mode_json()));
5694        let (qt, shared) = (self.qt_thread(), self.shared());
5695        show(&qt, &shared);
5696        work::show_work(&qt, &shared);
5697        std::thread::spawn(move || {
5698            crate::calls::refresh(true);
5699            crate::everywhere::apply();
5700            crate::share::exchange(&qt, &shared);
5701        });
5702    }
5703
5704    fn calls_setup_change(self: Pin<&mut Self>, verb: &QString, json: &QString) -> QString {
5705        QString::from(&crate::calls::setup_change(&verb.to_string(), &json.to_string()))
5706    }
5707
5708    /// The overrides changed, then every page shown again for the time it is.
5709    fn change_overrides(mut self: Pin<&mut Self>, change: impl FnOnce(&mut sioul_core::quiet::Overrides)) {
5710        let path = sioul_core::quiet::Overrides::default_path();
5711        let mut overrides = sioul_core::quiet::Overrides::load(&path);
5712        change(&mut overrides);
5713        // What the status line said of the time before is old news now.
5714        let said = overrides.save(&path).err().unwrap_or_default();
5715        self.as_mut().set_status(QString::from(&said));
5716        self.as_mut().set_mode(QString::from(&mode_json()));
5717        let (qt, shared) = (self.qt_thread(), self.shared());
5718        show(&qt, &shared);
5719        work::show_work(&qt, &shared);
5720    }
5721
5722    fn project_rows(&self) -> QString {
5723        QString::from(&crate::projects::rows(&self.shared()))
5724    }
5725
5726    fn project_page(&self, id: &QString) -> QString {
5727        QString::from(&crate::projects::page(&self.shared(), &id.to_string()))
5728    }
5729
5730    fn save_project(self: Pin<&mut Self>, id: &QString, edit: &QString) -> QString {
5731        QString::from(&crate::projects::save(&self.qt_thread(), &self.shared(), &id.to_string(), &edit.to_string()))
5732    }
5733
5734    fn time_page(&self, period: &QString, anchor: &QString, project: &QString) -> QString {
5735        QString::from(&crate::projects::time(&self.shared(), &period.to_string(), &anchor.to_string(), &project.to_string()))
5736    }
5737
5738    fn note_time(self: Pin<&mut Self>, edit: &QString) -> QString {
5739        QString::from(&crate::projects::note_time(&self.qt_thread(), &self.shared(), &edit.to_string()))
5740    }
5741
5742    fn stopped(&self) -> QString {
5743        QString::from(&crate::work::stopped(&self.shared()))
5744    }
5745
5746    fn set_stopped(self: Pin<&mut Self>, text: &QString) -> QString {
5747        QString::from(&crate::work::set_stopped(&self.qt_thread(), &self.shared(), &text.to_string()))
5748    }
5749
5750    fn change_time(self: Pin<&mut Self>, key: &QString, edit: &QString) -> QString {
5751        QString::from(&crate::projects::change_time(&self.qt_thread(), &self.shared(), &key.to_string(), &edit.to_string()))
5752    }
5753
5754    fn task_choices(&self, keep: &QString) -> QString {
5755        QString::from(&crate::projects::task_choices(&self.shared(), &keep.to_string()))
5756    }
5757
5758    fn remove_time(self: Pin<&mut Self>, key: &QString) -> QString {
5759        QString::from(&crate::projects::remove_time(&self.qt_thread(), &self.shared(), &key.to_string()))
5760    }
5761
5762    fn take_invoices(self: Pin<&mut Self>) -> QString {
5763        QString::from(&crate::projects::take_invoices(&self.shared()))
5764    }
5765
5766    fn make_invoice(self: Pin<&mut Self>, id: &QString) -> QString {
5767        QString::from(&crate::projects::make_invoice(&self.qt_thread(), &self.shared(), &id.to_string()))
5768    }
5769
5770    fn invoice_again(&self, number: &QString) -> QString {
5771        QString::from(&crate::projects::invoice_again(&number.to_string()))
5772    }
5773
5774    fn set_invoice_paid(self: Pin<&mut Self>, number: &QString, paid: bool) -> QString {
5775        QString::from(&crate::projects::set_paid(&self.qt_thread(), &self.shared(), &number.to_string(), paid))
5776    }
5777
5778    fn make_linked(self: Pin<&mut Self>, kind: &QString, from: &QString, key: &QString, start: f64) -> QString {
5779        QString::from(&work::make_linked(&self.qt_thread(), &self.shared(), &kind.to_string(), &from.to_string(), &key.to_string(), start))
5780    }
5781
5782    fn task_from_mail(self: Pin<&mut Self>, key: &QString) -> QString {
5783        QString::from(&work::task_from_mail(&self.qt_thread(), &self.shared(), &key.to_string()))
5784    }
5785
5786    fn note_from_mail(self: Pin<&mut Self>, key: &QString) -> QString {
5787        QString::from(&work::note_from_mail(&self.qt_thread(), &self.shared(), &key.to_string()))
5788    }
5789
5790    fn note_from_event(self: Pin<&mut Self>, key: &QString, start: f64) -> QString {
5791        QString::from(&work::note_from_event(&self.qt_thread(), &self.shared(), &key.to_string(), start))
5792    }
5793
5794    fn task_from_event(self: Pin<&mut Self>, key: &QString, start: f64) -> QString {
5795        QString::from(&work::task_from_event(&self.qt_thread(), &self.shared(), &key.to_string(), start))
5796    }
5797
5798    fn task_from_line(self: Pin<&mut Self>, path: &QString, line: i32) -> QString {
5799        QString::from(&work::task_from_line(&self.qt_thread(), &self.shared(), &path.to_string(), line))
5800    }
5801
5802    fn draft_for_task(self: Pin<&mut Self>, uid: &QString) -> QString {
5803        QString::from(&work::draft_for_task(&self.qt_thread(), &self.shared(), &uid.to_string()))
5804    }
5805
5806    fn mail_note(self: Pin<&mut Self>, path: &QString) -> QString {
5807        QString::from(&work::mail_note(&self.qt_thread(), &self.shared(), &path.to_string()))
5808    }
5809
5810    fn settings(&self, view: &QString) -> QString {
5811        let config = load_config();
5812        let lists: Vec<(String, String)> = sioul_core::tasks::lists().into_iter().filter(|c| !c.read_only).map(|c| (format!("{}/{}", c.account, c.id), c.label(&config, tr()))).collect();
5813        let store = config.notes_root_path().and_then(|root| ProjectStore::load(&root).ok());
5814        let mut rows = sioul_core::settings::for_view(&view.to_string(), &config, tr(), &lists, store.as_ref());
5815        // The categories are the tasks': the core does not read them.
5816        if view.to_string() == "tasks" {
5817            let categories = work::categories_in_use(&self.shared());
5818            if !categories.is_empty() {
5819                let at = rows.iter().position(|r| r.key == "tasks.kind").map_or(rows.len(), |i| i + 1);
5820                rows.insert(at, sioul_core::settings::categories(tr(), categories));
5821            }
5822        }
5823        // Whether the keyring keeps a key: the core does not read the keyring.
5824        for row in rows.iter_mut().filter(|r| r.key == "ai_key") {
5825            row.value = config::SettingValue::Bool(sioul_sync::shield_ai::api_key().is_some());
5826        }
5827        for row in rows.iter_mut().filter(|r| r.key == "github_token") {
5828            row.value = config::SettingValue::Bool(sioul_sync::github::token().is_some());
5829        }
5830        // Whether the session starts the reminders' watcher: the desktop's own entry says.
5831        for row in rows.iter_mut().filter(|r| r.key == "reminders_closed") {
5832            row.value = config::SettingValue::Bool(crate::remind::background());
5833        }
5834        // On a phone, whether Android lets the events' reminders come on time.
5835        if cfg!(target_os = "android") {
5836            for row in rows.iter_mut().filter(|r| r.key == "reminders.before_event") {
5837                row.help = format!("{} {}", row.help, tr().text(if crate::alarms::exact() { "set-reminders-before-exact" } else { "set-reminders-before-inexact" }, None));
5838            }
5839        }
5840        for row in rows.iter_mut().filter(|r| r.key == "passwords_shown") {
5841            row.value = config::SettingValue::Bool(work::view_flag(&self.shared(), "passwords-shown"));
5842        }
5843        // colour: what this screen gets, said under its switch; neither line on a
5844        // phone, whose sites open in the browser (docs/colour.md).
5845        if cfg!(target_os = "android") {
5846            rows.retain(|r| r.key != "screen_colours" && r.key != "calmer_colours");
5847        }
5848        #[cfg(not(target_os = "android"))]
5849        for row in rows.iter_mut().filter(|r| r.key == "screen_colours") {
5850            use crate::colour::Said;
5851            let said = match crate::colour::said() {
5852                Said::Converted(name) if !name.is_empty() => say("colour-said-converted", &[("profile", name)]),
5853                Said::Converted(_) => tr().text("colour-said-converted-unnamed", None),
5854                Said::Desktop => tr().text("colour-said-desktop", None),
5855                Said::Plain => tr().text("colour-said-plain", None),
5856                Said::Off | Said::Unknown => String::new(),
5857            };
5858            if !said.is_empty() {
5859                row.help = format!("{} {said}", row.help);
5860            }
5861        }
5862        // The phone's home screen card: its details, this device's own (homecard.rs).
5863        if crate::homecard::has_setting()
5864            && let Some(at) = rows.iter().position(|r| r.key == "passwords_shown")
5865        {
5866            let row = crate::homecard::setting(&rows[at].group, &rows[at].section, !work::view_flag(&self.shared(), crate::homecard::PLAIN));
5867            rows.insert(at + 1, row);
5868        }
5869        QString::from(&json(&rows))
5870    }
5871
5872    fn set_setting(self: Pin<&mut Self>, key: &QString, value: &QString) -> QString {
5873        self.change_setting(key, &QString::default(), value)
5874    }
5875
5876    fn change_setting(mut self: Pin<&mut Self>, key: &QString, shown: &QString, value: &QString) -> QString {
5877        let key = key.to_string();
5878        // What the row showed: what its change is set over the files from (none: the value as sent).
5879        // One that does not read is refused, said: never the whole value written over what changed meanwhile.
5880        let shown: Option<config::SettingValue> = match Some(shown.to_string()).filter(|s| !s.is_empty()).map(|s| serde_json::from_str(&s)) {
5881            Some(Ok(shown)) => Some(shown),
5882            Some(Err(e)) => return QString::from(&format!("{key}: {e}")),
5883            None => None,
5884        };
5885        let parsed: Result<config::SettingValue, String> = serde_json::from_str(&value.to_string()).map_err(|e| e.to_string());
5886        let result = parsed.and_then(|v| match (key.as_str(), v) {
5887            // The AI's key goes to the keyring, never to the configuration; an empty one is forgotten.
5888            ("ai_key", config::SettingValue::Text(secret)) => sioul_sync::shield_ai::save_api_key(&secret).map_err(|e| e.sentence(tr(), "Anthropic")),
5889            ("github_token", config::SettingValue::Text(secret)) => sioul_sync::github::save_token(&secret).map_err(|e| e.sentence(tr(), "GitHub")),
5890            ("task_categories", config::SettingValue::Rename(change)) => work::rename_category(&self.qt_thread(), &self.shared(), &change.from, &change.to),
5891            ("reminders_closed", config::SettingValue::Bool(on)) => crate::remind::set_background(on),
5892            ("passwords_shown", config::SettingValue::Bool(on)) => {
5893                work::set_view_flag(&self.shared(), "passwords-shown", on);
5894                Ok(())
5895            }
5896            ("home_card_details", config::SettingValue::Bool(on)) => {
5897                work::set_view_flag(&self.shared(), crate::homecard::PLAIN, !on);
5898                crate::homecard::now();
5899                Ok(())
5900            }
5901            (_, v) => sioul_core::settings::change(&config_path(), &key, shown.as_ref(), &v),
5902        });
5903        if let Err(e) = result {
5904            return QString::from(&e);
5905        }
5906        // What the setting changes is shown again.
5907        let (qt, shared) = (self.qt_thread(), self.shared());
5908        if let Some(project) = key.strip_prefix("project.").and_then(|k| k.strip_suffix(".routes")) {
5909            retie_project(project.to_string());
5910        }
5911        // A list, a calendar, an address book renamed or deleted: told to the server now.
5912        if key == "collections"
5913            && let Some(account) = serde_json::from_str::<serde_json::Value>(&value.to_string()).ok().and_then(|v| v["from"].as_str().and_then(|f| f.split('/').nth(1)).map(str::to_string))
5914        {
5915            pim::nudge(&shared, &account);
5916        }
5917        // colour: the sites' colours travel with the reading's settings (docs/colour.md).
5918        if key.starts_with("reading.") || key == "theme" || key == "places_named" || key == "screen_colours" || key == "calmer_colours" {
5919            self.as_mut().set_reading(QString::from(&reading_json()));
5920        }
5921        if key.contains("fetch_minutes") {
5922            update_paces(&shared);
5923        }
5924        // Hours or days off changed: what the time is for now, and what the Porch asks.
5925        if key.starts_with("window") || key == "time_off" {
5926            self.as_mut().set_mode(QString::from(&mode_json()));
5927        }
5928        if key.starts_with("mail.") {
5929            mail::show_mail(&qt, &shared);
5930        }
5931        // GitHub: started, brought again, or stopped.
5932        if key.starts_with("github") {
5933            if load_config().github.enabled {
5934                crate::github::start(&qt, &shared);
5935            } else {
5936                crate::github::stop(&shared);
5937            }
5938        }
5939        show(&qt, &shared);
5940        pim::show_pim(&qt, &shared);
5941        work::show_work(&qt, &shared);
5942        QString::default()
5943    }
5944
5945    fn forced_theme(&self) -> QString {
5946        QString::from(&std::env::var("SIOUL_THEME").unwrap_or_default())
5947    }
5948
5949    fn follow_icons(&self, dark: bool, chosen: bool) {
5950        crate::desktop::follow(dark, chosen);
5951    }
5952
5953    fn plain_notice(&self, title: &QString, body: &QString) {
5954        let (title, body) = (title.to_string(), body.to_string());
5955        std::thread::spawn(move || {
5956            if let Err(e) = sioul_sync::notify::remind(&title, &body, None) {
5957                eprintln!("{e}");
5958            }
5959        });
5960    }
5961
5962    fn set_realtime_mode(mut self: Pin<&mut Self>, on: bool) {
5963        let shared = self.shared();
5964        shared.realtime.store(on, Ordering::Relaxed);
5965        shared.opened_anyway.store(on || shared.opened_anyway.load(Ordering::Relaxed), Ordering::Relaxed);
5966        update_paces(&shared);
5967        self.as_mut().set_realtime(on);
5968        let line = tr().text(if on { "realtime-on" } else { "realtime-off" }, None);
5969        self.as_mut().set_status(QString::from(&line));
5970        show(&self.qt_thread(), &shared);
5971    }
5972
5973    fn grab_phone(&self) -> bool {
5974        std::env::var_os("SIOUL_GRAB_PHONE").is_some()
5975    }
5976
5977    fn grab_steps(&self) -> QString {
5978        let steps = std::env::var("SIOUL_GRAB_STEPS").unwrap_or_else(|_| "pages".into());
5979        // The other steps archive, delete and send mail, answer invitations:
5980        // against test servers, from the test build only. "demo", "phone" and
5981        // "drag" take pictures alone; "taskform" makes tasks and rates them, "review"
5982        // closes the day with its review (docs/reviews.md), "site-open", "site-quit" and
5983        // "site-during" open the test site, close its pages and the window
5984        // (tools/check-sites.py quit), "site-share" shares its screen (tools/check-sites.py share),
5985        // "site-devices" changes the devices of its calls (tools/check-sites.py devices),
5986        // colour: "site-colour" takes its pictures in the screen's colours (tools/check-colour.py),
5987        // "rail" shows the places with their icons alone and with their
5988        // names (main.qml), "pauses" free time and the pause (docs/pauses.md), "blocks" a task pinned to a
5989        // time and left to the plan again (docs/tasks.md), "unsubscribe" a newsletter open
5990        // with its Unsubscribe button (docs/client.md), "attention" what reaches you, by time,
5991        // by person, Sioul's own, its exceptions and do not disturb, and a person's sheet
5992        // (docs/attention.md), "line" the status line with all it
5993        // may hold, a new draft deleted and taken back (docs/design.md), "share-panel" the
5994        // sharing's tab before sharing, its two ways (docs/database.md), "join" the Porch's card
5995        // and the sign-in's panel at each step, stood in for, and a sharing device's "Add a
5996        // device", "Forget it now" and a phone's question whether it replaces another
5997        // (docs/database.md), "share-send" sharing
5998        // started in the profile, the build, the send's switch and "Send everything again",
5999        // then "Stop sharing" asking first, cancelled (docs/database.md), "spam" the spam
6000        // filter's settings and the words it puts beside mail (docs/spam-filter.md), "mail-search" the
6001        // search by conditions, its results, a selection held over a folder (docs/client.md), "mail-filters"
6002        // the mail filters in Mail's ⚙, their editor, a run's preview, a search made a filter, "security-key"
6003        // the setup of the demo's software security key and a message signed at Send with it (docs/client.md),
6004        // "calls" the calls a phone declined on a computer's Porch and a person's calls of the month (docs/porch.md),
6005        // "words" Settings ▸ Words, a line's lists changed and taken back (docs/words.md),
6006        // "health-gpg" a prescription's medicines and their takes as rows, and the security key's
6007        // GnuPG steps as buttons with their commands to copy (docs/health.md, docs/client.md),
6008        // "movetask" one of the demo's tasks moved to its other list (docs/tasks.md), "compose"
6009        // the writing window, an answer and a new message, its drafts made and deleted (docs/building.md),
6010        // "texts" the Texts page, its conversations and a text's states (docs/texts.md),
6011        // "ai" Settings ▸ AI agents, a project opened to agents on its page, an agent's text
6012        // draft in the Texts page used, nothing sent (docs/mcp.md),
6013        // "health" the Health page, its week, its settings (⚙), a block's menu and its change
6014        // for one day, closed unsaved (docs/health.md), "tiles" the costs as tiles in a task's
6015        // form, an event's and "How was it?", at three widths (docs/capacity.md), "attachments"
6016        // a message's attachments unfolded, a phone's words with "phone", nothing opened (docs/client.md),
6017        // "projects-file" the projects' file under its first name renamed from Settings
6018        // (make-demo.py --old-projects, docs/notes-folder.md), "memory" every page as "pages" opens
6019        // them, then the profile's sites, held at each moment measured (tools/measure-memory.py),
6020        // "bitwarden-wait" the unlock dialog waiting for Bitwarden, then its answer (no account
6021        // set: no server asked), "tray" the window hidden as to the tray and brought back,
6022        // "notes-shared" a note changed on another device while shown, read then typed in
6023        // (docs/database.md, "Notes"), "reach" the status line's button of what reaches you and
6024        // its card, each state pressed and back to As usual (docs/do-not-disturb.md), on a demo profile only.
6025        if cfg!(feature = "insecure-test-tls") || steps == "demo" || steps == "phone" || steps == "drag" || (["taskform", "review", "site-open", "site-quit", "site-during", "site-share", "site-devices", "site-colour", "rail", "pauses", "blocks", "unsubscribe", "attention", "line", "share-panel", "join", "share-send", "spam", "mail-search", "mail-filters", "security-key", "health-gpg", "calls", "words", "movetask", "compose", "texts", "ai", "health", "tiles", "attachments", "projects-file", "memory", "bitwarden-wait", "tray", "notes-shared", "microsoft", "reach"].contains(&steps.as_str()) && offline()) {
6026            return QString::from(&steps);
6027        }
6028        QString::from("pages")
6029    }
6030}
6031
6032// Sharing, part by part, and the versions kept before other devices' changes (`share`).
6033impl qobject::Sioul {
6034    fn set_notes_carried(self: Pin<&mut Self>, carried: bool) -> QString {
6035        QString::from(&crate::share::set_notes_carried(carried))
6036    }
6037
6038    fn set_share_part(self: Pin<&mut Self>, part: &QString, on: bool) -> QString {
6039        let problem = crate::share::set_part(&part.to_string(), on);
6040        if problem.is_empty() {
6041            crate::share::exchange(&self.qt_thread(), &self.shared());
6042        }
6043        QString::from(&problem)
6044    }
6045
6046    fn share_history(self: Pin<&mut Self>, filter: &QString) {
6047        let (qt, filter) = (self.qt_thread(), filter.to_string());
6048        std::thread::spawn(move || {
6049            let listed = crate::share::history(&filter);
6050            let _ = qt.queue(move |mut sioul| sioul.as_mut().share_listed(QString::from(&listed)));
6051        });
6052    }
6053
6054    fn share_put_back_preview(&self, part: &QString, file: &QString, stamp: &QString) -> QString {
6055        QString::from(&crate::share::put_back_preview(&part.to_string(), &file.to_string(), &stamp.to_string()))
6056    }
6057
6058    fn share_put_back(self: Pin<&mut Self>, part: &QString, file: &QString, stamp: &QString) {
6059        let (qt, shared) = (self.qt_thread(), self.shared());
6060        let (part, file, stamp) = (part.to_string(), file.to_string(), stamp.to_string());
6061        // Off the window's thread: it waits for an exchange running, and copies a file whole.
6062        std::thread::spawn(move || {
6063            // Your calendar's item or contact set aside: written back, then sent by its account's sync.
6064            let problem = if part == sioul_sync::history::ACCOUNTS {
6065                match crate::share::put_back_item(&file, &stamp) {
6066                    Ok(account) => {
6067                        crate::pim::nudge(&shared, &account);
6068                        String::new()
6069                    }
6070                    Err(e) => e,
6071                }
6072            } else {
6073                crate::share::put_back(&part, &file, &stamp)
6074            };
6075            let said = if problem.is_empty() { crate::share::put_back_said(&file, &stamp) } else { String::new() };
6076            let store = crate::share::store_of(&file);
6077            let done = problem.clone();
6078            let _ = qt.queue(move |mut sioul| {
6079                if !said.is_empty() {
6080                    sioul.as_mut().set_status(QString::from(&said));
6081                    // The pages showing it read it again.
6082                    sioul.as_mut().shared_in(QString::from(&store));
6083                }
6084                sioul.as_mut().share_put_back_done(QString::from(&done));
6085            });
6086            if problem.is_empty() {
6087                crate::work::show_work(&qt, &shared);
6088                crate::share::exchange(&qt, &shared);
6089            }
6090        });
6091    }
6092
6093    fn share_confirm_gone(self: Pin<&mut Self>, store: &QString) -> QString {
6094        let problem = crate::share::confirm_gone(&store.to_string());
6095        if problem.is_empty() {
6096            crate::share::exchange(&self.qt_thread(), &self.shared());
6097        }
6098        QString::from(&problem)
6099    }
6100
6101    fn share_withheld(&self) -> QString {
6102        QString::from(&json(&crate::share::withheld(false)))
6103    }
6104
6105    fn share_put_back_withheld(self: Pin<&mut Self>, store: &QString) {
6106        let (qt, shared, store) = (self.qt_thread(), self.shared(), store.to_string());
6107        std::thread::spawn(move || {
6108            let problem = crate::share::put_back_withheld(&store);
6109            let _ = qt.queue(move |mut sioul| sioul.as_mut().share_withheld_back(QString::from(&problem)));
6110            // Found back by the next exchange, as any file written here: nothing leaves.
6111            crate::work::show_work(&qt, &shared);
6112            crate::share::exchange(&qt, &shared);
6113        });
6114    }
6115
6116    fn share_take_out_withheld(self: Pin<&mut Self>, store: &QString) -> QString {
6117        let problem = crate::share::take_out_withheld(&store.to_string());
6118        if problem.is_empty() {
6119            crate::share::exchange(&self.qt_thread(), &self.shared());
6120        }
6121        QString::from(&problem)
6122    }
6123
6124    fn device_files(&self, id: &QString) -> QString {
6125        QString::from(&crate::share::device_files(&id.to_string()))
6126    }
6127
6128    fn device_remove_files(self: Pin<&mut Self>, id: &QString) -> QString {
6129        let problem = crate::share::remove_device_files(&id.to_string());
6130        // The doses read what counts now; the server's copies go at the next sending.
6131        crate::health::show_health(&self.qt_thread(), &self.shared());
6132        crate::share::exchange(&self.qt_thread(), &self.shared());
6133        QString::from(&problem)
6134    }
6135
6136    fn texts_attach(self: Pin<&mut Self>, json: &QString) {
6137        let (qt, json) = (self.qt_thread(), json.to_string());
6138        std::thread::spawn(move || {
6139            let attached = crate::texts::attach(&json);
6140            let _ = qt.queue(move |mut sioul| sioul.as_mut().texts_attached(QString::from(&attached)));
6141        });
6142    }
6143
6144    fn share_estimate(self: Pin<&mut Self>, part: &QString) {
6145        let (qt, part) = (self.qt_thread(), part.to_string());
6146        std::thread::spawn(move || {
6147            let counted = crate::share::estimate(&part);
6148            let _ = qt.queue(move |mut sioul| sioul.as_mut().share_estimated(QString::from(&counted)));
6149        });
6150    }
6151
6152    fn spam_status(&self) -> QString {
6153        QString::from(&crate::spam::status())
6154    }
6155
6156    fn spam_train(self: Pin<&mut Self>, named: &QString) -> QString {
6157        let named: Vec<String> = serde_json::from_str(&named.to_string()).unwrap_or_default();
6158        QString::from(&crate::spam::train(self.qt_thread(), self.shared(), named))
6159    }
6160
6161    fn spam_stop(self: Pin<&mut Self>) {
6162        crate::spam::stop(self.qt_thread());
6163    }
6164
6165    fn mail_filters(&self) -> QString {
6166        QString::from(&crate::filters::view())
6167    }
6168
6169    fn set_mail_filters(self: Pin<&mut Self>, filters: &QString, shown: &QString) -> QString {
6170        QString::from(&crate::filters::save(&filters.to_string(), &shown.to_string()))
6171    }
6172
6173    fn mail_filter_said(&self, filter: &QString) -> QString {
6174        QString::from(&crate::filters::said(&filter.to_string()))
6175    }
6176
6177    fn mail_filter_try(self: Pin<&mut Self>, filter: &QString) {
6178        crate::filters::try_one(self.qt_thread(), filter.to_string());
6179    }
6180
6181    fn mail_filters_preview(self: Pin<&mut Self>) {
6182        crate::filters::preview(self.qt_thread());
6183    }
6184
6185    fn mail_filters_run(self: Pin<&mut Self>) -> QString {
6186        QString::from(&crate::filters::run(&self.qt_thread(), &self.shared()))
6187    }
6188
6189    fn new_mail_filter(self: Pin<&mut Self>, given: &QString) -> QString {
6190        QString::from(&crate::filters::new_from(&given.to_string()))
6191    }
6192
6193    fn set_share_backup(self: Pin<&mut Self>, on: bool) -> QString {
6194        let problem = crate::share::set_backup(on);
6195        // Looked for and fetched with the exchange, off the window's thread.
6196        if problem.is_empty() {
6197            crate::share::exchange(&self.qt_thread(), &self.shared());
6198        }
6199        QString::from(&problem)
6200    }
6201
6202    fn set_share_send(self: Pin<&mut Self>, on: bool) -> QString {
6203        let problem = crate::share::set_send(on);
6204        // Switched on: what changed here goes with the next exchange, now.
6205        if problem.is_empty() && on {
6206            crate::share::exchange(&self.qt_thread(), &self.shared());
6207        }
6208        QString::from(&problem)
6209    }
6210    fn share_send_again(self: Pin<&mut Self>) {
6211        crate::share::send_again(&self.qt_thread(), &self.shared());
6212    }
6213    fn share_backup_place(self: Pin<&mut Self>, place: &QString) -> QString {
6214        let problem = crate::share::set_backup_place(&place.to_string());
6215        if problem.is_empty() {
6216            crate::share::exchange(&self.qt_thread(), &self.shared());
6217        }
6218        QString::from(&problem)
6219    }
6220
6221    fn start_sharing_on_server(self: Pin<&mut Self>, account: &QString, place: &QString, passphrase: &QString, again: &QString) {
6222        crate::share::start_on_server(&self.qt_thread(), &self.shared(), account.to_string(), place.to_string(), passphrase.to_string(), again.to_string());
6223    }
6224
6225    fn join_sign_in(self: Pin<&mut Self>, server: &QString) {
6226        crate::join::sign_in(&self.qt_thread(), server.to_string());
6227    }
6228
6229    fn join_open_again(self: Pin<&mut Self>) {
6230        crate::join::open_again(&self.qt_thread());
6231    }
6232
6233    fn join_look_again(self: Pin<&mut Self>) {
6234        crate::join::look_again(&self.qt_thread());
6235    }
6236
6237    fn join_cancel(self: Pin<&mut Self>) {
6238        crate::join::cancel(&self.qt_thread());
6239    }
6240
6241    fn join_status(&self) -> QString {
6242        QString::from(&crate::join::status())
6243    }
6244
6245    fn join_folder(self: Pin<&mut Self>, place: &QString, passphrase: &QString, again: &QString) {
6246        crate::join::join(&self.qt_thread(), &self.shared(), place.to_string(), passphrase.to_string(), again.to_string());
6247    }
6248
6249    fn join_done(self: Pin<&mut Self>) {
6250        crate::join::done(&self.qt_thread());
6251    }
6252
6253    fn join_replace(self: Pin<&mut Self>, device: &QString, yes: bool) -> QString {
6254        QString::from(&crate::join::replace(&self.qt_thread(), &self.shared(), &device.to_string(), yes))
6255    }
6256}
6257
6258#[cfg(test)]
6259mod tests {
6260    use super::*;
6261
6262    #[test]
6263    fn file_addresses_reach_the_file() {
6264        assert_eq!(file_url(Path::new("/tmp/Réunion #2 100%.pdf")), "file:///tmp/R%C3%A9union%20%232%20100%25.pdf");
6265        assert_eq!(local_path(&file_url(Path::new("/tmp/Réunion #2 100%.pdf"))), PathBuf::from("/tmp/Réunion #2 100%.pdf"));
6266        assert_eq!(local_path("file://localhost/tmp/a%20b"), PathBuf::from("/tmp/a b"));
6267        assert_eq!(local_path("/tmp/plain"), PathBuf::from("/tmp/plain"));
6268        // Windows' forms, both ways.
6269        assert_eq!(windows_slashed(r"C:\Users\me\a b.pdf"), "/C:/Users/me/a b.pdf");
6270        assert_eq!(windows_slashed(r"\\?\C:\Users\me"), "/C:/Users/me");
6271        assert_eq!(windows_slashed(r"\\server\share\a"), "//server/share/a");
6272        assert_eq!(windows_slashed(r"\\?\UNC\server\share\a"), "//server/share/a");
6273        assert_eq!(windows_path("/C:/Users/me/a b.pdf"), r"C:\Users\me\a b.pdf");
6274        assert_eq!(windows_path("server/share/a"), r"\\server\share\a");
6275    }
6276
6277    /// A phone makes its notices through Java only (docs/android.md,
6278    /// "Notifications"): every caller of `sioul_sync::notify` whose error
6279    /// would go to the status line (`tell`, `set_status`) stops on Android
6280    /// before it tries, so that a phone's status line never says
6281    /// "notifications are not made on Android yet" (the owner's new phone,
6282    /// 16 October 2026: the pause to move, on by default, said it after 45
6283    /// minutes). Read in the sources, as the cfg of a phone's build cannot run here.
6284    #[test]
6285    fn no_notice_tried_on_a_phone_says_its_error_in_the_status_line() {
6286        let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
6287        let calls = ["notify::remind(", "notify::code(", "notify::remind_offering(", "notify::mail("];
6288        let mut checked = Vec::new();
6289        for entry in std::fs::read_dir(&src).unwrap() {
6290            let path = entry.unwrap().path();
6291            if path.extension().is_none_or(|e| e != "rs") {
6292                continue;
6293            }
6294            let text = std::fs::read_to_string(&path).unwrap();
6295            // The tests' own lines are no callers.
6296            let text = text.split("#[cfg(test)]\nmod tests").next().unwrap_or_default();
6297            let lines: Vec<&str> = text.lines().collect();
6298            for (at, line) in lines.iter().enumerate() {
6299                if line.trim_start().starts_with("//") || !calls.iter().any(|call| line.contains(call)) {
6300                    continue;
6301                }
6302                // Its error said in the status line, within the lines after it.
6303                if !lines[at..(at + 4).min(lines.len())].iter().any(|l| l.contains("tell(") || l.contains("set_status(")) {
6304                    continue;
6305                }
6306                // The function it is in, from its first line.
6307                let start = (0..at).rev().find(|i| ["fn ", "pub fn ", "pub(crate) fn "].iter().any(|f| lines[*i].trim_start().starts_with(f))).unwrap_or(0);
6308                let before = lines[start..at].join("\n");
6309                let name = format!("{}:{}", path.file_name().unwrap().to_string_lossy(), at + 1);
6310                assert!(before.contains("cfg!(target_os = \"android\")") || before.contains("cfg(not(target_os = \"android\"))"), "{name}: a notice whose error goes to the status line is tried on a phone too");
6311                checked.push(name);
6312            }
6313        }
6314        // The pause to move, the codes, the sites' two, the doses due while closed.
6315        assert!(checked.len() >= 5, "{checked:?}");
6316    }
6317
6318    #[test]
6319    fn programs_are_not_opened() {
6320        for name in ["setup.exe", "Facture.PDF.exe", "invoice.exe.", "invoice.exe ", "run.sh", "x.desktop", "a.lnk", "b.js", "c.terminal", "d.iso"] {
6321            assert!(sioul_core::links::is_program(name), "{name}");
6322        }
6323        for name in ["facture.pdf", "photo.jpeg", "notes.txt", "archive.zip", "exe", "README"] {
6324            assert!(!sioul_core::links::is_program(name), "{name}");
6325        }
6326    }
6327
6328    #[test]
6329    fn new_sentences_read_in_both_languages() {
6330        for language in ["en", "fr"] {
6331            let tr = Translator::new(language);
6332            for id in ["attachment-program", "budget-origin-mail", "budget-origin-hand", "note-outside-notes", "setting-unknown-key", "reminders-entry-name", "reminders-entry-comment", "bank-rule-gone", "budget-line-gone", "mail-no-such-folder", "mail-not-in-accounts", "mail-move-needs-to", "list-read-only-mark", "import-bad-key"] {
6333                assert_ne!(tr.text(id, None), id, "{language}: {id}");
6334            }
6335        }
6336    }
6337
6338    #[test]
6339    fn attachment_names_stay_in_their_folder() {
6340        assert_eq!(one_file_name("a/b\\c.pdf"), "a_b_c.pdf");
6341        assert_eq!(one_file_name(".."), "attachment");
6342        assert_eq!(one_file_name(""), "attachment");
6343        if cfg!(windows) {
6344            assert_eq!(one_file_name("C:evil.txt"), "C_evil.txt");
6345            assert_eq!(one_file_name("con.txt"), "_con.txt");
6346        } else {
6347            assert_eq!(one_file_name("C:evil.txt"), "C:evil.txt");
6348        }
6349    }
6350}