Skip to main content

sioul_app/
lib.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2// Copyright © 2026 Aurélien Pierre
3
4//! `sioul-app`: Sioul's window. The Porch, budgets and accounts, over the same
5//! core as the command line; the window only lays out what the core decided.
6//! A library: the desktop program (main.rs) runs it, and so does Android's
7//! (android/main.cpp, docs/android.md).
8//!
9//! The window is made of QML pages, in `crates/sioul-app/qml/` (each one is
10//! described in the QML reference:
11//! <https://sioul.aurelienpierre.com/dev/qml.html>), and of this crate's
12//! Rust. CXX-Qt joins the two: [`backend`](backend/index.html) declares the
13//! QML type `Sioul`, whose properties the pages read and whose functions they
14//! call, and [`desktop`](desktop/index.html) a second one, `Desktop`, for the
15//! desktop's icons and title bar.
16//! What the pages show comes from [sioul-core](../sioul_core/index.html), as
17//! JSON with every sentence already in your language; what comes from servers
18//! is fetched with [sioul-sync](../sioul_sync/index.html) on other threads,
19//! so that the window never waits.
20//!
21//! Every module here is private: only the window uses them. This reference
22//! shows them all.
23//!
24//! # Where to start reading
25//!
26//! - [`run`]: the window, from the program's start to its end.
27//! - [`backend`](backend/index.html): the object `Sioul`. In its bridge, the
28//!   module `qobject`, each `#[qproperty]` is a value the pages read, and are
29//!   told about when it changes; each `#[qinvokable]` is a function a page
30//!   calls, its name in camelCase in QML (`text_with` is
31//!   `sioul.textWith(…)`). `SioulRust` holds what the object keeps.
32//! - [`work`](work/index.html) (tasks, notes, links, focus),
33//!   [`mail`](mail/index.html) and [`pim`](pim/index.html) (contacts and
34//!   calendars): the same pattern, one area at a time. Most functions there
35//!   return JSON for a page, or run their work off the window's thread and
36//!   hand the result back to it.
37//!
38//! Names such as docs/android.md are the design notes in the repository's
39//! `docs/` folder. The website shows them too, under the same name:
40//! <https://sioul.aurelienpierre.com/dev/android.html>.
41//!
42//! # Modules, by theme
43//!
44//! ## The window's objects
45//!
46//! - [`backend`](backend/index.html): the object behind the window, `Sioul`, and the work it runs off the window's thread.
47//! - [`desktop`](desktop/index.html): the desktop's icon theme, and where the desktop puts a window's buttons.
48//! - [`hours`](hours/index.html): what now is for (work, admin, leisure, a meal, sleep), as the pages and notifications ask it.
49//! - [`colour`](colour/index.html): the sites in the screen's own colours, and calmer colours (on computers).
50//!
51//! ## Mail
52//!
53//! - [`mail`](mail/index.html): the mail client: folders and their messages, what you do to them, drafts and sending.
54//! - [`mailsearch`](mailsearch/index.html): searching the mail by conditions, here and on the servers.
55//! - [`gmail`](gmail/index.html): Google's mail, signed in on Google's page.
56//! - [`crypto`](crypto/index.html): OpenPGP: protected messages opened for the reader, drafts signed or encrypted, your keys.
57//! - [`securitykey`](securitykey/index.html): your security key: signing at Send, opening on request, its PIN held a while, its setup.
58//! - [`senders`](senders/index.html): who someone is to you: where they stand, and why.
59//! - [`mailnote`](mailnote/index.html): new mail told as a notification, at the times it may come.
60//! - [`letters`](letters/index.html): paper letters: scans read in the background, each a card in the Porch.
61//! - [`outside`](outside/index.html): writing from other applications: shared files and text, `mailto:` links.
62//! - [`sites`](sites/index.html): sites kept in Sioul: the list, their notifications, logins filled from Bitwarden.
63//! - [`spam`](spam/index.html): Sioul's own spam filter in the mail settings, and on a computer "Train now" (docs/spam-filter.md).
64//!
65//! ## Tasks, projects and time
66//!
67//! - [`work`](work/index.html): tasks, notes, links and focus.
68//! - [`blocks`](blocks/index.html): time blocks: a task pinned to a time, as an event in a calendar.
69//! - [`capacity`](capacity/index.html): what a day holds, as the window uses it.
70//! - [`projects`](projects/index.html): projects, time and invoices.
71//! - [`github`](github/index.html): GitHub's issues and pull requests, in the local "GitHub" list.
72//! - [`reviews`](reviews/index.html): the two rituals: closing the work day, and closing the day before sleep.
73//! - [`remind`](remind/index.html): reminders before dates, each a quiet notification.
74//! - [`timenote`](timenote/index.html): the time running, in the system's notifications.
75//!
76//! ## Contacts and calendars
77//!
78//! - [`pim`](pim/index.html): contacts and calendars.
79//! - [`duplicates`](duplicates/index.html): duplicates in the contacts, found and cleaned when you ask.
80//! - [`map`](map/index.html): contacts on a map.
81//!
82//! ## Money and papers
83//!
84//! - [`bank`](bank/index.html): the money watch: bank accounts, where each movement went, what passed and what did not.
85//! - [`contracts`](contracts/index.html): contracts and subscriptions.
86//! - [`papers`](papers/index.html): the papers wallet.
87//!
88//! ## Pauses, do-not-disturb and health
89//!
90//! - [`pauses`](pauses/index.html): the two pauses, Free time and Pause.
91//! - [`dnd`](dnd/index.html): the system's do-not-disturb, during the pauses and Sioul's own, where the system lets an app set it.
92//! - [`everywhere`](everywhere/index.html): do-not-disturb on every device, as this device applies it.
93//! - [`health`](health/index.html): the Health page, its settings, and the minute that reminds a dose.
94//!
95//! ## Your devices
96//!
97//! - [`share`](share/index.html): sharing with your other devices: set up, one exchange a minute, versions put back.
98//! - [`join`](join/index.html): a device joining the others through your Nextcloud's own sign-in, a phone in another's place.
99//! - [`devices`](devices/index.html): this device's sessions, as your other devices learn them.
100//!
101//! ## On a phone
102//!
103//! - [`alarms`](alarms/index.html): doses reminded by Android's alarm clock while Sioul is away.
104//! - [`eventalarms`](eventalarms/index.html): reminders before events, by Android's alarm clock.
105//! - [`wake`](wake/index.html): the alarm at waking.
106//! - [`steps`](steps/index.html): the phone in the background: a service that keeps your devices in step.
107//! - [`homecard`](homecard/index.html): the card on the phone's home screen.
108//! - [`calls`](calls/index.html): calls screened on a phone.
109//! - [`attachments`](attachments/index.html): mail attachments opened in the app you choose and saved where you choose, without an antivirus to call.
110//! - [`appnotes`](appnotes/index.html): other apps' notifications on a phone: now, or how long they wait.
111//! - [`reaches`](reaches/index.html): Settings ▸ What reaches you in words, and a person's sheet.
112//! - [`access`](access/index.html): what Android withholds from Sioul on a phone, said where you look.
113//! - [`appointments`](appointments/index.html): a task made an appointment and an appointment a task, from the agenda.
114
115// What Android withholds from Sioul on a phone, said in the join's result and on the Porch (docs/android.md).
116mod access;
117mod alarms;
118mod appnotes;
119mod appointments;
120// Mail attachments on a phone: opened in the app you choose, saved where you choose (docs/client.md, "Antivirus").
121// texts: what a computer attaches, made fit for a multimedia message (pictures with Qt: cpp/pictures.cpp).
122#[cfg(not(target_os = "android"))]
123mod attach;
124mod attachments;
125mod backend;
126mod bank;
127mod blocks;
128mod calls;
129mod capacity;
130// colour: the sites in the screen's own colours, and calmer colours (docs/colour.md); computers only.
131#[cfg(not(target_os = "android"))]
132mod colour;
133mod contracts;
134mod crypto;
135mod desktop;
136mod devices;
137mod dnd;
138mod eventalarms;
139mod duplicates;
140// Do-not-disturb on every device, and the phone in the background (docs/do-not-disturb.md).
141mod everywhere;
142mod filters;
143mod github;
144// Google's mail signed in on Google's page (docs/google.md, "Mail").
145mod gmail;
146mod health;
147mod homecard;
148mod hours;
149// A device joining the others through your Nextcloud's sign-in (docs/database.md).
150mod join;
151mod mail;
152mod mailsearch;
153mod mailnote;
154#[cfg(target_os = "android")]
155mod nfc;
156mod letters;
157mod map;
158// Microsoft's mail signed in on Microsoft's page (docs/microsoft.md).
159mod outlook;
160mod outside;
161mod papers;
162mod pauses;
163mod phonemsgs;
164// texts: SMS phase (b).
165mod texts;
166mod pim;
167mod projects;
168// Settings ▸ What reaches you, in words, and a person's sheet (docs/attention.md).
169mod reaches;
170mod remind;
171mod replies;
172mod reviews;
173mod securitykey;
174mod senders;
175mod share;
176mod sites;
177mod spam;
178mod steps;
179mod timenote;
180mod wake;
181mod work;
182
183use cxx_qt_lib::{QGuiApplication, QQmlApplicationEngine, QString, QUrl};
184
185// SAFETY: declared as the C++ files of cpp/ (and, for a phone, android/main.cpp) define them: extern "C", the same types.
186unsafe extern "C" {
187    /// Qt Quick drawn as Qt WebEngine (the Sites page) needs; before the application is
188    /// made (cpp/webengine.cpp). Android has no Qt WebEngine: its Sites page opens sites in the browser.
189    #[cfg(not(target_os = "android"))]
190    fn sioul_start_web_engine();
191    /// On a computer, Sioul's application, a widgets application (cpp/application.cpp):
192    /// the system tray's menu is made of widgets on Plasma. `values` holds `count` C
193    /// strings, the program's arguments, which it copies; made with `new`.
194    #[cfg(not(target_os = "android"))]
195    fn sioul_new_application(count: i32, values: *const *const std::ffi::c_char) -> *mut std::ffi::c_void;
196    /// Sioul's own icon on its windows (cpp/appicon.cpp); once the application is made.
197    #[cfg(not(target_os = "android"))]
198    fn sioul_set_window_icon();
199    /// The activation token a second Sioul handed on, for the next window
200    /// brought forward on Wayland (cpp/application.cpp; `outside::activate_with_token`).
201    /// `token` is a C string, copied.
202    #[cfg(not(target_os = "android"))]
203    pub(crate) fn sioul_set_activation_token(token: *const std::ffi::c_char);
204    /// SIGTERM, SIGINT and SIGHUP end Sioul as its window's close does
205    /// (cpp/signals.cpp); once the application is made.
206    #[cfg(not(target_os = "android"))]
207    fn sioul_quit_on_signals();
208    /// Sioul has quit: the sites' pages closed as a browser closes its tabs,
209    /// before Qt WebEngine shuts down (cpp/webengine.cpp). `engine` is the
210    /// QQmlApplicationEngine.
211    #[cfg(not(target_os = "android"))]
212    fn sioul_close_sites(engine: *mut std::ffi::c_void);
213    /// On a phone, what the window draws with given back while Sioul is away
214    /// (android/main.cpp); once the window is made.
215    #[cfg(target_os = "android")]
216    fn sioul_android_lean_window();
217    /// Sioul's symbols font, and the system's fonts and image formats read on
218    /// a thread of their own (cpp/warmup.cpp); once the application is made.
219    fn sioul_warm_up();
220    /// CXX-Qt's registration of the window's QML module, resources and types
221    /// (cxx-qt-build); runs once however often it is called.
222    fn cxx_qt_init_crate_sioul_app() -> bool;
223}
224
225/// When Sioul started, for `timing`.
226static STARTED: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
227
228/// How long the start has taken so far, written to stderr (logcat on Android)
229/// on Android and when SIOUL_TIMING is set: what to make faster, measured.
230pub(crate) fn timing(what: &str) {
231    if timed() {
232        let ms = STARTED.get().map_or(0, |start| start.elapsed().as_millis());
233        eprintln!("Sioul: {what} after {ms} ms");
234    }
235}
236
237/// Whether the start is timed: always on Android, else with SIOUL_TIMING.
238fn timed() -> bool {
239    cfg!(target_os = "android") || std::env::var_os("SIOUL_TIMING").is_some()
240}
241
242/// Freed memory given back to the system as it is freed (glibc, Linux). By
243/// default glibc raises its thresholds after a large block is freed: blocks
244/// as large come from its heaps from then on, and each thread's heap keeps
245/// up to twice that free for itself. Sioul's threads read files of several
246/// megabytes, and Chromium's run in this process: the window held some 15 MB
247/// free and unreturned with every page opened, 18 MB more with sites. Fixed
248/// thresholds (a block of a quarter of a megabyte or more mapped on its own,
249/// at most an eighth kept free at a heap's end) cost a system call or two for
250/// each such block, which Sioul makes a few times a minute.
251fn steady_heap() {
252    #[cfg(all(target_os = "linux", target_env = "gnu"))]
253    {
254        // SAFETY: glibc's mallopt, as malloc.h declares it.
255        unsafe extern "C" {
256            fn mallopt(param: std::ffi::c_int, value: std::ffi::c_int) -> std::ffi::c_int;
257        }
258        // glibc's malloc.h.
259        const M_TRIM_THRESHOLD: std::ffi::c_int = -1;
260        const M_MMAP_THRESHOLD: std::ffi::c_int = -3;
261        // SAFETY: glibc's own settings, made before Sioul's threads start; nothing of Rust's is touched.
262        unsafe {
263            mallopt(M_MMAP_THRESHOLD, 256 * 1024);
264            mallopt(M_TRIM_THRESHOLD, 128 * 1024);
265        }
266    }
267}
268
269/// How long ago the process started (Linux, Android): what came before
270/// [`run`], the libraries loaded above all, for `timing`.
271#[cfg(any(target_os = "linux", target_os = "android"))]
272fn process_age() -> Option<std::time::Duration> {
273    #[repr(C)]
274    struct Timespec {
275        seconds: i64,
276        nanoseconds: i64,
277    }
278    // SAFETY: the C library's clock_gettime, as time.h declares it; `Timespec` is its struct timespec where Sioul runs (64-bit Linux and Android: two 64-bit fields).
279    unsafe extern "C" {
280        fn clock_gettime(clock: i32, time: *mut Timespec) -> i32;
281    }
282    // CLOCK_BOOTTIME: since the system started, its sleep included, as /proc counts.
283    const BOOT_TIME: i32 = 7;
284    let stat = std::fs::read_to_string("/proc/self/stat").ok()?;
285    // The fields after the program's name, which ends at the last ")": the
286    // 22nd, when the process started, in hundredths of a second since boot.
287    let started: f64 = stat.rsplit_once(')')?.1.split_whitespace().nth(19)?.parse().ok()?;
288    let mut now = Timespec { seconds: 0, nanoseconds: 0 };
289    // SAFETY: `now` is a timespec, which clock_gettime fills.
290    if unsafe { clock_gettime(BOOT_TIME, &mut now) } != 0 {
291        return None;
292    }
293    let uptime = now.seconds as f64 + now.nanoseconds as f64 / 1e9;
294    Some(std::time::Duration::from_secs_f64((uptime - started / 100.0).max(0.0)))
295}
296
297#[cfg(not(any(target_os = "linux", target_os = "android")))]
298fn process_age() -> Option<std::time::Duration> {
299    None
300}
301
302/// The window, until it is closed; returns what the program returns.
303pub fn run() -> i32 {
304    // Before anything else: no crate logs above "info", a security key's PIN never (securitykey.rs).
305    sioul_sync::securitykey::cap_logging();
306    steady_heap();
307    STARTED.get_or_init(std::time::Instant::now);
308    if let Some(age) = process_age().filter(|_| timed()) {
309        eprintln!("Sioul: the process started {} ms before Sioul did", age.as_millis());
310    }
311    // Done as the program starts already; asked for by name too, so that a
312    // linker reading each library once (GNU gold and ld) keeps it with the
313    // window, which is a library.
314    // SAFETY: CXX-Qt's own initializer, which does its work once (std::call_once).
315    unsafe { cxx_qt_init_crate_sioul_app() };
316    // Mail, keys, drafts and caches are kept in folders that are yours alone.
317    sioul_core::config::make_private_dirs();
318    // A configuration from before the words Sioul looks for keeps reading
319    // French, English and France's names, said once in it; one made later is
320    // left to its interface's language (docs/words.md).
321    if let Err(e) = sioul_core::words::keep_first_once(&backend::config_path(), &sioul_core::config::data_dir().join("words-first-start")) {
322        eprintln!("Sioul: the words' first start: {e}");
323    }
324    // A mail link clicked (`sioul-app mailto:…`), or Sioul started again:
325    // handed to the Sioul of this profile already open, if one is, which
326    // opens the draft or comes forward; else this one opens, and listens for
327    // the next (docs/client.md, "Writing from other applications").
328    let addresses = outside::addresses();
329    if outside::hand_over(&addresses) {
330        return 0;
331    }
332    outside::listen();
333    if !addresses.is_empty() {
334        outside::hand(&addresses);
335    }
336    // What an earlier run left attached on the Texts page goes, now that this is the profile's Sioul.
337    #[cfg(not(target_os = "android"))]
338    texts::at_start();
339    // Qt's QML engine looks, for each file it loads, for variants of it in
340    // folders named after the language, the system and the style ("+fr_FR/",
341    // "+android/", "+Material/"). Sioul has none, and on a phone the looking
342    // took a fifth of the start. Qt's own dialogs, drawn where the system has
343    // none, keep their plain look.
344    // SAFETY: no other thread reads the environment yet.
345    unsafe { std::env::set_var("QT_NO_BUILTIN_SELECTORS", "1") };
346    // Qt makes the tip shown on hover from "import QtQuick.Controls", in the
347    // system's style: KDE's Breeze and its Kirigami loaded at the first tip,
348    // Material on Android, for one tip drawn unlike the rest of Sioul, which
349    // is all in the Basic style. Basic, unless you chose another.
350    if std::env::var_os("QT_QUICK_CONTROLS_STYLE").is_none() {
351        // SAFETY: as above.
352        unsafe { std::env::set_var("QT_QUICK_CONTROLS_STYLE", "Basic") };
353    }
354    // Copies beside drafts that hold an encrypted message's text in clear
355    // (until 19 October 2026) encrypted again, off the window's start; after
356    // the environment is set, which that thread reads (review of 19 October
357    // 2026, F10).
358    std::thread::spawn(crypto::seal_clear_drafts);
359    // Where the system puts a window's buttons, read meanwhile for Sioul's own
360    // title bar (desktop.rs); after the environment is set, which it reads.
361    #[cfg(not(target_os = "android"))]
362    desktop::warm_up();
363    // SAFETY: called once, before any Qt object exists, as Qt WebEngine asks.
364    #[cfg(not(target_os = "android"))]
365    unsafe {
366        sioul_start_web_engine()
367    };
368    // On a computer, a widgets application: Plasma's system tray makes its menu
369    // of widgets, and aborted Sioul without one. A phone has no tray.
370    #[cfg(not(target_os = "android"))]
371    let mut app = widgets_application();
372    #[cfg(target_os = "android")]
373    let mut app = QGuiApplication::new();
374    if let Some(mut app) = app.as_mut() {
375        app.as_mut().set_application_name(&QString::from("Sioul"));
376        app.as_mut().set_application_version(&QString::from(sioul_core::build::DESCRIBED));
377    }
378    // SAFETY: the application exists; called once, on the main thread.
379    unsafe { sioul_warm_up() };
380    timing("the application made");
381    // Lets the desktop match the window with its desktop file, com.aurelienpierre.Sioul.desktop
382    // (the Flatpak's id too): its name and icon in the taskbar, under Wayland above all.
383    QGuiApplication::set_desktop_file_name(&QString::from("com.aurelienpierre.Sioul"));
384    // Android's windows have no icon.
385    #[cfg(not(target_os = "android"))]
386    // SAFETY: the application exists; called once, on the main thread.
387    unsafe {
388        sioul_set_window_icon()
389    };
390    // The session ending (SIGTERM), Ctrl+C, the terminal closed: Sioul quits as by its window.
391    #[cfg(not(target_os = "android"))]
392    // SAFETY: the application exists; called once, on the main thread.
393    unsafe {
394        sioul_quit_on_signals()
395    };
396    desktop::icons();
397    let mut engine = QQmlApplicationEngine::new();
398    if let Some(engine) = engine.as_mut() {
399        engine.load(&QUrl::from("qrc:/qt/qml/com/aurelienpierre/sioul/qml/main.qml"));
400    }
401    #[cfg(target_os = "android")]
402    // SAFETY: the window is made (above); called once, on the main thread.
403    unsafe {
404        sioul_android_lean_window()
405    };
406    timing("the window loaded");
407    // A session begins: your other devices learn this one is working (docs/database.md,
408    // "Devices"). Off the window's thread: the keyring may take a moment the first time.
409    std::thread::spawn(devices::window_opened);
410    let code = match app.as_mut() {
411        Some(app) => app.exec(),
412        None => 1,
413    };
414    // The sites' pages closed before Qt WebEngine shuts down with the
415    // application: some sites write their login back only then (docs/sites.md, "Closing").
416    #[cfg(not(target_os = "android"))]
417    if !engine.is_null() {
418        // SAFETY: the engine lives until this function ends; on the main thread, the windows gone.
419        unsafe { sioul_close_sites(engine.as_mut_ptr().cast()) };
420    }
421    // The time running's notification goes with the window (a phone's stays).
422    timenote::closing();
423    // On a computer, do-not-disturb's table as it holds without Sioul (its
424    // inhibition ended with it): sent by the exchange that follows.
425    everywhere::closing();
426    // What was marked goes out, and your other devices learn this one closed.
427    share::closing();
428    // The next Sioul started opens by itself rather than knocking here.
429    outside::stop_listening();
430    code
431}
432
433/// For the test of the bundled icons (tests/icons.rs): Sioul's icons set up
434/// as at its start (`desktop::icons`), then for colours chosen `dark` or
435/// light, as the window asks when its colours change (`desktop::follow`);
436/// returns the names among `names` that the icon theme then in use lacks,
437/// each by its very name. The application must be made first, on this
438/// thread; with SIOUL_THEME set, nothing of yours is read.
439#[doc(hidden)]
440pub fn icons_lacking(names: &[String], dark: bool) -> Vec<String> {
441    // SAFETY: CXX-Qt's own initializer (resources, types), which does its work once (std::call_once).
442    unsafe { cxx_qt_init_crate_sioul_app() };
443    desktop::icons();
444    desktop::follow(dark, true);
445    names.iter().filter(|name| !desktop::has_icon(name)).cloned().collect()
446}
447
448/// For the test of the bundled icons: the icon theme in use, as Qt names it
449/// ("sioul" for light colours, "sioul-dark" for dark ones).
450#[doc(hidden)]
451pub fn icon_theme() -> String {
452    desktop::theme_in_use()
453}
454
455/// For the test of attaching (tests/attach.rs): what a picture is here: its
456/// format by its first bytes (`attach::sniff`), whether this computer's Qt
457/// reads it, and decoded (its long side at most 1,600 pixels), its width,
458/// height and frames. The application must be made first.
459#[doc(hidden)]
460#[cfg(not(target_os = "android"))]
461pub fn picture_read(path: &std::path::Path) -> (String, bool, Option<(u32, u32, u32)>) {
462    let Some(format) = attach::head_of(path).as_deref().and_then(attach::sniff) else { return (String::new(), false, None) };
463    let reads = attach::Picture::reads(format);
464    let decoded = attach::Picture::open(path, format, attach::FIRST_SIDE).map(|p| (p.width, p.height, p.frames));
465    (format.to_string(), reads, decoded)
466}
467
468/// For the test of attaching: a picture made fit for a share of a message's
469/// bytes (`attach::reduce`): its long side, quality and size; none when it
470/// cannot be made small enough.
471#[doc(hidden)]
472#[cfg(not(target_os = "android"))]
473pub fn picture_reduced(source: &std::path::Path, share: u64, advised: u32, out: &std::path::Path) -> Option<(u32, u8, u64)> {
474    attach::reduce(source, share, advised, out)
475}
476
477/// For the test of attaching: the files chosen made fit for one message
478/// under `limit` bytes (`attach::make`), into `folder`: those that go (kind,
479/// content type, file, size) and those that do not (the file chosen, why).
480#[doc(hidden)]
481#[cfg(not(target_os = "android"))]
482#[allow(clippy::type_complexity)]
483pub fn attach_made(sources: &[std::path::PathBuf], limit: u64, advised: u32, folder: &std::path::Path) -> (Vec<(String, String, String, u64)>, Vec<(String, String)>) {
484    let (made, left) = attach::make(sources, limit, advised, folder);
485    (made.into_iter().map(|m| (m.kind, m.ct, m.path, m.size)).collect(), left.into_iter().map(|l| (l.source, l.why.to_string())).collect())
486}
487
488/// A widgets application (QApplication), as the QGuiApplication CXX-Qt works
489/// with, which it is: made by cpp/application.cpp from the program's
490/// arguments, as `QGuiApplication::new` makes its own.
491#[cfg(not(target_os = "android"))]
492fn widgets_application() -> cxx::UniquePtr<QGuiApplication> {
493    let arguments: Vec<std::ffi::CString> = std::env::args_os()
494        .filter_map(|argument| {
495            // Unix's arguments are bytes; Windows's, read as UTF-8, as CXX-Qt reads them.
496            #[cfg(unix)]
497            let bytes = std::os::unix::ffi::OsStrExt::as_bytes(argument.as_os_str()).to_vec();
498            #[cfg(windows)]
499            let bytes = argument.to_string_lossy().into_owned().into_bytes();
500            std::ffi::CString::new(bytes).ok()
501        })
502        .collect();
503    let pointers: Vec<*const std::ffi::c_char> = arguments.iter().map(|argument| argument.as_ptr()).collect();
504    let count = i32::try_from(pointers.len()).unwrap_or(0);
505    // SAFETY: `pointers` holds `count` C strings, alive during the call, which the
506    // application copies. It returns a QApplication made with `new`, a QGuiApplication,
507    // which the UniquePtr deletes as it deletes CXX-Qt's own; called once, on the main thread.
508    unsafe { cxx::UniquePtr::from_raw(sioul_new_application(count, pointers.as_ptr()).cast()) }
509}
510
511/// Android: [`run`], for the program Qt for Android starts (android/main.cpp).
512#[cfg(target_os = "android")]
513// SAFETY: no other symbol of the program has this name.
514#[unsafe(no_mangle)]
515pub extern "C" fn sioul_app_run() -> i32 {
516    run()
517}
518
519/// Android: the Java side lent to Sioul for Android's KeyStore (passwords)
520/// and the network's DNS servers (sender checks); false if it could not be
521/// taken, which the log says. Called by android/main.cpp, before [`run`].
522///
523/// # Safety
524/// `vm` is the process's JavaVM and `context` a global reference to the
525/// application's Context, never released; called once.
526#[cfg(target_os = "android")]
527// SAFETY: no other symbol of the program has this name.
528#[unsafe(no_mangle)]
529pub unsafe extern "C" fn sioul_android_init(vm: *mut std::ffi::c_void, context: *mut std::ffi::c_void) -> bool {
530    // Your security key held against the phone (NFC), through Java.
531    nfc::start();
532    // SAFETY: as the caller promises.
533    match unsafe { sioul_sync::android::init(vm, context) } {
534        Ok(()) => true,
535        Err(e) => {
536            eprintln!("Sioul: Android's keyring is not available: {e}");
537            false
538        }
539    }
540}