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://aurelienpierre.github.io/sioul/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://aurelienpierre.github.io/sioul/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//! - [`devices`](devices/index.html): this device's sessions, as your other devices learn them.
99//!
100//! ## On a phone
101//!
102//! - [`alarms`](alarms/index.html): doses reminded by Android's alarm clock while Sioul is away.
103//! - [`eventalarms`](eventalarms/index.html): reminders before events, by Android's alarm clock.
104//! - [`wake`](wake/index.html): the alarm at waking.
105//! - [`steps`](steps/index.html): the phone in the background: a service that keeps your devices in step.
106//! - [`homecard`](homecard/index.html): the card on the phone's home screen.
107//! - [`calls`](calls/index.html): calls screened on a phone.
108//! - [`attachments`](attachments/index.html): mail attachments opened in the app you choose and saved where you choose, without an antivirus to call.
109//! - [`appnotes`](appnotes/index.html): other apps' notifications on a phone: now, or how long they wait.
110//! - [`reaches`](reaches/index.html): Settings ▸ What reaches you in words, and a person's sheet.
111
112mod alarms;
113mod appnotes;
114// Mail attachments on a phone: opened in the app you choose, saved where you choose (docs/client.md, "Antivirus").
115mod attachments;
116mod backend;
117mod bank;
118mod blocks;
119mod calls;
120mod capacity;
121// colour: the sites in the screen's own colours, and calmer colours (docs/colour.md); computers only.
122#[cfg(not(target_os = "android"))]
123mod colour;
124mod contracts;
125mod crypto;
126mod desktop;
127mod devices;
128mod dnd;
129mod eventalarms;
130mod duplicates;
131// Do-not-disturb on every device, and the phone in the background (docs/do-not-disturb.md).
132mod everywhere;
133mod filters;
134mod github;
135// Google's mail signed in on Google's page (docs/google.md, "Mail").
136mod gmail;
137mod health;
138mod homecard;
139mod hours;
140mod mail;
141mod mailsearch;
142mod mailnote;
143mod letters;
144mod map;
145mod outside;
146mod papers;
147mod pauses;
148mod phonemsgs;
149// texts: SMS phase (b).
150mod texts;
151mod pim;
152mod projects;
153// Settings ▸ What reaches you, in words, and a person's sheet (docs/attention.md).
154mod reaches;
155mod remind;
156mod reviews;
157mod securitykey;
158mod senders;
159mod share;
160mod sites;
161mod spam;
162mod steps;
163mod timenote;
164mod wake;
165mod work;
166
167use cxx_qt_lib::{QGuiApplication, QQmlApplicationEngine, QString, QUrl};
168
169// SAFETY: declared as the C++ files of cpp/ (and, for a phone, android/main.cpp) define them: extern "C", the same types.
170unsafe extern "C" {
171    /// Qt Quick drawn as Qt WebEngine (the Sites page) needs; before the application is
172    /// made (cpp/webengine.cpp). Android has no Qt WebEngine: its Sites page opens sites in the browser.
173    #[cfg(not(target_os = "android"))]
174    fn sioul_start_web_engine();
175    /// On a computer, Sioul's application, a widgets application (cpp/application.cpp):
176    /// the system tray's menu is made of widgets on Plasma. `values` holds `count` C
177    /// strings, the program's arguments, which it copies; made with `new`.
178    #[cfg(not(target_os = "android"))]
179    fn sioul_new_application(count: i32, values: *const *const std::ffi::c_char) -> *mut std::ffi::c_void;
180    /// Sioul's own icon on its windows (cpp/appicon.cpp); once the application is made.
181    #[cfg(not(target_os = "android"))]
182    fn sioul_set_window_icon();
183    /// The activation token a second Sioul handed on, for the next window
184    /// brought forward on Wayland (cpp/application.cpp; `outside::activate_with_token`).
185    /// `token` is a C string, copied.
186    #[cfg(not(target_os = "android"))]
187    pub(crate) fn sioul_set_activation_token(token: *const std::ffi::c_char);
188    /// SIGTERM, SIGINT and SIGHUP end Sioul as its window's close does
189    /// (cpp/signals.cpp); once the application is made.
190    #[cfg(not(target_os = "android"))]
191    fn sioul_quit_on_signals();
192    /// Sioul has quit: the sites' pages closed as a browser closes its tabs,
193    /// before Qt WebEngine shuts down (cpp/webengine.cpp). `engine` is the
194    /// QQmlApplicationEngine.
195    #[cfg(not(target_os = "android"))]
196    fn sioul_close_sites(engine: *mut std::ffi::c_void);
197    /// On a phone, what the window draws with given back while Sioul is away
198    /// (android/main.cpp); once the window is made.
199    #[cfg(target_os = "android")]
200    fn sioul_android_lean_window();
201    /// Sioul's symbols font, and the system's fonts and image formats read on
202    /// a thread of their own (cpp/warmup.cpp); once the application is made.
203    fn sioul_warm_up();
204    /// CXX-Qt's registration of the window's QML module, resources and types
205    /// (cxx-qt-build); runs once however often it is called.
206    fn cxx_qt_init_crate_sioul_app() -> bool;
207}
208
209/// When Sioul started, for `timing`.
210static STARTED: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
211
212/// How long the start has taken so far, written to stderr (logcat on Android)
213/// on Android and when SIOUL_TIMING is set: what to make faster, measured.
214pub(crate) fn timing(what: &str) {
215    if timed() {
216        let ms = STARTED.get().map_or(0, |start| start.elapsed().as_millis());
217        eprintln!("Sioul: {what} after {ms} ms");
218    }
219}
220
221/// Whether the start is timed: always on Android, else with SIOUL_TIMING.
222fn timed() -> bool {
223    cfg!(target_os = "android") || std::env::var_os("SIOUL_TIMING").is_some()
224}
225
226/// Freed memory given back to the system as it is freed (glibc, Linux). By
227/// default glibc raises its thresholds after a large block is freed: blocks
228/// as large come from its heaps from then on, and each thread's heap keeps
229/// up to twice that free for itself. Sioul's threads read files of several
230/// megabytes, and Chromium's run in this process: the window held some 15 MB
231/// free and unreturned with every page opened, 18 MB more with sites. Fixed
232/// thresholds (a block of a quarter of a megabyte or more mapped on its own,
233/// at most an eighth kept free at a heap's end) cost a system call or two for
234/// each such block, which Sioul makes a few times a minute.
235fn steady_heap() {
236    #[cfg(all(target_os = "linux", target_env = "gnu"))]
237    {
238        // SAFETY: glibc's mallopt, as malloc.h declares it.
239        unsafe extern "C" {
240            fn mallopt(param: std::ffi::c_int, value: std::ffi::c_int) -> std::ffi::c_int;
241        }
242        // glibc's malloc.h.
243        const M_TRIM_THRESHOLD: std::ffi::c_int = -1;
244        const M_MMAP_THRESHOLD: std::ffi::c_int = -3;
245        // SAFETY: glibc's own settings, made before Sioul's threads start; nothing of Rust's is touched.
246        unsafe {
247            mallopt(M_MMAP_THRESHOLD, 256 * 1024);
248            mallopt(M_TRIM_THRESHOLD, 128 * 1024);
249        }
250    }
251}
252
253/// How long ago the process started (Linux, Android): what came before
254/// [`run`], the libraries loaded above all, for `timing`.
255#[cfg(any(target_os = "linux", target_os = "android"))]
256fn process_age() -> Option<std::time::Duration> {
257    #[repr(C)]
258    struct Timespec {
259        seconds: i64,
260        nanoseconds: i64,
261    }
262    // 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).
263    unsafe extern "C" {
264        fn clock_gettime(clock: i32, time: *mut Timespec) -> i32;
265    }
266    // CLOCK_BOOTTIME: since the system started, its sleep included, as /proc counts.
267    const BOOT_TIME: i32 = 7;
268    let stat = std::fs::read_to_string("/proc/self/stat").ok()?;
269    // The fields after the program's name, which ends at the last ")": the
270    // 22nd, when the process started, in hundredths of a second since boot.
271    let started: f64 = stat.rsplit_once(')')?.1.split_whitespace().nth(19)?.parse().ok()?;
272    let mut now = Timespec { seconds: 0, nanoseconds: 0 };
273    // SAFETY: `now` is a timespec, which clock_gettime fills.
274    if unsafe { clock_gettime(BOOT_TIME, &mut now) } != 0 {
275        return None;
276    }
277    let uptime = now.seconds as f64 + now.nanoseconds as f64 / 1e9;
278    Some(std::time::Duration::from_secs_f64((uptime - started / 100.0).max(0.0)))
279}
280
281#[cfg(not(any(target_os = "linux", target_os = "android")))]
282fn process_age() -> Option<std::time::Duration> {
283    None
284}
285
286/// The window, until it is closed; returns what the program returns.
287pub fn run() -> i32 {
288    // Before anything else: no crate logs above "info", a security key's PIN never (securitykey.rs).
289    sioul_sync::securitykey::cap_logging();
290    steady_heap();
291    STARTED.get_or_init(std::time::Instant::now);
292    if let Some(age) = process_age().filter(|_| timed()) {
293        eprintln!("Sioul: the process started {} ms before Sioul did", age.as_millis());
294    }
295    // Done as the program starts already; asked for by name too, so that a
296    // linker reading each library once (GNU gold and ld) keeps it with the
297    // window, which is a library.
298    // SAFETY: CXX-Qt's own initializer, which does its work once (std::call_once).
299    unsafe { cxx_qt_init_crate_sioul_app() };
300    // Mail, keys, drafts and caches are kept in folders that are yours alone.
301    sioul_core::config::make_private_dirs();
302    // A configuration from before the words Sioul looks for keeps reading
303    // French, English and France's names, said once in it; one made later is
304    // left to its interface's language (docs/words.md).
305    if let Err(e) = sioul_core::words::keep_first_once(&backend::config_path(), &sioul_core::config::data_dir().join("words-first-start")) {
306        eprintln!("Sioul: the words' first start: {e}");
307    }
308    // A mail link clicked (`sioul-app mailto:…`), or Sioul started again:
309    // handed to the Sioul of this profile already open, if one is, which
310    // opens the draft or comes forward; else this one opens, and listens for
311    // the next (docs/client.md, "Writing from other applications").
312    let addresses = outside::addresses();
313    if outside::hand_over(&addresses) {
314        return 0;
315    }
316    outside::listen();
317    if !addresses.is_empty() {
318        outside::hand(&addresses);
319    }
320    // Qt's QML engine looks, for each file it loads, for variants of it in
321    // folders named after the language, the system and the style ("+fr_FR/",
322    // "+android/", "+Material/"). Sioul has none, and on a phone the looking
323    // took a fifth of the start. Qt's own dialogs, drawn where the system has
324    // none, keep their plain look.
325    // SAFETY: no other thread reads the environment yet.
326    unsafe { std::env::set_var("QT_NO_BUILTIN_SELECTORS", "1") };
327    // Qt makes the tip shown on hover from "import QtQuick.Controls", in the
328    // system's style: KDE's Breeze and its Kirigami loaded at the first tip,
329    // Material on Android, for one tip drawn unlike the rest of Sioul, which
330    // is all in the Basic style. Basic, unless you chose another.
331    if std::env::var_os("QT_QUICK_CONTROLS_STYLE").is_none() {
332        // SAFETY: as above.
333        unsafe { std::env::set_var("QT_QUICK_CONTROLS_STYLE", "Basic") };
334    }
335    // Where the system puts a window's buttons, read meanwhile for Sioul's own
336    // title bar (desktop.rs); after the environment is set, which it reads.
337    #[cfg(not(target_os = "android"))]
338    desktop::warm_up();
339    // SAFETY: called once, before any Qt object exists, as Qt WebEngine asks.
340    #[cfg(not(target_os = "android"))]
341    unsafe {
342        sioul_start_web_engine()
343    };
344    // On a computer, a widgets application: Plasma's system tray makes its menu
345    // of widgets, and aborted Sioul without one. A phone has no tray.
346    #[cfg(not(target_os = "android"))]
347    let mut app = widgets_application();
348    #[cfg(target_os = "android")]
349    let mut app = QGuiApplication::new();
350    if let Some(mut app) = app.as_mut() {
351        app.as_mut().set_application_name(&QString::from("Sioul"));
352        app.as_mut().set_application_version(&QString::from(sioul_core::build::DESCRIBED));
353    }
354    // SAFETY: the application exists; called once, on the main thread.
355    unsafe { sioul_warm_up() };
356    timing("the application made");
357    // Lets the desktop match the window with its desktop file, com.aurelienpierre.Sioul.desktop
358    // (the Flatpak's id too): its name and icon in the taskbar, under Wayland above all.
359    QGuiApplication::set_desktop_file_name(&QString::from("com.aurelienpierre.Sioul"));
360    // Android's windows have no icon.
361    #[cfg(not(target_os = "android"))]
362    // SAFETY: the application exists; called once, on the main thread.
363    unsafe {
364        sioul_set_window_icon()
365    };
366    // The session ending (SIGTERM), Ctrl+C, the terminal closed: Sioul quits as by its window.
367    #[cfg(not(target_os = "android"))]
368    // SAFETY: the application exists; called once, on the main thread.
369    unsafe {
370        sioul_quit_on_signals()
371    };
372    desktop::icons();
373    let mut engine = QQmlApplicationEngine::new();
374    if let Some(engine) = engine.as_mut() {
375        engine.load(&QUrl::from("qrc:/qt/qml/com/aurelienpierre/sioul/qml/main.qml"));
376    }
377    #[cfg(target_os = "android")]
378    // SAFETY: the window is made (above); called once, on the main thread.
379    unsafe {
380        sioul_android_lean_window()
381    };
382    timing("the window loaded");
383    // A session begins: your other devices learn this one is working (docs/database.md,
384    // "Devices"). Off the window's thread: the keyring may take a moment the first time.
385    std::thread::spawn(devices::window_opened);
386    let code = match app.as_mut() {
387        Some(app) => app.exec(),
388        None => 1,
389    };
390    // The sites' pages closed before Qt WebEngine shuts down with the
391    // application: some sites write their login back only then (docs/sites.md, "Closing").
392    #[cfg(not(target_os = "android"))]
393    if !engine.is_null() {
394        // SAFETY: the engine lives until this function ends; on the main thread, the windows gone.
395        unsafe { sioul_close_sites(engine.as_mut_ptr().cast()) };
396    }
397    // The time running's notification goes with the window (a phone's stays).
398    timenote::closing();
399    // On a computer, do-not-disturb's table as it holds without Sioul (its
400    // inhibition ended with it): sent by the exchange that follows.
401    everywhere::closing();
402    // What was marked goes out, and your other devices learn this one closed.
403    share::closing();
404    // The next Sioul started opens by itself rather than knocking here.
405    outside::stop_listening();
406    code
407}
408
409/// A widgets application (QApplication), as the QGuiApplication CXX-Qt works
410/// with, which it is: made by cpp/application.cpp from the program's
411/// arguments, as `QGuiApplication::new` makes its own.
412#[cfg(not(target_os = "android"))]
413fn widgets_application() -> cxx::UniquePtr<QGuiApplication> {
414    let arguments: Vec<std::ffi::CString> = std::env::args_os()
415        .filter_map(|argument| {
416            // Unix's arguments are bytes; Windows's, read as UTF-8, as CXX-Qt reads them.
417            #[cfg(unix)]
418            let bytes = std::os::unix::ffi::OsStrExt::as_bytes(argument.as_os_str()).to_vec();
419            #[cfg(windows)]
420            let bytes = argument.to_string_lossy().into_owned().into_bytes();
421            std::ffi::CString::new(bytes).ok()
422        })
423        .collect();
424    let pointers: Vec<*const std::ffi::c_char> = arguments.iter().map(|argument| argument.as_ptr()).collect();
425    let count = i32::try_from(pointers.len()).unwrap_or(0);
426    // SAFETY: `pointers` holds `count` C strings, alive during the call, which the
427    // application copies. It returns a QApplication made with `new`, a QGuiApplication,
428    // which the UniquePtr deletes as it deletes CXX-Qt's own; called once, on the main thread.
429    unsafe { cxx::UniquePtr::from_raw(sioul_new_application(count, pointers.as_ptr()).cast()) }
430}
431
432/// Android: [`run`], for the program Qt for Android starts (android/main.cpp).
433#[cfg(target_os = "android")]
434// SAFETY: no other symbol of the program has this name.
435#[unsafe(no_mangle)]
436pub extern "C" fn sioul_app_run() -> i32 {
437    run()
438}
439
440/// Android: the Java side lent to Sioul for Android's KeyStore (passwords)
441/// and the network's DNS servers (sender checks); false if it could not be
442/// taken, which the log says. Called by android/main.cpp, before [`run`].
443///
444/// # Safety
445/// `vm` is the process's JavaVM and `context` a global reference to the
446/// application's Context, never released; called once.
447#[cfg(target_os = "android")]
448// SAFETY: no other symbol of the program has this name.
449#[unsafe(no_mangle)]
450pub unsafe extern "C" fn sioul_android_init(vm: *mut std::ffi::c_void, context: *mut std::ffi::c_void) -> bool {
451    // SAFETY: as the caller promises.
452    match unsafe { sioul_sync::android::init(vm, context) } {
453        Ok(()) => true,
454        Err(e) => {
455            eprintln!("Sioul: Android's keyring is not available: {e}");
456            false
457        }
458    }
459}