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