1use cxx_qt::{CxxQtType, Threading};
13use cxx_qt_lib::QString;
14use jiff::Zoned;
15use sioul_core::budget::{self, Ledger, MailLine};
16use sioul_core::card::ImapOrigin;
17use sioul_core::projects::ProjectStore;
18use sioul_core::config::{self, Account, Config, Priority, Security};
19use sioul_core::i18n::{self, Translator};
20use sioul_core::porch::{self, KnownSenders, SenderList, Triaged};
21use sioul_core::state::{MoneyState, PorchState};
22use sioul_core::{maildir, reading, view};
23use crate::{crypto, gmail, mail, outlook, pim, securitykey, work};
24use sioul_sync::antivirus::{self, Verdict};
25use sioul_sync::{Control, Learned, Report, SyncError, notify, secret};
26use std::collections::{BTreeMap, BTreeSet};
27use std::hash::{Hash, Hasher};
28use std::path::{Path, PathBuf};
29use std::pin::Pin;
30use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
31use std::sync::{Arc, Mutex, OnceLock};
32
33#[cxx_qt::bridge]
34pub mod qobject {
35 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 #[qproperty(QString, mail_search)]
61 #[qproperty(QString, undo_line)]
62 #[qproperty(QString, contacts)]
63 #[qproperty(QString, duplicates)]
65 #[qproperty(QString, agenda)]
66 #[qproperty(QString, overlaps)]
68 #[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 #[qproperty(bool, away)]
82 #[qproperty(QString, mode)]
83 #[qproperty(QString, forecast)]
84 #[qproperty(QString, places_found)]
85 type Sioul = super::SioulRust;
86
87 #[qinvokable]
89 fn text(self: &Sioul, id: &QString) -> QString;
90
91 #[qinvokable]
93 fn text_with(self: &Sioul, id: &QString, name: &QString, value: &QString) -> QString;
94
95 #[qinvokable]
97 fn text_counted(self: &Sioul, id: &QString, n: i32) -> QString;
98
99 #[qinvokable]
101 fn text_args(self: &Sioul, id: &QString, args: &QString) -> QString;
102
103 #[qinvokable]
105 fn start(self: Pin<&mut Sioul>);
106
107 #[qinvokable]
109 fn refresh(self: Pin<&mut Sioul>);
110
111 #[qinvokable]
113 fn sync_now(self: Pin<&mut Sioul>);
114
115 #[qinvokable]
117 fn open_anyway(self: Pin<&mut Sioul>);
118
119 #[qinvokable]
121 fn done(self: Pin<&mut Sioul>);
122
123 #[qinvokable]
127 fn read_message(self: Pin<&mut Sioul>, key: &QString);
128
129 #[qinvokable]
131 fn open_attachment(self: Pin<&mut Sioul>, key: &QString, index: i32);
132
133 #[qinvokable]
135 fn save_attachment(self: Pin<&mut Sioul>, key: &QString, index: i32);
136
137 #[qinvokable]
140 fn attachment_unchecked(self: Pin<&mut Sioul>, key: &QString, index: i32, what: i32);
141
142 #[qinvokable]
144 fn keep_attachment_as_paper(self: Pin<&mut Sioul>, key: &QString, index: i32);
145
146 #[qinvokable]
149 fn attachments_unscanned(self: &Sioul) -> bool;
150
151 #[qinvokable]
153 fn papers(self: &Sioul) -> QString;
154
155 #[qinvokable]
157 fn save_paper(self: Pin<&mut Sioul>, id: &QString, edit: &QString) -> QString;
158
159 #[qinvokable]
161 fn remove_paper(self: Pin<&mut Sioul>, id: &QString) -> QString;
162
163 #[qinvokable]
165 fn plan_renewal(self: Pin<&mut Sioul>, id: &QString) -> QString;
166
167 #[qinvokable]
169 fn let_in(self: Pin<&mut Sioul>, address: &QString);
170
171 #[qinvokable]
173 fn block(self: Pin<&mut Sioul>, entry: &QString);
174
175 #[qinvokable]
178 fn block_from(self: Pin<&mut Sioul>, entry: &QString, key: &QString);
179
180 #[qinvokable]
182 fn unblock(self: Pin<&mut Sioul>, entry: &QString);
183
184 #[qinvokable]
188 fn set_standing(self: Pin<&mut Sioul>, entry: &QString, standing: &QString);
189
190 #[qinvokable]
192 fn standing(self: &Sioul, address: &QString) -> QString;
193
194 #[qinvokable]
197 fn standing_of(self: &Sioul, addresses: &QString) -> QString;
198
199 #[qinvokable]
202 fn set_standing_of(self: Pin<&mut Sioul>, addresses: &QString, standing: &QString);
203
204 #[qinvokable]
207 fn person_of(self: &Sioul, key: &QString) -> QString;
208
209 #[qinvokable]
212 fn set_person(self: Pin<&mut Sioul>, key: &QString, standing: &QString);
213
214 #[qinvokable]
216 fn add_mail_line(self: Pin<&mut Sioul>, key: &QString, budget: &QString);
217
218 #[qinvokable]
220 fn ignore_mail_line(self: Pin<&mut Sioul>, key: &QString);
221
222 #[qinvokable]
224 fn discover(self: Pin<&mut Sioul>, address: &QString);
225
226 #[qinvokable]
230 fn discover_at_microsoft(self: Pin<&mut Sioul>, address: &QString);
231
232 #[qinvokable]
235 fn set_account_enabled(self: Pin<&mut Sioul>, id: &QString, on: bool) -> QString;
236
237 #[qinvokable]
240 fn scout_account(self: Pin<&mut Sioul>, address: &QString);
241
242 #[qinvokable]
244 fn add_mail_like(self: Pin<&mut Sioul>, address: &QString, host: &QString, port: i32, security: &QString, login: &QString);
245
246 #[qinvokable]
248 fn add_account(self: Pin<&mut Sioul>, address: &QString, host: &QString, port: i32, security: &QString, login: &QString, password: &QString);
249
250 #[qinvokable]
252 fn add_portal(self: Pin<&mut Sioul>, name: &QString, url: &QString);
253
254 #[qinvokable]
258 fn remove_account(self: Pin<&mut Sioul>, id: &QString, delete_mail: bool);
259
260 #[qinvokable]
264 fn set_account_password(self: Pin<&mut Sioul>, id: &QString, password: &QString);
265
266 #[qinvokable]
268 fn set_priority(self: Pin<&mut Sioul>, id: &QString, priority: &QString);
269
270 #[qinvokable]
272 fn open_folder(self: Pin<&mut Sioul>, account: &QString, folder: &QString, all: bool, query: &QString);
273
274 #[qinvokable]
276 fn opened(self: Pin<&mut Sioul>, key: &QString);
277
278 #[qinvokable]
282 fn act(self: Pin<&mut Sioul>, key: &QString, action: &QString, target: &QString);
283
284 #[qinvokable]
286 fn act_many(self: Pin<&mut Sioul>, keys: &QString, action: &QString);
287
288 #[qinvokable]
290 fn move_messages(self: Pin<&mut Sioul>, keys: &QString, account: &QString, folder: &QString);
291
292 #[qinvokable]
296 fn search_mail(self: Pin<&mut Sioul>, search: &QString, now: bool);
297
298 #[qinvokable]
300 fn clear_mail_search(self: Pin<&mut Sioul>);
301
302 #[qinvokable]
304 fn bring_message(self: Pin<&mut Sioul>, key: &QString);
305
306 #[qinvokable]
308 fn mail_search_fields(self: &Sioul) -> QString;
309
310 #[qinvokable]
314 fn unsubscribe(self: Pin<&mut Sioul>, key: &QString) -> QString;
315
316 #[qinvokable]
318 fn undo(self: Pin<&mut Sioul>);
319
320 #[qinvokable]
322 fn flush(self: Pin<&mut Sioul>);
323
324 #[qinvokable]
326 fn compose(self: Pin<&mut Sioul>, kind: &QString, key: &QString, account: &QString) -> QString;
327
328 #[qinvokable]
330 fn draft(self: &Sioul, id: &QString) -> QString;
331
332 #[qinvokable]
334 fn save_draft(self: Pin<&mut Sioul>, edit: &QString) -> QString;
335
336 #[qinvokable]
338 fn attach(self: Pin<&mut Sioul>, id: &QString, url: &QString) -> QString;
339
340 #[qinvokable]
342 fn detach(self: Pin<&mut Sioul>, id: &QString, index: i32, forwarded: bool);
343
344 #[qinvokable]
346 fn preview(self: &Sioul, markdown: &QString) -> QString;
347
348 #[qinvokable]
350 fn send(self: Pin<&mut Sioul>, id: &QString) -> QString;
351
352 #[qinvokable]
354 fn discard(self: Pin<&mut Sioul>, id: &QString);
355
356 #[qinvokable]
358 fn draft_closed(self: Pin<&mut Sioul>, id: &QString);
359
360 #[qinvokable]
362 fn place(self: &Sioul, key: &QString) -> QString;
363
364 #[qinvokable]
366 fn source(self: &Sioul, key: &QString) -> QString;
367
368 #[qinvokable]
370 fn set_writing(self: Pin<&mut Sioul>, id: &QString, name: &QString, signature: &QString);
371
372 #[qinvokable]
375 fn grab_folder(self: &Sioul) -> QString;
376
377 #[qinvokable]
379 fn search_contacts(self: Pin<&mut Sioul>, query: &QString);
380
381 #[qinvokable]
383 fn contact(self: &Sioul, key: &QString) -> QString;
384
385 #[qinvokable]
388 fn save_contact(self: Pin<&mut Sioul>, key: &QString, edit: &QString) -> QString;
389
390 #[qinvokable]
392 fn delete_contact(self: Pin<&mut Sioul>, key: &QString);
393
394 #[qinvokable]
396 fn show_contacts_category(self: Pin<&mut Sioul>, category: &QString);
397
398 #[qinvokable]
400 fn find_duplicates(self: Pin<&mut Sioul>);
401
402 #[qinvokable]
405 fn clean_contacts(self: Pin<&mut Sioul>, keys: &QString) -> QString;
406
407 #[qinvokable]
409 fn merge_contacts(self: Pin<&mut Sioul>, lead: &QString, other: &QString) -> QString;
410
411 #[qinvokable]
413 fn not_same_contacts(self: Pin<&mut Sioul>, first: &QString, second: &QString) -> QString;
414
415 #[qinvokable]
417 fn undo_contacts(self: Pin<&mut Sioul>, id: &QString) -> QString;
418
419 #[qinvokable]
421 fn show_days(self: Pin<&mut Sioul>, from: &QString, days: i32, upcoming: bool);
422
423 #[qinvokable]
425 fn calendars(self: &Sioul) -> QString;
426
427 #[qinvokable]
429 fn event_form(self: &Sioul, key: &QString) -> QString;
430
431 #[qinvokable]
434 fn save_event(self: Pin<&mut Sioul>, key: &QString, edit: &QString, calendar: &QString) -> QString;
435
436 #[qinvokable]
440 fn task_as_event(self: &Sioul, uid: &QString, start: f64, end: f64) -> QString;
441
442 #[qinvokable]
446 fn task_to_event(self: Pin<&mut Sioul>, uid: &QString, block: &QString, edit: &QString, calendar: &QString) -> QString;
447
448 #[qinvokable]
450 fn event_to_task_losses(self: &Sioul, key: &QString) -> QString;
451
452 #[qinvokable]
455 fn event_to_task(self: Pin<&mut Sioul>, key: &QString) -> QString;
456
457 #[qinvokable]
460 fn event_reminder(self: &Sioul, key: &QString, start: f64) -> QString;
461
462 #[qinvokable]
465 fn reminder_choices(self: &Sioul, current: &QString) -> QString;
466
467 #[qinvokable]
470 fn set_event_reminder(self: Pin<&mut Sioul>, key: &QString, value: &QString) -> QString;
471
472 #[qinvokable]
474 fn delete_event(self: Pin<&mut Sioul>, key: &QString, start: f64, only_this: bool);
475
476 #[qinvokable]
480 fn move_event(self: Pin<&mut Sioul>, key: &QString, start: f64, new_start: f64, new_end: f64, only_this: bool) -> QString;
481
482 #[qinvokable]
484 fn completions(self: &Sioul, typed: &QString) -> QString;
485
486 #[qinvokable]
488 fn contact_for(self: &Sioul, address: &QString) -> QString;
489
490 #[qinvokable]
492 fn add_sender(self: Pin<&mut Sioul>, name: &QString, address: &QString);
493
494 #[qinvokable]
496 fn answer_invitation(self: Pin<&mut Sioul>, key: &QString, answer: &QString);
497
498 #[qinvokable]
500 fn history_weeks(self: &Sioul) -> i32;
501
502 #[qinvokable]
504 fn day_start(self: &Sioul) -> i32;
505
506 #[qinvokable]
508 fn set_history(self: Pin<&mut Sioul>, weeks: i32);
509
510 #[qinvokable]
512 fn add_dav(self: Pin<&mut Sioul>, address: &QString, url: &QString, login: &QString, password: &QString);
513
514 #[qinvokable]
517 fn add_google(self: Pin<&mut Sioul>, address: &QString, client_id: &QString, client_secret: &QString, again: &QString);
518
519 #[qinvokable]
521 fn cancel_google(self: Pin<&mut Sioul>);
522
523 #[qinvokable]
528 fn add_google_mail(self: Pin<&mut Sioul>, address: &QString, client_id: &QString, client_secret: &QString, again: &QString);
529
530 #[qinvokable]
537 fn add_microsoft_mail(self: Pin<&mut Sioul>, address: &QString, kind: &QString, client_id: &QString, tenant: &QString, again: &QString);
538
539 #[qinvokable]
541 fn cancel_microsoft(self: Pin<&mut Sioul>);
542
543 #[qinvokable]
546 fn answer_microsoft(self: Pin<&mut Sioul>, yes: bool);
547
548 #[qinvokable]
550 fn microsoft_built_in(self: &Sioul) -> bool;
551
552 #[qinvokable]
557 fn microsoft_admin_text(self: &Sioul, address: &QString, client_id: &QString) -> QString;
558
559 #[qinvokable]
561 fn pgp_keys(self: &Sioul) -> QString;
562
563 #[qinvokable]
565 fn pgp_generate(self: Pin<&mut Sioul>, address: &QString);
566
567 #[qinvokable]
569 fn pgp_import(self: Pin<&mut Sioul>, url: &QString, passphrase: &QString) -> QString;
570
571 #[qinvokable]
573 fn pgp_export(self: Pin<&mut Sioul>, fingerprint: &QString);
574
575 #[qinvokable]
577 fn pgp_save_revocation(self: Pin<&mut Sioul>, fingerprint: &QString);
578
579 #[qinvokable]
581 fn pgp_remove(self: Pin<&mut Sioul>, fingerprint: &QString);
582
583 #[qinvokable]
585 fn pgp_lookup(self: Pin<&mut Sioul>, draft: &QString);
586
587 #[qinvokable]
591 fn security_key_for_draft(self: &Sioul, id: &QString) -> QString;
592
593 #[qinvokable]
597 fn sign_and_send(self: Pin<&mut Sioul>, id: &QString, pin: &QString);
598
599 #[qinvokable]
602 fn open_with_security_key(self: Pin<&mut Sioul>, key: &QString, pin: &QString);
603
604 #[qinvokable]
606 fn security_key_not_now(self: Pin<&mut Sioul>, context: &QString);
607
608 #[qinvokable]
610 fn open_nfc_settings(self: Pin<&mut Sioul>);
611
612 #[qinvokable]
614 fn let_gnupg_release(self: Pin<&mut Sioul>) -> QString;
615
616 #[qinvokable]
618 fn security_keys(self: &Sioul) -> QString;
619
620 #[qinvokable]
622 fn read_security_key(self: Pin<&mut Sioul>);
623
624 #[qinvokable]
627 fn find_security_key_certificate(self: Pin<&mut Sioul>, ident: &QString);
628
629 #[qinvokable]
631 fn import_security_key_certificate(self: Pin<&mut Sioul>, ident: &QString, url: &QString);
632
633 #[qinvokable]
636 fn import_security_key_from_gnupg(self: Pin<&mut Sioul>, ident: &QString);
637
638 #[qinvokable]
640 fn renew_security_key(self: Pin<&mut Sioul>, ident: &QString);
641
642 #[qinvokable]
644 fn send_security_key_to_keys_openpgp(self: Pin<&mut Sioul>, ident: &QString);
645
646 #[qinvokable]
648 fn forget_security_key_pin(self: Pin<&mut Sioul>);
649
650 #[qinvokable]
652 fn stop_using_security_key(self: Pin<&mut Sioul>, ident: &QString);
653
654 #[qinvokable]
656 fn show_tasks(self: Pin<&mut Sioul>, by: &QString, done: bool, query: &QString, project_id: &QString);
657
658 #[qinvokable]
660 fn filter_tasks(self: Pin<&mut Sioul>, kind: &QString, category: &QString);
661
662 #[qinvokable]
664 fn show_tasks_anyway(self: Pin<&mut Sioul>, on: bool);
665
666 #[qinvokable]
668 fn refresh_work(self: Pin<&mut Sioul>);
669
670 #[qinvokable]
672 fn task(self: &Sioul, uid: &QString) -> QString;
673
674 #[qinvokable]
679 fn new_task_form(self: &Sioul, from: &QString, key: &QString, start: f64, list: &QString) -> QString;
680
681 #[qinvokable]
684 fn save_task(self: Pin<&mut Sioul>, uid: &QString, edit: &QString, list: &QString) -> QString;
685
686 #[qinvokable]
688 fn capture(self: &Sioul, line: &QString) -> QString;
689
690 #[qinvokable]
692 fn add_task(self: Pin<&mut Sioul>, line: &QString, parent: &QString, list: &QString) -> QString;
693
694 #[qinvokable]
696 fn set_task_status(self: Pin<&mut Sioul>, uid: &QString, status: &QString) -> QString;
697
698 #[qinvokable]
701 fn set_felt(self: Pin<&mut Sioul>, uid: &QString, felt: &QString) -> QString;
702
703 #[qinvokable]
706 fn set_waits(self: Pin<&mut Sioul>, uid: &QString, other: &QString, wait: bool, gap_minutes: i32) -> QString;
707
708 #[qinvokable]
710 fn not_now(self: Pin<&mut Sioul>, uid: &QString);
711
712 #[qinvokable]
714 fn set_weather(self: Pin<&mut Sioul>, weather: &QString);
715
716 #[qinvokable]
718 fn delete_task(self: Pin<&mut Sioul>, uid: &QString);
719
720 #[qinvokable]
725 fn set_task_at(self: Pin<&mut Sioul>, uid: &QString, at: &QString) -> QString;
726
727 #[qinvokable]
732 fn pin_task(self: Pin<&mut Sioul>, uid: &QString, at: &QString, minutes: i32) -> QString;
733
734 #[qinvokable]
736 fn search_tasks(self: &Sioul, query: &QString, except: &QString) -> QString;
737
738 #[qinvokable]
740 fn new_list(self: Pin<&mut Sioul>, account: &QString, name: &QString) -> QString;
741
742 #[qinvokable]
744 fn list_accounts(self: &Sioul) -> QString;
745
746 #[qinvokable]
748 fn focus_start(self: Pin<&mut Sioul>, uid: &QString, minutes: i32);
749
750 #[qinvokable]
752 fn focus_pause(self: Pin<&mut Sioul>);
753
754 #[qinvokable]
756 fn focus_extend(self: Pin<&mut Sioul>, minutes: i32);
757
758 #[qinvokable]
760 fn focus_stop(self: Pin<&mut Sioul>, done: bool, note: &QString) -> QString;
761
762 #[qinvokable]
764 fn search_notes(self: Pin<&mut Sioul>, query: &QString);
765
766 #[qinvokable]
768 fn note(self: &Sioul, path: &QString) -> QString;
769
770 #[qinvokable]
774 fn save_note(self: Pin<&mut Sioul>, path: &QString, text: &QString, stamp: &QString) -> QString;
775
776 #[qinvokable]
778 fn create_note(self: Pin<&mut Sioul>, title: &QString, links: &QString) -> QString;
779
780 #[qinvokable]
782 fn related(self: &Sioul, uri: &QString) -> QString;
783
784 #[qinvokable]
786 fn link_things(self: Pin<&mut Sioul>, from: &QString, to: &QString) -> QString;
787
788 #[qinvokable]
790 fn unlink_things(self: Pin<&mut Sioul>, from: &QString, to: &QString) -> QString;
791
792 #[qinvokable]
794 fn search_things(self: &Sioul, query: &QString, kind: &QString, from: &QString) -> QString;
795
796 #[qinvokable]
798 fn uri_of(self: &Sioul, kind: &QString, id: &QString) -> QString;
799
800 #[qinvokable]
803 fn is_program(self: &Sioul, name: &QString) -> bool;
804
805 #[qinvokable]
807 fn first_frame(self: &Sioul);
808
809 #[qinvokable]
811 fn mark(self: &Sioul, what: &QString);
812
813 #[qinvokable]
815 fn phone_accounts(self: &Sioul) -> bool;
816
817 #[qinvokable]
819 fn choose_phone_account(self: Pin<&mut Sioul>);
820
821 #[qinvokable]
824 fn budget_detail(self: &Sioul, id: &QString, anchor: &QString, step: &QString) -> QString;
825
826 #[qinvokable]
829 fn keep_folder(self: Pin<&mut Sioul>, account: &QString, folder: &QString, kept: bool) -> QString;
830
831 #[qinvokable]
833 fn create_folder(self: Pin<&mut Sioul>, account: &QString, name: &QString);
834
835 #[qinvokable]
837 fn delete_folder(self: Pin<&mut Sioul>, account: &QString, folder: &QString);
838
839 #[qinvokable]
841 fn noise_url(self: &Sioul, kind: &QString) -> QString;
842
843 #[qinvokable]
845 fn calm_sounds(self: &Sioul) -> QString;
846
847 #[qinvokable]
850 fn move_task(self: Pin<&mut Sioul>, uid: &QString, list: &QString, confirmed: bool) -> QString;
851
852 #[qinvokable]
855 fn move_contact(self: Pin<&mut Sioul>, key: &QString, book: &QString, confirmed: bool) -> QString;
856
857 #[qinvokable]
859 fn address_books(self: &Sioul) -> QString;
860
861 #[qinvokable]
863 fn find_places(self: Pin<&mut Sioul>, name: &QString);
864
865 #[qinvokable]
867 fn set_weather_place(self: Pin<&mut Sioul>, name: &QString, latitude: f64, longitude: f64);
868
869 #[qinvokable]
871 fn health_page(self: &Sioul) -> QString;
872
873 #[qinvokable]
875 fn save_medicine(self: Pin<&mut Sioul>, id: &QString, edit: &QString) -> QString;
876
877 #[qinvokable]
879 fn save_prescription(self: Pin<&mut Sioul>, id: &QString, edit: &QString) -> QString;
880
881 #[qinvokable]
884 fn health_for_professional(self: &Sioul, prescription: &QString) -> QString;
885
886 #[qinvokable]
888 fn remove_health(self: Pin<&mut Sioul>, id: &QString) -> QString;
889
890 #[qinvokable]
892 fn refilled(self: Pin<&mut Sioul>, id: &QString) -> QString;
893
894 #[qinvokable]
897 fn set_dose_taken(self: Pin<&mut Sioul>, key: &QString, taken: bool);
898
899 #[qinvokable]
901 fn dose_not_taken(self: Pin<&mut Sioul>, key: &QString);
902
903 #[qinvokable]
906 fn dose_taken_at(self: Pin<&mut Sioul>, key: &QString, time: &QString) -> QString;
907
908 #[qinvokable]
910 fn dose_info(self: &Sioul, key: &QString) -> QString;
911
912 #[qinvokable]
915 fn dose_choose(self: Pin<&mut Sioul>, key: &QString, taken: bool) -> QString;
916
917 #[qinvokable]
921 fn device_off(self: Pin<&mut Sioul>, id: &QString, off: bool) -> QString;
922
923 #[qinvokable]
925 fn device_forget(self: Pin<&mut Sioul>, id: &QString) -> QString;
926
927 #[qinvokable]
929 fn needs(self: &Sioul) -> QString;
930
931 #[qinvokable]
933 fn save_needs(self: Pin<&mut Sioul>, edit: &QString, shown: &QString) -> QString;
934
935 #[qinvokable]
937 fn skip_need(self: Pin<&mut Sioul>, key: &QString, skip: bool) -> QString;
938
939 #[qinvokable]
941 fn needs_today(self: &Sioul) -> QString;
942
943 #[qinvokable]
945 fn refresh_health(self: Pin<&mut Sioul>);
946
947 #[qinvokable]
950 fn show_health_week(self: Pin<&mut Sioul>, day: &QString);
951
952 #[qinvokable]
955 fn change_need(self: Pin<&mut Sioul>, edit: &QString) -> QString;
956
957 #[qinvokable]
959 fn drag_need(self: Pin<&mut Sioul>, edit: &QString) -> QString;
960
961 #[qinvokable]
963 fn health_settings(self: &Sioul) -> QString;
964
965 #[qinvokable]
968 fn wake_settings(self: &Sioul, which: &QString);
969
970 #[qinvokable]
973 fn try_wake(self: &Sioul) -> QString;
974
975 #[qinvokable]
977 fn set_overlap_aside(self: Pin<&mut Sioul>, key: &QString) -> QString;
978
979 #[qinvokable]
982 fn move_need(self: Pin<&mut Sioul>, key: &QString, minutes: i32, time: &QString) -> QString;
983
984 #[qinvokable]
986 fn needs_later(self: &Sioul) -> i32;
987
988 #[qinvokable]
991 fn missed_doses(self: &Sioul) -> QString;
992
993 #[qinvokable]
995 fn touch(self: Pin<&mut Sioul>);
996
997 #[qinvokable]
1000 fn going_away(self: Pin<&mut Sioul>);
1001
1002 #[qinvokable]
1004 fn back_here(self: Pin<&mut Sioul>);
1005
1006 #[qinvokable]
1008 fn set_health(self: Pin<&mut Sioul>, key: &QString, value: &QString) -> QString;
1009
1010 #[qinvokable]
1012 fn movement_minutes(self: &Sioul) -> i32;
1013
1014 #[qinvokable]
1016 fn chat_minute(self: Pin<&mut Sioul>) -> bool;
1017
1018 #[qinvokable]
1020 fn chats_covered(self: &Sioul) -> bool;
1021
1022 #[qinvokable]
1024 fn memo_url(self: &Sioul) -> QString;
1025
1026 #[qinvokable]
1028 fn memo_recorded(self: Pin<&mut Sioul>, url: &QString) -> QString;
1029
1030 #[qinvokable]
1033 fn trash_note(self: Pin<&mut Sioul>, path: &QString) -> QString;
1034
1035 #[qinvokable]
1037 fn rename_note(self: Pin<&mut Sioul>, path: &QString, name: &QString) -> QString;
1038
1039 #[qinvokable]
1041 fn create_note_in(self: Pin<&mut Sioul>, folder: &QString, title: &QString) -> QString;
1042
1043 #[qinvokable]
1045 fn make_folder(self: Pin<&mut Sioul>, parent: &QString, name: &QString) -> QString;
1046
1047 #[qinvokable]
1049 fn rename_folder(self: Pin<&mut Sioul>, path: &QString, name: &QString) -> QString;
1050
1051 #[qinvokable]
1053 fn remove_folder(self: Pin<&mut Sioul>, path: &QString) -> QString;
1054
1055 #[qinvokable]
1057 fn remove_project(self: Pin<&mut Sioul>, id: &QString) -> QString;
1058
1059 #[qinvokable]
1061 fn save_budget(self: Pin<&mut Sioul>, id: &QString, edit: &QString) -> QString;
1062
1063 #[qinvokable]
1065 fn remove_budget(self: Pin<&mut Sioul>, what: &QString) -> QString;
1066
1067 #[qinvokable]
1071 fn remove_line(self: Pin<&mut Sioul>, uri: &QString, shown: &QString) -> QString;
1072
1073 #[qinvokable]
1075 fn set_notes_tree(self: Pin<&mut Sioul>, tree: bool);
1076
1077 #[qinvokable]
1079 fn view_flag(self: &Sioul, name: &QString) -> bool;
1080
1081 #[qinvokable]
1082 fn set_view_flag(self: Pin<&mut Sioul>, name: &QString, value: bool);
1083
1084
1085 #[qinvokable]
1087 fn add_movement(self: Pin<&mut Sioul>, edit: &QString) -> QString;
1088
1089 #[qinvokable]
1092 fn export_time_csv(self: Pin<&mut Sioul>, project: &QString, from: &QString, to: &QString, target: &QString) -> QString;
1093
1094 #[qinvokable]
1096 fn letters(self: &Sioul) -> QString;
1097
1098 #[qinvokable]
1100 fn letter_task(self: Pin<&mut Sioul>, id: &QString) -> QString;
1101
1102 #[qinvokable]
1104 fn letter_event(self: Pin<&mut Sioul>, id: &QString) -> QString;
1105
1106 #[qinvokable]
1108 fn letter_done(self: Pin<&mut Sioul>, id: &QString, project: &QString) -> QString;
1109
1110 #[qinvokable]
1112 fn letters_folder(self: &Sioul) -> QString;
1113
1114 #[qinvokable]
1117 fn letters_add(self: Pin<&mut Sioul>, files: &QString) -> QString;
1118
1119 #[qinvokable]
1121 fn bank(self: &Sioul) -> QString;
1122
1123 #[qinvokable]
1125 fn import_bank(self: Pin<&mut Sioul>, file: &QString) -> QString;
1126
1127 #[qinvokable]
1129 fn import_bank_into(self: Pin<&mut Sioul>, file: &QString, account: &QString) -> QString;
1130
1131 #[qinvokable]
1133 fn save_bank_account(self: Pin<&mut Sioul>, id: &QString, edit: &QString) -> QString;
1134
1135 #[qinvokable]
1136 fn remove_bank_account(self: Pin<&mut Sioul>, id: &QString) -> QString;
1137
1138 #[qinvokable]
1140 fn place_movement(self: Pin<&mut Sioul>, account: &QString, movement: &QString, choice: &QString) -> QString;
1141
1142 #[qinvokable]
1145 fn save_bank_rule(self: Pin<&mut Sioul>, rule: &QString, edit: &QString) -> QString;
1146
1147 #[qinvokable]
1148 fn remove_bank_rule(self: Pin<&mut Sioul>, rule: &QString) -> QString;
1149
1150 #[qinvokable]
1152 fn save_reserve(self: Pin<&mut Sioul>, id: &QString, edit: &QString) -> QString;
1153
1154 #[qinvokable]
1156 fn contracts(self: &Sioul) -> QString;
1157
1158 #[qinvokable]
1160 fn save_contract(self: Pin<&mut Sioul>, id: &QString, edit: &QString) -> QString;
1161
1162 #[qinvokable]
1164 fn remove_contract(self: Pin<&mut Sioul>, id: &QString) -> QString;
1165
1166 #[qinvokable]
1168 fn contract_letter(self: Pin<&mut Sioul>, id: &QString) -> QString;
1169
1170 #[qinvokable]
1172 fn contract_from(self: &Sioul, subject: &QString, from: &QString) -> QString;
1173
1174 #[qinvokable]
1176 fn routines(self: &Sioul) -> QString;
1177
1178 #[qinvokable]
1180 fn save_routine(self: Pin<&mut Sioul>, id: &QString, title: &QString, steps: &QString, by_itself: bool) -> QString;
1181
1182 #[qinvokable]
1184 fn remove_routine(self: Pin<&mut Sioul>, id: &QString) -> QString;
1185
1186 #[qinvokable]
1188 fn share_status(self: &Sioul, folder: &QString) -> QString;
1189
1190 #[qinvokable]
1192 fn start_sharing(self: Pin<&mut Sioul>, folder: &QString, passphrase: &QString, again: &QString) -> QString;
1193
1194 #[qinvokable]
1196 fn stop_sharing(self: Pin<&mut Sioul>) -> QString;
1197
1198 #[qinvokable]
1200 fn share_now(self: Pin<&mut Sioul>);
1201
1202 #[qinvokable]
1204 fn share_candidates(self: &Sioul) -> QString;
1205
1206 #[qinvokable]
1208 fn files_access(self: &Sioul) -> bool;
1209
1210 #[qinvokable]
1212 fn set_share_projects(self: Pin<&mut Sioul>, on: bool) -> QString;
1213
1214 #[qinvokable]
1216 fn set_share_part(self: Pin<&mut Sioul>, part: &QString, on: bool) -> QString;
1217
1218 #[qinvokable]
1221 fn set_notes_carried(self: Pin<&mut Sioul>, carried: bool) -> QString;
1222
1223 #[qinvokable]
1227 fn share_history(self: Pin<&mut Sioul>, filter: &QString);
1228
1229 #[qinvokable]
1231 fn share_put_back_preview(self: &Sioul, part: &QString, file: &QString, stamp: &QString) -> QString;
1232
1233 #[qinvokable]
1236 fn share_put_back(self: Pin<&mut Sioul>, part: &QString, file: &QString, stamp: &QString);
1237
1238 #[qinvokable]
1241 fn share_confirm_gone(self: Pin<&mut Sioul>, store: &QString) -> QString;
1242
1243 #[qinvokable]
1247 fn share_withheld(self: &Sioul) -> QString;
1248
1249 #[qinvokable]
1254 fn share_put_back_withheld(self: Pin<&mut Sioul>, store: &QString);
1255
1256 #[qinvokable]
1260 fn share_take_out_withheld(self: Pin<&mut Sioul>, store: &QString) -> QString;
1261
1262 #[qinvokable]
1265 fn device_files(self: &Sioul, id: &QString) -> QString;
1266
1267 #[qinvokable]
1272 fn device_remove_files(self: Pin<&mut Sioul>, id: &QString) -> QString;
1273
1274 #[qinvokable]
1277 fn share_estimate(self: Pin<&mut Sioul>, part: &QString);
1278
1279 #[qinvokable]
1281 fn spam_status(self: &Sioul) -> QString;
1282
1283 #[qinvokable]
1289 fn spam_train(self: Pin<&mut Sioul>, named: &QString) -> QString;
1290
1291 #[qinvokable]
1293 fn spam_stop(self: Pin<&mut Sioul>);
1294
1295 #[qinvokable]
1298 fn mail_filters(self: &Sioul) -> QString;
1299
1300 #[qinvokable]
1305 fn set_mail_filters(self: Pin<&mut Sioul>, filters: &QString, shown: &QString) -> QString;
1306
1307 #[qinvokable]
1309 fn mail_filter_said(self: &Sioul, filter: &QString) -> QString;
1310
1311 #[qinvokable]
1314 fn mail_filter_try(self: Pin<&mut Sioul>, filter: &QString);
1315
1316 #[qinvokable]
1319 fn mail_filters_preview(self: Pin<&mut Sioul>);
1320
1321 #[qinvokable]
1323 fn mail_filters_run(self: Pin<&mut Sioul>) -> QString;
1324
1325 #[qinvokable]
1329 fn new_mail_filter(self: Pin<&mut Sioul>, given: &QString) -> QString;
1330
1331 #[qinvokable]
1334 fn set_share_backup(self: Pin<&mut Sioul>, on: bool) -> QString;
1335
1336 #[qinvokable]
1340 fn set_share_send(self: Pin<&mut Sioul>, on: bool) -> QString;
1341
1342 #[qinvokable]
1346 fn share_send_again(self: Pin<&mut Sioul>);
1347
1348 #[qinvokable]
1352 fn share_backup_place(self: Pin<&mut Sioul>, place: &QString) -> QString;
1353
1354 #[qinvokable]
1359 fn start_sharing_on_server(self: Pin<&mut Sioul>, account: &QString, place: &QString, passphrase: &QString, again: &QString);
1360
1361 #[qinvokable]
1366 fn join_sign_in(self: Pin<&mut Sioul>, server: &QString);
1367
1368 #[qinvokable]
1370 fn join_open_again(self: Pin<&mut Sioul>);
1371
1372 #[qinvokable]
1375 fn join_look_again(self: Pin<&mut Sioul>);
1376
1377 #[qinvokable]
1380 fn join_cancel(self: Pin<&mut Sioul>);
1381
1382 #[qinvokable]
1385 fn join_status(self: &Sioul) -> QString;
1386
1387 #[qinvokable]
1391 fn join_folder(self: Pin<&mut Sioul>, place: &QString, passphrase: &QString, again: &QString);
1392
1393 #[qinvokable]
1395 fn join_done(self: Pin<&mut Sioul>);
1396
1397 #[qinvokable]
1401 fn join_replace(self: Pin<&mut Sioul>, device: &QString, yes: bool) -> QString;
1402
1403 #[qinvokable]
1405 fn folders_in(self: &Sioul, path: &QString) -> QString;
1406
1407 #[qinvokable]
1409 fn ask_files_access(self: Pin<&mut Sioul>);
1410
1411 #[qinvokable]
1415 fn change_line(self: Pin<&mut Sioul>, uri: &QString, shown: &QString, label: &QString, amount: f64, date: &QString) -> QString;
1416
1417 #[qinvokable]
1419 fn site_list(self: &Sioul) -> QString;
1420
1421 #[qinvokable]
1423 fn site_notified(self: Pin<&mut Sioul>, id: &QString, title: &QString, text: &QString);
1424
1425 #[qinvokable]
1427 fn site_seen(self: Pin<&mut Sioul>, id: &QString);
1428
1429 #[qinvokable]
1431 fn site_notices(self: &Sioul) -> QString;
1432
1433 #[qinvokable]
1435 fn site_for_sender(self: &Sioul, sender: &QString) -> QString;
1436
1437 #[qinvokable]
1439 fn set_site(self: Pin<&mut Sioul>, id: &QString, field: &QString, value: &QString) -> QString;
1440
1441 #[qinvokable]
1443 fn site_presets(self: &Sioul, country: &QString, region: &QString) -> QString;
1444
1445 #[qinvokable]
1447 fn add_site(self: Pin<&mut Sioul>, edit: &QString) -> QString;
1448
1449 #[qinvokable]
1451 fn site_presets_tree(self: &Sioul) -> QString;
1452
1453 #[qinvokable]
1455 fn add_sites(self: Pin<&mut Sioul>, rows: &QString) -> QString;
1456
1457 #[qinvokable]
1459 fn move_site(self: Pin<&mut Sioul>, id: &QString, delta: i32) -> QString;
1460
1461 #[qinvokable]
1463 fn remove_site(self: Pin<&mut Sioul>, id: &QString) -> QString;
1464
1465 #[qinvokable]
1467 fn google_built_in(self: &Sioul) -> bool;
1468
1469 #[qinvokable]
1471 fn call_devices(self: &Sioul) -> QString;
1472
1473 #[qinvokable]
1475 fn set_call_device(self: Pin<&mut Sioul>, which: &QString, name: &QString) -> QString;
1476
1477 #[qinvokable]
1480 fn calls_script(self: &Sioul) -> QString;
1481
1482 #[qinvokable]
1484 fn bitwarden_state(self: &Sioul) -> QString;
1485
1486 #[qinvokable]
1490 fn bitwarden_unlock(self: Pin<&mut Sioul>, password: &QString, provider: i32, code: &QString) -> i32;
1491
1492 #[qinvokable]
1494 fn bitwarden_lock(self: Pin<&mut Sioul>);
1495
1496 #[qinvokable]
1499 fn bitwarden_send_code(self: Pin<&mut Sioul>, password: &QString) -> i32;
1500
1501 #[qinvokable]
1504 fn bitwarden_passkey_begin(self: Pin<&mut Sioul>) -> i32;
1505
1506 #[qinvokable]
1509 fn bitwarden_passkey(self: Pin<&mut Sioul>, answer: &QString) -> i32;
1510
1511 #[qinvokable]
1515 fn bitwarden_search(self: Pin<&mut Sioul>, url: &QString, site: &QString, user: &QString) -> QString;
1516
1517 #[qinvokable]
1519 fn bitwarden_site(self: &Sioul, url: &QString) -> QString;
1520
1521 #[qinvokable]
1523 fn bitwarden_login(self: &Sioul, url: &QString, id: &QString) -> QString;
1524
1525 #[qinvokable]
1527 fn map_view(self: &Sioul) -> QString;
1528
1529 #[qinvokable]
1531 fn place_of(self: &Sioul, address: &QString) -> QString;
1532
1533 #[qinvokable]
1535 fn locate_addresses(self: Pin<&mut Sioul>, allow: bool);
1536
1537 #[qinvokable]
1539 fn refresh_mode(self: Pin<&mut Sioul>);
1540
1541 #[qinvokable]
1543 fn work_a_while(self: Pin<&mut Sioul>, minutes: i32);
1544
1545 #[qinvokable]
1549 fn done_for_the_day(self: Pin<&mut Sioul>) -> QString;
1550
1551 #[qinvokable]
1555 fn day_review(self: &Sioul, kind: &QString) -> QString;
1556
1557 #[qinvokable]
1561 fn close_review(self: Pin<&mut Sioul>, kind: &QString, date: &QString, felt: &QString, mix: &QString, memo: &QString) -> QString;
1562
1563 #[qinvokable]
1565 fn day_review_line(self: &Sioul, date: &QString) -> QString;
1566
1567 #[qinvokable]
1569 fn set_first_step(self: Pin<&mut Sioul>, text: &QString);
1570
1571 #[qinvokable]
1573 fn clear_first_step(self: Pin<&mut Sioul>);
1574
1575 #[qinvokable]
1577 fn usual_hours(self: Pin<&mut Sioul>);
1578
1579 #[qinvokable]
1582 fn work_until_tonight(self: Pin<&mut Sioul>);
1583
1584 #[qinvokable]
1587 fn set_free_time(self: Pin<&mut Sioul>, on: bool) -> QString;
1588
1589 #[qinvokable]
1591 fn keep_usual_end(self: Pin<&mut Sioul>);
1592
1593 #[qinvokable]
1595 fn set_free_nothing(self: Pin<&mut Sioul>, on: bool);
1596
1597 #[qinvokable]
1599 fn pause_now(self: Pin<&mut Sioul>);
1600
1601 #[qinvokable]
1603 fn come_back(self: Pin<&mut Sioul>) -> QString;
1604
1605 #[qinvokable]
1607 fn lighten_tomorrow(self: Pin<&mut Sioul>);
1608
1609 #[qinvokable]
1611 fn forget_pause(self: Pin<&mut Sioul>);
1612
1613 #[qinvokable]
1615 fn pause_screen(self: &Sioul) -> QString;
1616
1617 #[qinvokable]
1619 fn pause_setup(self: &Sioul) -> QString;
1620
1621 #[qinvokable]
1623 fn open_dnd(self: &Sioul, key: &QString);
1624
1625 #[qinvokable]
1628 fn dnd_toggle(self: Pin<&mut Sioul>, on: bool, minutes: i32);
1629
1630 #[qinvokable]
1635 fn reach_choose(self: Pin<&mut Sioul>, state: &QString, length: &QString);
1636
1637 #[qinvokable]
1641 fn let_phone_through(self: Pin<&mut Sioul>);
1642
1643 #[qinvokable]
1645 fn dnd_setup(self: &Sioul) -> QString;
1646
1647 #[qinvokable]
1649 fn dnd_change(self: Pin<&mut Sioul>, verb: &QString, json: &QString) -> QString;
1650
1651 #[qinvokable]
1653 fn app_notes_setup(self: &Sioul) -> QString;
1654
1655 #[qinvokable]
1657 fn app_notes_change(self: &Sioul, verb: &QString, json: &QString) -> QString;
1658
1659 #[qinvokable]
1662 fn calls_through(self: Pin<&mut Sioul>, on: bool, minutes: i32);
1663
1664 #[qinvokable]
1666 fn calls_view(self: &Sioul) -> QString;
1667
1668 #[qinvokable]
1670 fn calls_action(self: Pin<&mut Sioul>, verb: &QString, json: &QString) -> QString;
1671
1672 #[qinvokable]
1674 fn calls_setup(self: &Sioul) -> QString;
1675
1676 #[qinvokable]
1680 fn phone_messages(self: Pin<&mut Sioul>, verb: &QString, json: &QString) -> QString;
1681
1682 #[qinvokable]
1686 fn texts(self: Pin<&mut Sioul>, verb: &QString, json: &QString) -> QString;
1687
1688 #[qinvokable]
1692 fn texts_attach(self: Pin<&mut Sioul>, json: &QString);
1693
1694 #[qinvokable]
1697 fn reaches_view(self: &Sioul) -> QString;
1698
1699 #[qinvokable]
1702 fn reach_card(self: &Sioul) -> QString;
1703
1704 #[qinvokable]
1707 fn reach_talk(self: Pin<&mut Sioul>, verb: &QString, key: &QString) -> QString;
1708
1709 #[qinvokable]
1713 fn reaches_set(self: Pin<&mut Sioul>, row: &QString, words: &QString, shown: &QString) -> QString;
1714
1715 #[qinvokable]
1717 fn reaches_preset(self: Pin<&mut Sioul>, id: &QString) -> QString;
1718
1719 #[qinvokable]
1722 fn person_sheet(self: &Sioul, key: &QString, addresses: &QString) -> QString;
1723
1724 #[qinvokable]
1726 fn person_sheet_change(self: Pin<&mut Sioul>, key: &QString, addresses: &QString, verb: &QString, value: &QString) -> QString;
1727
1728 #[qinvokable]
1730 fn calls_setup_change(self: Pin<&mut Sioul>, verb: &QString, json: &QString) -> QString;
1731
1732 #[qinvokable]
1734 fn project_rows(self: &Sioul) -> QString;
1735
1736 #[qinvokable]
1738 fn project_page(self: &Sioul, id: &QString) -> QString;
1739
1740 #[qinvokable]
1742 fn save_project(self: Pin<&mut Sioul>, id: &QString, edit: &QString) -> QString;
1743
1744 #[qinvokable]
1746 fn time_page(self: &Sioul, period: &QString, anchor: &QString, project: &QString) -> QString;
1747
1748 #[qinvokable]
1750 fn note_time(self: Pin<&mut Sioul>, edit: &QString) -> QString;
1751
1752 #[qinvokable]
1754 fn remove_time(self: Pin<&mut Sioul>, key: &QString) -> QString;
1755
1756 #[qinvokable]
1758 fn stopped(self: &Sioul) -> QString;
1759
1760 #[qinvokable]
1762 fn set_stopped(self: Pin<&mut Sioul>, text: &QString) -> QString;
1763
1764 #[qinvokable]
1767 fn change_time(self: Pin<&mut Sioul>, key: &QString, edit: &QString) -> QString;
1768
1769 #[qinvokable]
1771 fn task_choices(self: &Sioul, keep: &QString) -> QString;
1772
1773 #[qinvokable]
1775 fn make_invoice(self: Pin<&mut Sioul>, id: &QString) -> QString;
1776
1777 #[qinvokable]
1779 fn take_invoices(self: Pin<&mut Sioul>) -> QString;
1780
1781 #[qinvokable]
1783 fn invoice_again(self: &Sioul, number: &QString) -> QString;
1784
1785 #[qinvokable]
1787 fn set_invoice_paid(self: Pin<&mut Sioul>, number: &QString, paid: bool) -> QString;
1788
1789 #[qinvokable]
1791 fn make_linked(self: Pin<&mut Sioul>, kind: &QString, from: &QString, key: &QString, start: f64) -> QString;
1792
1793 #[qinvokable]
1795 fn task_from_mail(self: Pin<&mut Sioul>, key: &QString) -> QString;
1796
1797 #[qinvokable]
1799 fn note_from_mail(self: Pin<&mut Sioul>, key: &QString) -> QString;
1800
1801 #[qinvokable]
1803 fn note_from_event(self: Pin<&mut Sioul>, key: &QString, start: f64) -> QString;
1804
1805 #[qinvokable]
1807 fn task_from_event(self: Pin<&mut Sioul>, key: &QString, start: f64) -> QString;
1808
1809 #[qinvokable]
1811 fn task_from_line(self: Pin<&mut Sioul>, path: &QString, line: i32) -> QString;
1812
1813 #[qinvokable]
1815 fn draft_for_task(self: Pin<&mut Sioul>, uid: &QString) -> QString;
1816
1817 #[qinvokable]
1819 fn mail_note(self: Pin<&mut Sioul>, path: &QString) -> QString;
1820
1821 #[qinvokable]
1823 fn settings(self: &Sioul, view: &QString) -> QString;
1824
1825 #[qinvokable]
1827 fn set_setting(self: Pin<&mut Sioul>, key: &QString, value: &QString) -> QString;
1828
1829 #[qinvokable]
1834 fn change_setting(self: Pin<&mut Sioul>, key: &QString, shown: &QString, value: &QString) -> QString;
1835
1836 #[qinvokable]
1838 fn forced_theme(self: &Sioul) -> QString;
1839
1840 #[qinvokable]
1844 fn follow_icons(self: &Sioul, dark: bool, chosen: bool);
1845
1846 #[qinvokable]
1850 fn plain_notice(self: &Sioul, title: &QString, body: &QString);
1851
1852 #[qinvokable]
1854 fn set_realtime_mode(self: Pin<&mut Sioul>, on: bool);
1855
1856 #[qinvokable]
1858 fn grab_phone(self: &Sioul) -> bool;
1859
1860 #[qinvokable]
1863 fn grab_steps(self: &Sioul) -> QString;
1864 }
1865
1866 #[auto_cxx_name]
1867 unsafe extern "RustQt" {
1869 #[qsignal]
1871 fn copy_requested(self: Pin<&mut Sioul>, text: QString);
1872
1873 #[qsignal]
1875 fn account_added(self: Pin<&mut Sioul>);
1876
1877 #[qsignal]
1879 fn account_password_done(self: Pin<&mut Sioul>, id: QString, problem: QString);
1880
1881 #[qsignal]
1883 fn letters_changed(self: Pin<&mut Sioul>);
1884
1885 #[qsignal]
1888 fn task_done(self: Pin<&mut Sioul>, uid: QString);
1889
1890 #[qsignal]
1894 fn shared_in(self: Pin<&mut Sioul>, stores: QString);
1895
1896 #[qsignal]
1898 fn share_listed(self: Pin<&mut Sioul>, versions: QString);
1899
1900 #[qsignal]
1902 fn share_put_back_done(self: Pin<&mut Sioul>, problem: QString);
1903
1904 #[qsignal]
1906 fn share_withheld_back(self: Pin<&mut Sioul>, problem: QString);
1907
1908 #[qsignal]
1910 fn share_estimated(self: Pin<&mut Sioul>, estimate: QString);
1911
1912 #[qsignal]
1915 fn texts_attached(self: Pin<&mut Sioul>, attached: QString);
1916
1917 #[qsignal]
1919 fn spam_changed(self: Pin<&mut Sioul>, status: QString);
1920
1921 #[qsignal]
1923 fn mail_filters_found(self: Pin<&mut Sioul>, found: QString);
1924
1925 #[qsignal]
1930 fn message_read(self: Pin<&mut Sioul>, key: QString, reading: QString, invitation: QString);
1931
1932 #[qsignal]
1935 fn share_started(self: Pin<&mut Sioul>, problem: QString);
1936
1937 #[qsignal]
1939 fn join_changed(self: Pin<&mut Sioul>);
1940
1941 #[qsignal]
1943 fn reminder_opened(self: Pin<&mut Sioul>, kind: QString, uri: QString, key: QString);
1944
1945 #[qsignal]
1949 fn tray_notice(self: Pin<&mut Sioul>, title: QString, body: QString, kind: QString, key: QString);
1950
1951 #[qsignal]
1953 fn open_url(self: Pin<&mut Sioul>, url: QString);
1954
1955 #[qsignal]
1959 fn microsoft_question(self: Pin<&mut Sioul>, question: QString);
1960
1961 #[qsignal]
1964 fn scan_unavailable(self: Pin<&mut Sioul>, key: QString, index: i32, what: i32, name: QString, hint: QString, big: bool);
1965
1966 #[qsignal]
1968 fn paper_kept(self: Pin<&mut Sioul>, file: QString, title: QString, kind: QString);
1969
1970 #[qsignal]
1973 fn compose_requested(self: Pin<&mut Sioul>, id: QString);
1974
1975 #[qsignal]
1979 fn draft_files_arrived(self: Pin<&mut Sioul>, id: QString, line: QString, problem: bool);
1980
1981 #[qsignal]
1983 fn keys_changed(self: Pin<&mut Sioul>);
1984
1985 #[qsignal]
1988 fn security_key_changed(self: Pin<&mut Sioul>, context: QString, state: QString);
1989
1990 #[qsignal]
1992 fn links_changed(self: Pin<&mut Sioul>);
1993
1994 #[qsignal]
1996 fn places_changed(self: Pin<&mut Sioul>);
1997
1998 #[qsignal]
2000 fn sites_changed(self: Pin<&mut Sioul>);
2001
2002 #[qsignal]
2004 fn bitwarden_found(self: Pin<&mut Sioul>, ticket: i32, found: QString);
2005
2006 #[qsignal]
2009 fn bitwarden_answered(self: Pin<&mut Sioul>, ticket: i32, answer: QString);
2010
2011 #[qsignal]
2014 fn phone_account_chosen(self: Pin<&mut Sioul>, name: QString, kind: QString);
2015 }
2016
2017 impl cxx_qt::Threading for Sioul {}
2018}
2019
2020use qobject::Sioul;
2021pub(crate) type QtThread = cxx_qt::CxxQtThread<Sioul>;
2022
2023#[cfg(target_os = "android")]
2025static PHONE_CHOOSER: std::sync::Mutex<Option<QtThread>> = std::sync::Mutex::new(None);
2026
2027#[cfg(target_os = "android")]
2028unsafe extern "C" {
2030 fn sioul_android_choose_account();
2032 fn sioul_android_files_access() -> bool;
2034 fn sioul_android_ask_files_access();
2036 pub(crate) fn sioul_android_broadcast(package: *const std::ffi::c_char, receiver: *const std::ffi::c_char, action: *const std::ffi::c_char);
2039}
2040
2041#[cfg(target_os = "android")]
2047#[unsafe(no_mangle)]
2049pub unsafe extern "C" fn sioul_android_account_chosen(name: *const std::ffi::c_char, kind: *const std::ffi::c_char) {
2050 let read = |text: *const std::ffi::c_char| if text.is_null() { String::new() } else { unsafe { std::ffi::CStr::from_ptr(text) }.to_string_lossy().into_owned() };
2052 let (name, kind) = (read(name), read(kind));
2053 let waiting = PHONE_CHOOSER.lock().ok().and_then(|mut w| w.take());
2054 if let Some(qt) = waiting {
2055 let _ = qt.queue(move |mut sioul| sioul.as_mut().phone_account_chosen(QString::from(&name), QString::from(&kind)));
2056 }
2057}
2058
2059#[derive(Default)]
2060pub struct SioulRust {
2061 porch: QString,
2062 budgets: QString,
2063 accounts: QString,
2064 blocked: QString,
2065 status: QString,
2066 busy: bool,
2067 found: QString,
2068 scouted: QString,
2069 scouting: bool,
2070 form_error: QString,
2071 form_busy: bool,
2072 mail_accounts: QString,
2073 mail_folder: QString,
2074 drafts: QString,
2075 mail_search: QString,
2076 undo_line: QString,
2077 contacts: QString,
2078 duplicates: QString,
2079 agenda: QString,
2080 overlaps: QString,
2081 health_view: QString,
2082 missed_view: QString,
2083 needs_view: QString,
2084 tasks: QString,
2085 notes: QString,
2086 focus_session: QString,
2087 reading: QString,
2088 realtime: bool,
2089 away: bool,
2090 mode: QString,
2091 forecast: QString,
2092 places_found: QString,
2093 shared: Arc<Shared>,
2094}
2095
2096#[derive(Default)]
2098pub(crate) struct Shared {
2099 opened_anyway: AtomicBool,
2103 pub(crate) reachable: AtomicBool,
2107 pub(crate) statuses: Mutex<BTreeMap<String, (String, bool)>>,
2109 pub(crate) password_wanted: Mutex<BTreeSet<String>>,
2111 shown: Mutex<BTreeMap<String, ImapOrigin>>,
2113 pub(crate) watchers: Mutex<BTreeMap<String, Arc<Control>>>,
2115 pub(crate) removing: Mutex<BTreeSet<String>>,
2118 learned: Mutex<BTreeSet<String>>,
2120 generation: AtomicU64,
2122 shown_generation: AtomicU64,
2123 clean: Mutex<BTreeSet<u64>>,
2125 pub(crate) open_folder: Mutex<Option<mail::OpenFolder>>,
2127 pub(crate) pending: Mutex<Vec<Arc<mail::Pending>>>,
2129 pub(crate) mail_generation: AtomicU64,
2130 pub(crate) mail_shown_generation: AtomicU64,
2131 said_at: Mutex<Option<std::time::Instant>>,
2134 pub(crate) pim: Mutex<pim::PimState>,
2136 pub(crate) pim_generation: AtomicU64,
2137 pub(crate) pim_shown_generation: AtomicU64,
2138 pub(crate) dav_watchers: Mutex<BTreeMap<String, Arc<Control>>>,
2140 pub(crate) work: Mutex<work::WorkState>,
2142 pub(crate) work_generation: AtomicU64,
2143 pub(crate) work_shown_generation: AtomicU64,
2144 pub(crate) loaded: Mutex<Option<Arc<sioul_core::links::Loaded>>>,
2146 pub(crate) realtime: AtomicBool,
2148 pub(crate) locating: AtomicBool,
2150 pub(crate) bitwarden: Mutex<Option<crate::sites::Vault>>,
2152 pub(crate) weather_fetching: AtomicBool,
2154 pub(crate) held_back: Mutex<std::collections::BTreeSet<String>>,
2156 pub(crate) oauth_stop: AtomicBool,
2158 pub(crate) microsoft_answer: Mutex<Option<std::sync::mpsc::Sender<bool>>>,
2160 pub(crate) github: Mutex<Option<Arc<sioul_sync::Control>>>,
2162 pub(crate) active: std::sync::atomic::AtomicI64,
2164 pub(crate) planned_at: std::sync::atomic::AtomicI64,
2166 pub(crate) planned_day: std::sync::atomic::AtomicI64,
2167 views_job: Job,
2169 pub(crate) mail_job: Job,
2170 pub(crate) pim_job: Job,
2171 pub(crate) work_job: Job,
2172 pub(crate) health_job: Job,
2173}
2174
2175const REPLAN_EVERY: i64 = 12 * 3600;
2178
2179const SAID_FOR: std::time::Duration = std::time::Duration::from_secs(30);
2181
2182pub(crate) fn config_path() -> PathBuf {
2183 config::default_path()
2184}
2185
2186pub(crate) fn load_config() -> Config {
2187 type Parsed = (PathBuf, u64, std::time::SystemTime, Config);
2192 static PARSED: Mutex<Option<Parsed>> = Mutex::new(None);
2193 let path = config_path();
2194 let settled = |time: &std::time::SystemTime| time.elapsed().is_ok_and(|age| age > std::time::Duration::from_secs(2));
2195 let stamp = std::fs::metadata(&path).ok().and_then(|m| Some((m.len(), m.modified().ok()?))).filter(|(_, time)| settled(time));
2196 if let Some((size, time)) = stamp
2197 && let Ok(parsed) = PARSED.lock()
2198 && let Some((p, s, t, config)) = parsed.as_ref()
2199 && *p == path
2200 && *s == size
2201 && *t == time
2202 {
2203 return config.clone();
2204 }
2205 let config = Config::load(&path).unwrap_or_default();
2206 sioul_core::words::set_current(sioul_core::words::Words::of(&config));
2208 if let (Some((size, time)), Ok(mut parsed)) = (stamp, PARSED.lock()) {
2209 *parsed = Some((path, size, time, config.clone()));
2210 }
2211 config
2212}
2213
2214pub(crate) fn tr() -> &'static Translator {
2216 static TRANSLATOR: OnceLock<Translator> = OnceLock::new();
2217 TRANSLATOR.get_or_init(|| Translator::new(&load_config().language.unwrap_or_else(session_language)))
2218}
2219
2220fn session_language() -> String {
2224 let said = ["LC_ALL", "LC_MESSAGES", "LANG"].iter().any(|v| std::env::var(v).is_ok_and(|l| !l.trim().is_empty()));
2225 if said {
2226 return i18n::system_language();
2227 }
2228 system_locale().unwrap_or_else(i18n::system_language)
2229}
2230
2231#[cfg(windows)]
2233fn system_locale() -> Option<String> {
2234 #[link(name = "kernel32")]
2235 unsafe extern "system" {
2237 fn GetUserDefaultLocaleName(name: *mut u16, size: i32) -> i32;
2238 }
2239 let mut name = [0u16; 85];
2241 let written = unsafe { GetUserDefaultLocaleName(name.as_mut_ptr(), name.len() as i32) };
2243 let length = usize::try_from(written).ok().filter(|n| *n > 1 && *n <= name.len())?;
2244 Some(String::from_utf16_lossy(&name[..length - 1]))
2245}
2246
2247#[cfg(target_os = "macos")]
2249fn system_locale() -> Option<String> {
2250 let out = std::process::Command::new("defaults").args(["read", "-g", "AppleLanguages"]).output().ok()?;
2252 String::from_utf8_lossy(&out.stdout).split('"').nth(1).map(str::to_string).filter(|l| !l.is_empty())
2253}
2254
2255#[cfg(not(any(windows, target_os = "macos")))]
2257fn system_locale() -> Option<String> {
2258 None
2259}
2260
2261pub(crate) fn say(id: &str, pairs: &[(&str, String)]) -> String {
2262 let mut args = i18n::args();
2263 for (key, value) in pairs {
2264 match value.parse::<i32>() {
2267 Ok(n) if n.to_string() == *value => args.set(key.to_string(), n),
2268 _ => args.set(key.to_string(), value.clone()),
2269 }
2270 }
2271 tr().text(id, Some(&args))
2272}
2273
2274pub(crate) fn json(value: &impl serde::Serialize) -> String {
2275 serde_json::to_string(value).unwrap_or_default()
2276}
2277
2278pub(crate) fn file_url(path: &Path) -> String {
2282 fn encoded(path: &str) -> String {
2283 path.bytes().map(|b| if b.is_ascii_alphanumeric() || b"/-._~!$&'()*+,;=:@".contains(&b) { char::from(b).to_string() } else { format!("%{b:02X}") }).collect()
2284 }
2285 let text = path.to_string_lossy().into_owned();
2286 #[cfg(windows)]
2287 let text = windows_slashed(&text);
2288 match text.strip_prefix("//") {
2289 Some(share) if cfg!(windows) => format!("file://{}", encoded(share)),
2291 _ => format!("file://{}", encoded(&text)),
2292 }
2293}
2294
2295#[cfg(any(windows, test))]
2298fn windows_slashed(text: &str) -> String {
2299 let plain = match text.strip_prefix(r"\\?\UNC\") {
2300 Some(rest) => format!(r"\\{rest}"),
2301 None => text.strip_prefix(r"\\?\").unwrap_or(text).to_string(),
2302 };
2303 let slashed = plain.replace('\\', "/");
2304 if slashed.starts_with("//") { slashed } else { format!("/{slashed}") }
2305}
2306
2307#[cfg(any(windows, test))]
2309fn windows_path(decoded: &str) -> String {
2310 let bytes = decoded.as_bytes();
2311 let path = if bytes.len() >= 3 && bytes[0] == b'/' && bytes[1].is_ascii_alphabetic() && bytes[2] == b':' {
2312 decoded[1..].to_string()
2313 } else if decoded.starts_with('/') {
2314 decoded.to_string()
2315 } else {
2316 format!("//{decoded}")
2317 };
2318 path.replace('/', "\\")
2319}
2320
2321pub(crate) fn local_path(url: &str) -> PathBuf {
2325 let Some(rest) = url.strip_prefix("file://") else { return PathBuf::from(url) };
2326 let rest = rest.strip_prefix("localhost/").map_or_else(|| rest.to_string(), |r| format!("/{r}"));
2328 let decoded = crate::projects::percent_decoded(&rest);
2329 #[cfg(windows)]
2330 let decoded = windows_path(&decoded);
2331 PathBuf::from(decoded)
2332}
2333
2334pub(crate) fn one_at_a_time(lock: &Mutex<()>) -> Option<std::sync::MutexGuard<'_, ()>> {
2337 match lock.try_lock() {
2338 Ok(guard) => Some(guard),
2339 Err(std::sync::TryLockError::Poisoned(poisoned)) => Some(poisoned.into_inner()),
2340 Err(std::sync::TryLockError::WouldBlock) => None,
2341 }
2342}
2343
2344#[derive(Default)]
2348pub(crate) struct Job {
2349 running: AtomicBool,
2350 again: AtomicBool,
2351 deferred: AtomicBool,
2353}
2354
2355pub(crate) static AWAY: AtomicBool = AtomicBool::new(false);
2359
2360pub(crate) fn coalesced(shared: &Arc<Shared>, which: fn(&Shared) -> &Job, work: impl Fn(&Arc<Shared>) + Send + 'static) {
2362 let job = which(shared);
2363 if AWAY.load(Ordering::SeqCst) {
2364 job.deferred.store(true, Ordering::SeqCst);
2365 return;
2366 }
2367 job.again.store(true, Ordering::SeqCst);
2368 if job.running.swap(true, Ordering::SeqCst) {
2369 return;
2370 }
2371 let shared = Arc::clone(shared);
2372 std::thread::spawn(move || {
2373 let job = which(&shared);
2374 loop {
2375 while job.again.swap(false, Ordering::SeqCst) {
2376 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| work(&shared)));
2378 }
2379 job.running.store(false, Ordering::SeqCst);
2380 if !job.again.load(Ordering::SeqCst) || job.running.swap(true, Ordering::SeqCst) {
2382 break;
2383 }
2384 }
2385 });
2386}
2387
2388pub(crate) fn reading_json() -> String {
2390 let config = load_config();
2391 let reading = config.reading;
2392 serde_json::json!({ "family": reading.family, "size": reading.size, "spacing": reading.spacing, "theme": config.theme.unwrap_or_default(), "places_named": config.places_named, "screen_colours": config.screen_colours.unwrap_or(true), "calmer_colours": config.calmer_colours.unwrap_or_default() }).to_string()
2394}
2395
2396struct World {
2398 config: Config,
2399 store: Option<ProjectStore>,
2400 known: SenderList,
2401 senders: porch::Senders,
2403}
2404
2405impl World {
2406 fn load() -> World {
2407 let config = load_config();
2408 World {
2409 store: config.notes_root_path().and_then(|root| ProjectStore::load(&root).ok()).map(|s| s.with_ties(&sioul_core::links::LocalLinks::load(&sioul_core::links::LocalLinks::default_path()))),
2411 known: KnownSenders::load(&config.known_senders_path()),
2412 senders: porch::Senders::load(&config),
2413 config,
2414 }
2415 }
2416
2417 fn gather(&self, state: &PorchState) -> Vec<Triaged> {
2419 let now = Zoned::now().timestamp().as_second();
2420 porch::gather(&self.config.mail_sources(), self.store.as_ref(), &self.known, &self.senders, state, now)
2421 }
2422
2423 fn ledger(&self) -> Option<Ledger> {
2424 self.config.notes_root_path().and_then(|root| Ledger::load_with_bank(&root, &sioul_core::words::Words::of(&self.config)).ok())
2426 }
2427
2428 fn ledger_path(&self) -> Option<PathBuf> {
2429 self.config.notes_root_path().map(|root| root.join(budget::LEDGER))
2430 }
2431
2432 fn mail_lines(&self, ledger: &Ledger) -> Vec<MailLine> {
2434 let ignored = MoneyState::load(&MoneyState::default_path()).ignored;
2435 budget::mail_lines(ledger, &self.gather(&PorchState::default()), &ignored, &sioul_core::words::Words::of(&self.config))
2436 }
2437
2438 fn judge(&self, key: &str) -> Option<(PathBuf, Triaged)> {
2440 let path = maildir::locate(Path::new(key))?.canonicalize().ok()?;
2441 let sources = self.config.mail_sources();
2442 let inside = sources.iter().any(|s| s.folder.canonicalize().is_ok_and(|f| path.starts_with(f)));
2443 if !inside {
2444 return None;
2445 }
2446 let now = Zoned::now().timestamp().as_second();
2447 let canonical: Vec<_> = sources
2448 .into_iter()
2449 .map(|mut s| {
2450 s.folder = s.folder.canonicalize().unwrap_or(s.folder);
2451 s
2452 })
2453 .collect();
2454 let triaged = porch::judge(std::slice::from_ref(&path), &canonical, self.store.as_ref(), &self.known, &porch::Senders::default(), now);
2455 triaged.into_iter().next().map(|t| (path, t))
2456 }
2457}
2458
2459struct Views {
2461 porch: String,
2462 budgets: String,
2463 accounts: String,
2464 blocked: String,
2465 shown: BTreeMap<String, ImapOrigin>,
2466}
2467
2468pub(crate) fn offline() -> bool {
2472 std::env::var_os("SIOUL_DEMO").is_some()
2473}
2474
2475pub(crate) fn weather_cache() -> PathBuf {
2477 config::state_dir().join("weather.json")
2478}
2479
2480const WEATHER_FRESH: i64 = 30 * 60;
2482
2483pub(crate) fn update_weather(qt: &QtThread, shared: &Arc<Shared>) {
2486 let config = load_config();
2487 let (Some(place), Some(latitude), Some(longitude)) = (config.weather.place.clone(), config.weather.latitude, config.weather.longitude) else {
2488 let _ = qt.queue(|mut sioul| sioul.as_mut().set_forecast(QString::default()));
2489 return;
2490 };
2491 let now = Zoned::now();
2492 let kept: Option<sioul_core::weather::Forecast> = std::fs::read_to_string(weather_cache()).ok().and_then(|t| serde_json::from_str(&t).ok());
2493 let shown = move |forecast: &sioul_core::weather::Forecast| {
2494 let view = sioul_core::weather::view(forecast, &Zoned::now(), tr());
2495 serde_json::json!({ "place": place, "view": view, "credit": tr().text("weather-credit", None) }).to_string()
2496 };
2497 if let Some(forecast) = kept.as_ref() {
2498 let line = shown(forecast);
2499 let _ = qt.queue(move |mut sioul| sioul.as_mut().set_forecast(QString::from(&line)));
2500 }
2501 let stale = kept.as_ref().is_none_or(|f| now.timestamp().as_second() - f.fetched >= WEATHER_FRESH);
2502 if !stale || offline() || shared.weather_fetching.swap(true, Ordering::Relaxed) {
2503 return;
2504 }
2505 let (qt, shared) = (qt.clone(), Arc::clone(shared));
2506 std::thread::spawn(move || {
2507 if let Ok(forecast) = sioul_sync::weather::forecast(latitude, longitude, Zoned::now().timestamp().as_second()) {
2508 if let Ok(text) = serde_json::to_string(&forecast) {
2509 let _ = std::fs::create_dir_all(config::state_dir());
2510 let _ = std::fs::write(weather_cache(), text);
2511 }
2512 crate::homecard::weather_seen();
2514 let line = shown(&forecast);
2515 let _ = qt.queue(move |mut sioul| sioul.as_mut().set_forecast(QString::from(&line)));
2516 }
2517 shared.weather_fetching.store(false, Ordering::Relaxed);
2518 });
2519}
2520
2521pub(crate) fn mode_now() -> sioul_core::quiet::Mode {
2523 crate::hours::mode_now()
2524}
2525
2526pub(crate) fn mode_json() -> String {
2528 let config = load_config();
2529 let now = Zoned::now();
2530 let overrides = sioul_core::quiet::Overrides::load(&sioul_core::quiet::Overrides::default_path());
2531 let mode = crate::hours::mode_at(&now);
2532 let until = mode.until.as_ref().map(|z| sioul_core::quiet::until_text(tr(), z, &now)).unwrap_or_default();
2533 let mut args = sioul_core::i18n::args();
2534 args.set("until", until.clone());
2535 args.set("label", if mode.label.trim().is_empty() { "none".to_string() } else { mode.label.clone() });
2536 let reason = serde_json::to_value(&mode.reason).ok().and_then(|v| v.as_str().map(str::to_string)).unwrap_or_default();
2537 use sioul_core::quiet::Reason;
2538 let line = match mode.reason {
2539 Reason::NoHours | Reason::Working => String::new(),
2540 Reason::TimeOff => tr().text("mode-time-off", Some(&args)),
2541 Reason::WorkingLate => tr().text("mode-working-late", Some(&args)),
2542 Reason::WorkNow => tr().text("mode-work-now-line", Some(&args)),
2543 Reason::AdminTime => tr().text("mode-admin", Some(&args)),
2544 Reason::DoneForTheDay => tr().text("mode-quiet", Some(&args)),
2546 Reason::Evening | Reason::DayOff => tr().text("mode-leisure", Some(&args)),
2548 Reason::Meal => tr().text("mode-meal", Some(&args)),
2549 Reason::WindingDown => tr().text("mode-wind-down", Some(&args)),
2550 Reason::Nap => tr().text("mode-nap", Some(&args)),
2551 Reason::Sleep => tr().text("mode-sleep", Some(&args)),
2552 Reason::FreeTime => tr().text(if sioul_core::pause::nothing_now(&overrides, &config.free_time) { "mode-free-time-nothing" } else { "mode-free-time" }, None),
2554 Reason::Paused => tr().text("mode-paused", None),
2555 Reason::Extended => String::new(),
2556 };
2557 let today = overrides.closed_today(&now);
2559 let set = |kind: &str| config.windows.iter().any(|w| w.kind() == kind);
2561 serde_json::json!({
2562 "quiet": mode.quiet,
2563 "sleep": mode.sleeps(),
2564 "time": mode.time.id(),
2565 "reason": reason,
2566 "until": until,
2567 "line": line,
2568 "hours": !config.week_hours().is_empty(),
2569 "work_hours": set("work"),
2570 "admin_hours": set("admin"),
2571 "night": crate::hours::night_set(),
2572 "shares": crate::share::configured(),
2574 "work_now": mode.reason == Reason::WorkNow,
2575 "today": today,
2576 "review": crate::reviews::offer_json(&now, &mode),
2578 "pauses": crate::pauses::moment(&config, &overrides, &now),
2580 "dnd": crate::everywhere::moment(),
2582 "calls": crate::calls::moment()
2584 })
2585 .to_string()
2586}
2587
2588fn compute(shared: &Shared) -> Views {
2589 let _cards = sioul_core::maildir::CardsKept::begin();
2591 let world = World::load();
2592 let now = Zoned::now();
2593 let hidden = mail::hidden_files(shared);
2594 let mut items = world.gather(&PorchState::load(&PorchState::default_path()));
2595 items.retain(|t| t.card.path.as_ref().is_none_or(|p| !hidden.contains(p)));
2596 crate::homecard::porch_seen(&items, &world.senders, world.store.as_ref());
2598 let mode = crate::hours::mode_at(&now);
2604 {
2605 let area_of = |t: &sioul_core::porch::Triaged| t.card.account.as_deref().and_then(|id| world.config.account(id)).and_then(|a| a.area.as_deref()).and_then(sioul_core::areas::Area::parse).unwrap_or(sioul_core::areas::Area::WORK);
2606 let attention = sioul_core::attention::Attention::of(&world.config);
2607 let mut moment = crate::hours::attention_at(&mode, &now);
2608 let always = sioul_core::everywhere::People::load(&sioul_core::everywhere::People::default_path());
2609 moment.reachable |= shared.opened_anyway.load(Ordering::Relaxed);
2612 items.retain(|t| attention.mail(t, &world.senders, &always, area_of(t), &moment).shown);
2613 }
2614 if mode.sleeps() {
2616 for t in &mut items {
2617 if matches!(t.lane, sioul_core::porch::Lane::Project(_)) {
2618 t.lane = sioul_core::porch::Lane::People;
2619 }
2620 }
2621 }
2622 let opened = shared.opened_anyway.load(Ordering::Relaxed) || shared.reachable.load(Ordering::Relaxed);
2624 let mut porch = view::porch(&items, &world.config, world.store.as_ref(), tr(), &now, opened || mode.quiet);
2625 porch.all = shared.opened_anyway.load(Ordering::Relaxed);
2626 crate::pauses::porch_rests(&mut porch, opened, &now);
2628 if mode.quiet {
2629 porch.status = serde_json::from_str::<serde_json::Value>(&mode_json()).ok().and_then(|v| v["line"].as_str().map(str::to_string)).unwrap_or_default();
2630 }
2631 let shown = if porch.open { PorchState::newest_shown(&items) } else { BTreeMap::new() };
2633 let budgets = world.ledger().map_or_else(String::new, |mut ledger| {
2634 if mode.time != sioul_core::areas::Time::Any {
2636 let area_of = |b: &sioul_core::budget::Budget| match b.area.as_deref() {
2638 Some("personal") => sioul_core::areas::Area::LEISURE,
2639 Some(other) => sioul_core::areas::Area::parse(other).unwrap_or(sioul_core::areas::Area::ADMIN),
2640 None => sioul_core::areas::Area::ADMIN,
2641 };
2642 ledger.budgets.retain(|b| sioul_core::areas::in_view(area_of(b), mode.time, mode.week));
2643 }
2644 let mail = world.mail_lines(&ledger);
2645 json(&view::budgets(&ledger, &mail, tr(), now.date()))
2646 });
2647 let statuses = shared.statuses.lock().map(|s| s.clone()).unwrap_or_default();
2648 let wanted = shared.password_wanted.lock().map(|w| w.clone()).unwrap_or_default();
2649 Views {
2650 porch: crate::appnotes::with_line(json(&porch)),
2652 budgets,
2653 accounts: json(&view::accounts(&world.config, tr(), &statuses, &wanted)),
2654 blocked: json(&world.senders.blocked.entries()),
2655 shown,
2656 }
2657}
2658
2659pub(crate) fn show(qt: &QtThread, shared: &Arc<Shared>) {
2661 crate::mailsearch::changed(qt, shared);
2662 let qt = qt.clone();
2663 coalesced(shared, |s| &s.views_job, move |shared| {
2664 let _cards = sioul_core::maildir::CardsKept::begin();
2668 mail::show_mail_here(&qt, shared);
2669 let generation = shared.generation.fetch_add(1, Ordering::Relaxed) + 1;
2670 let views = compute(shared);
2671 let _ = qt.queue(move |mut sioul| {
2672 let shared = Arc::clone(&sioul.rust().shared);
2673 if shared.generation.load(Ordering::Relaxed) != generation || shared.shown_generation.fetch_max(generation, Ordering::Relaxed) > generation {
2676 return;
2677 }
2678 if let Ok(mut shown) = shared.shown.lock() {
2679 *shown = views.shown;
2680 }
2681 sioul.as_mut().set_porch(QString::from(&views.porch));
2682 sioul.as_mut().set_budgets(QString::from(&views.budgets));
2683 sioul.as_mut().set_accounts(QString::from(&views.accounts));
2684 sioul.as_mut().set_blocked(QString::from(&views.blocked));
2685 if cfg!(target_os = "android") {
2687 std::thread::spawn(|| crate::outside::publish(false));
2688 }
2689 });
2690 });
2691}
2692
2693pub(crate) fn set_status(qt: &QtThread, line: String) {
2694 let _ = qt.queue(move |mut sioul| sioul.as_mut().set_status(QString::from(&line)));
2695}
2696
2697pub(crate) fn tell(qt: &QtThread, shared: &Shared, line: String) {
2699 if let Ok(mut said) = shared.said_at.lock() {
2700 *said = Some(std::time::Instant::now());
2701 }
2702 set_status(qt, line);
2703}
2704
2705fn start_watchers(qt: &QtThread, shared: &Arc<Shared>) -> usize {
2707 if offline() {
2708 return 0;
2709 }
2710 let accounts: Vec<Account> = load_config().accounts.into_iter().filter(Account::syncs).collect();
2711 let count = accounts.len();
2712 for account in accounts {
2713 start_watcher(qt, shared, account);
2714 }
2715 count
2716}
2717
2718pub(crate) fn start_new_watchers(qt: &QtThread, shared: &Arc<Shared>) {
2722 if offline() {
2723 return;
2724 }
2725 let seen = |id: &str| shared.statuses.lock().is_ok_and(|s| s.contains_key(id)) || shared.watchers.lock().is_ok_and(|w| w.contains_key(id)) || shared.dav_watchers.lock().is_ok_and(|w| w.contains_key(id));
2726 for account in load_config().accounts.into_iter().filter(|a| !seen(&a.id)) {
2727 if account.is_dav() {
2728 crate::pim::start_dav_watcher(qt, shared, account);
2729 } else if account.syncs() {
2730 start_watcher(qt, shared, account);
2731 }
2732 }
2733}
2734
2735fn give_password(qt: &QtThread, shared: &Arc<Shared>, id: String, password: String) {
2739 let (qt, shared) = (qt.clone(), Arc::clone(shared));
2740 std::thread::spawn(move || {
2741 let config = load_config();
2742 let outcome = match config.every_account().find(|a| a.id == id).cloned() {
2743 None => Err(say("account-unknown", &[("id", id.clone())])),
2744 Some(mut account) => {
2745 let host = account.host.clone().unwrap_or_default();
2746 let password = sioul_sync::tidy_password(&host, &password);
2747 let switched = !account.is_dav() && account.auth.as_deref() == Some("google");
2750 if switched {
2751 account.auth = None;
2752 }
2753 let tested = if account.is_dav() {
2754 let address = account.address.clone().unwrap_or_default();
2755 let login = account.login().unwrap_or(address.as_str()).to_string();
2756 sioul_sync::dav::test(&address, &login, &password, account.url.as_deref(), None).map(|_| ())
2757 } else {
2758 sioul_sync::test(&account, &password).map(|_| ())
2759 };
2760 tested
2761 .and_then(|()| secret::save(&account, &password))
2762 .map_err(|e| e.sentence(tr(), &account.id))
2763 .and_then(|()| if switched { config::set_auth(&config_path(), &account.id, None) } else { Ok(()) })
2764 .map(|()| {
2765 if switched {
2766 if let Some(control) = shared.watchers.lock().ok().and_then(|mut w| w.remove(&account.id)) {
2768 control.stop();
2769 }
2770 let _ = sioul_sync::google::revoke_for(sioul_sync::google::Purpose::Mail, account.address.as_deref().unwrap_or(""));
2771 }
2772 account
2773 })
2774 }
2775 };
2776 let problem = match outcome {
2777 Ok(account) => {
2778 want_password(&shared, &account.id, None);
2779 if let Ok(mut statuses) = shared.statuses.lock() {
2780 statuses.remove(&account.id);
2781 }
2782 if account.is_dav() {
2783 crate::pim::start_dav_watcher(&qt, &shared, account);
2784 } else {
2785 start_watcher(&qt, &shared, account);
2786 }
2787 String::new()
2788 }
2789 Err(e) => e,
2790 };
2791 let _ = qt.queue(move |mut sioul| {
2792 sioul.as_mut().set_form_busy(false);
2793 if problem.is_empty() {
2794 sioul.as_mut().set_status(QString::from(&say("account-password-kept", &[("account", id.clone())])));
2795 }
2796 sioul.as_mut().account_password_done(QString::from(&id), QString::from(&problem));
2797 });
2798 show(&qt, &shared);
2799 });
2800}
2801
2802#[allow(clippy::too_many_arguments)]
2805fn add_mail(qt: &QtThread, shared: &Arc<Shared>, address: String, host: String, port: u16, security: Security, login: String, password: Option<String>) {
2806 let (qt, shared) = (qt.clone(), Arc::clone(shared));
2807 std::thread::spawn(move || {
2808 let config = load_config();
2809 let outcome = match config.every_account().find(|a| a.address.as_deref() == Some(address.as_str()) && matches!(a.kind, config::AccountKind::Imap | config::AccountKind::Jmap)) {
2811 Some(existing) => Err(say("account-address-exists", &[("address", address.clone()), ("id", existing.id.clone())])),
2812 None => {
2813 let password = match password {
2814 Some(p) => Ok(p),
2815 None => config
2816 .every_account()
2817 .find(|a| a.address.as_deref() == Some(address.as_str()) && a.kind == config::AccountKind::Dav)
2818 .ok_or_else(|| say("account-unknown", &[("id", address.clone())]))
2819 .and_then(|dav| secret::password(dav).map_err(|e| e.sentence(tr(), &dav.id))),
2820 };
2821 password.and_then(|password| {
2822 let id = config.free_id(&address);
2823 let account = Account::imap(&id, &address, &host, port, security, Some(&login));
2824 let password = sioul_sync::tidy_password(&host, &password);
2825 sioul_sync::test(&account, &password)
2826 .and_then(|_| secret::save(&account, &password))
2827 .map_err(|e| e.sentence(tr(), &id))
2828 .and_then(|()| config::add_imap_account(&config_path(), &account))
2829 .map(|()| account)
2830 })
2831 }
2832 };
2833 let added = outcome.as_ref().ok().map(|a| say("account-added", &[("id", a.id.clone()), ("path", a.maildir_path().display().to_string())]));
2834 if let Ok(account) = outcome.as_ref() {
2835 start_watcher(&qt, &shared, account.clone());
2836 }
2837 let error = outcome.err();
2838 let _ = qt.queue(move |mut sioul| {
2839 sioul.as_mut().set_form_busy(false);
2840 match (added, error) {
2841 (Some(line), _) => {
2842 sioul.as_mut().set_found(QString::default());
2843 sioul.as_mut().set_scouted(QString::default());
2844 sioul.as_mut().set_status(QString::from(&line));
2845 sioul.as_mut().account_added();
2846 }
2847 (None, Some(e)) => sioul.as_mut().set_form_error(QString::from(&e)),
2848 (None, None) => {}
2849 }
2850 });
2851 show(&qt, &shared);
2852 });
2853}
2854
2855fn pace_of(config: &Config, account: &Account) -> std::time::Duration {
2857 let minutes = account.fetch_minutes.or(config.fetch_minutes).unwrap_or(config::DEFAULT_FETCH_MINUTES);
2858 std::time::Duration::from_secs(u64::from(minutes.max(1)) * 60)
2859}
2860
2861pub(crate) fn follow_reachable(qt: &QtThread, shared: &Arc<Shared>) {
2865 let now = crate::everywhere::reachable().is_some();
2866 if shared.reachable.swap(now, Ordering::Relaxed) != now {
2867 update_paces(shared);
2868 show(qt, shared);
2869 }
2870}
2871
2872pub(crate) fn update_paces(shared: &Shared) {
2874 let config = load_config();
2875 let realtime = shared.realtime.load(Ordering::Relaxed) || shared.reachable.load(Ordering::Relaxed);
2877 let Ok(watchers) = shared.watchers.lock() else { return };
2878 for account in config.accounts.iter() {
2879 if let Some(control) = watchers.get(&account.id) {
2880 control.set_pace(pace_of(&config, account));
2881 control.set_realtime(realtime);
2882 }
2883 }
2884}
2885
2886pub(crate) fn start_watcher(qt: &QtThread, shared: &Arc<Shared>, account: Account) {
2887 let control = Arc::new(Control::default());
2888 control.set_pace(pace_of(&load_config(), &account));
2889 control.set_realtime(shared.realtime.load(Ordering::Relaxed) || shared.reachable.load(Ordering::Relaxed));
2890 {
2891 let Ok(mut watchers) = shared.watchers.lock() else { return };
2892 if shared.removing.lock().is_ok_and(|removing| removing.contains(&account.id)) {
2894 return;
2895 }
2896 if let Some(running) = watchers.get(&account.id) {
2897 running.retry();
2899 return;
2900 }
2901 watchers.insert(account.id.clone(), Arc::clone(&control));
2902 }
2903 let (qt, shared) = (qt.clone(), Arc::clone(shared));
2904 std::thread::spawn(move || {
2905 match secret::password(&account) {
2906 Ok(password) => sioul_sync::watch(&account, &password, &control, |result| reported(&qt, &shared, &account, result)),
2907 Err(e) => reported(&qt, &shared, &account, Err(e)),
2908 }
2909 control.end();
2911 if let Ok(mut watchers) = shared.watchers.lock()
2913 && watchers.get(&account.id).is_some_and(|c| Arc::ptr_eq(c, &control))
2914 {
2915 watchers.remove(&account.id);
2916 }
2917 });
2918}
2919
2920pub(crate) fn want_password(shared: &Shared, id: &str, error: Option<&SyncError>) {
2923 if let Ok(mut wanted) = shared.password_wanted.lock() {
2924 if error.is_some_and(SyncError::wants_password) {
2925 wanted.insert(id.to_string());
2926 } else {
2927 wanted.remove(id);
2928 }
2929 }
2930}
2931
2932fn reported(qt: &QtThread, shared: &Arc<Shared>, account: &Account, result: Result<Report, SyncError>) {
2935 let (line, problem) = match &result {
2936 Ok(report) if report.first => {
2937 let mut args = tr().counted(report.new.len());
2938 args.set("account", account.id.clone());
2939 args.set("days", account.sync_days.unwrap_or(config::DEFAULT_SYNC_DAYS));
2940 (tr().text("sync-first", Some(&args)), false)
2941 }
2942 Ok(_) => (say("ui-synced-at", &[("time", Zoned::now().strftime("%H:%M").to_string())]), false),
2943 Err(e) => (e.sentence(tr(), &account.id), true),
2944 };
2945 if let Ok(mut statuses) = shared.statuses.lock() {
2946 statuses.insert(account.id.clone(), (line.clone(), problem));
2947 }
2948 want_password(shared, &account.id, result.as_ref().err());
2949 if let Ok(report) = &result {
2950 if !report.new.is_empty() {
2951 learn(qt, shared, &account.id);
2952 tie_to_projects(&report.new);
2953 }
2954 if !report.elsewhere.is_empty() {
2955 tie_to_projects(&report.elsewhere);
2956 }
2957 if report.held_back
2959 && shared.held_back.lock().is_ok_and(|mut said| said.insert(account.id.clone()))
2960 {
2961 let free = sioul_sync::disk::space(&account.maildir_path()).map_or(0, |(free, _)| free);
2962 set_status(qt, say("sync-held-back", &[("account", account.id.clone()), ("free", tr().decimal((free as f64 / f64::from(1u32 << 30)) as f32))]));
2963 }
2964 if !report.first && !report.new.is_empty() {
2965 notify_codes(qt, &report.new);
2966 sort_on_server(qt, shared, account, report.new.clone());
2967 let window = crate::mailnote::Window { qt: qt.clone(), active: shared.active.load(Ordering::Relaxed) };
2971 crate::mailnote::arrived_in_window(window, &account.id, &report.new, report.first);
2972 } else if !report.first && sioul_sync::filters::due(account) {
2973 sort_on_server(qt, shared, account, Vec::new());
2975 }
2976 read_shielded(qt, &account.id);
2977 }
2978 let recent = shared.said_at.lock().ok().and_then(|s| *s).is_some_and(|at| at.elapsed() < SAID_FOR);
2980 let quiet = recent && !problem;
2981 let _ = qt.queue(move |mut sioul| {
2982 sioul.as_mut().set_busy(false);
2983 if !quiet {
2984 sioul.as_mut().set_status(QString::from(&line));
2985 }
2986 });
2987 show(qt, shared);
2988}
2989
2990fn sort_on_server(qt: &QtThread, shared: &Arc<Shared>, account: &Account, new: Vec<PathBuf>) {
2998 let (account, qt, shared) = (account.clone(), qt.clone(), Arc::clone(shared));
2999 let window = crate::mailnote::Window { qt: qt.clone(), active: shared.active.load(Ordering::Relaxed) };
3000 std::thread::spawn(move || {
3001 let moved = crate::spam::after_fetch(&account, &new, false);
3002 let filtered = crate::filters::after_fetch(&account, &new, false);
3003 if !filtered.problem.is_empty() {
3004 set_status(&qt, filtered.problem.clone());
3005 }
3006 crate::mailnote::unfiltered_in_window(window, &account.id, &filtered.tell);
3007 if moved + filtered.acted > 0 {
3008 show(&qt, &shared);
3009 }
3010 });
3011}
3012
3013fn tie_to_projects(files: &[PathBuf]) {
3017 let config = load_config();
3018 let path = sioul_core::links::LocalLinks::default_path();
3019 let mut ties = sioul_core::links::LocalLinks::load(&path);
3020 let before = ties.links.len();
3021 crate::github::tie_mail(&config, files, &mut ties);
3023 if let Some(store) = config.notes_root_path().and_then(|root| ProjectStore::load(&root).ok()).map(|s| s.with_ties(&ties)) {
3024 let senders = porch::Senders::load(&config);
3025 for t in porch::judge(files, &config.mail_sources(), Some(&store), &KnownSenders::default(), &senders, Zoned::now().timestamp().as_second()) {
3026 if let (porch::Lane::Project(id), Some(mid)) = (&t.lane, t.card.message_id.as_deref()) {
3027 ties.add(&sioul_core::links::mail_uri(mid), &sioul_core::links::project_uri(id), "project");
3028 }
3029 }
3030 }
3031 let _ = sioul_core::links::LocalLinks::add_all(&path, &ties.links[before..]);
3034}
3035
3036fn retie_project(id: String) {
3042 std::thread::spawn(move || {
3043 let config = load_config();
3044 let Some(project) = config.notes_root_path().and_then(|root| ProjectStore::load(&root).ok()).and_then(|s| s.get(&id).cloned()) else { return };
3045 let routes: Vec<&sioul_core::projects::Route> = project.routes.iter().filter(|r| r.needs_body()).collect();
3046 if routes.is_empty() {
3047 return;
3048 }
3049 let path = sioul_core::links::LocalLinks::default_path();
3050 let mut ties = sioul_core::links::LocalLinks::load(&path);
3051 let before = ties.links.len();
3052 let gate = porch::Gate::load(&config, Zoned::now().timestamp().as_second());
3053 for account in config.accounts.iter().filter(|a| a.syncs()) {
3054 let files = sioul_core::mailindex::message_files(&account.maildir_path());
3055 for mid in gate.routed(&routes, files.iter().map(PathBuf::as_path)) {
3056 ties.add(&sioul_core::links::mail_uri(&mid), &sioul_core::links::project_uri(&project.id), "project");
3057 }
3058 }
3059 let _ = sioul_core::links::LocalLinks::add_all(&path, &ties.links[before..]);
3060 });
3061}
3062
3063fn read_shielded(qt: &QtThread, id: &str) {
3066 let config = load_config();
3067 if !config.account(id).is_some_and(|a| a.ai_reads()) {
3068 return;
3069 }
3070 if let Err(e) = sioul_sync::shield_ai::read_new(&config) {
3071 set_status(qt, e.sentence(tr(), "Anthropic"));
3072 }
3073}
3074
3075fn learn(qt: &QtThread, shared: &Shared, id: &str) {
3077 if shared.learned.lock().map_or(true, |l| l.contains(id)) {
3078 return;
3079 }
3080 let Some(account) = load_config().account(id).cloned() else { return };
3081 match sioul_sync::learn_provider(&config_path(), &account) {
3082 Ok(Learned::TooEarly) => {}
3083 Ok(learned) => {
3084 if let Ok(mut done) = shared.learned.lock() {
3085 done.insert(id.to_string());
3086 }
3087 match learned {
3088 Learned::Id(provider) => set_status(qt, say("sync-learned", &[("account", id.to_string()), ("id", provider)])),
3089 Learned::Nothing => set_status(qt, say("sync-not-learned", &[("account", id.to_string())])),
3090 _ => {}
3091 }
3092 }
3093 Err(e) => set_status(qt, e),
3094 }
3095}
3096
3097fn notify_codes(qt: &QtThread, files: &[PathBuf]) {
3101 if cfg!(target_os = "android") || !crate::hours::comes(sioul_core::attention::Kind::Codes) {
3104 return;
3105 }
3106 let config = load_config();
3107 let now = Zoned::now().timestamp().as_second();
3108 let senders = porch::Senders::load(&config);
3109 let judged = porch::judge(files, &config.mail_sources(), None, &KnownSenders::default(), &senders, now);
3111 for code in view::codes(&judged, tr()) {
3112 let copy = code.code.clone().map(|text| {
3113 let qt = qt.clone();
3114 let action: Box<dyn FnOnce() + Send> = Box::new(move || {
3115 let _ = qt.queue(move |sioul| sioul.copy_requested(QString::from(&text)));
3116 });
3117 (tr().text("notify-copy", None), action)
3118 });
3119 if let Err(e) = notify::code(&code.title, &code.body(), copy) {
3120 set_status(qt, e);
3121 }
3122 }
3123}
3124
3125fn attachment_cache(key: &str) -> PathBuf {
3127 let mut hasher = std::collections::hash_map::DefaultHasher::new();
3128 key.hash(&mut hasher);
3129 config::cache_dir().join("attachments").join(format!("{:016x}", hasher.finish()))
3130}
3131
3132fn forget_opened_attachments() {
3135 let Ok(entries) = std::fs::read_dir(config::cache_dir().join("attachments")) else { return };
3136 let day = std::time::Duration::from_secs(86_400);
3137 for entry in entries.filter_map(Result::ok) {
3138 if entry.metadata().and_then(|m| m.modified()).ok().and_then(|t| t.elapsed().ok()).is_some_and(|age| age > day) {
3139 let _ = std::fs::remove_dir_all(entry.path());
3140 }
3141 }
3142}
3143
3144fn mark_from_mail(file: &Path) {
3149 #[cfg(windows)]
3150 {
3151 let mut stream = file.as_os_str().to_owned();
3152 stream.push(":Zone.Identifier");
3153 let _ = std::fs::write(stream, "[ZoneTransfer]\r\nZoneId=3\r\n");
3154 }
3155 #[cfg(target_os = "macos")]
3156 {
3157 let stamp = format!("0081;{:x};Sioul;", jiff::Timestamp::now().as_second());
3158 let _ = std::process::Command::new("xattr").args(["-w", "com.apple.quarantine", &stamp]).arg(file).output();
3159 }
3160 #[cfg(not(any(windows, target_os = "macos")))]
3161 let _ = file;
3162}
3163
3164fn downloads() -> PathBuf {
3166 config::downloads_dir()
3167}
3168
3169fn free_path(folder: &Path, name: &str) -> PathBuf {
3171 let wanted = folder.join(name);
3172 if !wanted.exists() {
3173 return wanted;
3174 }
3175 let (stem, extension) = match name.rsplit_once('.') {
3176 Some((stem, ext)) if !stem.is_empty() => (stem.to_string(), format!(".{ext}")),
3177 _ => (name.to_string(), String::new()),
3178 };
3179 (2..).map(|n| folder.join(format!("{stem} ({n}){extension}"))).find(|p| !p.exists()).unwrap_or(wanted)
3180}
3181
3182fn one_file_name(name: &str) -> String {
3187 let odd = |c: char| matches!(c, '/' | '\\') || (cfg!(windows) && matches!(c, ':' | '<' | '>' | '"' | '|' | '?' | '*'));
3188 let mut name: String = name.chars().map(|c| if odd(c) { '_' } else { c }).collect();
3189 if cfg!(windows) {
3190 name = name.trim_end_matches(['.', ' ']).to_string();
3191 let stem = name.split('.').next().unwrap_or("").trim_end().to_ascii_uppercase();
3192 let numbered = stem.len() == 4 && (stem.starts_with("COM") || stem.starts_with("LPT")) && stem.as_bytes()[3].is_ascii_digit();
3193 if numbered || matches!(stem.as_str(), "CON" | "PRN" | "AUX" | "NUL") {
3194 name.insert(0, '_');
3195 }
3196 }
3197 if matches!(name.as_str(), "" | "." | "..") { "attachment".into() } else { name }
3198}
3199
3200#[derive(Clone, Copy, PartialEq, Eq)]
3204enum Afterwards {
3205 Open,
3206 Save,
3207 Paper,
3208}
3209
3210impl Afterwards {
3211 fn of(what: i32) -> Afterwards {
3212 match what {
3213 1 => Afterwards::Save,
3214 2 => Afterwards::Paper,
3215 _ => Afterwards::Open,
3216 }
3217 }
3218}
3219
3220fn checked_attachment(qt: &QtThread, shared: &Arc<Shared>, key: String, index: i32, afterwards: Afterwards, unchecked: bool) {
3226 let (qt, shared) = (qt.clone(), Arc::clone(shared));
3227 std::thread::spawn(move || {
3228 let world = World::load();
3229 let judged = world.judge(&key).filter(|(_, t)| t.lane != porch::Lane::SetAside);
3231 let subject = judged.as_ref().map(|(_, t)| t.card.subject.clone()).unwrap_or_default();
3232 let found = judged.and_then(|(path, _)| reading::attachment_from(&crypto::readable(&path)?, u32::try_from(index).ok()?));
3233 let Some((name, bytes)) = found else {
3234 set_status(&qt, tr().text("ui-attachments-closed", None));
3235 return;
3236 };
3237 let name = one_file_name(&name);
3238 let phone = crate::attachments::phone();
3239 if afterwards != Afterwards::Save && crate::attachments::kept_closed(&name, phone) {
3243 set_status(&qt, say("attachment-program", &[("name", name)]));
3244 return;
3245 }
3246 let folder = attachment_cache(&key);
3247 let file = folder.join(&name);
3248 if std::fs::create_dir_all(&folder).and_then(|()| std::fs::write(&file, &bytes)).is_err() {
3249 set_status(&qt, say("scan-error", &[("name", name), ("detail", folder.display().to_string())]));
3250 return;
3251 }
3252 mark_from_mail(&file);
3253 if !phone {
3254 set_status(&qt, say("scan-running", &[("name", name.clone())]));
3255 }
3256 static KEYS: OnceLock<std::collections::hash_map::RandomState> = OnceLock::new();
3258 let content = std::hash::BuildHasher::hash_one(KEYS.get_or_init(Default::default), &bytes);
3259 let known_clean = shared.clean.lock().is_ok_and(|c| c.contains(&content));
3260 let verdict = match crate::attachments::check(phone, known_clean, unchecked) {
3262 crate::attachments::Check::Scan => antivirus::scan(&file),
3263 crate::attachments::Check::Skip | crate::attachments::Check::Unchecked => Verdict::Clean,
3264 };
3265 let line = match verdict {
3266 Verdict::Clean => {
3267 if !unchecked
3268 && !phone
3269 && let Ok(mut clean) = shared.clean.lock()
3270 {
3271 clean.insert(content);
3272 }
3273 match afterwards {
3274 Afterwards::Open if phone => crate::attachments::open(&file, &name),
3276 Afterwards::Open => {
3277 let url = file_url(&file);
3278 let _ = qt.queue(move |sioul| sioul.open_url(QString::from(&url)));
3279 say("scan-clean", &[("name", name)])
3280 }
3281 Afterwards::Paper => {
3282 let kept = crate::papers::keep_checked(&file, &name, &subject);
3283 let _ = std::fs::remove_file(&file);
3284 match kept {
3285 Ok((path, title, kind)) => {
3286 let _ = qt.queue(move |mut sioul| sioul.as_mut().paper_kept(QString::from(&path), QString::from(&title), QString::from(kind)));
3287 say("papers-kept", &[("name", name)])
3288 }
3289 Err(e) => say("scan-error", &[("name", name), ("detail", e)]),
3290 }
3291 }
3292 Afterwards::Save if phone => crate::attachments::save(&qt, &file, &name),
3294 Afterwards::Save => {
3295 let target = free_path(&downloads(), &name);
3296 let saved = std::fs::create_dir_all(downloads()).and_then(|()| std::fs::copy(&file, &target));
3297 let _ = std::fs::remove_file(&file);
3298 match saved {
3299 Ok(_) => {
3300 mark_from_mail(&target);
3301 say("ui-saved", &[("path", target.display().to_string())])
3302 }
3303 Err(e) => say("scan-error", &[("name", name), ("detail", e.to_string())]),
3304 }
3305 }
3306 }
3307 }
3308 Verdict::Infected(threat) => {
3309 let _ = std::fs::remove_file(&file);
3310 say("scan-infected", &[("name", name), ("threat", threat)])
3311 }
3312 Verdict::Unavailable(_) | Verdict::TooBig => {
3315 let _ = std::fs::remove_file(&file);
3316 let what = match afterwards {
3317 Afterwards::Open => 0,
3318 Afterwards::Save => 1,
3319 Afterwards::Paper => 2,
3320 };
3321 let big = verdict == Verdict::TooBig;
3322 let (key, hint, shown) = (key.clone(), antivirus::install_hint(), name.clone());
3323 let _ = qt.queue(move |mut sioul| sioul.as_mut().scan_unavailable(QString::from(&key), index, what, QString::from(&shown), QString::from(&hint), big));
3324 match verdict {
3325 Verdict::Unavailable(problem) => say("scan-unavailable-short", &[("detail", problem)]),
3326 _ => say("scan-too-big", &[("name", name)]),
3327 }
3328 }
3329 };
3330 set_status(&qt, line);
3331 });
3332}
3333
3334#[derive(serde::Serialize)]
3336struct FoundView {
3337 host: String,
3338 port: u16,
3339 security: &'static str,
3340 login: String,
3341 by: String,
3343 hint: String,
3345 help_url: Option<&'static str>,
3347 google: bool,
3350 google_key: bool,
3352 microsoft: bool,
3355 microsoft_kind: &'static str,
3357 microsoft_key: bool,
3359 microsoft_admin: String,
3362}
3363
3364fn message_json(key: &str) -> String {
3367 let world = World::load();
3368 let own = mail::own_addresses(&world.config);
3369 let shown = world.judge(key).and_then(|(path, triaged)| {
3370 let raw = std::fs::read(&path).ok()?;
3371 crypto::learn_autocrypt(&raw, &triaged);
3372 let (opened, protection) = match crypto::open(&raw) {
3373 Some((opened, protection)) => (opened, Some(protection)),
3374 None => (None, None),
3375 };
3376 let mut shown = view::message_from(opened.as_deref().unwrap_or(&raw), &path, &triaged, &own, tr(), protection)?;
3377 shown.role = mail::locate(key).and_then(|(account, file)| sioul_sync::mailbox::folder_of(&account, &file)).map(|f| f.role);
3378 shown.unsubscribe = mail::unsubscribe_view(&world.config, &triaged);
3379 Some(shown)
3380 });
3381 shown.map_or_else(String::new, |m| json(&m))
3382}
3383
3384impl qobject::Sioul {
3385 pub(crate) fn shared(&self) -> Arc<Shared> {
3386 Arc::clone(&self.rust().shared)
3387 }
3388
3389 fn text(&self, id: &QString) -> QString {
3390 QString::from(&tr().text(&id.to_string(), None))
3391 }
3392
3393 fn text_args(&self, id: &QString, args: &QString) -> QString {
3394 let given: serde_json::Map<String, serde_json::Value> = serde_json::from_str(&args.to_string()).unwrap_or_default();
3395 let pairs: Vec<(&str, String)> = given.iter().map(|(k, v)| (k.as_str(), v.as_str().map_or_else(|| v.to_string(), str::to_string))).collect();
3396 QString::from(&say(&id.to_string(), &pairs))
3397 }
3398
3399 fn text_with(&self, id: &QString, name: &QString, value: &QString) -> QString {
3400 QString::from(&say(&id.to_string(), &[(&name.to_string(), value.to_string())]))
3401 }
3402
3403 fn text_counted(&self, id: &QString, n: i32) -> QString {
3404 QString::from(&tr().text(&id.to_string(), Some(&tr().counted(usize::try_from(n).unwrap_or(0)))))
3405 }
3406
3407 fn start(mut self: Pin<&mut Self>) {
3408 if let Err(e) = config::migrate_sites_at_start(&config_path()) {
3411 eprintln!("{e}");
3412 }
3413 self.as_mut().set_reading(QString::from(&reading_json()));
3414 self.as_mut().set_mode(QString::from(&mode_json()));
3415 crate::outside::started(self.as_mut());
3417 crate::pauses::tick(&self.qt_thread(), &self.shared());
3419 crate::sites::favicon_tick(&self.qt_thread());
3421 update_weather(&self.qt_thread(), &self.shared());
3422 let (qt, shared) = (self.qt_thread(), self.shared());
3423 if let Ok(mut state) = shared.work.lock() {
3425 *state = work::WorkState::load();
3426 }
3427 show(&qt, &shared);
3428 pim::show_pim(&qt, &shared);
3429 work::show_work(&qt, &shared);
3430 start_watchers(&qt, &shared);
3431 if std::env::var_os("SIOUL_GRAB").is_none() || cfg!(feature = "insecure-test-tls") {
3433 pim::start_dav_watchers(&qt, &shared);
3434 crate::github::start(&qt, &shared);
3435 }
3436 if !offline() {
3438 std::thread::spawn(|| {
3439 let _ = antivirus::refresh_signatures();
3440 });
3441 }
3442 forget_opened_attachments();
3444 if offline() {
3447 return;
3448 }
3449 let (qt, shared) = (qt.clone(), Arc::clone(&shared));
3450 std::thread::spawn(move || {
3451 let checked: usize = load_config().mail_sources().iter().map(|s| sioul_sync::verify::verify_folder(&s.folder, false)).sum();
3452 if checked > 0 {
3453 show(&qt, &shared);
3454 }
3455 });
3456 }
3457
3458 fn refresh(self: Pin<&mut Self>) {
3459 show(&self.qt_thread(), &self.shared());
3460 }
3461
3462 fn sync_now(mut self: Pin<&mut Self>) {
3465 let (qt, shared) = (self.qt_thread(), self.shared());
3466 crate::share::exchange(&qt, &shared);
3467 show(&qt, &shared);
3468 pim::show_pim(&qt, &shared);
3469 work::show_work(&qt, &shared);
3470 update_weather(&qt, &shared);
3471 let calendars = pim::start_dav_watchers(&qt, &shared);
3472 crate::github::start(&qt, &shared);
3473 if start_watchers(&qt, &shared) == 0 && calendars == 0 {
3474 self.as_mut().set_status(QString::from(&tr().text("sync-nothing", None)));
3475 return;
3476 }
3477 self.as_mut().set_busy(true);
3478 self.as_mut().set_status(QString::from(&tr().text("ui-refreshing", None)));
3479 }
3480
3481 fn open_anyway(self: Pin<&mut Self>) {
3482 let shared = self.shared();
3483 shared.opened_anyway.store(true, Ordering::Relaxed);
3484 show(&self.qt_thread(), &shared);
3485 }
3486
3487 fn done(mut self: Pin<&mut Self>) {
3488 let shared = self.shared();
3489 let newest = shared.shown.lock().map(|s| s.clone()).unwrap_or_default();
3490 let path = PorchState::default_path();
3491 let mut state = PorchState::load(&path);
3492 state.close(&newest);
3493 let line = match state.save(&path) {
3494 Ok(()) => tr().text(if newest.is_empty() { "done-nothing" } else { "done-closed" }, None),
3495 Err(e) => e,
3496 };
3497 shared.opened_anyway.store(false, Ordering::Relaxed);
3498 self.as_mut().set_status(QString::from(&line));
3499 show(&self.qt_thread(), &shared);
3500 }
3501
3502 fn read_message(self: Pin<&mut Self>, key: &QString) {
3503 let (qt, key) = (self.qt_thread(), key.to_string());
3504 std::thread::spawn(move || {
3505 let reading = std::panic::catch_unwind(|| message_json(&key)).unwrap_or_default();
3507 let invitation = std::panic::catch_unwind(|| pim::invitation(&key)).unwrap_or_default();
3508 let _ = qt.queue(move |mut sioul| sioul.as_mut().message_read(QString::from(&key), QString::from(&reading), QString::from(&invitation)));
3509 });
3510 }
3511
3512 fn open_attachment(mut self: Pin<&mut Self>, key: &QString, index: i32) {
3513 if !crate::attachments::phone() {
3514 self.as_mut().set_status(QString::from(&tr().text("scan-running-any", None)));
3515 }
3516 checked_attachment(&self.qt_thread(), &self.shared(), key.to_string(), index, Afterwards::Open, false);
3517 }
3518
3519 fn save_attachment(mut self: Pin<&mut Self>, key: &QString, index: i32) {
3520 if !crate::attachments::phone() {
3521 self.as_mut().set_status(QString::from(&tr().text("scan-running-any", None)));
3522 }
3523 checked_attachment(&self.qt_thread(), &self.shared(), key.to_string(), index, Afterwards::Save, false);
3524 }
3525
3526 fn attachments_unscanned(&self) -> bool {
3527 crate::attachments::phone()
3528 }
3529
3530 fn attachment_unchecked(self: Pin<&mut Self>, key: &QString, index: i32, what: i32) {
3531 checked_attachment(&self.qt_thread(), &self.shared(), key.to_string(), index, Afterwards::of(what), true);
3532 }
3533
3534 fn keep_attachment_as_paper(mut self: Pin<&mut Self>, key: &QString, index: i32) {
3535 if !crate::attachments::phone() {
3536 self.as_mut().set_status(QString::from(&tr().text("scan-running-any", None)));
3537 }
3538 checked_attachment(&self.qt_thread(), &self.shared(), key.to_string(), index, Afterwards::Paper, false);
3539 }
3540
3541 fn papers(&self) -> QString {
3542 QString::from(&crate::papers::view())
3543 }
3544
3545 fn save_paper(self: Pin<&mut Self>, id: &QString, edit: &QString) -> QString {
3546 QString::from(&crate::papers::save(&id.to_string(), &edit.to_string()))
3547 }
3548
3549 fn remove_paper(self: Pin<&mut Self>, id: &QString) -> QString {
3550 QString::from(&crate::papers::remove(&id.to_string()))
3551 }
3552
3553 fn plan_renewal(self: Pin<&mut Self>, id: &QString) -> QString {
3554 QString::from(&crate::papers::plan_renewal(&self.qt_thread(), &self.shared(), &id.to_string()))
3555 }
3556
3557 fn let_in(mut self: Pin<&mut Self>, address: &QString) {
3558 let address = address.to_string();
3559 let line = match KnownSenders::let_in(&load_config().known_senders_path(), &address) {
3560 Ok(()) => say("ui-let-in-done", &[("address", address)]),
3561 Err(e) => e,
3562 };
3563 self.as_mut().set_status(QString::from(&line));
3564 show(&self.qt_thread(), &self.shared());
3565 }
3566
3567 fn block(self: Pin<&mut Self>, entry: &QString) {
3568 self.set_standing(entry, &QString::from("blocked"));
3569 }
3570
3571 fn block_from(self: Pin<&mut Self>, entry: &QString, key: &QString) {
3572 crate::mail::label_blocked(&key.to_string());
3573 self.block(entry);
3574 }
3575
3576 fn unblock(mut self: Pin<&mut Self>, entry: &QString) {
3577 let line = crate::senders::unblock(&entry.to_string());
3578 self.as_mut().set_status(QString::from(&line));
3579 let (qt, shared) = (self.qt_thread(), self.shared());
3580 show(&qt, &shared);
3581 pim::show_pim(&qt, &shared);
3582 }
3583
3584 fn set_standing(self: Pin<&mut Self>, entry: &QString, standing: &QString) {
3585 let entry = entry.to_string().trim().to_ascii_lowercase();
3586 self.set_standing_of(&QString::from(&serde_json::json!([entry]).to_string()), standing);
3588 }
3589
3590 fn standing(&self, address: &QString) -> QString {
3591 QString::from(porch::Senders::load(&load_config()).who(&address.to_string()).id())
3592 }
3593
3594 fn standing_of(&self, addresses: &QString) -> QString {
3595 QString::from(&crate::senders::standing_json(&addresses.to_string()))
3596 }
3597
3598 fn set_standing_of(mut self: Pin<&mut Self>, addresses: &QString, standing: &QString) {
3599 let line = crate::senders::set_standing_of(&addresses.to_string(), &standing.to_string());
3600 if line.is_empty() {
3601 return;
3602 }
3603 self.as_mut().set_status(QString::from(&line));
3604 let (qt, shared) = (self.qt_thread(), self.shared());
3605 show(&qt, &shared);
3606 pim::show_pim(&qt, &shared);
3607 }
3608
3609 fn person_of(&self, key: &QString) -> QString {
3610 QString::from(&crate::senders::person_json(&key.to_string()))
3611 }
3612
3613 fn set_person(mut self: Pin<&mut Self>, key: &QString, standing: &QString) {
3614 let line = crate::senders::set_person(&key.to_string(), &standing.to_string());
3615 if line.is_empty() {
3616 return;
3617 }
3618 self.as_mut().set_status(QString::from(&line));
3619 let (qt, shared) = (self.qt_thread(), self.shared());
3620 show(&qt, &shared);
3621 pim::show_pim(&qt, &shared);
3622 }
3623
3624 fn add_mail_line(mut self: Pin<&mut Self>, key: &QString, budget: &QString) {
3625 let (key, budget) = (key.to_string(), budget.to_string());
3626 let world = World::load();
3627 let outcome = (|| {
3628 let ledger = world.ledger().ok_or_else(|| tr().text("ui-no-budgets", None))?;
3629 let path = world.ledger_path().ok_or_else(|| tr().text("ui-no-budgets", None))?;
3630 let mail = world.mail_lines(&ledger);
3631 let found = mail.iter().find(|l| l.key == key).ok_or_else(|| tr().text("note-no-amount", None))?;
3632 let line = found.line(&budget).ok_or_else(|| tr().text("note-no-amount", None))?;
3633 let origin = say("budget-origin-mail", &[("subject", found.subject.clone()), ("sender", found.sender.clone()), ("date", Zoned::now().date().to_string())]);
3634 budget::record_line(&path, &line, &origin)?;
3635 Ok::<String, String>(say("ui-line-added", &[("budget", ledger.budget_title(&budget).to_string())]))
3636 })();
3637 let line = outcome.unwrap_or_else(|e| e);
3638 self.as_mut().set_status(QString::from(&line));
3639 show(&self.qt_thread(), &self.shared());
3640 }
3641
3642 fn ignore_mail_line(mut self: Pin<&mut Self>, key: &QString) {
3643 let path = MoneyState::default_path();
3644 let mut state = MoneyState::load(&path);
3645 state.ignored.insert(key.to_string());
3646 let line = match state.save(&path) {
3647 Ok(()) => tr().text("ui-line-ignored", None),
3648 Err(e) => e,
3649 };
3650 self.as_mut().set_status(QString::from(&line));
3651 show(&self.qt_thread(), &self.shared());
3652 }
3653
3654 fn discover(self: Pin<&mut Self>, address: &QString) {
3655 self.find_servers(address, false);
3656 }
3657
3658 fn discover_at_microsoft(self: Pin<&mut Self>, address: &QString) {
3659 self.find_servers(address, true);
3660 }
3661
3662 fn find_servers(mut self: Pin<&mut Self>, address: &QString, at_microsoft: bool) {
3664 let address = address.to_string().trim().to_ascii_lowercase();
3665 let qt = self.qt_thread();
3666 self.as_mut().set_form_busy(true);
3667 self.as_mut().set_form_error(QString::default());
3668 self.as_mut().set_found(QString::default());
3669 std::thread::spawn(move || {
3670 let said = at_microsoft.then_some(sioul_sync::discover::MicrosoftKind::Unknown);
3674 let known = said.or_else(|| load_config().every_account().find(|a| a.kind == config::AccountKind::Imap && a.address.as_deref() == Some(address.as_str()) && a.host.as_deref().is_some_and(sioul_sync::microsoft::is_imap_host)).map(|a| match (a.is_microsoft(), a.is_microsoft_work()) {
3675 (true, false) => sioul_sync::discover::MicrosoftKind::Personal,
3676 (true, true) => sioul_sync::discover::MicrosoftKind::Work,
3677 _ => sioul_sync::discover::MicrosoftKind::Unknown,
3678 }));
3679 let found = match known {
3680 Some(kind) => Ok(sioul_sync::discover::microsoft_found(&address, kind)),
3681 None => sioul_sync::discover(&address),
3682 };
3683 let result = found.map(|found| {
3684 let server = say(
3685 "account-server",
3686 &[
3687 ("host", found.host.clone()),
3688 ("port", found.port.to_string()),
3689 ("security", tr().text(&format!("security-{}", found.security.as_str()), None)),
3690 ],
3691 );
3692 let google = found.by == sioul_sync::FoundBy::Google || found.host.eq_ignore_ascii_case(sioul_sync::discover::GOOGLE_IMAP);
3694 let microsoft_kind = match found.by {
3696 sioul_sync::FoundBy::Microsoft(kind) => Some(kind),
3697 _ if sioul_sync::microsoft::is_imap_host(&found.host) => Some(sioul_sync::discover::MicrosoftKind::Unknown),
3698 _ => None,
3699 };
3700 json(&FoundView {
3701 by: say("account-found", &[("server", server), ("by", tr().text(found.by.message_id(), None))]),
3702 hint: tr().text(if google { "account-gmail-hint" } else { "account-app-password-hint" }, None),
3703 help_url: google.then_some(sioul_sync::GMAIL_APP_PASSWORDS),
3704 google,
3705 google_key: google && gmail::has_own_key(&address),
3706 microsoft: microsoft_kind.is_some(),
3707 microsoft_kind: microsoft_kind.map_or("", |k| k.as_str()),
3708 microsoft_key: microsoft_kind.is_some() && outlook::has_own_app(&address),
3709 microsoft_admin: if microsoft_kind.is_some_and(|k| k != sioul_sync::discover::MicrosoftKind::Personal) { outlook::admin_text(&address) } else { String::new() },
3710 host: found.host,
3711 port: found.port,
3712 security: found.security.as_str(),
3713 login: found.username,
3714 })
3715 });
3716 let _ = qt.queue(move |mut sioul| {
3717 sioul.as_mut().set_form_busy(false);
3718 match result {
3719 Ok(found) => sioul.as_mut().set_found(QString::from(&found)),
3720 Err(e) => sioul.as_mut().set_form_error(QString::from(&e.sentence(tr(), &address))),
3721 }
3722 });
3723 });
3724 }
3725
3726 fn add_account(mut self: Pin<&mut Self>, address: &QString, host: &QString, port: i32, security: &QString, login: &QString, password: &QString) {
3727 let (address, host, login, password) = (address.to_string().trim().to_ascii_lowercase(), host.to_string().trim().to_string(), login.to_string().trim().to_string(), password.to_string());
3728 let security = if security.to_string() == "starttls" { Security::Starttls } else { Security::Tls };
3729 let port = u16::try_from(port).ok().filter(|p| *p > 0).unwrap_or(if security == Security::Tls { 993 } else { 143 });
3730 self.as_mut().set_form_busy(true);
3731 self.as_mut().set_form_error(QString::default());
3732 add_mail(&self.qt_thread(), &self.shared(), address, host, port, security, login, Some(password));
3733 }
3734
3735 fn add_mail_like(mut self: Pin<&mut Self>, address: &QString, host: &QString, port: i32, security: &QString, login: &QString) {
3736 let (address, host, login) = (address.to_string().trim().to_ascii_lowercase(), host.to_string().trim().to_string(), login.to_string().trim().to_string());
3737 let security = if security.to_string() == "starttls" { Security::Starttls } else { Security::Tls };
3738 let port = u16::try_from(port).ok().filter(|p| *p > 0).unwrap_or(if security == Security::Tls { 993 } else { 143 });
3739 self.as_mut().set_form_busy(true);
3740 self.as_mut().set_form_error(QString::default());
3741 add_mail(&self.qt_thread(), &self.shared(), address, host, port, security, login, None);
3742 }
3743
3744 fn set_account_enabled(mut self: Pin<&mut Self>, id: &QString, on: bool) -> QString {
3745 let id = id.to_string();
3746 let config = load_config();
3747 let Some(account) = config.every_account().find(|a| a.id == id).cloned() else { return QString::from(&say("account-unknown", &[("id", id)])) };
3748 if let Err(e) = config::set_enabled(&config_path(), &id, on) {
3749 return QString::from(&e);
3750 }
3751 let (qt, shared) = (self.qt_thread(), self.shared());
3752 if account.kind == config::AccountKind::Dav {
3753 sioul_core::vdir::set_account_off(&id, !on);
3755 if on {
3756 pim::start_dav_watchers(&qt, &shared);
3757 } else {
3758 pim::stop_dav_watcher(&shared, &id);
3759 }
3760 } else if on {
3761 start_watchers(&qt, &shared);
3762 } else if let Some(control) = shared.watchers.lock().ok().and_then(|mut w| w.remove(&id)) {
3763 control.stop();
3764 }
3765 self.as_mut().set_status(QString::from(&say(if on { "account-on" } else { "account-off" }, &[("id", id)])));
3766 show(&qt, &shared);
3767 pim::show_pim(&qt, &shared);
3768 mail::show_mail(&qt, &shared);
3769 work::show_work(&qt, &shared);
3771 QString::default()
3772 }
3773
3774 fn scout_account(mut self: Pin<&mut Self>, address: &QString) {
3775 let address = address.to_string().trim().to_ascii_lowercase();
3776 let qt = self.qt_thread();
3777 self.as_mut().set_scouting(true);
3778 self.as_mut().set_scouted(QString::default());
3779 std::thread::spawn(move || {
3780 let config = load_config();
3781 let mine = |kind: config::AccountKind| config.every_account().find(|a| a.address.as_deref().map(str::to_lowercase).as_deref() == Some(address.as_str()) && a.kind == kind).cloned();
3782 let (mail_have, dav_have) = (mine(config::AccountKind::Imap), mine(config::AccountKind::Dav));
3783 let password = dav_have.as_ref().and_then(|a| secret::password(a).ok());
3785 let login = dav_have.as_ref().and_then(|a| a.login().map(str::to_string)).unwrap_or_else(|| address.clone());
3786 let found = sioul_sync::scout::scout(&address, dav_have.as_ref().and_then(|a| a.host.as_deref()), password.as_deref().map(|p| (login.as_str(), p)));
3787 let mail = found.mail.as_ref().map(|m| {
3788 let server = say("account-server", &[("host", m.host.clone()), ("port", m.port.to_string()), ("security", tr().text(&format!("security-{}", m.security.as_str()), None))]);
3789 serde_json::json!({ "host": m.host, "port": m.port, "security": m.security.as_str(), "login": m.username, "line": server, "have": mail_have.as_ref().map(|a| a.id.clone()) })
3790 });
3791 let dav = found.dav.as_ref().map(|url| serde_json::json!({ "url": url, "have": dav_have.as_ref().map(|a| a.id.clone()) }));
3792 let nextcloud = found.nextcloud.as_ref().map(|n| {
3793 let apps: Vec<serde_json::Value> = n
3794 .apps
3795 .iter()
3796 .filter_map(|a| sioul_sync::scout::known_app(a))
3797 .map(|(id, in_sioul)| serde_json::json!({ "id": id, "title": tr().text(&format!("scout-app-{id}"), None), "in_sioul": in_sioul }))
3798 .collect();
3799 serde_json::json!({ "product": n.product, "version": n.version, "server": n.server, "apps": apps, "asked": password.is_some() })
3800 });
3801 let answer = serde_json::json!({ "address": address, "mail": mail, "dav": dav, "nextcloud": nextcloud, "dav_password": password.is_some() }).to_string();
3802 let _ = qt.queue(move |mut sioul| {
3803 sioul.as_mut().set_scouting(false);
3804 sioul.as_mut().set_scouted(QString::from(&answer));
3805 });
3806 });
3807 }
3808
3809 fn add_portal(mut self: Pin<&mut Self>, name: &QString, url: &QString) {
3810 let (id, url) = (config::slug(&name.to_string()), url.to_string().trim().to_string());
3811 let config = load_config();
3812 let outcome = if config.site(&id).is_some() {
3813 Err(say("account-exists", &[("id", id.clone())]))
3814 } else if id.is_empty() || !url.starts_with("https://") {
3815 Err(tr().text("site-https-only", None))
3817 } else {
3818 config::add_site(&config_path(), &id, &name.to_string(), &url).map(|()| say("account-portal-added", &[("id", id.clone()), ("url", url.clone())]))
3819 };
3820 match outcome {
3821 Ok(line) => self.as_mut().set_status(QString::from(&line)),
3822 Err(e) => self.as_mut().set_form_error(QString::from(&e)),
3823 }
3824 show(&self.qt_thread(), &self.shared());
3825 }
3826
3827 fn set_account_password(mut self: Pin<&mut Self>, id: &QString, password: &QString) {
3828 self.as_mut().set_form_busy(true);
3829 give_password(&self.qt_thread(), &self.shared(), id.to_string(), password.to_string());
3830 }
3831
3832 fn remove_account(mut self: Pin<&mut Self>, id: &QString, delete_mail: bool) {
3833 let id = id.to_string();
3834 let shared = self.shared();
3835 let config = load_config();
3836 let microsoft = config.every_account().any(|a| a.id == id && a.is_microsoft());
3840 let watcher = shared.watchers.lock().ok().and_then(|mut watchers| {
3841 if microsoft && let Ok(mut removing) = shared.removing.lock() {
3842 removing.insert(id.clone());
3843 }
3844 watchers.remove(&id)
3845 });
3846 if let Some(control) = &watcher {
3847 control.stop();
3848 }
3849 if let Ok(mut statuses) = shared.statuses.lock() {
3850 statuses.remove(&id);
3851 }
3852 let line = match config.every_account().find(|a| a.id == id) {
3854 None => say("account-unknown", &[("id", id.clone())]),
3855 Some(account) if account.is_microsoft() => {
3858 outlook::remove(&self.qt_thread(), &shared, account.clone(), delete_mail, watcher);
3859 return;
3860 }
3861 Some(account) => {
3862 if account.is_dav() {
3863 pim::stop_dav_watcher(&shared, &id);
3864 }
3865 let shared_password = config.every_account().any(|a| a.id != id && a.host == account.host && a.login() == account.login());
3868 let forgotten = if account.auth.as_deref() == Some("google") {
3869 let purpose = if account.is_dav() { sioul_sync::google::Purpose::Pim } else { sioul_sync::google::Purpose::Mail };
3871 sioul_sync::google::revoke_for(purpose, account.address.as_deref().unwrap_or("")).map_err(|e| e.sentence(tr(), &id))
3872 } else if (account.syncs() || account.is_dav()) && !shared_password {
3873 secret::forget(account).map_err(|e| e.sentence(tr(), &id))
3874 } else {
3875 Ok(())
3876 };
3877 match forgotten.and_then(|()| config::remove_account(&config_path(), &id)) {
3878 Ok(()) => say("account-removed", &[("id", id.clone()), ("path", account.maildir_path().display().to_string())]),
3879 Err(e) => e,
3880 }
3881 }
3882 };
3883 self.as_mut().set_status(QString::from(&line));
3884 show(&self.qt_thread(), &shared);
3885 pim::show_pim(&self.qt_thread(), &shared);
3887 work::show_work(&self.qt_thread(), &shared);
3888 }
3889
3890 fn set_priority(mut self: Pin<&mut Self>, id: &QString, priority: &QString) {
3891 let id = id.to_string();
3892 let line = match Priority::parse(&priority.to_string()) {
3893 Some(priority) => match config::set_priority(&config_path(), &id, priority) {
3894 Ok(()) => say("ui-priority-set", &[("id", id), ("priority", tr().text(&format!("priority-{}", priority.as_str()), None))]),
3895 Err(e) => e,
3896 },
3897 None => return,
3898 };
3899 self.as_mut().set_status(QString::from(&line));
3900 show(&self.qt_thread(), &self.shared());
3901 }
3902
3903 fn open_folder(self: Pin<&mut Self>, account: &QString, folder: &QString, all: bool, query: &QString) {
3904 let shared = self.shared();
3905 if let Ok(mut open) = shared.open_folder.lock() {
3906 *open = Some(mail::OpenFolder { account: account.to_string(), folder: folder.to_string(), all, query: query.to_string() });
3907 }
3908 mail::show_mail(&self.qt_thread(), &shared);
3909 }
3910
3911 fn opened(self: Pin<&mut Self>, key: &QString) {
3912 mail::opened(&self.qt_thread(), &self.shared(), &key.to_string());
3913 }
3914
3915 fn act(self: Pin<&mut Self>, key: &QString, action: &QString, target: &QString) {
3916 mail::act(&self.qt_thread(), &self.shared(), &key.to_string(), &action.to_string(), &target.to_string());
3917 }
3918
3919 fn act_many(self: Pin<&mut Self>, keys: &QString, action: &QString) {
3920 let keys: Vec<String> = serde_json::from_str(&keys.to_string()).unwrap_or_default();
3921 mail::act_many(&self.qt_thread(), &self.shared(), &keys, &action.to_string());
3922 }
3923
3924 fn move_messages(self: Pin<&mut Self>, keys: &QString, account: &QString, folder: &QString) {
3925 let keys: Vec<String> = serde_json::from_str(&keys.to_string()).unwrap_or_default();
3926 mail::move_many(&self.qt_thread(), &self.shared(), &keys, &account.to_string(), &folder.to_string());
3927 }
3928
3929 fn unsubscribe(self: Pin<&mut Self>, key: &QString) -> QString {
3930 let world = World::load();
3931 let triaged = world.judge(&key.to_string()).map(|(_, triaged)| triaged);
3932 QString::from(&mail::unsubscribe(&self.qt_thread(), &self.shared(), &world.config, triaged))
3933 }
3934
3935 fn undo(mut self: Pin<&mut Self>) {
3936 if let Some(draft) = mail::undo(&self.qt_thread(), &self.shared()) {
3937 self.as_mut().compose_requested(QString::from(&draft));
3938 }
3939 }
3940
3941 fn flush(self: Pin<&mut Self>) {
3942 mail::flush(&self.shared());
3943 securitykey::closing();
3944 }
3945
3946 fn compose(mut self: Pin<&mut Self>, kind: &QString, key: &QString, account: &QString) -> QString {
3947 match mail::compose(&kind.to_string(), &key.to_string(), &account.to_string()) {
3948 Ok(id) => {
3949 mail::show_mail(&self.qt_thread(), &self.shared());
3950 QString::from(&id)
3951 }
3952 Err(e) => {
3953 self.as_mut().set_status(QString::from(&e));
3954 QString::default()
3955 }
3956 }
3957 }
3958
3959 fn draft(&self, id: &QString) -> QString {
3960 QString::from(&mail::draft(&id.to_string()).unwrap_or_default())
3961 }
3962
3963 fn save_draft(self: Pin<&mut Self>, edit: &QString) -> QString {
3964 let line = mail::save_draft(&edit.to_string()).unwrap_or_else(|e| e);
3965 mail::show_mail(&self.qt_thread(), &self.shared());
3966 QString::from(&line)
3967 }
3968
3969 fn attach(self: Pin<&mut Self>, id: &QString, url: &QString) -> QString {
3970 QString::from(&mail::attach(&id.to_string(), &url.to_string()).err().unwrap_or_default())
3971 }
3972
3973 fn detach(mut self: Pin<&mut Self>, id: &QString, index: i32, forwarded: bool) {
3974 if let Err(e) = mail::detach(&id.to_string(), usize::try_from(index).unwrap_or(usize::MAX), forwarded) {
3975 self.as_mut().set_status(QString::from(&e));
3976 }
3977 }
3978
3979 fn preview(&self, markdown: &QString) -> QString {
3980 QString::from(&sioul_core::compose::markdown_html(&markdown.to_string()))
3981 }
3982
3983 fn send(self: Pin<&mut Self>, id: &QString) -> QString {
3984 QString::from(&mail::send(&self.qt_thread(), &self.shared(), &id.to_string()).unwrap_or_default())
3985 }
3986
3987 fn discard(self: Pin<&mut Self>, id: &QString) {
3988 mail::discard(&self.qt_thread(), &self.shared(), &id.to_string());
3989 }
3990
3991 fn draft_closed(self: Pin<&mut Self>, id: &QString) {
3992 mail::closed(&self.qt_thread(), &self.shared(), &id.to_string());
3993 }
3994
3995 fn place(&self, key: &QString) -> QString {
3996 QString::from(&mail::place(&key.to_string()))
3997 }
3998
3999 fn source(&self, key: &QString) -> QString {
4000 QString::from(&mail::source(&key.to_string()))
4001 }
4002
4003 fn set_writing(mut self: Pin<&mut Self>, id: &QString, name: &QString, signature: &QString) {
4004 let id = id.to_string();
4005 let line = match config::set_writing(&config_path(), &id, &name.to_string(), &signature.to_string()) {
4006 Ok(()) => say("ui-writing-saved", &[("id", id)]),
4007 Err(e) => e,
4008 };
4009 self.as_mut().set_status(QString::from(&line));
4010 show(&self.qt_thread(), &self.shared());
4011 }
4012
4013 fn grab_folder(&self) -> QString {
4014 QString::from(&std::env::var("SIOUL_GRAB").unwrap_or_default())
4015 }
4016
4017 fn search_contacts(self: Pin<&mut Self>, query: &QString) {
4018 let shared = self.shared();
4019 if let Ok(mut state) = shared.pim.lock() {
4020 state.query = query.to_string();
4021 }
4022 pim::show_pim(&self.qt_thread(), &shared);
4023 }
4024
4025 fn contact(&self, key: &QString) -> QString {
4026 QString::from(&pim::contact(&key.to_string()))
4027 }
4028
4029 fn save_contact(self: Pin<&mut Self>, key: &QString, edit: &QString) -> QString {
4030 let answer = match pim::save_contact(&self.qt_thread(), &self.shared(), &key.to_string(), &edit.to_string()) {
4031 Ok(key) => serde_json::json!({ "key": key }),
4032 Err(error) => serde_json::json!({ "error": error }),
4033 };
4034 QString::from(&answer.to_string())
4035 }
4036
4037 fn delete_contact(self: Pin<&mut Self>, key: &QString) {
4038 pim::delete_contact(&self.qt_thread(), &self.shared(), &key.to_string());
4039 }
4040
4041 fn show_contacts_category(self: Pin<&mut Self>, category: &QString) {
4042 let shared = self.shared();
4043 if let Ok(mut state) = shared.pim.lock() {
4044 state.category = category.to_string();
4045 }
4046 pim::show_pim(&self.qt_thread(), &shared);
4047 }
4048
4049 fn find_duplicates(self: Pin<&mut Self>) {
4050 crate::duplicates::find(&self.qt_thread(), &self.shared());
4051 }
4052
4053 fn clean_contacts(self: Pin<&mut Self>, keys: &QString) -> QString {
4054 QString::from(&crate::duplicates::clean(&self.qt_thread(), &self.shared(), &keys.to_string()))
4055 }
4056
4057 fn merge_contacts(self: Pin<&mut Self>, lead: &QString, other: &QString) -> QString {
4058 QString::from(&crate::duplicates::merge(&self.qt_thread(), &self.shared(), &lead.to_string(), &other.to_string()))
4059 }
4060
4061 fn not_same_contacts(self: Pin<&mut Self>, first: &QString, second: &QString) -> QString {
4062 QString::from(&crate::duplicates::not_same(&self.qt_thread(), &self.shared(), &first.to_string(), &second.to_string()))
4063 }
4064
4065 fn undo_contacts(self: Pin<&mut Self>, id: &QString) -> QString {
4066 QString::from(&crate::duplicates::undo(&self.qt_thread(), &self.shared(), &id.to_string()))
4067 }
4068
4069 fn show_days(self: Pin<&mut Self>, from: &QString, days: i32, upcoming: bool) {
4070 let shared = self.shared();
4071 if let Ok(mut state) = shared.pim.lock() {
4072 state.from = from.to_string().parse().unwrap_or_else(|_| Zoned::now().date());
4073 state.days = i64::from(days.clamp(1, 366));
4074 state.upcoming = upcoming;
4075 }
4076 pim::show_pim(&self.qt_thread(), &shared);
4077 }
4078
4079 fn calendars(&self) -> QString {
4080 QString::from(&pim::calendars())
4081 }
4082
4083 fn event_form(&self, key: &QString) -> QString {
4084 QString::from(&pim::event(&key.to_string()))
4085 }
4086
4087 fn save_event(self: Pin<&mut Self>, key: &QString, edit: &QString, calendar: &QString) -> QString {
4088 let result = pim::save_event(&self.qt_thread(), &self.shared(), &key.to_string(), &edit.to_string(), &calendar.to_string());
4089 QString::from(&result.err().unwrap_or_default())
4090 }
4091
4092 fn task_as_event(&self, uid: &QString, start: f64, end: f64) -> QString {
4093 QString::from(&crate::appointments::form(&self.shared(), &uid.to_string(), start as i64, end as i64))
4094 }
4095
4096 fn task_to_event(self: Pin<&mut Self>, uid: &QString, block: &QString, edit: &QString, calendar: &QString) -> QString {
4097 QString::from(&crate::appointments::task_to_event(&self.qt_thread(), &self.shared(), &uid.to_string(), &block.to_string(), &edit.to_string(), &calendar.to_string()))
4098 }
4099
4100 fn event_to_task_losses(&self, key: &QString) -> QString {
4101 QString::from(&crate::appointments::task_losses(&key.to_string()))
4102 }
4103
4104 fn event_to_task(self: Pin<&mut Self>, key: &QString) -> QString {
4105 QString::from(&crate::appointments::event_to_task(&self.qt_thread(), &self.shared(), &key.to_string()))
4106 }
4107
4108 fn event_reminder(&self, key: &QString, start: f64) -> QString {
4109 QString::from(&crate::remind::of_event(&key.to_string(), start as i64))
4110 }
4111
4112 fn reminder_choices(&self, current: &QString) -> QString {
4113 QString::from(&crate::remind::choices(¤t.to_string()))
4114 }
4115
4116 fn set_event_reminder(self: Pin<&mut Self>, key: &QString, value: &QString) -> QString {
4117 let result = crate::remind::set_event(&self.qt_thread(), &self.shared(), &key.to_string(), &value.to_string());
4118 QString::from(&result.err().unwrap_or_default())
4119 }
4120
4121 fn move_event(self: Pin<&mut Self>, key: &QString, start: f64, new_start: f64, new_end: f64, only_this: bool) -> QString {
4122 QString::from(&pim::move_event(&self.qt_thread(), &self.shared(), &key.to_string(), start as i64, new_start as i64, new_end as i64, only_this))
4123 }
4124
4125 fn delete_event(self: Pin<&mut Self>, key: &QString, start: f64, only_this: bool) {
4126 pim::delete_event(&self.qt_thread(), &self.shared(), &key.to_string(), start as i64, only_this);
4127 }
4128
4129 fn completions(&self, typed: &QString) -> QString {
4130 QString::from(&pim::completions(&typed.to_string()))
4131 }
4132
4133 fn contact_for(&self, address: &QString) -> QString {
4134 QString::from(&pim::contact_for(&address.to_string()))
4135 }
4136
4137 fn add_sender(mut self: Pin<&mut Self>, name: &QString, address: &QString) {
4138 if let Err(e) = pim::add_sender(&self.qt_thread(), &self.shared(), &name.to_string(), &address.to_string()) {
4139 self.as_mut().set_status(QString::from(&e));
4140 }
4141 }
4142
4143 fn answer_invitation(mut self: Pin<&mut Self>, key: &QString, answer: &QString) {
4144 let (qt, shared, key, answer) = (self.qt_thread(), self.shared(), key.to_string(), answer.to_string());
4146 self.as_mut().set_status(QString::from(&tr().text("invitation-answering", None)));
4147 std::thread::spawn(move || {
4148 if let Err(e) = pim::answer(&qt, &shared, &key, &answer) {
4149 tell(&qt, &shared, e);
4150 }
4151 });
4152 }
4153
4154 fn history_weeks(&self) -> i32 {
4155 i32::try_from(load_config().history_weeks.unwrap_or(config::DEFAULT_HISTORY_WEEKS)).unwrap_or(2)
4156 }
4157
4158 fn day_start(&self) -> i32 {
4159 i32::try_from(load_config().agenda.day_start.unwrap_or(7).min(23)).unwrap_or(7)
4160 }
4161
4162 fn set_history(mut self: Pin<&mut Self>, weeks: i32) {
4163 let weeks = u32::try_from(weeks.max(0)).unwrap_or(0);
4164 let line = match config::set_history(&config_path(), weeks) {
4165 Ok(()) if weeks == 0 => tr().text("ui-history-everything", None),
4166 Ok(()) => say("ui-history-set", &[("weeks", weeks.to_string())]),
4167 Err(e) => e,
4168 };
4169 self.as_mut().set_status(QString::from(&line));
4170 let (qt, shared) = (self.qt_thread(), self.shared());
4172 start_watchers(&qt, &shared);
4173 show(&qt, &shared);
4174 pim::show_pim(&qt, &shared);
4175 }
4176
4177 fn add_dav(mut self: Pin<&mut Self>, address: &QString, url: &QString, login: &QString, password: &QString) {
4178 self.as_mut().set_form_busy(true);
4179 self.as_mut().set_form_error(QString::default());
4180 pim::add_account(&self.qt_thread(), &self.shared(), address.to_string(), url.to_string(), login.to_string(), password.to_string());
4181 }
4182
4183 fn add_google(mut self: Pin<&mut Self>, address: &QString, client_id: &QString, client_secret: &QString, again: &QString) {
4184 self.as_mut().set_form_busy(true);
4185 self.as_mut().set_form_error(QString::default());
4186 let again = Some(again.to_string()).filter(|a| !a.is_empty());
4187 pim::add_google_account(&self.qt_thread(), &self.shared(), address.to_string(), client_id.to_string(), client_secret.to_string(), again);
4188 }
4189
4190 fn cancel_google(self: Pin<&mut Self>) {
4191 self.shared().oauth_stop.store(true, Ordering::Relaxed);
4192 }
4193
4194 fn add_google_mail(mut self: Pin<&mut Self>, address: &QString, client_id: &QString, client_secret: &QString, again: &QString) {
4195 self.as_mut().set_form_busy(true);
4196 self.as_mut().set_form_error(QString::default());
4197 let again = Some(again.to_string()).filter(|a| !a.is_empty());
4198 gmail::add_google_mail(&self.qt_thread(), &self.shared(), address.to_string(), client_id.to_string(), client_secret.to_string(), again);
4199 }
4200
4201 fn add_microsoft_mail(mut self: Pin<&mut Self>, address: &QString, kind: &QString, client_id: &QString, tenant: &QString, again: &QString) {
4202 self.as_mut().set_form_busy(true);
4203 self.as_mut().set_form_error(QString::default());
4204 let again = Some(again.to_string()).filter(|a| !a.is_empty());
4205 outlook::add_microsoft_mail(&self.qt_thread(), &self.shared(), address.to_string(), kind.to_string(), client_id.to_string(), tenant.to_string(), again);
4206 }
4207
4208 fn cancel_microsoft(self: Pin<&mut Self>) {
4209 self.shared().oauth_stop.store(true, Ordering::Relaxed);
4210 }
4211
4212 fn answer_microsoft(self: Pin<&mut Self>, yes: bool) {
4213 if let Some(answer) = self.shared().microsoft_answer.lock().ok().and_then(|mut waiting| waiting.take()) {
4214 let _ = answer.send(yes);
4215 }
4216 }
4217
4218 fn microsoft_built_in(&self) -> bool {
4219 sioul_sync::microsoft::built_in().is_some()
4220 }
4221
4222 fn microsoft_admin_text(&self, address: &QString, client_id: &QString) -> QString {
4223 QString::from(&outlook::admin_text_for(&address.to_string(), &client_id.to_string()))
4224 }
4225
4226 fn pgp_keys(&self) -> QString {
4227 QString::from(&crypto::keys_view())
4228 }
4229
4230 fn pgp_generate(mut self: Pin<&mut Self>, address: &QString) {
4231 let (qt, shared, address) = (self.qt_thread(), self.shared(), address.to_string());
4233 self.as_mut().set_status(QString::from(&tr().text("pgp-making", None)));
4234 std::thread::spawn(move || {
4235 let line = crypto::generate(&address).unwrap_or_else(|e| e);
4236 tell(&qt, &shared, line);
4237 let _ = qt.queue(|mut sioul| sioul.as_mut().keys_changed());
4238 });
4239 }
4240
4241 fn pgp_import(mut self: Pin<&mut Self>, url: &QString, passphrase: &QString) -> QString {
4242 match crypto::import(&url.to_string(), &passphrase.to_string()) {
4243 Ok(line) => {
4244 self.as_mut().set_status(QString::from(&line));
4245 self.as_mut().keys_changed();
4246 QString::default()
4247 }
4248 Err(e) => QString::from(&e),
4249 }
4250 }
4251
4252 fn pgp_export(mut self: Pin<&mut Self>, fingerprint: &QString) {
4253 let line = match crypto::export(&fingerprint.to_string(), &downloads()) {
4254 Ok(path) => say("ui-saved", &[("path", path.display().to_string())]),
4255 Err(e) => e,
4256 };
4257 self.as_mut().set_status(QString::from(&line));
4258 }
4259
4260 fn pgp_save_revocation(mut self: Pin<&mut Self>, fingerprint: &QString) {
4261 let line = match sioul_core::pgp::save_revocation(&fingerprint.to_string(), &downloads()) {
4262 Ok(path) => say("pgp-revocation-saved", &[("path", path.display().to_string())]),
4263 Err(e) => e,
4264 };
4265 self.as_mut().set_status(QString::from(&line));
4266 }
4267
4268 fn pgp_remove(mut self: Pin<&mut Self>, fingerprint: &QString) {
4269 let line = match sioul_core::pgp::remove(&fingerprint.to_string()) {
4270 Ok(()) => tr().text("pgp-removed", None),
4271 Err(e) => e,
4272 };
4273 self.as_mut().set_status(QString::from(&line));
4274 self.as_mut().keys_changed();
4275 }
4276
4277 fn pgp_lookup(mut self: Pin<&mut Self>, draft: &QString) {
4278 let (qt, shared, id) = (self.qt_thread(), self.shared(), draft.to_string());
4279 self.as_mut().set_status(QString::from(&tr().text("pgp-looking", None)));
4280 std::thread::spawn(move || {
4281 let Some(draft) = sioul_core::compose::Draft::by_id(&id) else { return };
4282 tell(&qt, &shared, crypto::lookup(&draft));
4283 let _ = qt.queue(|mut sioul| sioul.as_mut().keys_changed());
4284 });
4285 }
4286
4287 fn security_key_for_draft(&self, id: &QString) -> QString {
4288 QString::from(&securitykey::for_draft(&id.to_string()))
4289 }
4290
4291 fn sign_and_send(self: Pin<&mut Self>, id: &QString, pin: &QString) {
4292 securitykey::sign_and_send(&self.qt_thread(), &self.shared(), &id.to_string(), pin.to_string());
4293 }
4294
4295 fn open_with_security_key(self: Pin<&mut Self>, key: &QString, pin: &QString) {
4296 securitykey::open_message(&self.qt_thread(), &self.shared(), &key.to_string(), pin.to_string());
4297 }
4298
4299 fn security_key_not_now(self: Pin<&mut Self>, context: &QString) {
4300 securitykey::not_now(&context.to_string());
4301 }
4302
4303 fn open_nfc_settings(self: Pin<&mut Self>) {
4304 securitykey::open_nfc_settings();
4305 }
4306
4307 fn let_gnupg_release(self: Pin<&mut Self>) -> QString {
4308 QString::from(&securitykey::release())
4309 }
4310
4311 fn security_keys(&self) -> QString {
4312 QString::from(&securitykey::known_view())
4313 }
4314
4315 fn read_security_key(self: Pin<&mut Self>) {
4316 securitykey::read_for_setup(&self.qt_thread());
4317 }
4318
4319 fn find_security_key_certificate(self: Pin<&mut Self>, ident: &QString) {
4320 securitykey::look_for_certificate(&self.qt_thread(), &ident.to_string());
4321 }
4322
4323 fn import_security_key_certificate(self: Pin<&mut Self>, ident: &QString, url: &QString) {
4324 securitykey::import_certificate(&self.qt_thread(), &ident.to_string(), &url.to_string());
4325 }
4326
4327 fn import_security_key_from_gnupg(self: Pin<&mut Self>, ident: &QString) {
4328 securitykey::import_from_gnupg(&self.qt_thread(), &ident.to_string());
4329 }
4330
4331 fn renew_security_key(self: Pin<&mut Self>, ident: &QString) {
4332 securitykey::renew(&self.qt_thread(), &ident.to_string());
4333 }
4334
4335 fn send_security_key_to_keys_openpgp(self: Pin<&mut Self>, ident: &QString) {
4336 securitykey::send_to_keys_openpgp(&self.qt_thread(), &ident.to_string());
4337 }
4338
4339 fn forget_security_key_pin(mut self: Pin<&mut Self>) {
4340 let line = securitykey::forget_pin();
4341 self.as_mut().set_status(QString::from(&line));
4342 self.as_mut().keys_changed();
4343 }
4344
4345 fn stop_using_security_key(self: Pin<&mut Self>, ident: &QString) {
4346 securitykey::stop_using(&self.qt_thread(), &self.shared(), &ident.to_string());
4347 }
4348
4349 fn show_tasks(self: Pin<&mut Self>, by: &QString, done: bool, query: &QString, project_id: &QString) {
4350 work::set_view(&self.qt_thread(), &self.shared(), &by.to_string(), done, &query.to_string(), &project_id.to_string());
4351 }
4352
4353 fn filter_tasks(self: Pin<&mut Self>, kind: &QString, category: &QString) {
4354 work::set_filter(&self.qt_thread(), &self.shared(), &kind.to_string(), &category.to_string());
4355 }
4356
4357 fn refresh_work(self: Pin<&mut Self>) {
4358 work::show_work(&self.qt_thread(), &self.shared());
4359 }
4360
4361 fn show_tasks_anyway(self: Pin<&mut Self>, on: bool) {
4362 work::show_anyway(&self.qt_thread(), &self.shared(), on);
4363 }
4364
4365 fn task(&self, uid: &QString) -> QString {
4366 QString::from(&work::task(&self.shared(), &uid.to_string()))
4367 }
4368
4369 fn new_task_form(&self, from: &QString, key: &QString, start: f64, list: &QString) -> QString {
4370 QString::from(&work::new_form(&self.shared(), &from.to_string(), &key.to_string(), start, &list.to_string()))
4371 }
4372
4373 fn save_task(self: Pin<&mut Self>, uid: &QString, edit: &QString, list: &QString) -> QString {
4374 QString::from(&work::save(&self.qt_thread(), &self.shared(), &uid.to_string(), &edit.to_string(), &list.to_string()))
4375 }
4376
4377 fn capture(&self, line: &QString) -> QString {
4378 QString::from(&work::captured(&self.shared(), &line.to_string()))
4379 }
4380
4381 fn add_task(self: Pin<&mut Self>, line: &QString, parent: &QString, list: &QString) -> QString {
4382 QString::from(&work::add(&self.qt_thread(), &self.shared(), &line.to_string(), &parent.to_string(), &list.to_string()))
4383 }
4384
4385 fn set_task_status(self: Pin<&mut Self>, uid: &QString, status: &QString) -> QString {
4386 QString::from(&work::set_status(&self.qt_thread(), &self.shared(), &uid.to_string(), &status.to_string()))
4387 }
4388
4389 fn set_felt(self: Pin<&mut Self>, uid: &QString, felt: &QString) -> QString {
4390 QString::from(&work::set_felt(&self.qt_thread(), &self.shared(), &uid.to_string(), &felt.to_string()))
4391 }
4392
4393 fn set_waits(self: Pin<&mut Self>, uid: &QString, other: &QString, wait: bool, gap_minutes: i32) -> QString {
4394 QString::from(&work::set_waits(&self.qt_thread(), &self.shared(), &uid.to_string(), &other.to_string(), wait, i64::from(gap_minutes)))
4395 }
4396
4397 fn not_now(self: Pin<&mut Self>, uid: &QString) {
4398 work::not_now(&self.qt_thread(), &self.shared(), &uid.to_string());
4399 }
4400
4401 fn set_weather(self: Pin<&mut Self>, weather: &QString) {
4402 work::set_weather(&self.qt_thread(), &self.shared(), &weather.to_string());
4403 crate::reviews::weather_said(sioul_core::today::Weather::parse(&weather.to_string()));
4405 }
4406
4407 fn set_task_at(self: Pin<&mut Self>, uid: &QString, at: &QString) -> QString {
4408 QString::from(&work::set_at(&self.qt_thread(), &self.shared(), &uid.to_string(), &at.to_string(), 0))
4409 }
4410
4411 fn pin_task(self: Pin<&mut Self>, uid: &QString, at: &QString, minutes: i32) -> QString {
4412 QString::from(&work::set_at(&self.qt_thread(), &self.shared(), &uid.to_string(), &at.to_string(), u32::try_from(minutes).unwrap_or(0)))
4413 }
4414
4415 fn delete_task(self: Pin<&mut Self>, uid: &QString) {
4416 work::delete(&self.qt_thread(), &self.shared(), &uid.to_string());
4417 }
4418
4419 fn search_tasks(&self, query: &QString, except: &QString) -> QString {
4420 QString::from(&work::search(&self.shared(), &query.to_string(), &except.to_string()))
4421 }
4422
4423 fn new_list(self: Pin<&mut Self>, account: &QString, name: &QString) -> QString {
4424 QString::from(&work::new_list(&self.qt_thread(), &self.shared(), &account.to_string(), &name.to_string()))
4425 }
4426
4427 fn list_accounts(&self) -> QString {
4428 QString::from(&work::list_accounts())
4429 }
4430
4431 fn focus_start(self: Pin<&mut Self>, uid: &QString, minutes: i32) {
4432 work::focus_start(&self.qt_thread(), &self.shared(), &uid.to_string(), minutes);
4433 }
4434
4435 fn focus_pause(self: Pin<&mut Self>) {
4436 work::focus_pause(&self.qt_thread(), &self.shared());
4437 }
4438
4439 fn focus_extend(self: Pin<&mut Self>, minutes: i32) {
4440 work::focus_extend(&self.qt_thread(), &self.shared(), minutes);
4441 }
4442
4443 fn focus_stop(self: Pin<&mut Self>, done: bool, note: &QString) -> QString {
4444 QString::from(&work::focus_stop(&self.qt_thread(), &self.shared(), done, ¬e.to_string()))
4445 }
4446
4447 fn search_notes(self: Pin<&mut Self>, query: &QString) {
4448 work::search_notes(&self.qt_thread(), &self.shared(), &query.to_string());
4449 }
4450
4451 fn note(&self, path: &QString) -> QString {
4452 QString::from(&work::note(&self.shared(), &path.to_string()))
4453 }
4454
4455 fn save_note(self: Pin<&mut Self>, path: &QString, text: &QString, stamp: &QString) -> QString {
4456 QString::from(&work::save_note(&self.qt_thread(), &self.shared(), &path.to_string(), &text.to_string(), &stamp.to_string()))
4457 }
4458
4459 fn create_note(self: Pin<&mut Self>, title: &QString, links: &QString) -> QString {
4460 QString::from(&work::create_note(&self.qt_thread(), &self.shared(), &title.to_string(), &links.to_string()))
4461 }
4462
4463 fn related(&self, uri: &QString) -> QString {
4464 QString::from(&work::related(&self.shared(), &uri.to_string()))
4465 }
4466
4467 fn link_things(mut self: Pin<&mut Self>, from: &QString, to: &QString) -> QString {
4468 let line = work::link(&self.qt_thread(), &self.shared(), &from.to_string(), &to.to_string());
4469 self.as_mut().set_status(QString::from(&line));
4470 QString::from(&line)
4471 }
4472
4473 fn unlink_things(mut self: Pin<&mut Self>, from: &QString, to: &QString) -> QString {
4474 let line = work::unlink(&self.qt_thread(), &self.shared(), &from.to_string(), &to.to_string());
4475 self.as_mut().set_status(QString::from(&line));
4476 QString::from(&line)
4477 }
4478
4479 fn search_things(&self, query: &QString, kind: &QString, from: &QString) -> QString {
4480 QString::from(&work::search_things(&self.shared(), &query.to_string(), &kind.to_string(), &from.to_string()))
4481 }
4482
4483 fn uri_of(&self, kind: &QString, id: &QString) -> QString {
4484 QString::from(&work::uri_of(&kind.to_string(), &id.to_string()))
4485 }
4486
4487 fn is_program(&self, name: &QString) -> bool {
4488 crate::attachments::kept_closed(&name.to_string(), crate::attachments::phone())
4489 }
4490
4491 fn first_frame(&self) {
4492 crate::timing("the first frame drawn");
4493 }
4494
4495 fn mark(&self, what: &QString) {
4496 crate::timing(&what.to_string());
4497 }
4498
4499 fn phone_accounts(&self) -> bool {
4500 cfg!(target_os = "android")
4501 }
4502
4503 fn choose_phone_account(self: Pin<&mut Self>) {
4504 #[cfg(target_os = "android")]
4505 {
4506 if let Ok(mut waiting) = PHONE_CHOOSER.lock() {
4507 *waiting = Some(self.qt_thread());
4508 }
4509 unsafe { sioul_android_choose_account() };
4512 }
4513 #[cfg(not(target_os = "android"))]
4514 let _ = self;
4515 }
4516
4517 fn budget_detail(&self, id: &QString, anchor: &QString, step: &QString) -> QString {
4518 let world = World::load();
4519 let today = Zoned::now().date();
4520 let Some(ledger) = world.ledger() else { return QString::default() };
4521 let Some(found) = ledger.budgets.iter().find(|b| b.id == id.to_string()) else { return QString::default() };
4522 let anchor = anchor.to_string().parse().unwrap_or(today);
4523 QString::from(&json(&view::budget_detail(&ledger, found, anchor, &step.to_string(), tr(), today)))
4524 }
4525
4526 fn keep_folder(self: Pin<&mut Self>, account: &QString, folder: &QString, kept: bool) -> QString {
4527 let (id, folder) = (account.to_string(), folder.to_string());
4528 let path = config_path();
4531 let changed = sioul_core::filelock::with_lock(&path, || {
4532 let config = config::Config::load(&path)?;
4533 let Some(account) = config.account(&id).cloned() else { return Ok(None) };
4534 let mut skipped = account.skip_folders.clone();
4535 skipped.retain(|f| f != &folder);
4536 if !kept {
4537 skipped.push(folder.clone());
4538 }
4539 config::set_value(&path, &format!("account.{id}.skip_folders"), &config::SettingValue::Texts(skipped)).map(|()| Some(account))
4540 });
4541 let account = match changed {
4542 Ok(Some(account)) => account,
4543 Ok(None) => return QString::default(),
4544 Err(e) => return QString::from(&e),
4545 };
4546 let (qt, shared) = (self.qt_thread(), self.shared());
4547 std::thread::spawn(move || {
4548 if !kept && let Err(e) = sioul_sync::mailbox::forget_folder(&account, &folder) {
4549 set_status(&qt, e.sentence(tr(), &account.id));
4550 }
4551 if let Some(control) = shared.watchers.lock().ok().and_then(|w| w.get(&account.id).cloned()) {
4552 control.nudge();
4553 }
4554 mail::show_mail(&qt, &shared);
4555 });
4556 QString::default()
4557 }
4558
4559 fn create_folder(self: Pin<&mut Self>, account: &QString, name: &QString) {
4560 let (id, name) = (account.to_string(), name.to_string());
4561 let (qt, shared) = (self.qt_thread(), self.shared());
4562 std::thread::spawn(move || {
4563 let Some(account) = load_config().account(&id).cloned() else { return };
4564 let made = sioul_sync::secret::password(&account).and_then(|p| sioul_sync::mailbox::create_folder(&account, &p, &name));
4565 match made {
4566 Ok(()) => tell(&qt, &shared, say("folder-made", &[("name", name.clone())])),
4567 Err(e) => tell(&qt, &shared, e.sentence(tr(), &account.id)),
4568 }
4569 mail::show_mail(&qt, &shared);
4570 });
4571 }
4572
4573 fn delete_folder(self: Pin<&mut Self>, account: &QString, folder: &QString) {
4574 let (id, folder) = (account.to_string(), folder.to_string());
4575 let (qt, shared) = (self.qt_thread(), self.shared());
4576 std::thread::spawn(move || {
4577 let Some(account) = load_config().account(&id).cloned() else { return };
4578 let gone = sioul_sync::secret::password(&account).and_then(|p| sioul_sync::mailbox::delete_folder(&account, &p, &folder));
4579 match gone {
4580 Ok(()) => tell(&qt, &shared, say("folder-deleted", &[("name", folder.clone())])),
4581 Err(sioul_sync::SyncError::Message(m)) if m == sioul_sync::mailbox::NOT_EMPTY => tell(&qt, &shared, say("folder-not-empty", &[("name", folder.clone())])),
4582 Err(e) => tell(&qt, &shared, e.sentence(tr(), &account.id)),
4583 }
4584 mail::show_mail(&qt, &shared);
4585 });
4586 }
4587
4588 fn trash_note(self: Pin<&mut Self>, path: &QString) -> QString {
4589 QString::from(&work::trash_note(&self.qt_thread(), &self.shared(), &path.to_string()))
4590 }
4591
4592 fn rename_note(self: Pin<&mut Self>, path: &QString, name: &QString) -> QString {
4593 QString::from(&work::rename_note(&self.qt_thread(), &self.shared(), &path.to_string(), &name.to_string()))
4594 }
4595
4596 fn create_note_in(self: Pin<&mut Self>, folder: &QString, title: &QString) -> QString {
4597 QString::from(&work::create_note_in(&self.qt_thread(), &self.shared(), &folder.to_string(), &title.to_string()))
4598 }
4599
4600 fn make_folder(self: Pin<&mut Self>, parent: &QString, name: &QString) -> QString {
4601 QString::from(&work::make_folder(&self.qt_thread(), &self.shared(), &parent.to_string(), &name.to_string()))
4602 }
4603
4604 fn rename_folder(self: Pin<&mut Self>, path: &QString, name: &QString) -> QString {
4605 QString::from(&work::rename_folder(&self.qt_thread(), &self.shared(), &path.to_string(), &name.to_string()))
4606 }
4607
4608 fn remove_folder(self: Pin<&mut Self>, path: &QString) -> QString {
4609 QString::from(&work::remove_folder(&self.qt_thread(), &self.shared(), &path.to_string()))
4610 }
4611
4612 fn remove_project(self: Pin<&mut Self>, id: &QString) -> QString {
4613 QString::from(&crate::projects::remove(&self.qt_thread(), &self.shared(), &id.to_string()))
4614 }
4615
4616 fn save_budget(self: Pin<&mut Self>, id: &QString, edit: &QString) -> QString {
4617 QString::from(&crate::projects::save_budget(&self.qt_thread(), &self.shared(), &id.to_string(), &edit.to_string()))
4618 }
4619
4620 fn letters(&self) -> QString {
4621 QString::from(&crate::letters::view())
4622 }
4623
4624 fn letter_task(self: Pin<&mut Self>, id: &QString) -> QString {
4625 QString::from(&crate::letters::make_task(&self.qt_thread(), &self.shared(), &id.to_string()))
4626 }
4627
4628 fn letter_event(self: Pin<&mut Self>, id: &QString) -> QString {
4629 QString::from(&crate::letters::make_event(&self.qt_thread(), &self.shared(), &id.to_string()))
4630 }
4631
4632 fn letter_done(self: Pin<&mut Self>, id: &QString, project: &QString) -> QString {
4633 QString::from(&crate::letters::done(&id.to_string(), &project.to_string()))
4634 }
4635
4636 fn letters_folder(&self) -> QString {
4637 QString::from(&crate::letters::folder())
4638 }
4639
4640 fn letters_add(self: Pin<&mut Self>, files: &QString) -> QString {
4641 QString::from(&crate::letters::add(&files.to_string()))
4642 }
4643
4644 fn bank(&self) -> QString {
4645 QString::from(&crate::bank::view())
4646 }
4647
4648 fn import_bank(self: Pin<&mut Self>, file: &QString) -> QString {
4649 let (ok, said) = crate::bank::import(&file.to_string());
4650 show(&self.qt_thread(), &self.shared());
4652 QString::from(&serde_json::json!({ "ok": ok, "said": said }).to_string())
4653 }
4654
4655 fn import_bank_into(self: Pin<&mut Self>, file: &QString, account: &QString) -> QString {
4656 let (ok, said) = crate::bank::import_into(&file.to_string(), &account.to_string());
4657 show(&self.qt_thread(), &self.shared());
4658 QString::from(&serde_json::json!({ "ok": ok, "said": said }).to_string())
4659 }
4660
4661 fn save_bank_account(self: Pin<&mut Self>, id: &QString, edit: &QString) -> QString {
4662 let problem = crate::bank::save_account(&id.to_string(), &edit.to_string());
4663 show(&self.qt_thread(), &self.shared());
4664 QString::from(&problem)
4665 }
4666
4667 fn remove_bank_account(self: Pin<&mut Self>, id: &QString) -> QString {
4668 let problem = crate::bank::remove_account(&id.to_string());
4669 show(&self.qt_thread(), &self.shared());
4670 QString::from(&problem)
4671 }
4672
4673 fn place_movement(self: Pin<&mut Self>, account: &QString, movement: &QString, choice: &QString) -> QString {
4674 let problem = crate::bank::place_movement(&account.to_string(), &movement.to_string(), &choice.to_string());
4675 show(&self.qt_thread(), &self.shared());
4676 QString::from(&problem)
4677 }
4678
4679 fn save_bank_rule(self: Pin<&mut Self>, rule: &QString, edit: &QString) -> QString {
4680 let problem = crate::bank::save_rule(&rule.to_string(), &edit.to_string());
4681 show(&self.qt_thread(), &self.shared());
4682 QString::from(&problem)
4683 }
4684
4685 fn remove_bank_rule(self: Pin<&mut Self>, rule: &QString) -> QString {
4686 let problem = crate::bank::remove_rule(&rule.to_string());
4687 show(&self.qt_thread(), &self.shared());
4688 QString::from(&problem)
4689 }
4690
4691 fn save_reserve(self: Pin<&mut Self>, id: &QString, edit: &QString) -> QString {
4692 let problem = crate::bank::save_reserve(&id.to_string(), &edit.to_string());
4693 show(&self.qt_thread(), &self.shared());
4694 QString::from(&problem)
4695 }
4696
4697 fn contracts(&self) -> QString {
4698 QString::from(&crate::contracts::view())
4699 }
4700
4701 fn save_contract(self: Pin<&mut Self>, id: &QString, edit: &QString) -> QString {
4702 QString::from(&crate::contracts::save(&id.to_string(), &edit.to_string()))
4703 }
4704
4705 fn remove_contract(self: Pin<&mut Self>, id: &QString) -> QString {
4706 QString::from(&crate::contracts::remove(&id.to_string()))
4707 }
4708
4709 fn contract_letter(self: Pin<&mut Self>, id: &QString) -> QString {
4710 QString::from(&crate::contracts::letter(&id.to_string()))
4711 }
4712
4713 fn contract_from(&self, subject: &QString, from: &QString) -> QString {
4714 QString::from(&crate::contracts::from_card(&subject.to_string(), &from.to_string()))
4715 }
4716
4717 fn routines(&self) -> QString {
4718 QString::from(&work::routines(&self.shared()))
4719 }
4720
4721 fn save_routine(self: Pin<&mut Self>, id: &QString, title: &QString, steps: &QString, by_itself: bool) -> QString {
4722 QString::from(&work::save_routine(&id.to_string(), &title.to_string(), &steps.to_string(), by_itself))
4723 }
4724
4725 fn remove_routine(self: Pin<&mut Self>, id: &QString) -> QString {
4726 QString::from(&work::remove_routine(&id.to_string()))
4727 }
4728
4729 fn share_status(&self, folder: &QString) -> QString {
4730 QString::from(&crate::share::status(&folder.to_string()))
4731 }
4732
4733 fn start_sharing(self: Pin<&mut Self>, folder: &QString, passphrase: &QString, again: &QString) -> QString {
4734 let problem = crate::share::start(&folder.to_string(), &passphrase.to_string(), &again.to_string());
4735 if problem.is_empty() {
4736 crate::share::exchange(&self.qt_thread(), &self.shared());
4737 }
4738 QString::from(&problem)
4739 }
4740
4741 fn stop_sharing(self: Pin<&mut Self>) -> QString {
4742 QString::from(&crate::share::stop())
4743 }
4744
4745 fn share_candidates(&self) -> QString {
4746 QString::from(&json(&crate::share::candidates()))
4747 }
4748
4749 fn set_share_projects(self: Pin<&mut Self>, on: bool) -> QString {
4750 let problem = crate::share::set_projects(on);
4751 if problem.is_empty() {
4752 crate::share::exchange(&self.qt_thread(), &self.shared());
4753 }
4754 QString::from(&problem)
4755 }
4756
4757 fn folders_in(&self, path: &QString) -> QString {
4758 QString::from(&crate::share::folders_in(&path.to_string()))
4759 }
4760
4761 fn files_access(&self) -> bool {
4762 #[cfg(target_os = "android")]
4763 {
4764 unsafe { sioul_android_files_access() }
4766 }
4767 #[cfg(not(target_os = "android"))]
4768 true
4769 }
4770
4771 fn ask_files_access(self: Pin<&mut Self>) {
4772 #[cfg(target_os = "android")]
4774 unsafe {
4775 sioul_android_ask_files_access()
4776 };
4777 let _ = self;
4778 }
4779
4780 fn share_now(self: Pin<&mut Self>) {
4781 crate::share::exchange(&self.qt_thread(), &self.shared());
4782 }
4783
4784 fn export_time_csv(mut self: Pin<&mut Self>, project: &QString, from: &QString, to: &QString, target: &QString) -> QString {
4785 match crate::projects::export_time_csv(&self.shared(), &project.to_string(), &from.to_string(), &to.to_string(), &target.to_string()) {
4786 Ok(path) => {
4787 self.as_mut().set_status(QString::from(&say("time-exported", &[("path", path)])));
4788 QString::default()
4789 }
4790 Err(e) => QString::from(&e),
4791 }
4792 }
4793
4794 fn change_line(self: Pin<&mut Self>, uri: &QString, shown: &QString, label: &QString, amount: f64, date: &QString) -> QString {
4795 QString::from(&crate::projects::change_line(&self.qt_thread(), &self.shared(), &uri.to_string(), &shown.to_string(), &label.to_string(), amount, &date.to_string()))
4796 }
4797
4798 fn remove_budget(self: Pin<&mut Self>, what: &QString) -> QString {
4799 QString::from(&crate::projects::remove_budget(&self.qt_thread(), &self.shared(), &what.to_string()))
4800 }
4801
4802 fn remove_line(self: Pin<&mut Self>, uri: &QString, shown: &QString) -> QString {
4803 QString::from(&crate::projects::remove_line(&self.qt_thread(), &self.shared(), &uri.to_string(), &shown.to_string()))
4804 }
4805
4806 fn noise_url(&self, kind: &QString) -> QString {
4807 match sioul_core::sounds::noise_file(&config::cache_dir().join("sounds"), &kind.to_string()) {
4808 Ok(path) => QString::from(&file_url(&path)),
4809 Err(_) => QString::default(),
4810 }
4811 }
4812
4813 fn calm_sounds(&self) -> QString {
4814 QString::from(&work::calm_sounds(&self.shared()))
4815 }
4816
4817 fn move_task(self: Pin<&mut Self>, uid: &QString, list: &QString, confirmed: bool) -> QString {
4818 QString::from(&work::move_task(&self.qt_thread(), &self.shared(), &uid.to_string(), &list.to_string(), confirmed))
4819 }
4820
4821 fn move_contact(self: Pin<&mut Self>, key: &QString, book: &QString, confirmed: bool) -> QString {
4822 QString::from(&pim::move_contact(&self.qt_thread(), &self.shared(), &key.to_string(), &book.to_string(), confirmed))
4823 }
4824
4825 fn address_books(&self) -> QString {
4826 QString::from(&pim::books())
4827 }
4828
4829 fn find_places(self: Pin<&mut Self>, name: &QString) {
4830 let (qt, name) = (self.qt_thread(), name.to_string());
4831 let language = tr().text("qt-locale", None).chars().take(2).collect::<String>();
4832 std::thread::spawn(move || {
4833 let found = sioul_sync::weather::places(&name, &language).map(|p| json(&p)).unwrap_or_else(|e| serde_json::json!({ "error": e.sentence(tr(), "Open-Meteo") }).to_string());
4834 let _ = qt.queue(move |mut sioul| sioul.as_mut().set_places_found(QString::from(&found)));
4835 });
4836 }
4837
4838 fn set_weather_place(mut self: Pin<&mut Self>, name: &QString, latitude: f64, longitude: f64) {
4839 let name = name.to_string();
4840 let path = config_path();
4841 let written = if name.trim().is_empty() {
4842 ["weather.place", "weather.latitude", "weather.longitude"].iter().try_for_each(|k| config::set_value(&path, k, &config::SettingValue::Text(String::new())))
4843 } else {
4844 config::set_value(&path, "weather.place", &config::SettingValue::Text(name))
4845 .and_then(|()| config::set_value(&path, "weather.latitude", &config::SettingValue::Float(latitude)))
4846 .and_then(|()| config::set_value(&path, "weather.longitude", &config::SettingValue::Float(longitude)))
4847 };
4848 if let Err(e) = written {
4849 self.as_mut().set_status(QString::from(&e));
4850 }
4851 let _ = std::fs::remove_file(weather_cache());
4852 self.as_mut().set_forecast(QString::default());
4853 update_weather(&self.qt_thread(), &self.shared());
4854 crate::homecard::weather_seen();
4856 }
4857
4858 fn health_page(&self) -> QString {
4859 QString::from(&crate::health::page())
4860 }
4861
4862 fn save_medicine(self: Pin<&mut Self>, id: &QString, edit: &QString) -> QString {
4863 QString::from(&crate::health::save_medicine(&id.to_string(), &edit.to_string()))
4864 }
4865
4866 fn save_prescription(self: Pin<&mut Self>, id: &QString, edit: &QString) -> QString {
4867 QString::from(&crate::health::save_prescription(&id.to_string(), &edit.to_string()))
4868 }
4869
4870 fn health_for_professional(&self, prescription: &QString) -> QString {
4871 QString::from(&crate::health::for_professional(&prescription.to_string()))
4872 }
4873
4874 fn remove_health(self: Pin<&mut Self>, id: &QString) -> QString {
4875 QString::from(&crate::health::remove(&id.to_string()))
4876 }
4877
4878 fn refilled(self: Pin<&mut Self>, id: &QString) -> QString {
4879 QString::from(&crate::health::refilled(&id.to_string()))
4880 }
4881
4882 fn set_dose_taken(self: Pin<&mut Self>, key: &QString, taken: bool) {
4883 let problem = crate::health::set_taken(&key.to_string(), taken);
4884 if !problem.is_empty() {
4885 tell(&self.qt_thread(), &self.shared(), problem);
4886 }
4887 crate::share::exchange(&self.qt_thread(), &self.shared());
4889 }
4890
4891 fn dose_not_taken(self: Pin<&mut Self>, key: &QString) {
4892 let problem = crate::health::not_taken(&key.to_string());
4893 if !problem.is_empty() {
4894 tell(&self.qt_thread(), &self.shared(), problem);
4895 }
4896 crate::share::exchange(&self.qt_thread(), &self.shared());
4897 }
4898
4899 fn dose_taken_at(self: Pin<&mut Self>, key: &QString, time: &QString) -> QString {
4900 let problem = crate::health::taken_late(&key.to_string(), &time.to_string());
4901 if problem.is_empty() {
4902 crate::share::exchange(&self.qt_thread(), &self.shared());
4903 }
4904 QString::from(&problem)
4905 }
4906
4907 fn dose_info(&self, key: &QString) -> QString {
4908 QString::from(&crate::health::dose_info(&key.to_string()))
4909 }
4910
4911 fn dose_choose(self: Pin<&mut Self>, key: &QString, taken: bool) -> QString {
4912 let problem = crate::health::choose(&key.to_string(), taken);
4913 crate::share::exchange(&self.qt_thread(), &self.shared());
4915 crate::health::show_health(&self.qt_thread(), &self.shared());
4916 QString::from(&problem)
4917 }
4918
4919 fn device_off(self: Pin<&mut Self>, id: &QString, off: bool) -> QString {
4920 let problem = crate::health::device_off(&id.to_string(), off);
4921 crate::health::show_health(&self.qt_thread(), &self.shared());
4922 QString::from(&problem)
4923 }
4924
4925 fn device_forget(self: Pin<&mut Self>, id: &QString) -> QString {
4926 QString::from(&crate::health::device_forget(&id.to_string()))
4927 }
4928
4929 fn needs(&self) -> QString {
4930 QString::from(&crate::health::needs_page())
4931 }
4932
4933 fn save_needs(self: Pin<&mut Self>, edit: &QString, shown: &QString) -> QString {
4934 let problem = crate::health::save_needs(&edit.to_string(), &shown.to_string());
4935 crate::work::show_work(&self.qt_thread(), &self.shared());
4937 QString::from(&problem)
4938 }
4939
4940 fn skip_need(self: Pin<&mut Self>, key: &QString, skip: bool) -> QString {
4941 let problem = crate::health::skip_today(&key.to_string(), skip);
4942 crate::work::show_work(&self.qt_thread(), &self.shared());
4943 QString::from(&problem)
4944 }
4945
4946 fn needs_today(&self) -> QString {
4947 QString::from(&crate::health::needs_today())
4948 }
4949
4950 fn refresh_health(self: Pin<&mut Self>) {
4951 crate::health::show_health(&self.qt_thread(), &self.shared());
4952 }
4953
4954 fn show_health_week(self: Pin<&mut Self>, day: &QString) {
4955 crate::health::show_week(&day.to_string());
4956 crate::health::show_health(&self.qt_thread(), &self.shared());
4957 }
4958
4959 fn drag_need(self: Pin<&mut Self>, edit: &QString) -> QString {
4960 let (qt, shared) = (self.qt_thread(), self.shared());
4961 let problem = crate::health::drag_need(&qt, &shared, &edit.to_string());
4962 work::show_work(&qt, &shared);
4964 crate::health::show_health(&qt, &shared);
4965 QString::from(&problem)
4966 }
4967
4968 fn change_need(self: Pin<&mut Self>, edit: &QString) -> QString {
4969 let problem = crate::health::change_need(&edit.to_string());
4970 crate::work::show_work(&self.qt_thread(), &self.shared());
4972 crate::health::show_health(&self.qt_thread(), &self.shared());
4973 QString::from(&problem)
4974 }
4975
4976 fn health_settings(&self) -> QString {
4977 QString::from(&crate::health::settings_view())
4978 }
4979
4980 fn wake_settings(&self, which: &QString) {
4981 crate::wake::open_settings(&which.to_string());
4982 }
4983
4984 fn try_wake(&self) -> QString {
4985 QString::from(&crate::wake::try_now())
4986 }
4987
4988 fn set_overlap_aside(self: Pin<&mut Self>, key: &QString) -> QString {
4989 let problem = crate::pim::set_overlap_aside(&key.to_string());
4990 pim::show_pim(&self.qt_thread(), &self.shared());
4991 QString::from(&problem)
4992 }
4993
4994 fn move_need(self: Pin<&mut Self>, key: &QString, minutes: i32, time: &QString) -> QString {
4995 let problem = crate::health::move_today(&key.to_string(), i64::from(minutes), &time.to_string());
4996 crate::work::show_work(&self.qt_thread(), &self.shared());
4998 QString::from(&problem)
4999 }
5000
5001 fn needs_later(&self) -> i32 {
5002 i32::try_from(crate::health::later_minutes()).unwrap_or(15)
5003 }
5004
5005 fn missed_doses(&self) -> QString {
5006 QString::from(&crate::health::missed())
5007 }
5008
5009 fn going_away(mut self: Pin<&mut Self>) {
5012 self.as_mut().set_away(true);
5013 AWAY.store(true, Ordering::SeqCst);
5014 std::thread::spawn(crate::alarms::schedule);
5015 std::thread::spawn(crate::eventalarms::schedule);
5017 crate::homecard::now();
5019 crate::share::set_shown(false);
5020 let (qt, shared) = (self.qt_thread(), self.shared());
5021 std::thread::spawn(move || {
5022 crate::mail::quiet_watchers(&shared);
5023 crate::mail::finish_all(&qt, &shared);
5024 crate::share::closing();
5025 crate::share::nudge(&qt, &shared, 0, false);
5027 });
5028 }
5029
5030 fn back_here(mut self: Pin<&mut Self>) {
5031 self.as_mut().set_away(false);
5032 AWAY.store(false, Ordering::SeqCst);
5033 if let Some(key) = crate::alarms::opened() {
5035 self.as_mut().reminder_opened(QString::from("dose"), QString::default(), QString::from(&key));
5036 }
5037 if let Some((kind, key)) = crate::eventalarms::opened() {
5039 self.as_mut().reminder_opened(QString::from(&kind), QString::default(), QString::from(&key));
5040 }
5041 if let Some((kind, uri, key)) = crate::homecard::opened() {
5044 self.as_mut().reminder_opened(QString::from(kind), QString::from(&uri), QString::from(&key));
5045 }
5046 let (qt, shared) = (self.qt_thread(), self.shared());
5048 if shared.views_job.deferred.swap(false, Ordering::SeqCst) | shared.mail_job.deferred.swap(false, Ordering::SeqCst) {
5049 show(&qt, &shared);
5050 }
5051 if shared.pim_job.deferred.swap(false, Ordering::SeqCst) {
5052 pim::show_pim(&qt, &shared);
5053 }
5054 if shared.work_job.deferred.swap(false, Ordering::SeqCst) {
5055 work::show_work(&qt, &shared);
5056 }
5057 if shared.health_job.deferred.swap(false, Ordering::SeqCst) {
5058 crate::health::show_health(&qt, &shared);
5059 }
5060 crate::share::set_shown(true);
5061 crate::outside::take(self.as_mut());
5063 std::thread::spawn(|| crate::outside::publish(true));
5064 crate::pauses::tick(&self.qt_thread(), &self.shared());
5066 let now = jiff::Timestamp::now().as_second();
5067 self.shared().active.store(now, std::sync::atomic::Ordering::Relaxed);
5068 let (qt, shared) = (self.qt_thread(), self.shared());
5069 std::thread::spawn(move || {
5070 crate::devices::window_opened();
5072 let _ = crate::share::keeper("health", sioul_sync::lease::Rule::FollowsYou, now, false);
5073 crate::share::nudge(&qt, &shared, 0, true);
5076 crate::mail::wake_watchers(&shared);
5077 });
5078 }
5079
5080 fn touch(self: Pin<&mut Self>) {
5081 self.shared().active.store(jiff::Timestamp::now().as_second(), std::sync::atomic::Ordering::Relaxed);
5082 }
5083
5084 fn set_health(self: Pin<&mut Self>, key: &QString, value: &QString) -> QString {
5085 QString::from(&crate::health::set_setting(&key.to_string(), &value.to_string()))
5086 }
5087
5088 fn movement_minutes(&self) -> i32 {
5089 crate::health::movement_minutes()
5090 }
5091
5092 fn chat_minute(self: Pin<&mut Self>) -> bool {
5093 crate::health::chat_minute()
5094 }
5095
5096 fn chats_covered(&self) -> bool {
5097 crate::health::chats_covered()
5098 }
5099
5100 fn memo_url(&self) -> QString {
5101 QString::from(&work::memo_url())
5102 }
5103
5104 fn memo_recorded(self: Pin<&mut Self>, url: &QString) -> QString {
5105 let (qt, shared) = (self.qt_thread(), self.shared());
5106 work::show_work(&qt, &shared);
5107 QString::from(&work::note_path_of(&url.to_string()))
5108 }
5109
5110 fn set_notes_tree(self: Pin<&mut Self>, tree: bool) {
5111 work::set_notes_tree(&self.qt_thread(), &self.shared(), tree);
5112 }
5113
5114 fn view_flag(&self, name: &QString) -> bool {
5115 work::view_flag(&self.shared(), &name.to_string())
5116 }
5117
5118 fn set_view_flag(self: Pin<&mut Self>, name: &QString, value: bool) {
5119 work::set_view_flag(&self.shared(), &name.to_string(), value);
5120 }
5121
5122 fn add_movement(mut self: Pin<&mut Self>, edit: &QString) -> QString {
5123 #[derive(serde::Deserialize)]
5124 struct Movement {
5125 kind: String,
5127 budget: String,
5128 label: String,
5129 amount: f64,
5130 #[serde(default)]
5131 date: String,
5132 #[serde(default)]
5134 every: String,
5135 #[serde(default)]
5136 day: i8,
5137 #[serde(default)]
5138 month: Option<i8>,
5139 #[serde(default)]
5140 from: String,
5141 #[serde(default)]
5142 until: String,
5143 #[serde(default)]
5144 estimate: bool,
5145 }
5146 let world = World::load();
5147 let outcome = (|| {
5148 let movement: Movement = serde_json::from_str(&edit.to_string()).map_err(|e| e.to_string())?;
5149 let ledger = world.ledger().ok_or_else(|| tr().text("ui-no-budgets", None))?;
5150 let path = world.ledger_path().ok_or_else(|| tr().text("ui-no-budgets", None))?;
5151 if movement.amount == 0.0 || !movement.amount.is_finite() {
5152 return Err(tr().text("budget-add-bad-amount", None));
5153 }
5154 let today = Zoned::now().date();
5155 let origin = say("budget-origin-hand", &[("date", today.to_string())]);
5156 if movement.kind == "recurring" {
5157 let preset = budget::PresetEdit {
5158 budget: movement.budget.clone(),
5159 label: movement.label.clone(),
5160 amount: movement.amount,
5161 every: movement.every.clone(),
5162 day: movement.day,
5163 month: movement.month,
5164 from: movement.from.clone(),
5165 until: movement.until.clone(),
5166 estimate: movement.estimate,
5167 };
5168 budget::record_preset(&path, &preset, &origin)?;
5169 } else {
5170 let date: jiff::civil::Date = movement.date.parse().unwrap_or(today);
5171 let line = budget::Line {
5172 budget: movement.budget.clone(),
5173 date,
5174 amount: sioul_core::money::Money::from_units(movement.amount),
5175 label: movement.label.trim().to_string(),
5176 planned: date > today,
5177 reserve: None,
5178 links: Vec::new(),
5179 preset: None,
5180 };
5181 budget::record_line(&path, &line, &origin)?;
5182 }
5183 Ok::<String, String>(say("budget-added", &[("budget", ledger.budget_title(&movement.budget).to_string())]))
5184 })();
5185 let (line, problem) = match outcome {
5186 Ok(line) => (line, String::new()),
5187 Err(e) => (e.clone(), e),
5188 };
5189 self.as_mut().set_status(QString::from(&line));
5190 show(&self.qt_thread(), &self.shared());
5191 QString::from(&problem)
5192 }
5193
5194 fn site_list(&self) -> QString {
5195 QString::from(&crate::sites::list())
5196 }
5197
5198 fn site_notified(self: Pin<&mut Self>, id: &QString, title: &QString, text: &QString) {
5199 crate::sites::notified(&self.qt_thread(), &self.shared(), &id.to_string(), &title.to_string(), &text.to_string());
5200 }
5201
5202 fn site_seen(self: Pin<&mut Self>, id: &QString) {
5203 crate::sites::seen(&self.qt_thread(), &id.to_string());
5204 }
5205
5206 fn site_notices(&self) -> QString {
5207 QString::from(&crate::sites::waiting())
5208 }
5209
5210 fn site_for_sender(&self, sender: &QString) -> QString {
5211 QString::from(&crate::sites::announced_by(&sender.to_string()))
5212 }
5213
5214 fn set_site(mut self: Pin<&mut Self>, id: &QString, field: &QString, value: &QString) -> QString {
5215 let (field, value) = (field.to_string(), value.to_string());
5216 let setting = match field.as_str() {
5217 "realtime" | "muted" | "background" | "microphone" | "camera" | "screen" => config::SettingValue::Bool(value == "true"),
5218 "site" | "area" | "name" => config::SettingValue::Text(value.trim().to_string()),
5219 "categories" => config::SettingValue::Texts(serde_json::from_str::<Vec<String>>(&value).unwrap_or_default().into_iter().map(|c| c.trim().to_string()).filter(|c| !c.is_empty()).collect()),
5221 "url" if value.trim().starts_with("https://") => config::SettingValue::Text(value.trim().to_string()),
5223 "url" => return QString::from(&tr().text("site-https-only", None)),
5224 _ => return QString::default(),
5225 };
5226 let result = config::set_value(&config_path(), &format!("site.{}.{field}", id), &setting);
5227 self.as_mut().sites_changed();
5228 QString::from(&result.err().unwrap_or_default())
5229 }
5230
5231 fn site_presets(&self, country: &QString, region: &QString) -> QString {
5232 QString::from(&crate::sites::presets(&country.to_string(), ®ion.to_string()))
5233 }
5234
5235 fn add_site(mut self: Pin<&mut Self>, edit: &QString) -> QString {
5236 let answer = crate::sites::add(&edit.to_string());
5237 self.as_mut().sites_changed();
5238 QString::from(&answer)
5239 }
5240
5241 fn site_presets_tree(&self) -> QString {
5242 QString::from(&crate::sites::presets_tree())
5243 }
5244
5245 fn add_sites(mut self: Pin<&mut Self>, rows: &QString) -> QString {
5246 let answer = crate::sites::add_many(&rows.to_string());
5247 self.as_mut().sites_changed();
5248 QString::from(&answer)
5249 }
5250
5251 fn move_site(mut self: Pin<&mut Self>, id: &QString, delta: i32) -> QString {
5252 let said = crate::sites::move_site(&id.to_string(), i64::from(delta));
5253 self.as_mut().sites_changed();
5254 QString::from(&said)
5255 }
5256
5257 fn remove_site(mut self: Pin<&mut Self>, id: &QString) -> QString {
5258 let problem = crate::sites::remove(&id.to_string());
5259 self.as_mut().sites_changed();
5260 QString::from(&problem)
5261 }
5262
5263 fn google_built_in(&self) -> bool {
5264 sioul_sync::google::built_in().is_some()
5265 }
5266
5267 fn call_devices(&self) -> QString {
5268 QString::from(&serde_json::to_string(&load_config().calls).unwrap_or_default())
5269 }
5270
5271 fn calls_script(&self) -> QString {
5272 QString::from(include_str!("calls.js"))
5273 }
5274
5275 fn set_call_device(mut self: Pin<&mut Self>, which: &QString, name: &QString) -> QString {
5276 let which = which.to_string();
5277 if !matches!(which.as_str(), "camera" | "microphone" | "speaker") {
5278 return QString::default();
5279 }
5280 let name = name.to_string().trim().to_string();
5281 let result = config::set_value(&config_path(), &format!("calls.{which}"), &config::SettingValue::Text(name));
5282 self.as_mut().sites_changed();
5283 QString::from(&result.err().unwrap_or_default())
5284 }
5285
5286 fn bitwarden_state(&self) -> QString {
5287 QString::from(&crate::sites::bitwarden_state(&self.shared()))
5288 }
5289
5290 fn bitwarden_unlock(self: Pin<&mut Self>, password: &QString, provider: i32, code: &QString) -> i32 {
5291 let (shared, password, code) = (self.shared(), password.to_string(), code.to_string());
5292 crate::sites::bitwarden_later(&self.qt_thread(), move || crate::sites::bitwarden_unlock(&shared, &password, provider, &code))
5293 }
5294
5295 fn bitwarden_lock(self: Pin<&mut Self>) {
5296 crate::sites::bitwarden_lock(&self.shared());
5297 }
5298
5299 fn bitwarden_send_code(self: Pin<&mut Self>, password: &QString) -> i32 {
5300 let password = password.to_string();
5301 crate::sites::bitwarden_later(&self.qt_thread(), move || crate::sites::bitwarden_send_code(&password))
5302 }
5303
5304 fn bitwarden_passkey_begin(self: Pin<&mut Self>) -> i32 {
5305 crate::sites::bitwarden_later(&self.qt_thread(), crate::sites::bitwarden_passkey_begin)
5306 }
5307
5308 fn bitwarden_passkey(self: Pin<&mut Self>, answer: &QString) -> i32 {
5309 let (shared, answer) = (self.shared(), answer.to_string());
5310 crate::sites::bitwarden_later(&self.qt_thread(), move || crate::sites::bitwarden_passkey(&shared, &answer))
5311 }
5312
5313 fn bitwarden_search(self: Pin<&mut Self>, url: &QString, site: &QString, user: &QString) -> QString {
5314 QString::from(&crate::sites::bitwarden_search(&self.qt_thread(), &self.shared(), &url.to_string(), &site.to_string(), &user.to_string()))
5315 }
5316
5317 fn bitwarden_site(&self, url: &QString) -> QString {
5318 QString::from(&crate::sites::bitwarden_site(&self.shared(), &url.to_string()))
5319 }
5320
5321 fn bitwarden_login(&self, url: &QString, id: &QString) -> QString {
5322 QString::from(&crate::sites::bitwarden_login(&self.shared(), &url.to_string(), &id.to_string()))
5323 }
5324
5325 fn map_view(&self) -> QString {
5326 QString::from(&crate::map::view(&self.shared()))
5327 }
5328
5329 fn place_of(&self, address: &QString) -> QString {
5330 QString::from(&crate::map::place_of(&address.to_string()))
5331 }
5332
5333 fn locate_addresses(mut self: Pin<&mut Self>, allow: bool) {
5334 if allow && !load_config().map.geocode {
5335 if let Err(e) = config::set_value(&config_path(), "map.geocode", &config::SettingValue::Bool(true)) {
5336 self.as_mut().set_status(QString::from(&e));
5337 return;
5338 }
5339 }
5340 crate::map::locate(&self.qt_thread(), &self.shared());
5341 }
5342
5343 fn refresh_mode(mut self: Pin<&mut Self>) {
5344 let (qt, shared_tick) = (self.qt_thread(), self.shared());
5346 std::thread::spawn(move || crate::health::tick(&qt, &shared_tick));
5347 let (qt_time, shared_time) = (self.qt_thread(), self.shared());
5350 std::thread::spawn(move || crate::timenote::follow(&qt_time, &shared_time));
5351 let qt_spam = self.qt_thread();
5353 std::thread::spawn(move || crate::spam::by_itself(qt_spam));
5354 crate::pauses::tick(&self.qt_thread(), &self.shared());
5356 if *self.as_ref().away() {
5360 if Zoned::now().minute() % 5 == 0 {
5361 crate::share::exchange_tick(&self.qt_thread(), &self.shared());
5362 }
5363 return;
5364 }
5365 crate::homecard::tick();
5367 crate::outside::take(self.as_mut());
5369 let now = Zoned::now();
5371 let (stamp, day) = (now.timestamp().as_second(), i64::from(now.date().year()) * 1000 + i64::from(now.date().day_of_year()));
5372 let shared = self.shared();
5373 let last = shared.planned_at.load(Ordering::Relaxed);
5374 if last == 0 {
5375 shared.planned_at.store(stamp, Ordering::Relaxed);
5376 shared.planned_day.store(day, Ordering::Relaxed);
5377 } else if stamp - last >= REPLAN_EVERY || shared.planned_day.load(Ordering::Relaxed) != day {
5378 if shared.planned_day.load(Ordering::Relaxed) != day {
5380 pim::show_pim(&self.qt_thread(), &shared);
5381 }
5382 shared.planned_at.store(stamp, Ordering::Relaxed);
5383 shared.planned_day.store(day, Ordering::Relaxed);
5384 work::show_work(&self.qt_thread(), &shared);
5385 }
5386 crate::share::exchange_tick(&self.qt_thread(), &self.shared());
5389 crate::share::nudge_tick(&self.qt_thread(), &self.shared());
5390 let shared_invoices = self.shared();
5393 std::thread::spawn(move || crate::projects::keep_invoices(&shared_invoices));
5394 crate::sites::gather_tick(&self.qt_thread(), &self.shared());
5396 crate::sites::favicon_tick(&self.qt_thread());
5398 crate::remind::tick(&self.qt_thread());
5400 crate::mailnote::tick_in_window(crate::mailnote::Window { qt: self.qt_thread(), active: self.shared().active.load(Ordering::Relaxed) });
5402 crate::reviews::tick(&self.qt_thread(), &self.shared());
5404 crate::letters::tick(&self.qt_thread(), &self.shared());
5406 update_weather(&self.qt_thread(), &self.shared());
5408 let line = mode_json();
5409 if self.as_ref().mode().to_string() != line {
5410 let was_quiet = self.as_ref().mode().to_string().contains("\"quiet\":true");
5411 self.as_mut().set_mode(QString::from(&line));
5412 if was_quiet != line.contains("\"quiet\":true") {
5414 let (qt, shared) = (self.qt_thread(), self.shared());
5415 show(&qt, &shared);
5416 work::show_work(&qt, &shared);
5417 }
5418 }
5419 }
5420
5421 fn work_a_while(self: Pin<&mut Self>, minutes: i32) {
5422 let until = Zoned::now().timestamp().as_second() + i64::from(minutes.max(1)) * 60;
5423 self.change_overrides(|o| {
5424 o.rest_until = None;
5425 o.work_until = Some(until);
5426 });
5427 }
5428
5429 pub(crate) fn done_for_the_day(mut self: Pin<&mut Self>) -> QString {
5430 let (qt, shared) = (self.qt_thread(), self.shared());
5431 let closed = work::closing(&shared);
5432 let path = sioul_core::quiet::Overrides::default_path();
5433 let previous = sioul_core::quiet::Overrides::load(&path);
5434 let mut next = previous.clone();
5435 next.work_until = None;
5436 next.rest_until = Some(closed.back.timestamp().as_second());
5437 next.rest_from = Some(Zoned::now().timestamp().as_second());
5438 next.first_step = Some(closed.first.text.clone()).filter(|s| !s.is_empty());
5439 next.first_step_task = Some(closed.first.uid.clone()).filter(|s| !s.is_empty());
5440 next.first_step_day = Some(closed.back.date());
5441 if let Err(e) = next.save(&path) {
5442 self.as_mut().set_status(QString::from(&e));
5443 return QString::default();
5444 }
5445 if sioul_core::timelog::running().is_some() {
5447 work::focus_stop(&qt, &shared, false, "");
5448 }
5449 let screen = json(&closed.closing);
5450 mail::rest(&qt, &shared, previous, next, serde_json::from_str::<serde_json::Value>(&screen).ok().and_then(|v| v["put_away"].as_str().map(str::to_string)).unwrap_or_default());
5451 self.as_mut().set_mode(QString::from(&mode_json()));
5452 show(&qt, &shared);
5453 work::show_work(&qt, &shared);
5454 QString::from(&screen)
5455 }
5456
5457 fn day_review(&self, kind: &QString) -> QString {
5458 QString::from(&crate::reviews::view(&kind.to_string()))
5459 }
5460
5461 fn close_review(self: Pin<&mut Self>, kind: &QString, date: &QString, felt: &QString, mix: &QString, memo: &QString) -> QString {
5462 crate::reviews::close(self, &kind.to_string(), &date.to_string(), &felt.to_string(), &mix.to_string(), &memo.to_string())
5463 }
5464
5465 fn day_review_line(&self, date: &QString) -> QString {
5466 QString::from(&crate::reviews::line(&date.to_string()))
5467 }
5468
5469 fn set_first_step(self: Pin<&mut Self>, text: &QString) {
5470 let text = text.to_string().trim().to_string();
5471 self.change_overrides(|o| {
5472 if o.first_step.as_deref() != Some(text.as_str()) {
5473 o.first_step_task = None;
5475 o.first_step = Some(text).filter(|t| !t.is_empty());
5476 }
5477 });
5478 }
5479
5480 fn clear_first_step(self: Pin<&mut Self>) {
5481 self.change_overrides(|o| {
5482 o.first_step = None;
5483 o.first_step_task = None;
5484 });
5485 }
5486
5487 fn usual_hours(self: Pin<&mut Self>) {
5488 self.change_overrides(|o| {
5489 o.work_until = None;
5490 o.rest_until = None;
5491 o.work_now = None;
5492 });
5493 }
5494
5495 fn work_until_tonight(self: Pin<&mut Self>) {
5496 let Some(night) = crate::everywhere::tonight(&Zoned::now()) else { return };
5497 self.change_overrides(|o| {
5498 o.rest_until = None;
5499 o.work_until = Some(night);
5500 });
5501 }
5502
5503 fn set_free_time(self: Pin<&mut Self>, on: bool) -> QString {
5504 crate::pauses::set_free(self, on)
5505 }
5506
5507 fn keep_usual_end(self: Pin<&mut Self>) {
5508 crate::pauses::keep_usual_end(self);
5509 }
5510
5511 fn set_free_nothing(self: Pin<&mut Self>, on: bool) {
5512 crate::pauses::set_free_nothing(self, on);
5513 }
5514
5515 fn pause_now(self: Pin<&mut Self>) {
5516 crate::pauses::press(self);
5517 }
5518
5519 fn come_back(self: Pin<&mut Self>) -> QString {
5520 crate::pauses::come_back(self)
5521 }
5522
5523 fn lighten_tomorrow(self: Pin<&mut Self>) {
5524 crate::pauses::lighten_tomorrow(self);
5525 }
5526
5527 fn forget_pause(self: Pin<&mut Self>) {
5528 crate::pauses::forget(self);
5529 }
5530
5531 fn pause_screen(&self) -> QString {
5532 QString::from(&crate::pauses::screen())
5533 }
5534
5535 fn pause_setup(&self) -> QString {
5536 QString::from(&crate::pauses::setup())
5537 }
5538
5539 fn open_dnd(&self, key: &QString) {
5540 crate::dnd::open(&key.to_string());
5541 }
5542
5543 fn dnd_toggle(self: Pin<&mut Self>, on: bool, minutes: i32) {
5544 crate::everywhere::toggle(self, on, minutes);
5545 }
5546
5547 fn reach_choose(self: Pin<&mut Self>, state: &QString, length: &QString) {
5548 crate::everywhere::choose(self, &state.to_string(), &length.to_string());
5549 }
5550
5551 fn let_phone_through(self: Pin<&mut Self>) {
5552 crate::everywhere::let_through(self);
5553 }
5554
5555 fn dnd_setup(&self) -> QString {
5556 QString::from(&crate::everywhere::setup())
5557 }
5558
5559 fn dnd_change(self: Pin<&mut Self>, verb: &QString, json: &QString) -> QString {
5560 let (answer, changed) = crate::everywhere::change(&verb.to_string(), &json.to_string());
5561 if changed {
5563 crate::share::exchange(&self.qt_thread(), &self.shared());
5564 }
5565 QString::from(&answer)
5566 }
5567
5568 fn app_notes_setup(&self) -> QString {
5569 QString::from(&crate::appnotes::setup())
5570 }
5571
5572 fn app_notes_change(&self, verb: &QString, json: &QString) -> QString {
5573 QString::from(&crate::appnotes::change(&verb.to_string(), &json.to_string()))
5574 }
5575
5576 fn calls_through(mut self: Pin<&mut Self>, on: bool, minutes: i32) {
5577 if let Err(e) = crate::calls::press(on, minutes) {
5578 self.as_mut().set_status(QString::from(&e));
5579 return;
5580 }
5581 let (qt, shared) = (self.qt_thread(), self.shared());
5583 std::thread::spawn(move || {
5584 crate::calls::refresh(true);
5585 let _ = qt.queue(|mut sioul| sioul.as_mut().set_mode(QString::from(&mode_json())));
5586 crate::share::exchange(&qt, &shared);
5587 });
5588 }
5589
5590 fn calls_view(&self) -> QString {
5591 QString::from(&crate::calls::view())
5592 }
5593
5594 fn calls_action(self: Pin<&mut Self>, verb: &QString, json: &QString) -> QString {
5595 let answer = crate::calls::act(&verb.to_string(), &json.to_string());
5596 if answer.contains("\"shared\":true") {
5598 let (qt, shared) = (self.qt_thread(), self.shared());
5599 std::thread::spawn(move || {
5600 crate::calls::refresh(true);
5601 crate::share::exchange(&qt, &shared);
5602 });
5603 }
5604 QString::from(&answer)
5605 }
5606
5607 fn calls_setup(&self) -> QString {
5608 QString::from(&crate::calls::setup())
5609 }
5610
5611 fn phone_messages(self: Pin<&mut Self>, verb: &QString, json: &QString) -> QString {
5612 let (answer, shared) = crate::phonemsgs::call(&verb.to_string(), &json.to_string());
5613 if shared {
5615 crate::share::exchange(&self.qt_thread(), &self.shared());
5616 }
5617 QString::from(&answer)
5618 }
5619
5620 fn texts(self: Pin<&mut Self>, verb: &QString, json: &QString) -> QString {
5622 let (answer, shared) = crate::texts::call(&verb.to_string(), &json.to_string());
5623 if shared {
5625 crate::share::exchange(&self.qt_thread(), &self.shared());
5626 }
5627 QString::from(&answer)
5628 }
5629
5630 fn reaches_view(&self) -> QString {
5631 QString::from(&crate::reaches::view(self.shared().realtime.load(Ordering::Relaxed)))
5632 }
5633
5634 fn reach_talk(mut self: Pin<&mut Self>, verb: &QString, key: &QString) -> QString {
5635 let key = key.to_string();
5636 let said = match verb.to_string().as_str() {
5637 "now" => crate::appnotes::release_talk(&key),
5638 "then" => crate::appnotes::let_talk_then(&key),
5639 _ => String::new(),
5640 };
5641 if !said.is_empty() {
5642 self.as_mut().set_status(QString::from(&said));
5643 }
5644 QString::from(&said)
5645 }
5646
5647 fn reach_card(&self) -> QString {
5648 QString::from(&crate::reaches::now_card(self.shared().realtime.load(Ordering::Relaxed)))
5649 }
5650
5651 fn reaches_set(self: Pin<&mut Self>, row: &QString, words: &QString, shown: &QString) -> QString {
5652 let problem = crate::reaches::set_row(&row.to_string(), &words.to_string(), &shown.to_string());
5653 if problem.is_empty() {
5654 self.attention_changed();
5655 }
5656 QString::from(&problem)
5657 }
5658
5659 fn reaches_preset(self: Pin<&mut Self>, id: &QString) -> QString {
5660 let problem = crate::reaches::preset(&id.to_string());
5661 if problem.is_empty() {
5662 self.attention_changed();
5663 }
5664 QString::from(&problem)
5665 }
5666
5667 fn person_sheet(&self, key: &QString, addresses: &QString) -> QString {
5668 QString::from(&crate::reaches::person(&key.to_string(), &addresses.to_string()))
5669 }
5670
5671 fn person_sheet_change(mut self: Pin<&mut Self>, key: &QString, addresses: &QString, verb: &QString, value: &QString) -> QString {
5672 let answer = crate::reaches::person_change(&key.to_string(), &addresses.to_string(), &verb.to_string(), &value.to_string());
5673 let line = serde_json::from_str::<serde_json::Value>(&answer).ok().and_then(|a| a["line"].as_str().map(str::to_string)).unwrap_or_default();
5674 if !line.is_empty() {
5675 self.as_mut().set_status(QString::from(&line));
5676 }
5677 let (qt, shared) = (self.qt_thread(), self.shared());
5679 show(&qt, &shared);
5680 pim::show_pim(&qt, &shared);
5681 std::thread::spawn(move || {
5682 crate::calls::refresh(true);
5683 crate::everywhere::apply();
5684 crate::share::exchange(&qt, &shared);
5685 });
5686 QString::from(&answer)
5687 }
5688
5689 fn attention_changed(mut self: Pin<&mut Self>) {
5693 self.as_mut().set_mode(QString::from(&mode_json()));
5694 let (qt, shared) = (self.qt_thread(), self.shared());
5695 show(&qt, &shared);
5696 work::show_work(&qt, &shared);
5697 std::thread::spawn(move || {
5698 crate::calls::refresh(true);
5699 crate::everywhere::apply();
5700 crate::share::exchange(&qt, &shared);
5701 });
5702 }
5703
5704 fn calls_setup_change(self: Pin<&mut Self>, verb: &QString, json: &QString) -> QString {
5705 QString::from(&crate::calls::setup_change(&verb.to_string(), &json.to_string()))
5706 }
5707
5708 fn change_overrides(mut self: Pin<&mut Self>, change: impl FnOnce(&mut sioul_core::quiet::Overrides)) {
5710 let path = sioul_core::quiet::Overrides::default_path();
5711 let mut overrides = sioul_core::quiet::Overrides::load(&path);
5712 change(&mut overrides);
5713 let said = overrides.save(&path).err().unwrap_or_default();
5715 self.as_mut().set_status(QString::from(&said));
5716 self.as_mut().set_mode(QString::from(&mode_json()));
5717 let (qt, shared) = (self.qt_thread(), self.shared());
5718 show(&qt, &shared);
5719 work::show_work(&qt, &shared);
5720 }
5721
5722 fn project_rows(&self) -> QString {
5723 QString::from(&crate::projects::rows(&self.shared()))
5724 }
5725
5726 fn project_page(&self, id: &QString) -> QString {
5727 QString::from(&crate::projects::page(&self.shared(), &id.to_string()))
5728 }
5729
5730 fn save_project(self: Pin<&mut Self>, id: &QString, edit: &QString) -> QString {
5731 QString::from(&crate::projects::save(&self.qt_thread(), &self.shared(), &id.to_string(), &edit.to_string()))
5732 }
5733
5734 fn time_page(&self, period: &QString, anchor: &QString, project: &QString) -> QString {
5735 QString::from(&crate::projects::time(&self.shared(), &period.to_string(), &anchor.to_string(), &project.to_string()))
5736 }
5737
5738 fn note_time(self: Pin<&mut Self>, edit: &QString) -> QString {
5739 QString::from(&crate::projects::note_time(&self.qt_thread(), &self.shared(), &edit.to_string()))
5740 }
5741
5742 fn stopped(&self) -> QString {
5743 QString::from(&crate::work::stopped(&self.shared()))
5744 }
5745
5746 fn set_stopped(self: Pin<&mut Self>, text: &QString) -> QString {
5747 QString::from(&crate::work::set_stopped(&self.qt_thread(), &self.shared(), &text.to_string()))
5748 }
5749
5750 fn change_time(self: Pin<&mut Self>, key: &QString, edit: &QString) -> QString {
5751 QString::from(&crate::projects::change_time(&self.qt_thread(), &self.shared(), &key.to_string(), &edit.to_string()))
5752 }
5753
5754 fn task_choices(&self, keep: &QString) -> QString {
5755 QString::from(&crate::projects::task_choices(&self.shared(), &keep.to_string()))
5756 }
5757
5758 fn remove_time(self: Pin<&mut Self>, key: &QString) -> QString {
5759 QString::from(&crate::projects::remove_time(&self.qt_thread(), &self.shared(), &key.to_string()))
5760 }
5761
5762 fn take_invoices(self: Pin<&mut Self>) -> QString {
5763 QString::from(&crate::projects::take_invoices(&self.shared()))
5764 }
5765
5766 fn make_invoice(self: Pin<&mut Self>, id: &QString) -> QString {
5767 QString::from(&crate::projects::make_invoice(&self.qt_thread(), &self.shared(), &id.to_string()))
5768 }
5769
5770 fn invoice_again(&self, number: &QString) -> QString {
5771 QString::from(&crate::projects::invoice_again(&number.to_string()))
5772 }
5773
5774 fn set_invoice_paid(self: Pin<&mut Self>, number: &QString, paid: bool) -> QString {
5775 QString::from(&crate::projects::set_paid(&self.qt_thread(), &self.shared(), &number.to_string(), paid))
5776 }
5777
5778 fn make_linked(self: Pin<&mut Self>, kind: &QString, from: &QString, key: &QString, start: f64) -> QString {
5779 QString::from(&work::make_linked(&self.qt_thread(), &self.shared(), &kind.to_string(), &from.to_string(), &key.to_string(), start))
5780 }
5781
5782 fn task_from_mail(self: Pin<&mut Self>, key: &QString) -> QString {
5783 QString::from(&work::task_from_mail(&self.qt_thread(), &self.shared(), &key.to_string()))
5784 }
5785
5786 fn note_from_mail(self: Pin<&mut Self>, key: &QString) -> QString {
5787 QString::from(&work::note_from_mail(&self.qt_thread(), &self.shared(), &key.to_string()))
5788 }
5789
5790 fn note_from_event(self: Pin<&mut Self>, key: &QString, start: f64) -> QString {
5791 QString::from(&work::note_from_event(&self.qt_thread(), &self.shared(), &key.to_string(), start))
5792 }
5793
5794 fn task_from_event(self: Pin<&mut Self>, key: &QString, start: f64) -> QString {
5795 QString::from(&work::task_from_event(&self.qt_thread(), &self.shared(), &key.to_string(), start))
5796 }
5797
5798 fn task_from_line(self: Pin<&mut Self>, path: &QString, line: i32) -> QString {
5799 QString::from(&work::task_from_line(&self.qt_thread(), &self.shared(), &path.to_string(), line))
5800 }
5801
5802 fn draft_for_task(self: Pin<&mut Self>, uid: &QString) -> QString {
5803 QString::from(&work::draft_for_task(&self.qt_thread(), &self.shared(), &uid.to_string()))
5804 }
5805
5806 fn mail_note(self: Pin<&mut Self>, path: &QString) -> QString {
5807 QString::from(&work::mail_note(&self.qt_thread(), &self.shared(), &path.to_string()))
5808 }
5809
5810 fn settings(&self, view: &QString) -> QString {
5811 let config = load_config();
5812 let lists: Vec<(String, String)> = sioul_core::tasks::lists().into_iter().filter(|c| !c.read_only).map(|c| (format!("{}/{}", c.account, c.id), c.label(&config, tr()))).collect();
5813 let store = config.notes_root_path().and_then(|root| ProjectStore::load(&root).ok());
5814 let mut rows = sioul_core::settings::for_view(&view.to_string(), &config, tr(), &lists, store.as_ref());
5815 if view.to_string() == "tasks" {
5817 let categories = work::categories_in_use(&self.shared());
5818 if !categories.is_empty() {
5819 let at = rows.iter().position(|r| r.key == "tasks.kind").map_or(rows.len(), |i| i + 1);
5820 rows.insert(at, sioul_core::settings::categories(tr(), categories));
5821 }
5822 }
5823 for row in rows.iter_mut().filter(|r| r.key == "ai_key") {
5825 row.value = config::SettingValue::Bool(sioul_sync::shield_ai::api_key().is_some());
5826 }
5827 for row in rows.iter_mut().filter(|r| r.key == "github_token") {
5828 row.value = config::SettingValue::Bool(sioul_sync::github::token().is_some());
5829 }
5830 for row in rows.iter_mut().filter(|r| r.key == "reminders_closed") {
5832 row.value = config::SettingValue::Bool(crate::remind::background());
5833 }
5834 if cfg!(target_os = "android") {
5836 for row in rows.iter_mut().filter(|r| r.key == "reminders.before_event") {
5837 row.help = format!("{} {}", row.help, tr().text(if crate::alarms::exact() { "set-reminders-before-exact" } else { "set-reminders-before-inexact" }, None));
5838 }
5839 }
5840 for row in rows.iter_mut().filter(|r| r.key == "passwords_shown") {
5841 row.value = config::SettingValue::Bool(work::view_flag(&self.shared(), "passwords-shown"));
5842 }
5843 if cfg!(target_os = "android") {
5846 rows.retain(|r| r.key != "screen_colours" && r.key != "calmer_colours");
5847 }
5848 #[cfg(not(target_os = "android"))]
5849 for row in rows.iter_mut().filter(|r| r.key == "screen_colours") {
5850 use crate::colour::Said;
5851 let said = match crate::colour::said() {
5852 Said::Converted(name) if !name.is_empty() => say("colour-said-converted", &[("profile", name)]),
5853 Said::Converted(_) => tr().text("colour-said-converted-unnamed", None),
5854 Said::Desktop => tr().text("colour-said-desktop", None),
5855 Said::Plain => tr().text("colour-said-plain", None),
5856 Said::Off | Said::Unknown => String::new(),
5857 };
5858 if !said.is_empty() {
5859 row.help = format!("{} {said}", row.help);
5860 }
5861 }
5862 if crate::homecard::has_setting()
5864 && let Some(at) = rows.iter().position(|r| r.key == "passwords_shown")
5865 {
5866 let row = crate::homecard::setting(&rows[at].group, &rows[at].section, !work::view_flag(&self.shared(), crate::homecard::PLAIN));
5867 rows.insert(at + 1, row);
5868 }
5869 QString::from(&json(&rows))
5870 }
5871
5872 fn set_setting(self: Pin<&mut Self>, key: &QString, value: &QString) -> QString {
5873 self.change_setting(key, &QString::default(), value)
5874 }
5875
5876 fn change_setting(mut self: Pin<&mut Self>, key: &QString, shown: &QString, value: &QString) -> QString {
5877 let key = key.to_string();
5878 let shown: Option<config::SettingValue> = match Some(shown.to_string()).filter(|s| !s.is_empty()).map(|s| serde_json::from_str(&s)) {
5881 Some(Ok(shown)) => Some(shown),
5882 Some(Err(e)) => return QString::from(&format!("{key}: {e}")),
5883 None => None,
5884 };
5885 let parsed: Result<config::SettingValue, String> = serde_json::from_str(&value.to_string()).map_err(|e| e.to_string());
5886 let result = parsed.and_then(|v| match (key.as_str(), v) {
5887 ("ai_key", config::SettingValue::Text(secret)) => sioul_sync::shield_ai::save_api_key(&secret).map_err(|e| e.sentence(tr(), "Anthropic")),
5889 ("github_token", config::SettingValue::Text(secret)) => sioul_sync::github::save_token(&secret).map_err(|e| e.sentence(tr(), "GitHub")),
5890 ("task_categories", config::SettingValue::Rename(change)) => work::rename_category(&self.qt_thread(), &self.shared(), &change.from, &change.to),
5891 ("reminders_closed", config::SettingValue::Bool(on)) => crate::remind::set_background(on),
5892 ("passwords_shown", config::SettingValue::Bool(on)) => {
5893 work::set_view_flag(&self.shared(), "passwords-shown", on);
5894 Ok(())
5895 }
5896 ("home_card_details", config::SettingValue::Bool(on)) => {
5897 work::set_view_flag(&self.shared(), crate::homecard::PLAIN, !on);
5898 crate::homecard::now();
5899 Ok(())
5900 }
5901 (_, v) => sioul_core::settings::change(&config_path(), &key, shown.as_ref(), &v),
5902 });
5903 if let Err(e) = result {
5904 return QString::from(&e);
5905 }
5906 let (qt, shared) = (self.qt_thread(), self.shared());
5908 if let Some(project) = key.strip_prefix("project.").and_then(|k| k.strip_suffix(".routes")) {
5909 retie_project(project.to_string());
5910 }
5911 if key == "collections"
5913 && let Some(account) = serde_json::from_str::<serde_json::Value>(&value.to_string()).ok().and_then(|v| v["from"].as_str().and_then(|f| f.split('/').nth(1)).map(str::to_string))
5914 {
5915 pim::nudge(&shared, &account);
5916 }
5917 if key.starts_with("reading.") || key == "theme" || key == "places_named" || key == "screen_colours" || key == "calmer_colours" {
5919 self.as_mut().set_reading(QString::from(&reading_json()));
5920 }
5921 if key.contains("fetch_minutes") {
5922 update_paces(&shared);
5923 }
5924 if key.starts_with("window") || key == "time_off" {
5926 self.as_mut().set_mode(QString::from(&mode_json()));
5927 }
5928 if key.starts_with("mail.") {
5929 mail::show_mail(&qt, &shared);
5930 }
5931 if key.starts_with("github") {
5933 if load_config().github.enabled {
5934 crate::github::start(&qt, &shared);
5935 } else {
5936 crate::github::stop(&shared);
5937 }
5938 }
5939 show(&qt, &shared);
5940 pim::show_pim(&qt, &shared);
5941 work::show_work(&qt, &shared);
5942 QString::default()
5943 }
5944
5945 fn forced_theme(&self) -> QString {
5946 QString::from(&std::env::var("SIOUL_THEME").unwrap_or_default())
5947 }
5948
5949 fn follow_icons(&self, dark: bool, chosen: bool) {
5950 crate::desktop::follow(dark, chosen);
5951 }
5952
5953 fn plain_notice(&self, title: &QString, body: &QString) {
5954 let (title, body) = (title.to_string(), body.to_string());
5955 std::thread::spawn(move || {
5956 if let Err(e) = sioul_sync::notify::remind(&title, &body, None) {
5957 eprintln!("{e}");
5958 }
5959 });
5960 }
5961
5962 fn set_realtime_mode(mut self: Pin<&mut Self>, on: bool) {
5963 let shared = self.shared();
5964 shared.realtime.store(on, Ordering::Relaxed);
5965 shared.opened_anyway.store(on || shared.opened_anyway.load(Ordering::Relaxed), Ordering::Relaxed);
5966 update_paces(&shared);
5967 self.as_mut().set_realtime(on);
5968 let line = tr().text(if on { "realtime-on" } else { "realtime-off" }, None);
5969 self.as_mut().set_status(QString::from(&line));
5970 show(&self.qt_thread(), &shared);
5971 }
5972
5973 fn grab_phone(&self) -> bool {
5974 std::env::var_os("SIOUL_GRAB_PHONE").is_some()
5975 }
5976
5977 fn grab_steps(&self) -> QString {
5978 let steps = std::env::var("SIOUL_GRAB_STEPS").unwrap_or_else(|_| "pages".into());
5979 if cfg!(feature = "insecure-test-tls") || steps == "demo" || steps == "phone" || steps == "drag" || (["taskform", "review", "site-open", "site-quit", "site-during", "site-share", "site-devices", "site-colour", "rail", "pauses", "blocks", "unsubscribe", "attention", "line", "share-panel", "join", "share-send", "spam", "mail-search", "mail-filters", "security-key", "health-gpg", "calls", "words", "movetask", "compose", "texts", "ai", "health", "tiles", "attachments", "projects-file", "memory", "bitwarden-wait", "tray", "notes-shared", "microsoft", "reach"].contains(&steps.as_str()) && offline()) {
6026 return QString::from(&steps);
6027 }
6028 QString::from("pages")
6029 }
6030}
6031
6032impl qobject::Sioul {
6034 fn set_notes_carried(self: Pin<&mut Self>, carried: bool) -> QString {
6035 QString::from(&crate::share::set_notes_carried(carried))
6036 }
6037
6038 fn set_share_part(self: Pin<&mut Self>, part: &QString, on: bool) -> QString {
6039 let problem = crate::share::set_part(&part.to_string(), on);
6040 if problem.is_empty() {
6041 crate::share::exchange(&self.qt_thread(), &self.shared());
6042 }
6043 QString::from(&problem)
6044 }
6045
6046 fn share_history(self: Pin<&mut Self>, filter: &QString) {
6047 let (qt, filter) = (self.qt_thread(), filter.to_string());
6048 std::thread::spawn(move || {
6049 let listed = crate::share::history(&filter);
6050 let _ = qt.queue(move |mut sioul| sioul.as_mut().share_listed(QString::from(&listed)));
6051 });
6052 }
6053
6054 fn share_put_back_preview(&self, part: &QString, file: &QString, stamp: &QString) -> QString {
6055 QString::from(&crate::share::put_back_preview(&part.to_string(), &file.to_string(), &stamp.to_string()))
6056 }
6057
6058 fn share_put_back(self: Pin<&mut Self>, part: &QString, file: &QString, stamp: &QString) {
6059 let (qt, shared) = (self.qt_thread(), self.shared());
6060 let (part, file, stamp) = (part.to_string(), file.to_string(), stamp.to_string());
6061 std::thread::spawn(move || {
6063 let problem = if part == sioul_sync::history::ACCOUNTS {
6065 match crate::share::put_back_item(&file, &stamp) {
6066 Ok(account) => {
6067 crate::pim::nudge(&shared, &account);
6068 String::new()
6069 }
6070 Err(e) => e,
6071 }
6072 } else {
6073 crate::share::put_back(&part, &file, &stamp)
6074 };
6075 let said = if problem.is_empty() { crate::share::put_back_said(&file, &stamp) } else { String::new() };
6076 let store = crate::share::store_of(&file);
6077 let done = problem.clone();
6078 let _ = qt.queue(move |mut sioul| {
6079 if !said.is_empty() {
6080 sioul.as_mut().set_status(QString::from(&said));
6081 sioul.as_mut().shared_in(QString::from(&store));
6083 }
6084 sioul.as_mut().share_put_back_done(QString::from(&done));
6085 });
6086 if problem.is_empty() {
6087 crate::work::show_work(&qt, &shared);
6088 crate::share::exchange(&qt, &shared);
6089 }
6090 });
6091 }
6092
6093 fn share_confirm_gone(self: Pin<&mut Self>, store: &QString) -> QString {
6094 let problem = crate::share::confirm_gone(&store.to_string());
6095 if problem.is_empty() {
6096 crate::share::exchange(&self.qt_thread(), &self.shared());
6097 }
6098 QString::from(&problem)
6099 }
6100
6101 fn share_withheld(&self) -> QString {
6102 QString::from(&json(&crate::share::withheld(false)))
6103 }
6104
6105 fn share_put_back_withheld(self: Pin<&mut Self>, store: &QString) {
6106 let (qt, shared, store) = (self.qt_thread(), self.shared(), store.to_string());
6107 std::thread::spawn(move || {
6108 let problem = crate::share::put_back_withheld(&store);
6109 let _ = qt.queue(move |mut sioul| sioul.as_mut().share_withheld_back(QString::from(&problem)));
6110 crate::work::show_work(&qt, &shared);
6112 crate::share::exchange(&qt, &shared);
6113 });
6114 }
6115
6116 fn share_take_out_withheld(self: Pin<&mut Self>, store: &QString) -> QString {
6117 let problem = crate::share::take_out_withheld(&store.to_string());
6118 if problem.is_empty() {
6119 crate::share::exchange(&self.qt_thread(), &self.shared());
6120 }
6121 QString::from(&problem)
6122 }
6123
6124 fn device_files(&self, id: &QString) -> QString {
6125 QString::from(&crate::share::device_files(&id.to_string()))
6126 }
6127
6128 fn device_remove_files(self: Pin<&mut Self>, id: &QString) -> QString {
6129 let problem = crate::share::remove_device_files(&id.to_string());
6130 crate::health::show_health(&self.qt_thread(), &self.shared());
6132 crate::share::exchange(&self.qt_thread(), &self.shared());
6133 QString::from(&problem)
6134 }
6135
6136 fn texts_attach(self: Pin<&mut Self>, json: &QString) {
6137 let (qt, json) = (self.qt_thread(), json.to_string());
6138 std::thread::spawn(move || {
6139 let attached = crate::texts::attach(&json);
6140 let _ = qt.queue(move |mut sioul| sioul.as_mut().texts_attached(QString::from(&attached)));
6141 });
6142 }
6143
6144 fn share_estimate(self: Pin<&mut Self>, part: &QString) {
6145 let (qt, part) = (self.qt_thread(), part.to_string());
6146 std::thread::spawn(move || {
6147 let counted = crate::share::estimate(&part);
6148 let _ = qt.queue(move |mut sioul| sioul.as_mut().share_estimated(QString::from(&counted)));
6149 });
6150 }
6151
6152 fn spam_status(&self) -> QString {
6153 QString::from(&crate::spam::status())
6154 }
6155
6156 fn spam_train(self: Pin<&mut Self>, named: &QString) -> QString {
6157 let named: Vec<String> = serde_json::from_str(&named.to_string()).unwrap_or_default();
6158 QString::from(&crate::spam::train(self.qt_thread(), self.shared(), named))
6159 }
6160
6161 fn spam_stop(self: Pin<&mut Self>) {
6162 crate::spam::stop(self.qt_thread());
6163 }
6164
6165 fn mail_filters(&self) -> QString {
6166 QString::from(&crate::filters::view())
6167 }
6168
6169 fn set_mail_filters(self: Pin<&mut Self>, filters: &QString, shown: &QString) -> QString {
6170 QString::from(&crate::filters::save(&filters.to_string(), &shown.to_string()))
6171 }
6172
6173 fn mail_filter_said(&self, filter: &QString) -> QString {
6174 QString::from(&crate::filters::said(&filter.to_string()))
6175 }
6176
6177 fn mail_filter_try(self: Pin<&mut Self>, filter: &QString) {
6178 crate::filters::try_one(self.qt_thread(), filter.to_string());
6179 }
6180
6181 fn mail_filters_preview(self: Pin<&mut Self>) {
6182 crate::filters::preview(self.qt_thread());
6183 }
6184
6185 fn mail_filters_run(self: Pin<&mut Self>) -> QString {
6186 QString::from(&crate::filters::run(&self.qt_thread(), &self.shared()))
6187 }
6188
6189 fn new_mail_filter(self: Pin<&mut Self>, given: &QString) -> QString {
6190 QString::from(&crate::filters::new_from(&given.to_string()))
6191 }
6192
6193 fn set_share_backup(self: Pin<&mut Self>, on: bool) -> QString {
6194 let problem = crate::share::set_backup(on);
6195 if problem.is_empty() {
6197 crate::share::exchange(&self.qt_thread(), &self.shared());
6198 }
6199 QString::from(&problem)
6200 }
6201
6202 fn set_share_send(self: Pin<&mut Self>, on: bool) -> QString {
6203 let problem = crate::share::set_send(on);
6204 if problem.is_empty() && on {
6206 crate::share::exchange(&self.qt_thread(), &self.shared());
6207 }
6208 QString::from(&problem)
6209 }
6210 fn share_send_again(self: Pin<&mut Self>) {
6211 crate::share::send_again(&self.qt_thread(), &self.shared());
6212 }
6213 fn share_backup_place(self: Pin<&mut Self>, place: &QString) -> QString {
6214 let problem = crate::share::set_backup_place(&place.to_string());
6215 if problem.is_empty() {
6216 crate::share::exchange(&self.qt_thread(), &self.shared());
6217 }
6218 QString::from(&problem)
6219 }
6220
6221 fn start_sharing_on_server(self: Pin<&mut Self>, account: &QString, place: &QString, passphrase: &QString, again: &QString) {
6222 crate::share::start_on_server(&self.qt_thread(), &self.shared(), account.to_string(), place.to_string(), passphrase.to_string(), again.to_string());
6223 }
6224
6225 fn join_sign_in(self: Pin<&mut Self>, server: &QString) {
6226 crate::join::sign_in(&self.qt_thread(), server.to_string());
6227 }
6228
6229 fn join_open_again(self: Pin<&mut Self>) {
6230 crate::join::open_again(&self.qt_thread());
6231 }
6232
6233 fn join_look_again(self: Pin<&mut Self>) {
6234 crate::join::look_again(&self.qt_thread());
6235 }
6236
6237 fn join_cancel(self: Pin<&mut Self>) {
6238 crate::join::cancel(&self.qt_thread());
6239 }
6240
6241 fn join_status(&self) -> QString {
6242 QString::from(&crate::join::status())
6243 }
6244
6245 fn join_folder(self: Pin<&mut Self>, place: &QString, passphrase: &QString, again: &QString) {
6246 crate::join::join(&self.qt_thread(), &self.shared(), place.to_string(), passphrase.to_string(), again.to_string());
6247 }
6248
6249 fn join_done(self: Pin<&mut Self>) {
6250 crate::join::done(&self.qt_thread());
6251 }
6252
6253 fn join_replace(self: Pin<&mut Self>, device: &QString, yes: bool) -> QString {
6254 QString::from(&crate::join::replace(&self.qt_thread(), &self.shared(), &device.to_string(), yes))
6255 }
6256}
6257
6258#[cfg(test)]
6259mod tests {
6260 use super::*;
6261
6262 #[test]
6263 fn file_addresses_reach_the_file() {
6264 assert_eq!(file_url(Path::new("/tmp/Réunion #2 100%.pdf")), "file:///tmp/R%C3%A9union%20%232%20100%25.pdf");
6265 assert_eq!(local_path(&file_url(Path::new("/tmp/Réunion #2 100%.pdf"))), PathBuf::from("/tmp/Réunion #2 100%.pdf"));
6266 assert_eq!(local_path("file://localhost/tmp/a%20b"), PathBuf::from("/tmp/a b"));
6267 assert_eq!(local_path("/tmp/plain"), PathBuf::from("/tmp/plain"));
6268 assert_eq!(windows_slashed(r"C:\Users\me\a b.pdf"), "/C:/Users/me/a b.pdf");
6270 assert_eq!(windows_slashed(r"\\?\C:\Users\me"), "/C:/Users/me");
6271 assert_eq!(windows_slashed(r"\\server\share\a"), "//server/share/a");
6272 assert_eq!(windows_slashed(r"\\?\UNC\server\share\a"), "//server/share/a");
6273 assert_eq!(windows_path("/C:/Users/me/a b.pdf"), r"C:\Users\me\a b.pdf");
6274 assert_eq!(windows_path("server/share/a"), r"\\server\share\a");
6275 }
6276
6277 #[test]
6285 fn no_notice_tried_on_a_phone_says_its_error_in_the_status_line() {
6286 let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
6287 let calls = ["notify::remind(", "notify::code(", "notify::remind_offering(", "notify::mail("];
6288 let mut checked = Vec::new();
6289 for entry in std::fs::read_dir(&src).unwrap() {
6290 let path = entry.unwrap().path();
6291 if path.extension().is_none_or(|e| e != "rs") {
6292 continue;
6293 }
6294 let text = std::fs::read_to_string(&path).unwrap();
6295 let text = text.split("#[cfg(test)]\nmod tests").next().unwrap_or_default();
6297 let lines: Vec<&str> = text.lines().collect();
6298 for (at, line) in lines.iter().enumerate() {
6299 if line.trim_start().starts_with("//") || !calls.iter().any(|call| line.contains(call)) {
6300 continue;
6301 }
6302 if !lines[at..(at + 4).min(lines.len())].iter().any(|l| l.contains("tell(") || l.contains("set_status(")) {
6304 continue;
6305 }
6306 let start = (0..at).rev().find(|i| ["fn ", "pub fn ", "pub(crate) fn "].iter().any(|f| lines[*i].trim_start().starts_with(f))).unwrap_or(0);
6308 let before = lines[start..at].join("\n");
6309 let name = format!("{}:{}", path.file_name().unwrap().to_string_lossy(), at + 1);
6310 assert!(before.contains("cfg!(target_os = \"android\")") || before.contains("cfg(not(target_os = \"android\"))"), "{name}: a notice whose error goes to the status line is tried on a phone too");
6311 checked.push(name);
6312 }
6313 }
6314 assert!(checked.len() >= 5, "{checked:?}");
6316 }
6317
6318 #[test]
6319 fn programs_are_not_opened() {
6320 for name in ["setup.exe", "Facture.PDF.exe", "invoice.exe.", "invoice.exe ", "run.sh", "x.desktop", "a.lnk", "b.js", "c.terminal", "d.iso"] {
6321 assert!(sioul_core::links::is_program(name), "{name}");
6322 }
6323 for name in ["facture.pdf", "photo.jpeg", "notes.txt", "archive.zip", "exe", "README"] {
6324 assert!(!sioul_core::links::is_program(name), "{name}");
6325 }
6326 }
6327
6328 #[test]
6329 fn new_sentences_read_in_both_languages() {
6330 for language in ["en", "fr"] {
6331 let tr = Translator::new(language);
6332 for id in ["attachment-program", "budget-origin-mail", "budget-origin-hand", "note-outside-notes", "setting-unknown-key", "reminders-entry-name", "reminders-entry-comment", "bank-rule-gone", "budget-line-gone", "mail-no-such-folder", "mail-not-in-accounts", "mail-move-needs-to", "list-read-only-mark", "import-bad-key"] {
6333 assert_ne!(tr.text(id, None), id, "{language}: {id}");
6334 }
6335 }
6336 }
6337
6338 #[test]
6339 fn attachment_names_stay_in_their_folder() {
6340 assert_eq!(one_file_name("a/b\\c.pdf"), "a_b_c.pdf");
6341 assert_eq!(one_file_name(".."), "attachment");
6342 assert_eq!(one_file_name(""), "attachment");
6343 if cfg!(windows) {
6344 assert_eq!(one_file_name("C:evil.txt"), "C_evil.txt");
6345 assert_eq!(one_file_name("con.txt"), "_con.txt");
6346 } else {
6347 assert_eq!(one_file_name("C:evil.txt"), "C:evil.txt");
6348 }
6349 }
6350}