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}