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, 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]
229 fn set_account_enabled(self: Pin<&mut Sioul>, id: &QString, on: bool) -> QString;
230
231 #[qinvokable]
234 fn scout_account(self: Pin<&mut Sioul>, address: &QString);
235
236 #[qinvokable]
238 fn add_mail_like(self: Pin<&mut Sioul>, address: &QString, host: &QString, port: i32, security: &QString, login: &QString);
239
240 #[qinvokable]
242 fn add_account(self: Pin<&mut Sioul>, address: &QString, host: &QString, port: i32, security: &QString, login: &QString, password: &QString);
243
244 #[qinvokable]
246 fn add_portal(self: Pin<&mut Sioul>, name: &QString, url: &QString);
247
248 #[qinvokable]
250 fn remove_account(self: Pin<&mut Sioul>, id: &QString);
251
252 #[qinvokable]
256 fn set_account_password(self: Pin<&mut Sioul>, id: &QString, password: &QString);
257
258 #[qinvokable]
260 fn set_priority(self: Pin<&mut Sioul>, id: &QString, priority: &QString);
261
262 #[qinvokable]
264 fn open_folder(self: Pin<&mut Sioul>, account: &QString, folder: &QString, all: bool, query: &QString);
265
266 #[qinvokable]
268 fn opened(self: Pin<&mut Sioul>, key: &QString);
269
270 #[qinvokable]
274 fn act(self: Pin<&mut Sioul>, key: &QString, action: &QString, target: &QString);
275
276 #[qinvokable]
278 fn act_many(self: Pin<&mut Sioul>, keys: &QString, action: &QString);
279
280 #[qinvokable]
282 fn move_messages(self: Pin<&mut Sioul>, keys: &QString, account: &QString, folder: &QString);
283
284 #[qinvokable]
288 fn search_mail(self: Pin<&mut Sioul>, search: &QString, now: bool);
289
290 #[qinvokable]
292 fn clear_mail_search(self: Pin<&mut Sioul>);
293
294 #[qinvokable]
296 fn bring_message(self: Pin<&mut Sioul>, key: &QString);
297
298 #[qinvokable]
300 fn mail_search_fields(self: &Sioul) -> QString;
301
302 #[qinvokable]
306 fn unsubscribe(self: Pin<&mut Sioul>, key: &QString) -> QString;
307
308 #[qinvokable]
310 fn undo(self: Pin<&mut Sioul>);
311
312 #[qinvokable]
314 fn flush(self: Pin<&mut Sioul>);
315
316 #[qinvokable]
318 fn compose(self: Pin<&mut Sioul>, kind: &QString, key: &QString, account: &QString) -> QString;
319
320 #[qinvokable]
322 fn draft(self: &Sioul, id: &QString) -> QString;
323
324 #[qinvokable]
326 fn save_draft(self: Pin<&mut Sioul>, edit: &QString) -> QString;
327
328 #[qinvokable]
330 fn attach(self: Pin<&mut Sioul>, id: &QString, url: &QString) -> QString;
331
332 #[qinvokable]
334 fn detach(self: Pin<&mut Sioul>, id: &QString, index: i32, forwarded: bool);
335
336 #[qinvokable]
338 fn preview(self: &Sioul, markdown: &QString) -> QString;
339
340 #[qinvokable]
342 fn send(self: Pin<&mut Sioul>, id: &QString) -> QString;
343
344 #[qinvokable]
346 fn discard(self: Pin<&mut Sioul>, id: &QString);
347
348 #[qinvokable]
350 fn draft_closed(self: Pin<&mut Sioul>, id: &QString);
351
352 #[qinvokable]
354 fn place(self: &Sioul, key: &QString) -> QString;
355
356 #[qinvokable]
358 fn source(self: &Sioul, key: &QString) -> QString;
359
360 #[qinvokable]
362 fn set_writing(self: Pin<&mut Sioul>, id: &QString, name: &QString, signature: &QString);
363
364 #[qinvokable]
367 fn grab_folder(self: &Sioul) -> QString;
368
369 #[qinvokable]
371 fn search_contacts(self: Pin<&mut Sioul>, query: &QString);
372
373 #[qinvokable]
375 fn contact(self: &Sioul, key: &QString) -> QString;
376
377 #[qinvokable]
380 fn save_contact(self: Pin<&mut Sioul>, key: &QString, edit: &QString) -> QString;
381
382 #[qinvokable]
384 fn delete_contact(self: Pin<&mut Sioul>, key: &QString);
385
386 #[qinvokable]
388 fn show_contacts_category(self: Pin<&mut Sioul>, category: &QString);
389
390 #[qinvokable]
392 fn find_duplicates(self: Pin<&mut Sioul>);
393
394 #[qinvokable]
397 fn clean_contacts(self: Pin<&mut Sioul>, keys: &QString) -> QString;
398
399 #[qinvokable]
401 fn merge_contacts(self: Pin<&mut Sioul>, lead: &QString, other: &QString) -> QString;
402
403 #[qinvokable]
405 fn not_same_contacts(self: Pin<&mut Sioul>, first: &QString, second: &QString) -> QString;
406
407 #[qinvokable]
409 fn undo_contacts(self: Pin<&mut Sioul>, id: &QString) -> QString;
410
411 #[qinvokable]
413 fn show_days(self: Pin<&mut Sioul>, from: &QString, days: i32, upcoming: bool);
414
415 #[qinvokable]
417 fn calendars(self: &Sioul) -> QString;
418
419 #[qinvokable]
421 fn event_form(self: &Sioul, key: &QString) -> QString;
422
423 #[qinvokable]
426 fn save_event(self: Pin<&mut Sioul>, key: &QString, edit: &QString, calendar: &QString) -> QString;
427
428 #[qinvokable]
431 fn event_reminder(self: &Sioul, key: &QString, start: f64) -> QString;
432
433 #[qinvokable]
436 fn reminder_choices(self: &Sioul, current: &QString) -> QString;
437
438 #[qinvokable]
441 fn set_event_reminder(self: Pin<&mut Sioul>, key: &QString, value: &QString) -> QString;
442
443 #[qinvokable]
445 fn delete_event(self: Pin<&mut Sioul>, key: &QString, start: f64, only_this: bool);
446
447 #[qinvokable]
451 fn move_event(self: Pin<&mut Sioul>, key: &QString, start: f64, new_start: f64, new_end: f64, only_this: bool) -> QString;
452
453 #[qinvokable]
455 fn completions(self: &Sioul, typed: &QString) -> QString;
456
457 #[qinvokable]
459 fn contact_for(self: &Sioul, address: &QString) -> QString;
460
461 #[qinvokable]
463 fn add_sender(self: Pin<&mut Sioul>, name: &QString, address: &QString);
464
465 #[qinvokable]
467 fn answer_invitation(self: Pin<&mut Sioul>, key: &QString, answer: &QString);
468
469 #[qinvokable]
471 fn history_weeks(self: &Sioul) -> i32;
472
473 #[qinvokable]
475 fn day_start(self: &Sioul) -> i32;
476
477 #[qinvokable]
479 fn set_history(self: Pin<&mut Sioul>, weeks: i32);
480
481 #[qinvokable]
483 fn add_dav(self: Pin<&mut Sioul>, address: &QString, url: &QString, login: &QString, password: &QString);
484
485 #[qinvokable]
488 fn add_google(self: Pin<&mut Sioul>, address: &QString, client_id: &QString, client_secret: &QString, again: &QString);
489
490 #[qinvokable]
492 fn cancel_google(self: Pin<&mut Sioul>);
493
494 #[qinvokable]
499 fn add_google_mail(self: Pin<&mut Sioul>, address: &QString, client_id: &QString, client_secret: &QString, again: &QString);
500
501 #[qinvokable]
503 fn pgp_keys(self: &Sioul) -> QString;
504
505 #[qinvokable]
507 fn pgp_generate(self: Pin<&mut Sioul>, address: &QString);
508
509 #[qinvokable]
511 fn pgp_import(self: Pin<&mut Sioul>, url: &QString, passphrase: &QString) -> QString;
512
513 #[qinvokable]
515 fn pgp_export(self: Pin<&mut Sioul>, fingerprint: &QString);
516
517 #[qinvokable]
519 fn pgp_save_revocation(self: Pin<&mut Sioul>, fingerprint: &QString);
520
521 #[qinvokable]
523 fn pgp_remove(self: Pin<&mut Sioul>, fingerprint: &QString);
524
525 #[qinvokable]
527 fn pgp_lookup(self: Pin<&mut Sioul>, draft: &QString);
528
529 #[qinvokable]
533 fn security_key_for_draft(self: &Sioul, id: &QString) -> QString;
534
535 #[qinvokable]
539 fn sign_and_send(self: Pin<&mut Sioul>, id: &QString, pin: &QString);
540
541 #[qinvokable]
544 fn open_with_security_key(self: Pin<&mut Sioul>, key: &QString, pin: &QString);
545
546 #[qinvokable]
548 fn security_key_not_now(self: Pin<&mut Sioul>, context: &QString);
549
550 #[qinvokable]
552 fn let_gnupg_release(self: Pin<&mut Sioul>) -> QString;
553
554 #[qinvokable]
556 fn security_keys(self: &Sioul) -> QString;
557
558 #[qinvokable]
560 fn read_security_key(self: Pin<&mut Sioul>);
561
562 #[qinvokable]
565 fn find_security_key_certificate(self: Pin<&mut Sioul>, ident: &QString);
566
567 #[qinvokable]
569 fn import_security_key_certificate(self: Pin<&mut Sioul>, ident: &QString, url: &QString);
570
571 #[qinvokable]
574 fn import_security_key_from_gnupg(self: Pin<&mut Sioul>, ident: &QString);
575
576 #[qinvokable]
578 fn renew_security_key(self: Pin<&mut Sioul>, ident: &QString);
579
580 #[qinvokable]
582 fn send_security_key_to_keys_openpgp(self: Pin<&mut Sioul>, ident: &QString);
583
584 #[qinvokable]
586 fn forget_security_key_pin(self: Pin<&mut Sioul>);
587
588 #[qinvokable]
590 fn stop_using_security_key(self: Pin<&mut Sioul>, ident: &QString);
591
592 #[qinvokable]
594 fn show_tasks(self: Pin<&mut Sioul>, by: &QString, done: bool, query: &QString, project_id: &QString);
595
596 #[qinvokable]
598 fn filter_tasks(self: Pin<&mut Sioul>, kind: &QString, category: &QString);
599
600 #[qinvokable]
602 fn show_tasks_anyway(self: Pin<&mut Sioul>, on: bool);
603
604 #[qinvokable]
606 fn refresh_work(self: Pin<&mut Sioul>);
607
608 #[qinvokable]
610 fn task(self: &Sioul, uid: &QString) -> QString;
611
612 #[qinvokable]
617 fn new_task_form(self: &Sioul, from: &QString, key: &QString, start: f64, list: &QString) -> QString;
618
619 #[qinvokable]
622 fn save_task(self: Pin<&mut Sioul>, uid: &QString, edit: &QString, list: &QString) -> QString;
623
624 #[qinvokable]
626 fn capture(self: &Sioul, line: &QString) -> QString;
627
628 #[qinvokable]
630 fn add_task(self: Pin<&mut Sioul>, line: &QString, parent: &QString, list: &QString) -> QString;
631
632 #[qinvokable]
634 fn set_task_status(self: Pin<&mut Sioul>, uid: &QString, status: &QString) -> QString;
635
636 #[qinvokable]
639 fn set_felt(self: Pin<&mut Sioul>, uid: &QString, felt: &QString) -> QString;
640
641 #[qinvokable]
644 fn set_waits(self: Pin<&mut Sioul>, uid: &QString, other: &QString, wait: bool, gap_minutes: i32) -> QString;
645
646 #[qinvokable]
648 fn not_now(self: Pin<&mut Sioul>, uid: &QString);
649
650 #[qinvokable]
652 fn set_weather(self: Pin<&mut Sioul>, weather: &QString);
653
654 #[qinvokable]
656 fn delete_task(self: Pin<&mut Sioul>, uid: &QString);
657
658 #[qinvokable]
663 fn set_task_at(self: Pin<&mut Sioul>, uid: &QString, at: &QString) -> QString;
664
665 #[qinvokable]
670 fn pin_task(self: Pin<&mut Sioul>, uid: &QString, at: &QString, minutes: i32) -> QString;
671
672 #[qinvokable]
674 fn search_tasks(self: &Sioul, query: &QString, except: &QString) -> QString;
675
676 #[qinvokable]
678 fn new_list(self: Pin<&mut Sioul>, account: &QString, name: &QString) -> QString;
679
680 #[qinvokable]
682 fn list_accounts(self: &Sioul) -> QString;
683
684 #[qinvokable]
686 fn focus_start(self: Pin<&mut Sioul>, uid: &QString, minutes: i32);
687
688 #[qinvokable]
690 fn focus_pause(self: Pin<&mut Sioul>);
691
692 #[qinvokable]
694 fn focus_extend(self: Pin<&mut Sioul>, minutes: i32);
695
696 #[qinvokable]
698 fn focus_stop(self: Pin<&mut Sioul>, done: bool, note: &QString) -> QString;
699
700 #[qinvokable]
702 fn search_notes(self: Pin<&mut Sioul>, query: &QString);
703
704 #[qinvokable]
706 fn note(self: &Sioul, path: &QString) -> QString;
707
708 #[qinvokable]
712 fn save_note(self: Pin<&mut Sioul>, path: &QString, text: &QString, stamp: &QString) -> QString;
713
714 #[qinvokable]
716 fn create_note(self: Pin<&mut Sioul>, title: &QString, links: &QString) -> QString;
717
718 #[qinvokable]
720 fn related(self: &Sioul, uri: &QString) -> QString;
721
722 #[qinvokable]
724 fn link_things(self: Pin<&mut Sioul>, from: &QString, to: &QString) -> QString;
725
726 #[qinvokable]
728 fn unlink_things(self: Pin<&mut Sioul>, from: &QString, to: &QString) -> QString;
729
730 #[qinvokable]
732 fn search_things(self: &Sioul, query: &QString, kind: &QString, from: &QString) -> QString;
733
734 #[qinvokable]
736 fn uri_of(self: &Sioul, kind: &QString, id: &QString) -> QString;
737
738 #[qinvokable]
741 fn is_program(self: &Sioul, name: &QString) -> bool;
742
743 #[qinvokable]
745 fn first_frame(self: &Sioul);
746
747 #[qinvokable]
749 fn mark(self: &Sioul, what: &QString);
750
751 #[qinvokable]
753 fn phone_accounts(self: &Sioul) -> bool;
754
755 #[qinvokable]
757 fn choose_phone_account(self: Pin<&mut Sioul>);
758
759 #[qinvokable]
762 fn budget_detail(self: &Sioul, id: &QString, anchor: &QString, step: &QString) -> QString;
763
764 #[qinvokable]
767 fn keep_folder(self: Pin<&mut Sioul>, account: &QString, folder: &QString, kept: bool) -> QString;
768
769 #[qinvokable]
771 fn create_folder(self: Pin<&mut Sioul>, account: &QString, name: &QString);
772
773 #[qinvokable]
775 fn delete_folder(self: Pin<&mut Sioul>, account: &QString, folder: &QString);
776
777 #[qinvokable]
779 fn noise_url(self: &Sioul, kind: &QString) -> QString;
780
781 #[qinvokable]
783 fn calm_sounds(self: &Sioul) -> QString;
784
785 #[qinvokable]
788 fn move_task(self: Pin<&mut Sioul>, uid: &QString, list: &QString, confirmed: bool) -> QString;
789
790 #[qinvokable]
793 fn move_contact(self: Pin<&mut Sioul>, key: &QString, book: &QString, confirmed: bool) -> QString;
794
795 #[qinvokable]
797 fn address_books(self: &Sioul) -> QString;
798
799 #[qinvokable]
801 fn find_places(self: Pin<&mut Sioul>, name: &QString);
802
803 #[qinvokable]
805 fn set_weather_place(self: Pin<&mut Sioul>, name: &QString, latitude: f64, longitude: f64);
806
807 #[qinvokable]
809 fn health_page(self: &Sioul) -> QString;
810
811 #[qinvokable]
813 fn save_medicine(self: Pin<&mut Sioul>, id: &QString, edit: &QString) -> QString;
814
815 #[qinvokable]
817 fn save_prescription(self: Pin<&mut Sioul>, id: &QString, edit: &QString) -> QString;
818
819 #[qinvokable]
822 fn health_for_professional(self: &Sioul, prescription: &QString) -> QString;
823
824 #[qinvokable]
826 fn remove_health(self: Pin<&mut Sioul>, id: &QString) -> QString;
827
828 #[qinvokable]
830 fn refilled(self: Pin<&mut Sioul>, id: &QString) -> QString;
831
832 #[qinvokable]
835 fn set_dose_taken(self: Pin<&mut Sioul>, key: &QString, taken: bool);
836
837 #[qinvokable]
839 fn dose_not_taken(self: Pin<&mut Sioul>, key: &QString);
840
841 #[qinvokable]
844 fn dose_taken_at(self: Pin<&mut Sioul>, key: &QString, time: &QString) -> QString;
845
846 #[qinvokable]
848 fn dose_info(self: &Sioul, key: &QString) -> QString;
849
850 #[qinvokable]
853 fn dose_choose(self: Pin<&mut Sioul>, key: &QString, taken: bool) -> QString;
854
855 #[qinvokable]
859 fn device_off(self: Pin<&mut Sioul>, id: &QString, off: bool) -> QString;
860
861 #[qinvokable]
863 fn device_forget(self: Pin<&mut Sioul>, id: &QString) -> QString;
864
865 #[qinvokable]
867 fn needs(self: &Sioul) -> QString;
868
869 #[qinvokable]
871 fn save_needs(self: Pin<&mut Sioul>, edit: &QString, shown: &QString) -> QString;
872
873 #[qinvokable]
875 fn skip_need(self: Pin<&mut Sioul>, key: &QString, skip: bool) -> QString;
876
877 #[qinvokable]
879 fn needs_today(self: &Sioul) -> QString;
880
881 #[qinvokable]
883 fn refresh_health(self: Pin<&mut Sioul>);
884
885 #[qinvokable]
888 fn show_health_week(self: Pin<&mut Sioul>, day: &QString);
889
890 #[qinvokable]
893 fn change_need(self: Pin<&mut Sioul>, edit: &QString) -> QString;
894
895 #[qinvokable]
897 fn drag_need(self: Pin<&mut Sioul>, edit: &QString) -> QString;
898
899 #[qinvokable]
901 fn health_settings(self: &Sioul) -> QString;
902
903 #[qinvokable]
906 fn wake_settings(self: &Sioul, which: &QString);
907
908 #[qinvokable]
911 fn try_wake(self: &Sioul) -> QString;
912
913 #[qinvokable]
915 fn set_overlap_aside(self: Pin<&mut Sioul>, key: &QString) -> QString;
916
917 #[qinvokable]
920 fn move_need(self: Pin<&mut Sioul>, key: &QString, minutes: i32, time: &QString) -> QString;
921
922 #[qinvokable]
924 fn needs_later(self: &Sioul) -> i32;
925
926 #[qinvokable]
929 fn missed_doses(self: &Sioul) -> QString;
930
931 #[qinvokable]
933 fn touch(self: Pin<&mut Sioul>);
934
935 #[qinvokable]
938 fn going_away(self: Pin<&mut Sioul>);
939
940 #[qinvokable]
942 fn back_here(self: Pin<&mut Sioul>);
943
944 #[qinvokable]
946 fn set_health(self: Pin<&mut Sioul>, key: &QString, value: &QString) -> QString;
947
948 #[qinvokable]
950 fn movement_minutes(self: &Sioul) -> i32;
951
952 #[qinvokable]
954 fn chat_minute(self: Pin<&mut Sioul>) -> bool;
955
956 #[qinvokable]
958 fn chats_covered(self: &Sioul) -> bool;
959
960 #[qinvokable]
962 fn memo_url(self: &Sioul) -> QString;
963
964 #[qinvokable]
966 fn memo_recorded(self: Pin<&mut Sioul>, url: &QString) -> QString;
967
968 #[qinvokable]
971 fn trash_note(self: Pin<&mut Sioul>, path: &QString) -> QString;
972
973 #[qinvokable]
975 fn rename_note(self: Pin<&mut Sioul>, path: &QString, name: &QString) -> QString;
976
977 #[qinvokable]
979 fn create_note_in(self: Pin<&mut Sioul>, folder: &QString, title: &QString) -> QString;
980
981 #[qinvokable]
983 fn make_folder(self: Pin<&mut Sioul>, parent: &QString, name: &QString) -> QString;
984
985 #[qinvokable]
987 fn rename_folder(self: Pin<&mut Sioul>, path: &QString, name: &QString) -> QString;
988
989 #[qinvokable]
991 fn remove_folder(self: Pin<&mut Sioul>, path: &QString) -> QString;
992
993 #[qinvokable]
995 fn remove_project(self: Pin<&mut Sioul>, id: &QString) -> QString;
996
997 #[qinvokable]
999 fn save_budget(self: Pin<&mut Sioul>, id: &QString, edit: &QString) -> QString;
1000
1001 #[qinvokable]
1003 fn remove_budget(self: Pin<&mut Sioul>, what: &QString) -> QString;
1004
1005 #[qinvokable]
1009 fn remove_line(self: Pin<&mut Sioul>, uri: &QString, shown: &QString) -> QString;
1010
1011 #[qinvokable]
1013 fn set_notes_tree(self: Pin<&mut Sioul>, tree: bool);
1014
1015 #[qinvokable]
1017 fn view_flag(self: &Sioul, name: &QString) -> bool;
1018
1019 #[qinvokable]
1020 fn set_view_flag(self: Pin<&mut Sioul>, name: &QString, value: bool);
1021
1022
1023 #[qinvokable]
1025 fn add_movement(self: Pin<&mut Sioul>, edit: &QString) -> QString;
1026
1027 #[qinvokable]
1030 fn export_time_csv(self: Pin<&mut Sioul>, project: &QString, from: &QString, to: &QString, target: &QString) -> QString;
1031
1032 #[qinvokable]
1034 fn letters(self: &Sioul) -> QString;
1035
1036 #[qinvokable]
1038 fn letter_task(self: Pin<&mut Sioul>, id: &QString) -> QString;
1039
1040 #[qinvokable]
1042 fn letter_event(self: Pin<&mut Sioul>, id: &QString) -> QString;
1043
1044 #[qinvokable]
1046 fn letter_done(self: Pin<&mut Sioul>, id: &QString, project: &QString) -> QString;
1047
1048 #[qinvokable]
1050 fn bank(self: &Sioul) -> QString;
1051
1052 #[qinvokable]
1054 fn import_bank(self: Pin<&mut Sioul>, file: &QString) -> QString;
1055
1056 #[qinvokable]
1058 fn import_bank_into(self: Pin<&mut Sioul>, file: &QString, account: &QString) -> QString;
1059
1060 #[qinvokable]
1062 fn save_bank_account(self: Pin<&mut Sioul>, id: &QString, edit: &QString) -> QString;
1063
1064 #[qinvokable]
1065 fn remove_bank_account(self: Pin<&mut Sioul>, id: &QString) -> QString;
1066
1067 #[qinvokable]
1069 fn place_movement(self: Pin<&mut Sioul>, account: &QString, movement: &QString, choice: &QString) -> QString;
1070
1071 #[qinvokable]
1074 fn save_bank_rule(self: Pin<&mut Sioul>, rule: &QString, edit: &QString) -> QString;
1075
1076 #[qinvokable]
1077 fn remove_bank_rule(self: Pin<&mut Sioul>, rule: &QString) -> QString;
1078
1079 #[qinvokable]
1081 fn save_reserve(self: Pin<&mut Sioul>, id: &QString, edit: &QString) -> QString;
1082
1083 #[qinvokable]
1085 fn contracts(self: &Sioul) -> QString;
1086
1087 #[qinvokable]
1089 fn save_contract(self: Pin<&mut Sioul>, id: &QString, edit: &QString) -> QString;
1090
1091 #[qinvokable]
1093 fn remove_contract(self: Pin<&mut Sioul>, id: &QString) -> QString;
1094
1095 #[qinvokable]
1097 fn contract_letter(self: Pin<&mut Sioul>, id: &QString) -> QString;
1098
1099 #[qinvokable]
1101 fn contract_from(self: &Sioul, subject: &QString, from: &QString) -> QString;
1102
1103 #[qinvokable]
1105 fn routines(self: &Sioul) -> QString;
1106
1107 #[qinvokable]
1109 fn save_routine(self: Pin<&mut Sioul>, id: &QString, title: &QString, steps: &QString, by_itself: bool) -> QString;
1110
1111 #[qinvokable]
1113 fn remove_routine(self: Pin<&mut Sioul>, id: &QString) -> QString;
1114
1115 #[qinvokable]
1117 fn share_status(self: &Sioul, folder: &QString) -> QString;
1118
1119 #[qinvokable]
1121 fn start_sharing(self: Pin<&mut Sioul>, folder: &QString, passphrase: &QString, again: &QString) -> QString;
1122
1123 #[qinvokable]
1125 fn stop_sharing(self: Pin<&mut Sioul>) -> QString;
1126
1127 #[qinvokable]
1129 fn share_now(self: Pin<&mut Sioul>);
1130
1131 #[qinvokable]
1133 fn share_candidates(self: &Sioul) -> QString;
1134
1135 #[qinvokable]
1137 fn files_access(self: &Sioul) -> bool;
1138
1139 #[qinvokable]
1141 fn set_share_projects(self: Pin<&mut Sioul>, on: bool) -> QString;
1142
1143 #[qinvokable]
1145 fn set_share_part(self: Pin<&mut Sioul>, part: &QString, on: bool) -> QString;
1146
1147 #[qinvokable]
1150 fn set_notes_carried(self: Pin<&mut Sioul>, carried: bool) -> QString;
1151
1152 #[qinvokable]
1156 fn share_history(self: Pin<&mut Sioul>, filter: &QString);
1157
1158 #[qinvokable]
1160 fn share_put_back_preview(self: &Sioul, part: &QString, file: &QString, stamp: &QString) -> QString;
1161
1162 #[qinvokable]
1165 fn share_put_back(self: Pin<&mut Sioul>, part: &QString, file: &QString, stamp: &QString);
1166
1167 #[qinvokable]
1170 fn share_confirm_gone(self: Pin<&mut Sioul>, store: &QString) -> QString;
1171
1172 #[qinvokable]
1175 fn share_estimate(self: Pin<&mut Sioul>, part: &QString);
1176
1177 #[qinvokable]
1179 fn spam_status(self: &Sioul) -> QString;
1180
1181 #[qinvokable]
1185 fn spam_train(self: Pin<&mut Sioul>) -> QString;
1186
1187 #[qinvokable]
1189 fn spam_stop(self: Pin<&mut Sioul>);
1190
1191 #[qinvokable]
1194 fn mail_filters(self: &Sioul) -> QString;
1195
1196 #[qinvokable]
1201 fn set_mail_filters(self: Pin<&mut Sioul>, filters: &QString, shown: &QString) -> QString;
1202
1203 #[qinvokable]
1205 fn mail_filter_said(self: &Sioul, filter: &QString) -> QString;
1206
1207 #[qinvokable]
1210 fn mail_filter_try(self: Pin<&mut Sioul>, filter: &QString);
1211
1212 #[qinvokable]
1215 fn mail_filters_preview(self: Pin<&mut Sioul>);
1216
1217 #[qinvokable]
1219 fn mail_filters_run(self: Pin<&mut Sioul>) -> QString;
1220
1221 #[qinvokable]
1225 fn new_mail_filter(self: Pin<&mut Sioul>, given: &QString) -> QString;
1226
1227 #[qinvokable]
1230 fn set_share_backup(self: Pin<&mut Sioul>, on: bool) -> QString;
1231
1232 #[qinvokable]
1236 fn set_share_send(self: Pin<&mut Sioul>, on: bool) -> QString;
1237
1238 #[qinvokable]
1242 fn share_send_again(self: Pin<&mut Sioul>);
1243
1244 #[qinvokable]
1248 fn share_backup_place(self: Pin<&mut Sioul>, place: &QString) -> QString;
1249
1250 #[qinvokable]
1255 fn start_sharing_on_server(self: Pin<&mut Sioul>, account: &QString, place: &QString, passphrase: &QString, again: &QString);
1256
1257 #[qinvokable]
1259 fn folders_in(self: &Sioul, path: &QString) -> QString;
1260
1261 #[qinvokable]
1263 fn ask_files_access(self: Pin<&mut Sioul>);
1264
1265 #[qinvokable]
1269 fn change_line(self: Pin<&mut Sioul>, uri: &QString, shown: &QString, label: &QString, amount: f64, date: &QString) -> QString;
1270
1271 #[qinvokable]
1273 fn site_list(self: &Sioul) -> QString;
1274
1275 #[qinvokable]
1277 fn site_notified(self: Pin<&mut Sioul>, id: &QString, title: &QString, text: &QString);
1278
1279 #[qinvokable]
1281 fn site_seen(self: Pin<&mut Sioul>, id: &QString);
1282
1283 #[qinvokable]
1285 fn site_notices(self: &Sioul) -> QString;
1286
1287 #[qinvokable]
1289 fn site_for_sender(self: &Sioul, sender: &QString) -> QString;
1290
1291 #[qinvokable]
1293 fn set_site(self: Pin<&mut Sioul>, id: &QString, field: &QString, value: &QString) -> QString;
1294
1295 #[qinvokable]
1297 fn site_presets(self: &Sioul, country: &QString, region: &QString) -> QString;
1298
1299 #[qinvokable]
1301 fn add_site(self: Pin<&mut Sioul>, edit: &QString) -> QString;
1302
1303 #[qinvokable]
1305 fn site_presets_tree(self: &Sioul) -> QString;
1306
1307 #[qinvokable]
1309 fn add_sites(self: Pin<&mut Sioul>, rows: &QString) -> QString;
1310
1311 #[qinvokable]
1313 fn move_site(self: Pin<&mut Sioul>, id: &QString, delta: i32) -> QString;
1314
1315 #[qinvokable]
1317 fn remove_site(self: Pin<&mut Sioul>, id: &QString) -> QString;
1318
1319 #[qinvokable]
1321 fn google_built_in(self: &Sioul) -> bool;
1322
1323 #[qinvokable]
1325 fn call_devices(self: &Sioul) -> QString;
1326
1327 #[qinvokable]
1329 fn set_call_device(self: Pin<&mut Sioul>, which: &QString, name: &QString) -> QString;
1330
1331 #[qinvokable]
1334 fn calls_script(self: &Sioul) -> QString;
1335
1336 #[qinvokable]
1338 fn bitwarden_state(self: &Sioul) -> QString;
1339
1340 #[qinvokable]
1344 fn bitwarden_unlock(self: Pin<&mut Sioul>, password: &QString, provider: i32, code: &QString) -> i32;
1345
1346 #[qinvokable]
1348 fn bitwarden_lock(self: Pin<&mut Sioul>);
1349
1350 #[qinvokable]
1353 fn bitwarden_send_code(self: Pin<&mut Sioul>, password: &QString) -> i32;
1354
1355 #[qinvokable]
1358 fn bitwarden_passkey_begin(self: Pin<&mut Sioul>) -> i32;
1359
1360 #[qinvokable]
1363 fn bitwarden_passkey(self: Pin<&mut Sioul>, answer: &QString) -> i32;
1364
1365 #[qinvokable]
1369 fn bitwarden_search(self: Pin<&mut Sioul>, url: &QString, site: &QString, user: &QString) -> QString;
1370
1371 #[qinvokable]
1373 fn bitwarden_site(self: &Sioul, url: &QString) -> QString;
1374
1375 #[qinvokable]
1377 fn bitwarden_login(self: &Sioul, url: &QString, id: &QString) -> QString;
1378
1379 #[qinvokable]
1381 fn map_view(self: &Sioul) -> QString;
1382
1383 #[qinvokable]
1385 fn place_of(self: &Sioul, address: &QString) -> QString;
1386
1387 #[qinvokable]
1389 fn locate_addresses(self: Pin<&mut Sioul>, allow: bool);
1390
1391 #[qinvokable]
1393 fn refresh_mode(self: Pin<&mut Sioul>);
1394
1395 #[qinvokable]
1397 fn work_a_while(self: Pin<&mut Sioul>, minutes: i32);
1398
1399 #[qinvokable]
1403 fn done_for_the_day(self: Pin<&mut Sioul>) -> QString;
1404
1405 #[qinvokable]
1408 fn day_review(self: &Sioul, kind: &QString) -> QString;
1409
1410 #[qinvokable]
1413 fn close_review(self: Pin<&mut Sioul>, kind: &QString, felt: &QString, mix: &QString, memo: &QString) -> QString;
1414
1415 #[qinvokable]
1417 fn day_review_line(self: &Sioul, date: &QString) -> QString;
1418
1419 #[qinvokable]
1421 fn set_first_step(self: Pin<&mut Sioul>, text: &QString);
1422
1423 #[qinvokable]
1425 fn clear_first_step(self: Pin<&mut Sioul>);
1426
1427 #[qinvokable]
1429 fn usual_hours(self: Pin<&mut Sioul>);
1430
1431 #[qinvokable]
1434 fn set_work_now(self: Pin<&mut Sioul>, on: bool);
1435
1436 #[qinvokable]
1439 fn set_free_time(self: Pin<&mut Sioul>, on: bool) -> QString;
1440
1441 #[qinvokable]
1443 fn keep_usual_end(self: Pin<&mut Sioul>);
1444
1445 #[qinvokable]
1447 fn set_free_nothing(self: Pin<&mut Sioul>, on: bool);
1448
1449 #[qinvokable]
1451 fn pause_now(self: Pin<&mut Sioul>);
1452
1453 #[qinvokable]
1455 fn come_back(self: Pin<&mut Sioul>) -> QString;
1456
1457 #[qinvokable]
1459 fn lighten_tomorrow(self: Pin<&mut Sioul>);
1460
1461 #[qinvokable]
1463 fn forget_pause(self: Pin<&mut Sioul>);
1464
1465 #[qinvokable]
1467 fn pause_screen(self: &Sioul) -> QString;
1468
1469 #[qinvokable]
1471 fn pause_setup(self: &Sioul) -> QString;
1472
1473 #[qinvokable]
1475 fn open_dnd(self: &Sioul, key: &QString);
1476
1477 #[qinvokable]
1480 fn dnd_toggle(self: Pin<&mut Sioul>, on: bool, minutes: i32);
1481
1482 #[qinvokable]
1484 fn dnd_setup(self: &Sioul) -> QString;
1485
1486 #[qinvokable]
1488 fn dnd_change(self: Pin<&mut Sioul>, verb: &QString, json: &QString) -> QString;
1489
1490 #[qinvokable]
1492 fn app_notes_setup(self: &Sioul) -> QString;
1493
1494 #[qinvokable]
1496 fn app_notes_change(self: &Sioul, verb: &QString, json: &QString) -> QString;
1497
1498 #[qinvokable]
1501 fn calls_through(self: Pin<&mut Sioul>, on: bool, minutes: i32);
1502
1503 #[qinvokable]
1505 fn calls_view(self: &Sioul) -> QString;
1506
1507 #[qinvokable]
1509 fn calls_action(self: Pin<&mut Sioul>, verb: &QString, json: &QString) -> QString;
1510
1511 #[qinvokable]
1513 fn calls_setup(self: &Sioul) -> QString;
1514
1515 #[qinvokable]
1519 fn phone_messages(self: Pin<&mut Sioul>, verb: &QString, json: &QString) -> QString;
1520
1521 #[qinvokable]
1524 fn texts(self: Pin<&mut Sioul>, verb: &QString, json: &QString) -> QString;
1525
1526 #[qinvokable]
1529 fn reaches_view(self: &Sioul) -> QString;
1530
1531 #[qinvokable]
1535 fn reaches_set(self: Pin<&mut Sioul>, row: &QString, words: &QString, shown: &QString) -> QString;
1536
1537 #[qinvokable]
1539 fn reaches_preset(self: Pin<&mut Sioul>, id: &QString) -> QString;
1540
1541 #[qinvokable]
1544 fn person_sheet(self: &Sioul, key: &QString, addresses: &QString) -> QString;
1545
1546 #[qinvokable]
1548 fn person_sheet_change(self: Pin<&mut Sioul>, key: &QString, addresses: &QString, verb: &QString, value: &QString) -> QString;
1549
1550 #[qinvokable]
1552 fn calls_setup_change(self: Pin<&mut Sioul>, verb: &QString, json: &QString) -> QString;
1553
1554 #[qinvokable]
1556 fn project_rows(self: &Sioul) -> QString;
1557
1558 #[qinvokable]
1560 fn project_page(self: &Sioul, id: &QString) -> QString;
1561
1562 #[qinvokable]
1564 fn save_project(self: Pin<&mut Sioul>, id: &QString, edit: &QString) -> QString;
1565
1566 #[qinvokable]
1568 fn time_page(self: &Sioul, period: &QString, anchor: &QString, project: &QString) -> QString;
1569
1570 #[qinvokable]
1572 fn note_time(self: Pin<&mut Sioul>, edit: &QString) -> QString;
1573
1574 #[qinvokable]
1576 fn remove_time(self: Pin<&mut Sioul>, key: &QString) -> QString;
1577
1578 #[qinvokable]
1580 fn stopped(self: &Sioul) -> QString;
1581
1582 #[qinvokable]
1584 fn set_stopped(self: Pin<&mut Sioul>, text: &QString) -> QString;
1585
1586 #[qinvokable]
1589 fn change_time(self: Pin<&mut Sioul>, key: &QString, edit: &QString) -> QString;
1590
1591 #[qinvokable]
1593 fn task_choices(self: &Sioul, keep: &QString) -> QString;
1594
1595 #[qinvokable]
1597 fn make_invoice(self: Pin<&mut Sioul>, id: &QString) -> QString;
1598
1599 #[qinvokable]
1601 fn take_invoices(self: Pin<&mut Sioul>) -> QString;
1602
1603 #[qinvokable]
1605 fn invoice_again(self: &Sioul, number: &QString) -> QString;
1606
1607 #[qinvokable]
1609 fn set_invoice_paid(self: Pin<&mut Sioul>, number: &QString, paid: bool) -> QString;
1610
1611 #[qinvokable]
1613 fn make_linked(self: Pin<&mut Sioul>, kind: &QString, from: &QString, key: &QString, start: f64) -> QString;
1614
1615 #[qinvokable]
1617 fn task_from_mail(self: Pin<&mut Sioul>, key: &QString) -> QString;
1618
1619 #[qinvokable]
1621 fn note_from_mail(self: Pin<&mut Sioul>, key: &QString) -> QString;
1622
1623 #[qinvokable]
1625 fn note_from_event(self: Pin<&mut Sioul>, key: &QString, start: f64) -> QString;
1626
1627 #[qinvokable]
1629 fn task_from_event(self: Pin<&mut Sioul>, key: &QString, start: f64) -> QString;
1630
1631 #[qinvokable]
1633 fn task_from_line(self: Pin<&mut Sioul>, path: &QString, line: i32) -> QString;
1634
1635 #[qinvokable]
1637 fn draft_for_task(self: Pin<&mut Sioul>, uid: &QString) -> QString;
1638
1639 #[qinvokable]
1641 fn mail_note(self: Pin<&mut Sioul>, path: &QString) -> QString;
1642
1643 #[qinvokable]
1645 fn settings(self: &Sioul, view: &QString) -> QString;
1646
1647 #[qinvokable]
1649 fn set_setting(self: Pin<&mut Sioul>, key: &QString, value: &QString) -> QString;
1650
1651 #[qinvokable]
1656 fn change_setting(self: Pin<&mut Sioul>, key: &QString, shown: &QString, value: &QString) -> QString;
1657
1658 #[qinvokable]
1660 fn forced_theme(self: &Sioul) -> QString;
1661
1662 #[qinvokable]
1664 fn set_realtime_mode(self: Pin<&mut Sioul>, on: bool);
1665
1666 #[qinvokable]
1668 fn grab_phone(self: &Sioul) -> bool;
1669
1670 #[qinvokable]
1673 fn grab_steps(self: &Sioul) -> QString;
1674 }
1675
1676 #[auto_cxx_name]
1677 unsafe extern "RustQt" {
1679 #[qsignal]
1681 fn copy_requested(self: Pin<&mut Sioul>, text: QString);
1682
1683 #[qsignal]
1685 fn account_added(self: Pin<&mut Sioul>);
1686
1687 #[qsignal]
1689 fn account_password_done(self: Pin<&mut Sioul>, id: QString, problem: QString);
1690
1691 #[qsignal]
1693 fn letters_changed(self: Pin<&mut Sioul>);
1694
1695 #[qsignal]
1698 fn task_done(self: Pin<&mut Sioul>, uid: QString);
1699
1700 #[qsignal]
1704 fn shared_in(self: Pin<&mut Sioul>, stores: QString);
1705
1706 #[qsignal]
1708 fn share_listed(self: Pin<&mut Sioul>, versions: QString);
1709
1710 #[qsignal]
1712 fn share_put_back_done(self: Pin<&mut Sioul>, problem: QString);
1713
1714 #[qsignal]
1716 fn share_estimated(self: Pin<&mut Sioul>, estimate: QString);
1717
1718 #[qsignal]
1720 fn spam_changed(self: Pin<&mut Sioul>, status: QString);
1721
1722 #[qsignal]
1724 fn mail_filters_found(self: Pin<&mut Sioul>, found: QString);
1725
1726 #[qsignal]
1731 fn message_read(self: Pin<&mut Sioul>, key: QString, reading: QString, invitation: QString);
1732
1733 #[qsignal]
1736 fn share_started(self: Pin<&mut Sioul>, problem: QString);
1737
1738 #[qsignal]
1740 fn reminder_opened(self: Pin<&mut Sioul>, kind: QString, uri: QString, key: QString);
1741
1742 #[qsignal]
1744 fn open_url(self: Pin<&mut Sioul>, url: QString);
1745
1746 #[qsignal]
1749 fn scan_unavailable(self: Pin<&mut Sioul>, key: QString, index: i32, what: i32, name: QString, hint: QString, big: bool);
1750
1751 #[qsignal]
1753 fn paper_kept(self: Pin<&mut Sioul>, file: QString, title: QString, kind: QString);
1754
1755 #[qsignal]
1758 fn compose_requested(self: Pin<&mut Sioul>, id: QString);
1759
1760 #[qsignal]
1764 fn draft_files_arrived(self: Pin<&mut Sioul>, id: QString, line: QString, problem: bool);
1765
1766 #[qsignal]
1768 fn keys_changed(self: Pin<&mut Sioul>);
1769
1770 #[qsignal]
1773 fn security_key_changed(self: Pin<&mut Sioul>, context: QString, state: QString);
1774
1775 #[qsignal]
1777 fn links_changed(self: Pin<&mut Sioul>);
1778
1779 #[qsignal]
1781 fn places_changed(self: Pin<&mut Sioul>);
1782
1783 #[qsignal]
1785 fn sites_changed(self: Pin<&mut Sioul>);
1786
1787 #[qsignal]
1789 fn bitwarden_found(self: Pin<&mut Sioul>, ticket: i32, found: QString);
1790
1791 #[qsignal]
1794 fn bitwarden_answered(self: Pin<&mut Sioul>, ticket: i32, answer: QString);
1795
1796 #[qsignal]
1799 fn phone_account_chosen(self: Pin<&mut Sioul>, name: QString, kind: QString);
1800 }
1801
1802 impl cxx_qt::Threading for Sioul {}
1803}
1804
1805use qobject::Sioul;
1806pub(crate) type QtThread = cxx_qt::CxxQtThread<Sioul>;
1807
1808#[cfg(target_os = "android")]
1810static PHONE_CHOOSER: std::sync::Mutex<Option<QtThread>> = std::sync::Mutex::new(None);
1811
1812#[cfg(target_os = "android")]
1813unsafe extern "C" {
1815 fn sioul_android_choose_account();
1817 fn sioul_android_files_access() -> bool;
1819 fn sioul_android_ask_files_access();
1821 pub(crate) fn sioul_android_broadcast(package: *const std::ffi::c_char, receiver: *const std::ffi::c_char, action: *const std::ffi::c_char);
1824}
1825
1826#[cfg(target_os = "android")]
1832#[unsafe(no_mangle)]
1834pub unsafe extern "C" fn sioul_android_account_chosen(name: *const std::ffi::c_char, kind: *const std::ffi::c_char) {
1835 let read = |text: *const std::ffi::c_char| if text.is_null() { String::new() } else { unsafe { std::ffi::CStr::from_ptr(text) }.to_string_lossy().into_owned() };
1837 let (name, kind) = (read(name), read(kind));
1838 let waiting = PHONE_CHOOSER.lock().ok().and_then(|mut w| w.take());
1839 if let Some(qt) = waiting {
1840 let _ = qt.queue(move |mut sioul| sioul.as_mut().phone_account_chosen(QString::from(&name), QString::from(&kind)));
1841 }
1842}
1843
1844#[derive(Default)]
1845pub struct SioulRust {
1846 porch: QString,
1847 budgets: QString,
1848 accounts: QString,
1849 blocked: QString,
1850 status: QString,
1851 busy: bool,
1852 found: QString,
1853 scouted: QString,
1854 scouting: bool,
1855 form_error: QString,
1856 form_busy: bool,
1857 mail_accounts: QString,
1858 mail_folder: QString,
1859 drafts: QString,
1860 mail_search: QString,
1861 undo_line: QString,
1862 contacts: QString,
1863 duplicates: QString,
1864 agenda: QString,
1865 overlaps: QString,
1866 health_view: QString,
1867 missed_view: QString,
1868 needs_view: QString,
1869 tasks: QString,
1870 notes: QString,
1871 focus_session: QString,
1872 reading: QString,
1873 realtime: bool,
1874 away: bool,
1875 mode: QString,
1876 forecast: QString,
1877 places_found: QString,
1878 shared: Arc<Shared>,
1879}
1880
1881#[derive(Default)]
1883pub(crate) struct Shared {
1884 opened_anyway: AtomicBool,
1886 pub(crate) statuses: Mutex<BTreeMap<String, (String, bool)>>,
1888 pub(crate) password_wanted: Mutex<BTreeSet<String>>,
1890 shown: Mutex<BTreeMap<String, ImapOrigin>>,
1892 pub(crate) watchers: Mutex<BTreeMap<String, Arc<Control>>>,
1894 learned: Mutex<BTreeSet<String>>,
1896 generation: AtomicU64,
1898 shown_generation: AtomicU64,
1899 clean: Mutex<BTreeSet<u64>>,
1901 pub(crate) open_folder: Mutex<Option<mail::OpenFolder>>,
1903 pub(crate) pending: Mutex<Vec<Arc<mail::Pending>>>,
1905 pub(crate) mail_generation: AtomicU64,
1906 pub(crate) mail_shown_generation: AtomicU64,
1907 said_at: Mutex<Option<std::time::Instant>>,
1910 pub(crate) pim: Mutex<pim::PimState>,
1912 pub(crate) pim_generation: AtomicU64,
1913 pub(crate) pim_shown_generation: AtomicU64,
1914 pub(crate) dav_watchers: Mutex<BTreeMap<String, Arc<Control>>>,
1916 pub(crate) work: Mutex<work::WorkState>,
1918 pub(crate) work_generation: AtomicU64,
1919 pub(crate) work_shown_generation: AtomicU64,
1920 pub(crate) loaded: Mutex<Option<Arc<sioul_core::links::Loaded>>>,
1922 pub(crate) realtime: AtomicBool,
1924 pub(crate) locating: AtomicBool,
1926 pub(crate) bitwarden: Mutex<Option<crate::sites::Vault>>,
1928 pub(crate) weather_fetching: AtomicBool,
1930 pub(crate) held_back: Mutex<std::collections::BTreeSet<String>>,
1932 pub(crate) google_stop: AtomicBool,
1934 pub(crate) github: Mutex<Option<Arc<sioul_sync::Control>>>,
1936 pub(crate) active: std::sync::atomic::AtomicI64,
1938 pub(crate) planned_at: std::sync::atomic::AtomicI64,
1940 pub(crate) planned_day: std::sync::atomic::AtomicI64,
1941 views_job: Job,
1943 pub(crate) mail_job: Job,
1944 pub(crate) pim_job: Job,
1945 pub(crate) work_job: Job,
1946 pub(crate) health_job: Job,
1947}
1948
1949const REPLAN_EVERY: i64 = 12 * 3600;
1952
1953const SAID_FOR: std::time::Duration = std::time::Duration::from_secs(30);
1955
1956pub(crate) fn config_path() -> PathBuf {
1957 config::default_path()
1958}
1959
1960pub(crate) fn load_config() -> Config {
1961 type Parsed = (PathBuf, u64, std::time::SystemTime, Config);
1966 static PARSED: Mutex<Option<Parsed>> = Mutex::new(None);
1967 let path = config_path();
1968 let settled = |time: &std::time::SystemTime| time.elapsed().is_ok_and(|age| age > std::time::Duration::from_secs(2));
1969 let stamp = std::fs::metadata(&path).ok().and_then(|m| Some((m.len(), m.modified().ok()?))).filter(|(_, time)| settled(time));
1970 if let Some((size, time)) = stamp
1971 && let Ok(parsed) = PARSED.lock()
1972 && let Some((p, s, t, config)) = parsed.as_ref()
1973 && *p == path
1974 && *s == size
1975 && *t == time
1976 {
1977 return config.clone();
1978 }
1979 let config = Config::load(&path).unwrap_or_default();
1980 sioul_core::words::set_current(sioul_core::words::Words::of(&config));
1982 if let (Some((size, time)), Ok(mut parsed)) = (stamp, PARSED.lock()) {
1983 *parsed = Some((path, size, time, config.clone()));
1984 }
1985 config
1986}
1987
1988pub(crate) fn tr() -> &'static Translator {
1990 static TRANSLATOR: OnceLock<Translator> = OnceLock::new();
1991 TRANSLATOR.get_or_init(|| Translator::new(&load_config().language.unwrap_or_else(session_language)))
1992}
1993
1994fn session_language() -> String {
1998 let said = ["LC_ALL", "LC_MESSAGES", "LANG"].iter().any(|v| std::env::var(v).is_ok_and(|l| !l.trim().is_empty()));
1999 if said {
2000 return i18n::system_language();
2001 }
2002 system_locale().unwrap_or_else(i18n::system_language)
2003}
2004
2005#[cfg(windows)]
2007fn system_locale() -> Option<String> {
2008 #[link(name = "kernel32")]
2009 unsafe extern "system" {
2011 fn GetUserDefaultLocaleName(name: *mut u16, size: i32) -> i32;
2012 }
2013 let mut name = [0u16; 85];
2015 let written = unsafe { GetUserDefaultLocaleName(name.as_mut_ptr(), name.len() as i32) };
2017 let length = usize::try_from(written).ok().filter(|n| *n > 1 && *n <= name.len())?;
2018 Some(String::from_utf16_lossy(&name[..length - 1]))
2019}
2020
2021#[cfg(target_os = "macos")]
2023fn system_locale() -> Option<String> {
2024 let out = std::process::Command::new("defaults").args(["read", "-g", "AppleLanguages"]).output().ok()?;
2026 String::from_utf8_lossy(&out.stdout).split('"').nth(1).map(str::to_string).filter(|l| !l.is_empty())
2027}
2028
2029#[cfg(not(any(windows, target_os = "macos")))]
2031fn system_locale() -> Option<String> {
2032 None
2033}
2034
2035pub(crate) fn say(id: &str, pairs: &[(&str, String)]) -> String {
2036 let mut args = i18n::args();
2037 for (key, value) in pairs {
2038 match value.parse::<i32>() {
2041 Ok(n) if n.to_string() == *value => args.set(key.to_string(), n),
2042 _ => args.set(key.to_string(), value.clone()),
2043 }
2044 }
2045 tr().text(id, Some(&args))
2046}
2047
2048pub(crate) fn json(value: &impl serde::Serialize) -> String {
2049 serde_json::to_string(value).unwrap_or_default()
2050}
2051
2052pub(crate) fn file_url(path: &Path) -> String {
2056 fn encoded(path: &str) -> String {
2057 path.bytes().map(|b| if b.is_ascii_alphanumeric() || b"/-._~!$&'()*+,;=:@".contains(&b) { char::from(b).to_string() } else { format!("%{b:02X}") }).collect()
2058 }
2059 let text = path.to_string_lossy().into_owned();
2060 #[cfg(windows)]
2061 let text = windows_slashed(&text);
2062 match text.strip_prefix("//") {
2063 Some(share) if cfg!(windows) => format!("file://{}", encoded(share)),
2065 _ => format!("file://{}", encoded(&text)),
2066 }
2067}
2068
2069#[cfg(any(windows, test))]
2072fn windows_slashed(text: &str) -> String {
2073 let plain = match text.strip_prefix(r"\\?\UNC\") {
2074 Some(rest) => format!(r"\\{rest}"),
2075 None => text.strip_prefix(r"\\?\").unwrap_or(text).to_string(),
2076 };
2077 let slashed = plain.replace('\\', "/");
2078 if slashed.starts_with("//") { slashed } else { format!("/{slashed}") }
2079}
2080
2081#[cfg(any(windows, test))]
2083fn windows_path(decoded: &str) -> String {
2084 let bytes = decoded.as_bytes();
2085 let path = if bytes.len() >= 3 && bytes[0] == b'/' && bytes[1].is_ascii_alphabetic() && bytes[2] == b':' {
2086 decoded[1..].to_string()
2087 } else if decoded.starts_with('/') {
2088 decoded.to_string()
2089 } else {
2090 format!("//{decoded}")
2091 };
2092 path.replace('/', "\\")
2093}
2094
2095pub(crate) fn local_path(url: &str) -> PathBuf {
2099 let Some(rest) = url.strip_prefix("file://") else { return PathBuf::from(url) };
2100 let rest = rest.strip_prefix("localhost/").map_or_else(|| rest.to_string(), |r| format!("/{r}"));
2102 let decoded = crate::projects::percent_decoded(&rest);
2103 #[cfg(windows)]
2104 let decoded = windows_path(&decoded);
2105 PathBuf::from(decoded)
2106}
2107
2108pub(crate) fn one_at_a_time(lock: &Mutex<()>) -> Option<std::sync::MutexGuard<'_, ()>> {
2111 match lock.try_lock() {
2112 Ok(guard) => Some(guard),
2113 Err(std::sync::TryLockError::Poisoned(poisoned)) => Some(poisoned.into_inner()),
2114 Err(std::sync::TryLockError::WouldBlock) => None,
2115 }
2116}
2117
2118#[derive(Default)]
2122pub(crate) struct Job {
2123 running: AtomicBool,
2124 again: AtomicBool,
2125 deferred: AtomicBool,
2127}
2128
2129pub(crate) static AWAY: AtomicBool = AtomicBool::new(false);
2133
2134pub(crate) fn coalesced(shared: &Arc<Shared>, which: fn(&Shared) -> &Job, work: impl Fn(&Arc<Shared>) + Send + 'static) {
2136 let job = which(shared);
2137 if AWAY.load(Ordering::SeqCst) {
2138 job.deferred.store(true, Ordering::SeqCst);
2139 return;
2140 }
2141 job.again.store(true, Ordering::SeqCst);
2142 if job.running.swap(true, Ordering::SeqCst) {
2143 return;
2144 }
2145 let shared = Arc::clone(shared);
2146 std::thread::spawn(move || {
2147 let job = which(&shared);
2148 loop {
2149 while job.again.swap(false, Ordering::SeqCst) {
2150 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| work(&shared)));
2152 }
2153 job.running.store(false, Ordering::SeqCst);
2154 if !job.again.load(Ordering::SeqCst) || job.running.swap(true, Ordering::SeqCst) {
2156 break;
2157 }
2158 }
2159 });
2160}
2161
2162pub(crate) fn reading_json() -> String {
2164 let config = load_config();
2165 let reading = config.reading;
2166 serde_json::json!({ "family": reading.family, "size": reading.size, "spacing": reading.spacing, "theme": config.theme.unwrap_or_default(), "places_named": config.places_named, "screen_colours": config.screen_colours.unwrap_or(true), "calmer_colours": config.calmer_colours.unwrap_or_default() }).to_string()
2168}
2169
2170struct World {
2172 config: Config,
2173 store: Option<ProjectStore>,
2174 known: SenderList,
2175 senders: porch::Senders,
2177}
2178
2179impl World {
2180 fn load() -> World {
2181 let config = load_config();
2182 World {
2183 store: config.notes_root_path().and_then(|root| ProjectStore::load(&root).ok()).map(|s| s.with_ties(&sioul_core::links::LocalLinks::load(&sioul_core::links::LocalLinks::default_path()))),
2185 known: KnownSenders::load(&config.known_senders_path()),
2186 senders: porch::Senders::load(&config),
2187 config,
2188 }
2189 }
2190
2191 fn gather(&self, state: &PorchState) -> Vec<Triaged> {
2193 let now = Zoned::now().timestamp().as_second();
2194 porch::gather(&self.config.mail_sources(), self.store.as_ref(), &self.known, &self.senders, state, now)
2195 }
2196
2197 fn ledger(&self) -> Option<Ledger> {
2198 self.config.notes_root_path().and_then(|root| Ledger::load_with_bank(&root, &sioul_core::words::Words::of(&self.config)).ok())
2200 }
2201
2202 fn ledger_path(&self) -> Option<PathBuf> {
2203 self.config.notes_root_path().map(|root| root.join(budget::LEDGER))
2204 }
2205
2206 fn mail_lines(&self, ledger: &Ledger) -> Vec<MailLine> {
2208 let ignored = MoneyState::load(&MoneyState::default_path()).ignored;
2209 budget::mail_lines(ledger, &self.gather(&PorchState::default()), &ignored, &sioul_core::words::Words::of(&self.config))
2210 }
2211
2212 fn judge(&self, key: &str) -> Option<(PathBuf, Triaged)> {
2214 let path = maildir::locate(Path::new(key))?.canonicalize().ok()?;
2215 let sources = self.config.mail_sources();
2216 let inside = sources.iter().any(|s| s.folder.canonicalize().is_ok_and(|f| path.starts_with(f)));
2217 if !inside {
2218 return None;
2219 }
2220 let now = Zoned::now().timestamp().as_second();
2221 let canonical: Vec<_> = sources
2222 .into_iter()
2223 .map(|mut s| {
2224 s.folder = s.folder.canonicalize().unwrap_or(s.folder);
2225 s
2226 })
2227 .collect();
2228 let triaged = porch::judge(std::slice::from_ref(&path), &canonical, self.store.as_ref(), &self.known, &porch::Senders::default(), now);
2229 triaged.into_iter().next().map(|t| (path, t))
2230 }
2231}
2232
2233struct Views {
2235 porch: String,
2236 budgets: String,
2237 accounts: String,
2238 blocked: String,
2239 shown: BTreeMap<String, ImapOrigin>,
2240}
2241
2242pub(crate) fn offline() -> bool {
2246 std::env::var_os("SIOUL_DEMO").is_some()
2247}
2248
2249pub(crate) fn weather_cache() -> PathBuf {
2251 config::state_dir().join("weather.json")
2252}
2253
2254const WEATHER_FRESH: i64 = 30 * 60;
2256
2257pub(crate) fn update_weather(qt: &QtThread, shared: &Arc<Shared>) {
2260 let config = load_config();
2261 let (Some(place), Some(latitude), Some(longitude)) = (config.weather.place.clone(), config.weather.latitude, config.weather.longitude) else {
2262 let _ = qt.queue(|mut sioul| sioul.as_mut().set_forecast(QString::default()));
2263 return;
2264 };
2265 let now = Zoned::now();
2266 let kept: Option<sioul_core::weather::Forecast> = std::fs::read_to_string(weather_cache()).ok().and_then(|t| serde_json::from_str(&t).ok());
2267 let shown = move |forecast: &sioul_core::weather::Forecast| {
2268 let view = sioul_core::weather::view(forecast, &Zoned::now(), tr());
2269 serde_json::json!({ "place": place, "view": view, "credit": tr().text("weather-credit", None) }).to_string()
2270 };
2271 if let Some(forecast) = kept.as_ref() {
2272 let line = shown(forecast);
2273 let _ = qt.queue(move |mut sioul| sioul.as_mut().set_forecast(QString::from(&line)));
2274 }
2275 let stale = kept.as_ref().is_none_or(|f| now.timestamp().as_second() - f.fetched >= WEATHER_FRESH);
2276 if !stale || offline() || shared.weather_fetching.swap(true, Ordering::Relaxed) {
2277 return;
2278 }
2279 let (qt, shared) = (qt.clone(), Arc::clone(shared));
2280 std::thread::spawn(move || {
2281 if let Ok(forecast) = sioul_sync::weather::forecast(latitude, longitude, Zoned::now().timestamp().as_second()) {
2282 if let Ok(text) = serde_json::to_string(&forecast) {
2283 let _ = std::fs::create_dir_all(config::state_dir());
2284 let _ = std::fs::write(weather_cache(), text);
2285 }
2286 crate::homecard::weather_seen();
2288 let line = shown(&forecast);
2289 let _ = qt.queue(move |mut sioul| sioul.as_mut().set_forecast(QString::from(&line)));
2290 }
2291 shared.weather_fetching.store(false, Ordering::Relaxed);
2292 });
2293}
2294
2295pub(crate) fn mode_now() -> sioul_core::quiet::Mode {
2297 crate::hours::mode_now()
2298}
2299
2300fn end_work_now() {
2303 let path = sioul_core::quiet::Overrides::default_path();
2304 let mut overrides = sioul_core::quiet::Overrides::load(&path);
2305 if overrides.work_now.take().is_some()
2306 && let Err(e) = overrides.save(&path)
2307 {
2308 eprintln!("{e}");
2309 }
2310}
2311
2312pub(crate) fn mode_json() -> String {
2313 let config = load_config();
2314 let now = Zoned::now();
2315 let overrides = sioul_core::quiet::Overrides::load(&sioul_core::quiet::Overrides::default_path());
2316 let mode = crate::hours::mode_at(&now);
2317 let until = mode.until.as_ref().map(|z| sioul_core::quiet::until_text(tr(), z, &now)).unwrap_or_default();
2318 let mut args = sioul_core::i18n::args();
2319 args.set("until", until.clone());
2320 args.set("label", if mode.label.trim().is_empty() { "none".to_string() } else { mode.label.clone() });
2321 let reason = serde_json::to_value(&mode.reason).ok().and_then(|v| v.as_str().map(str::to_string)).unwrap_or_default();
2322 use sioul_core::quiet::Reason;
2323 let line = match mode.reason {
2324 Reason::NoHours | Reason::Working => String::new(),
2325 Reason::TimeOff => tr().text("mode-time-off", Some(&args)),
2326 Reason::WorkingLate => tr().text("mode-working-late", Some(&args)),
2327 Reason::WorkNow => tr().text("mode-work-now-line", Some(&args)),
2328 Reason::AdminTime => tr().text("mode-admin", Some(&args)),
2329 Reason::DoneForTheDay => tr().text("mode-quiet", Some(&args)),
2331 Reason::Evening | Reason::DayOff => tr().text("mode-leisure", Some(&args)),
2333 Reason::Meal => tr().text("mode-meal", Some(&args)),
2334 Reason::WindingDown => tr().text("mode-wind-down", Some(&args)),
2335 Reason::Nap => tr().text("mode-nap", Some(&args)),
2336 Reason::Sleep => tr().text("mode-sleep", Some(&args)),
2337 Reason::FreeTime => tr().text(if sioul_core::pause::nothing_now(&overrides, &config.free_time) { "mode-free-time-nothing" } else { "mode-free-time" }, None),
2339 Reason::Paused => tr().text("mode-paused", None),
2340 Reason::Extended => String::new(),
2341 };
2342 let today = overrides.closed_today(&now);
2344 let set = |kind: &str| config.windows.iter().any(|w| w.kind() == kind);
2346 serde_json::json!({
2347 "quiet": mode.quiet,
2348 "sleep": mode.sleeps(),
2349 "time": mode.time.id(),
2350 "reason": reason,
2351 "until": until,
2352 "line": line,
2353 "hours": !config.week_hours().is_empty(),
2354 "work_hours": set("work"),
2355 "admin_hours": set("admin"),
2356 "night": crate::hours::night_set(),
2357 "work_now": mode.reason == Reason::WorkNow,
2358 "today": today,
2359 "review": crate::reviews::offer_json(&now, &mode),
2361 "pauses": crate::pauses::moment(&config, &overrides, &now),
2363 "dnd": crate::everywhere::moment(),
2365 "calls": crate::calls::moment()
2367 })
2368 .to_string()
2369}
2370
2371fn compute(shared: &Shared) -> Views {
2372 let _cards = sioul_core::maildir::CardsKept::begin();
2374 let world = World::load();
2375 let now = Zoned::now();
2376 let hidden = mail::hidden_files(shared);
2377 let mut items = world.gather(&PorchState::load(&PorchState::default_path()));
2378 items.retain(|t| t.card.path.as_ref().is_none_or(|p| !hidden.contains(p)));
2379 crate::homecard::porch_seen(&items, &world.senders, world.store.as_ref());
2381 let mode = crate::hours::mode_at(&now);
2387 {
2388 let area_of = |t: &sioul_core::porch::Triaged| t.card.account.as_deref().and_then(|id| world.config.account(id)).and_then(|a| a.area.as_deref()).and_then(sioul_core::areas::Area::parse).unwrap_or(sioul_core::areas::Area::WORK);
2389 let attention = sioul_core::attention::Attention::of(&world.config);
2390 let moment = crate::hours::attention_at(&mode, &now);
2391 let always = sioul_core::everywhere::People::load(&sioul_core::everywhere::People::default_path());
2392 items.retain(|t| attention.mail(t, &world.senders, &always, area_of(t), &moment).shown);
2393 }
2394 if mode.sleeps() {
2396 for t in &mut items {
2397 if matches!(t.lane, sioul_core::porch::Lane::Project(_)) {
2398 t.lane = sioul_core::porch::Lane::People;
2399 }
2400 }
2401 }
2402 let opened = shared.opened_anyway.load(Ordering::Relaxed);
2403 let mut porch = view::porch(&items, &world.config, world.store.as_ref(), tr(), &now, opened || mode.quiet);
2404 crate::pauses::porch_rests(&mut porch, opened, &now);
2406 if mode.quiet {
2407 porch.status = serde_json::from_str::<serde_json::Value>(&mode_json()).ok().and_then(|v| v["line"].as_str().map(str::to_string)).unwrap_or_default();
2408 }
2409 let shown = if porch.open { PorchState::newest_shown(&items) } else { BTreeMap::new() };
2411 let budgets = world.ledger().map_or_else(String::new, |mut ledger| {
2412 if mode.time != sioul_core::areas::Time::Any {
2414 let area_of = |b: &sioul_core::budget::Budget| match b.area.as_deref() {
2416 Some("personal") => sioul_core::areas::Area::LEISURE,
2417 Some(other) => sioul_core::areas::Area::parse(other).unwrap_or(sioul_core::areas::Area::ADMIN),
2418 None => sioul_core::areas::Area::ADMIN,
2419 };
2420 ledger.budgets.retain(|b| sioul_core::areas::in_view(area_of(b), mode.time, mode.week));
2421 }
2422 let mail = world.mail_lines(&ledger);
2423 json(&view::budgets(&ledger, &mail, tr(), now.date()))
2424 });
2425 let statuses = shared.statuses.lock().map(|s| s.clone()).unwrap_or_default();
2426 let wanted = shared.password_wanted.lock().map(|w| w.clone()).unwrap_or_default();
2427 Views {
2428 porch: crate::appnotes::with_line(json(&porch)),
2430 budgets,
2431 accounts: json(&view::accounts(&world.config, tr(), &statuses, &wanted)),
2432 blocked: json(&world.senders.blocked.entries()),
2433 shown,
2434 }
2435}
2436
2437pub(crate) fn show(qt: &QtThread, shared: &Arc<Shared>) {
2439 crate::mailsearch::changed(qt, shared);
2440 let qt = qt.clone();
2441 coalesced(shared, |s| &s.views_job, move |shared| {
2442 let _cards = sioul_core::maildir::CardsKept::begin();
2446 mail::show_mail_here(&qt, shared);
2447 let generation = shared.generation.fetch_add(1, Ordering::Relaxed) + 1;
2448 let views = compute(shared);
2449 let _ = qt.queue(move |mut sioul| {
2450 let shared = Arc::clone(&sioul.rust().shared);
2451 if shared.generation.load(Ordering::Relaxed) != generation || shared.shown_generation.fetch_max(generation, Ordering::Relaxed) > generation {
2454 return;
2455 }
2456 if let Ok(mut shown) = shared.shown.lock() {
2457 *shown = views.shown;
2458 }
2459 sioul.as_mut().set_porch(QString::from(&views.porch));
2460 sioul.as_mut().set_budgets(QString::from(&views.budgets));
2461 sioul.as_mut().set_accounts(QString::from(&views.accounts));
2462 sioul.as_mut().set_blocked(QString::from(&views.blocked));
2463 if cfg!(target_os = "android") {
2465 std::thread::spawn(|| crate::outside::publish(false));
2466 }
2467 });
2468 });
2469}
2470
2471pub(crate) fn set_status(qt: &QtThread, line: String) {
2472 let _ = qt.queue(move |mut sioul| sioul.as_mut().set_status(QString::from(&line)));
2473}
2474
2475pub(crate) fn tell(qt: &QtThread, shared: &Shared, line: String) {
2477 if let Ok(mut said) = shared.said_at.lock() {
2478 *said = Some(std::time::Instant::now());
2479 }
2480 set_status(qt, line);
2481}
2482
2483fn start_watchers(qt: &QtThread, shared: &Arc<Shared>) -> usize {
2485 if offline() {
2486 return 0;
2487 }
2488 let accounts: Vec<Account> = load_config().accounts.into_iter().filter(Account::syncs).collect();
2489 let count = accounts.len();
2490 for account in accounts {
2491 start_watcher(qt, shared, account);
2492 }
2493 count
2494}
2495
2496pub(crate) fn start_new_watchers(qt: &QtThread, shared: &Arc<Shared>) {
2500 if offline() {
2501 return;
2502 }
2503 let seen = |id: &str| shared.statuses.lock().is_ok_and(|s| s.contains_key(id)) || shared.watchers.lock().is_ok_and(|w| w.contains_key(id)) || shared.dav_watchers.lock().is_ok_and(|w| w.contains_key(id));
2504 for account in load_config().accounts.into_iter().filter(|a| !seen(&a.id)) {
2505 if account.is_dav() {
2506 crate::pim::start_dav_watcher(qt, shared, account);
2507 } else if account.syncs() {
2508 start_watcher(qt, shared, account);
2509 }
2510 }
2511}
2512
2513fn give_password(qt: &QtThread, shared: &Arc<Shared>, id: String, password: String) {
2517 let (qt, shared) = (qt.clone(), Arc::clone(shared));
2518 std::thread::spawn(move || {
2519 let config = load_config();
2520 let outcome = match config.every_account().find(|a| a.id == id).cloned() {
2521 None => Err(say("account-unknown", &[("id", id.clone())])),
2522 Some(mut account) => {
2523 let host = account.host.clone().unwrap_or_default();
2524 let password = sioul_sync::tidy_password(&host, &password);
2525 let switched = !account.is_dav() && account.auth.as_deref() == Some("google");
2528 if switched {
2529 account.auth = None;
2530 }
2531 let tested = if account.is_dav() {
2532 let address = account.address.clone().unwrap_or_default();
2533 let login = account.login().unwrap_or(address.as_str()).to_string();
2534 sioul_sync::dav::test(&address, &login, &password, account.url.as_deref(), None).map(|_| ())
2535 } else {
2536 sioul_sync::test(&account, &password).map(|_| ())
2537 };
2538 tested
2539 .and_then(|()| secret::save(&account, &password))
2540 .map_err(|e| e.sentence(tr(), &account.id))
2541 .and_then(|()| if switched { config::set_auth(&config_path(), &account.id, None) } else { Ok(()) })
2542 .map(|()| {
2543 if switched {
2544 if let Some(control) = shared.watchers.lock().ok().and_then(|mut w| w.remove(&account.id)) {
2546 control.stop();
2547 }
2548 let _ = sioul_sync::google::revoke_for(sioul_sync::google::Purpose::Mail, account.address.as_deref().unwrap_or(""));
2549 }
2550 account
2551 })
2552 }
2553 };
2554 let problem = match outcome {
2555 Ok(account) => {
2556 want_password(&shared, &account.id, None);
2557 if let Ok(mut statuses) = shared.statuses.lock() {
2558 statuses.remove(&account.id);
2559 }
2560 if account.is_dav() {
2561 crate::pim::start_dav_watcher(&qt, &shared, account);
2562 } else {
2563 start_watcher(&qt, &shared, account);
2564 }
2565 String::new()
2566 }
2567 Err(e) => e,
2568 };
2569 let _ = qt.queue(move |mut sioul| {
2570 sioul.as_mut().set_form_busy(false);
2571 if problem.is_empty() {
2572 sioul.as_mut().set_status(QString::from(&say("account-password-kept", &[("account", id.clone())])));
2573 }
2574 sioul.as_mut().account_password_done(QString::from(&id), QString::from(&problem));
2575 });
2576 show(&qt, &shared);
2577 });
2578}
2579
2580#[allow(clippy::too_many_arguments)]
2583fn add_mail(qt: &QtThread, shared: &Arc<Shared>, address: String, host: String, port: u16, security: Security, login: String, password: Option<String>) {
2584 let (qt, shared) = (qt.clone(), Arc::clone(shared));
2585 std::thread::spawn(move || {
2586 let config = load_config();
2587 let outcome = match config.every_account().find(|a| a.address.as_deref() == Some(address.as_str()) && matches!(a.kind, config::AccountKind::Imap | config::AccountKind::Jmap)) {
2589 Some(existing) => Err(say("account-address-exists", &[("address", address.clone()), ("id", existing.id.clone())])),
2590 None => {
2591 let password = match password {
2592 Some(p) => Ok(p),
2593 None => config
2594 .every_account()
2595 .find(|a| a.address.as_deref() == Some(address.as_str()) && a.kind == config::AccountKind::Dav)
2596 .ok_or_else(|| say("account-unknown", &[("id", address.clone())]))
2597 .and_then(|dav| secret::password(dav).map_err(|e| e.sentence(tr(), &dav.id))),
2598 };
2599 password.and_then(|password| {
2600 let id = config.free_id(&address);
2601 let account = Account::imap(&id, &address, &host, port, security, Some(&login));
2602 let password = sioul_sync::tidy_password(&host, &password);
2603 sioul_sync::test(&account, &password)
2604 .and_then(|_| secret::save(&account, &password))
2605 .map_err(|e| e.sentence(tr(), &id))
2606 .and_then(|()| config::add_imap_account(&config_path(), &account))
2607 .map(|()| account)
2608 })
2609 }
2610 };
2611 let added = outcome.as_ref().ok().map(|a| say("account-added", &[("id", a.id.clone()), ("path", a.maildir_path().display().to_string())]));
2612 if let Ok(account) = outcome.as_ref() {
2613 start_watcher(&qt, &shared, account.clone());
2614 }
2615 let error = outcome.err();
2616 let _ = qt.queue(move |mut sioul| {
2617 sioul.as_mut().set_form_busy(false);
2618 match (added, error) {
2619 (Some(line), _) => {
2620 sioul.as_mut().set_found(QString::default());
2621 sioul.as_mut().set_scouted(QString::default());
2622 sioul.as_mut().set_status(QString::from(&line));
2623 sioul.as_mut().account_added();
2624 }
2625 (None, Some(e)) => sioul.as_mut().set_form_error(QString::from(&e)),
2626 (None, None) => {}
2627 }
2628 });
2629 show(&qt, &shared);
2630 });
2631}
2632
2633fn pace_of(config: &Config, account: &Account) -> std::time::Duration {
2635 let minutes = account.fetch_minutes.or(config.fetch_minutes).unwrap_or(config::DEFAULT_FETCH_MINUTES);
2636 std::time::Duration::from_secs(u64::from(minutes.max(1)) * 60)
2637}
2638
2639pub(crate) fn update_paces(shared: &Shared) {
2641 let config = load_config();
2642 let realtime = shared.realtime.load(Ordering::Relaxed);
2643 let Ok(watchers) = shared.watchers.lock() else { return };
2644 for account in config.accounts.iter() {
2645 if let Some(control) = watchers.get(&account.id) {
2646 control.set_pace(pace_of(&config, account));
2647 control.set_realtime(realtime);
2648 }
2649 }
2650}
2651
2652pub(crate) fn start_watcher(qt: &QtThread, shared: &Arc<Shared>, account: Account) {
2653 let control = Arc::new(Control::default());
2654 control.set_pace(pace_of(&load_config(), &account));
2655 control.set_realtime(shared.realtime.load(Ordering::Relaxed));
2656 {
2657 let Ok(mut watchers) = shared.watchers.lock() else { return };
2658 if let Some(running) = watchers.get(&account.id) {
2659 running.retry();
2661 return;
2662 }
2663 watchers.insert(account.id.clone(), Arc::clone(&control));
2664 }
2665 let (qt, shared) = (qt.clone(), Arc::clone(shared));
2666 std::thread::spawn(move || {
2667 match secret::password(&account) {
2668 Ok(password) => sioul_sync::watch(&account, &password, &control, |result| reported(&qt, &shared, &account, result)),
2669 Err(e) => reported(&qt, &shared, &account, Err(e)),
2670 }
2671 if let Ok(mut watchers) = shared.watchers.lock()
2673 && watchers.get(&account.id).is_some_and(|c| Arc::ptr_eq(c, &control))
2674 {
2675 watchers.remove(&account.id);
2676 }
2677 });
2678}
2679
2680pub(crate) fn want_password(shared: &Shared, id: &str, error: Option<&SyncError>) {
2683 if let Ok(mut wanted) = shared.password_wanted.lock() {
2684 if error.is_some_and(SyncError::wants_password) {
2685 wanted.insert(id.to_string());
2686 } else {
2687 wanted.remove(id);
2688 }
2689 }
2690}
2691
2692fn reported(qt: &QtThread, shared: &Arc<Shared>, account: &Account, result: Result<Report, SyncError>) {
2695 let (line, problem) = match &result {
2696 Ok(report) if report.first => {
2697 let mut args = tr().counted(report.new.len());
2698 args.set("account", account.id.clone());
2699 args.set("days", account.sync_days.unwrap_or(config::DEFAULT_SYNC_DAYS));
2700 (tr().text("sync-first", Some(&args)), false)
2701 }
2702 Ok(_) => (say("ui-synced-at", &[("time", Zoned::now().strftime("%H:%M").to_string())]), false),
2703 Err(e) => (e.sentence(tr(), &account.id), true),
2704 };
2705 if let Ok(mut statuses) = shared.statuses.lock() {
2706 statuses.insert(account.id.clone(), (line.clone(), problem));
2707 }
2708 want_password(shared, &account.id, result.as_ref().err());
2709 if let Ok(report) = &result {
2710 if !report.new.is_empty() {
2711 learn(qt, shared, &account.id);
2712 tie_to_projects(&report.new);
2713 }
2714 if !report.elsewhere.is_empty() {
2715 tie_to_projects(&report.elsewhere);
2716 }
2717 if report.held_back
2719 && shared.held_back.lock().is_ok_and(|mut said| said.insert(account.id.clone()))
2720 {
2721 let free = sioul_sync::disk::space(&account.maildir_path()).map_or(0, |(free, _)| free);
2722 set_status(qt, say("sync-held-back", &[("account", account.id.clone()), ("free", tr().decimal((free as f64 / f64::from(1u32 << 30)) as f32))]));
2723 }
2724 if !report.first && !report.new.is_empty() {
2725 notify_codes(qt, &report.new);
2726 sort_on_server(qt, shared, account, report.new.clone());
2727 let window = crate::mailnote::Window { qt: qt.clone(), active: shared.active.load(Ordering::Relaxed) };
2731 crate::mailnote::arrived_in_window(window, &account.id, &report.new, report.first);
2732 } else if !report.first && sioul_sync::filters::due(account) {
2733 sort_on_server(qt, shared, account, Vec::new());
2735 }
2736 read_shielded(qt, &account.id);
2737 }
2738 let recent = shared.said_at.lock().ok().and_then(|s| *s).is_some_and(|at| at.elapsed() < SAID_FOR);
2740 let quiet = recent && !problem;
2741 let _ = qt.queue(move |mut sioul| {
2742 sioul.as_mut().set_busy(false);
2743 if !quiet {
2744 sioul.as_mut().set_status(QString::from(&line));
2745 }
2746 });
2747 show(qt, shared);
2748}
2749
2750fn sort_on_server(qt: &QtThread, shared: &Arc<Shared>, account: &Account, new: Vec<PathBuf>) {
2758 let (account, qt, shared) = (account.clone(), qt.clone(), Arc::clone(shared));
2759 let window = crate::mailnote::Window { qt: qt.clone(), active: shared.active.load(Ordering::Relaxed) };
2760 std::thread::spawn(move || {
2761 let moved = crate::spam::after_fetch(&account, &new, false);
2762 let filtered = crate::filters::after_fetch(&account, &new, false);
2763 if !filtered.problem.is_empty() {
2764 set_status(&qt, filtered.problem.clone());
2765 }
2766 crate::mailnote::unfiltered_in_window(window, &account.id, &filtered.tell);
2767 if moved + filtered.acted > 0 {
2768 show(&qt, &shared);
2769 }
2770 });
2771}
2772
2773fn tie_to_projects(files: &[PathBuf]) {
2777 let config = load_config();
2778 let path = sioul_core::links::LocalLinks::default_path();
2779 let mut ties = sioul_core::links::LocalLinks::load(&path);
2780 let before = ties.links.len();
2781 crate::github::tie_mail(&config, files, &mut ties);
2783 if let Some(store) = config.notes_root_path().and_then(|root| ProjectStore::load(&root).ok()).map(|s| s.with_ties(&ties)) {
2784 let senders = porch::Senders::load(&config);
2785 for t in porch::judge(files, &config.mail_sources(), Some(&store), &KnownSenders::default(), &senders, Zoned::now().timestamp().as_second()) {
2786 if let (porch::Lane::Project(id), Some(mid)) = (&t.lane, t.card.message_id.as_deref()) {
2787 ties.add(&sioul_core::links::mail_uri(mid), &sioul_core::links::project_uri(id), "project");
2788 }
2789 }
2790 }
2791 let _ = sioul_core::links::LocalLinks::add_all(&path, &ties.links[before..]);
2794}
2795
2796fn retie_project(id: String) {
2802 std::thread::spawn(move || {
2803 let config = load_config();
2804 let Some(project) = config.notes_root_path().and_then(|root| ProjectStore::load(&root).ok()).and_then(|s| s.get(&id).cloned()) else { return };
2805 let routes: Vec<&sioul_core::projects::Route> = project.routes.iter().filter(|r| r.needs_body()).collect();
2806 if routes.is_empty() {
2807 return;
2808 }
2809 let path = sioul_core::links::LocalLinks::default_path();
2810 let mut ties = sioul_core::links::LocalLinks::load(&path);
2811 let before = ties.links.len();
2812 let gate = porch::Gate::load(&config, Zoned::now().timestamp().as_second());
2813 for account in config.accounts.iter().filter(|a| a.syncs()) {
2814 let files = sioul_core::mailindex::message_files(&account.maildir_path());
2815 for mid in gate.routed(&routes, files.iter().map(PathBuf::as_path)) {
2816 ties.add(&sioul_core::links::mail_uri(&mid), &sioul_core::links::project_uri(&project.id), "project");
2817 }
2818 }
2819 let _ = sioul_core::links::LocalLinks::add_all(&path, &ties.links[before..]);
2820 });
2821}
2822
2823fn read_shielded(qt: &QtThread, id: &str) {
2826 let config = load_config();
2827 if !config.account(id).is_some_and(|a| a.shield && a.shield_ai) {
2828 return;
2829 }
2830 if let Err(e) = sioul_sync::shield_ai::read_new(&config) {
2831 set_status(qt, e.sentence(tr(), "Anthropic"));
2832 }
2833}
2834
2835fn learn(qt: &QtThread, shared: &Shared, id: &str) {
2837 if shared.learned.lock().map_or(true, |l| l.contains(id)) {
2838 return;
2839 }
2840 let Some(account) = load_config().account(id).cloned() else { return };
2841 match sioul_sync::learn_provider(&config_path(), &account) {
2842 Ok(Learned::TooEarly) => {}
2843 Ok(learned) => {
2844 if let Ok(mut done) = shared.learned.lock() {
2845 done.insert(id.to_string());
2846 }
2847 match learned {
2848 Learned::Id(provider) => set_status(qt, say("sync-learned", &[("account", id.to_string()), ("id", provider)])),
2849 Learned::Nothing => set_status(qt, say("sync-not-learned", &[("account", id.to_string())])),
2850 _ => {}
2851 }
2852 }
2853 Err(e) => set_status(qt, e),
2854 }
2855}
2856
2857fn notify_codes(qt: &QtThread, files: &[PathBuf]) {
2859 if !crate::hours::comes(sioul_core::attention::Kind::Codes) {
2862 return;
2863 }
2864 let config = load_config();
2865 let now = Zoned::now().timestamp().as_second();
2866 let senders = porch::Senders::load(&config);
2867 let judged = porch::judge(files, &config.mail_sources(), None, &KnownSenders::default(), &senders, now);
2869 for code in view::codes(&judged, tr()) {
2870 let copy = code.code.clone().map(|text| {
2871 let qt = qt.clone();
2872 let action: Box<dyn FnOnce() + Send> = Box::new(move || {
2873 let _ = qt.queue(move |sioul| sioul.copy_requested(QString::from(&text)));
2874 });
2875 (tr().text("notify-copy", None), action)
2876 });
2877 if let Err(e) = notify::code(&code.title, &code.body(), copy) {
2878 set_status(qt, e);
2879 }
2880 }
2881}
2882
2883fn attachment_cache(key: &str) -> PathBuf {
2885 let mut hasher = std::collections::hash_map::DefaultHasher::new();
2886 key.hash(&mut hasher);
2887 config::cache_dir().join("attachments").join(format!("{:016x}", hasher.finish()))
2888}
2889
2890fn forget_opened_attachments() {
2893 let Ok(entries) = std::fs::read_dir(config::cache_dir().join("attachments")) else { return };
2894 let day = std::time::Duration::from_secs(86_400);
2895 for entry in entries.filter_map(Result::ok) {
2896 if entry.metadata().and_then(|m| m.modified()).ok().and_then(|t| t.elapsed().ok()).is_some_and(|age| age > day) {
2897 let _ = std::fs::remove_dir_all(entry.path());
2898 }
2899 }
2900}
2901
2902fn mark_from_mail(file: &Path) {
2907 #[cfg(windows)]
2908 {
2909 let mut stream = file.as_os_str().to_owned();
2910 stream.push(":Zone.Identifier");
2911 let _ = std::fs::write(stream, "[ZoneTransfer]\r\nZoneId=3\r\n");
2912 }
2913 #[cfg(target_os = "macos")]
2914 {
2915 let stamp = format!("0081;{:x};Sioul;", jiff::Timestamp::now().as_second());
2916 let _ = std::process::Command::new("xattr").args(["-w", "com.apple.quarantine", &stamp]).arg(file).output();
2917 }
2918 #[cfg(not(any(windows, target_os = "macos")))]
2919 let _ = file;
2920}
2921
2922fn downloads() -> PathBuf {
2924 config::downloads_dir()
2925}
2926
2927fn free_path(folder: &Path, name: &str) -> PathBuf {
2929 let wanted = folder.join(name);
2930 if !wanted.exists() {
2931 return wanted;
2932 }
2933 let (stem, extension) = match name.rsplit_once('.') {
2934 Some((stem, ext)) if !stem.is_empty() => (stem.to_string(), format!(".{ext}")),
2935 _ => (name.to_string(), String::new()),
2936 };
2937 (2..).map(|n| folder.join(format!("{stem} ({n}){extension}"))).find(|p| !p.exists()).unwrap_or(wanted)
2938}
2939
2940fn one_file_name(name: &str) -> String {
2945 let odd = |c: char| matches!(c, '/' | '\\') || (cfg!(windows) && matches!(c, ':' | '<' | '>' | '"' | '|' | '?' | '*'));
2946 let mut name: String = name.chars().map(|c| if odd(c) { '_' } else { c }).collect();
2947 if cfg!(windows) {
2948 name = name.trim_end_matches(['.', ' ']).to_string();
2949 let stem = name.split('.').next().unwrap_or("").trim_end().to_ascii_uppercase();
2950 let numbered = stem.len() == 4 && (stem.starts_with("COM") || stem.starts_with("LPT")) && stem.as_bytes()[3].is_ascii_digit();
2951 if numbered || matches!(stem.as_str(), "CON" | "PRN" | "AUX" | "NUL") {
2952 name.insert(0, '_');
2953 }
2954 }
2955 if matches!(name.as_str(), "" | "." | "..") { "attachment".into() } else { name }
2956}
2957
2958#[derive(Clone, Copy, PartialEq, Eq)]
2962enum Afterwards {
2963 Open,
2964 Save,
2965 Paper,
2966}
2967
2968impl Afterwards {
2969 fn of(what: i32) -> Afterwards {
2970 match what {
2971 1 => Afterwards::Save,
2972 2 => Afterwards::Paper,
2973 _ => Afterwards::Open,
2974 }
2975 }
2976}
2977
2978fn checked_attachment(qt: &QtThread, shared: &Arc<Shared>, key: String, index: i32, afterwards: Afterwards, unchecked: bool) {
2984 let (qt, shared) = (qt.clone(), Arc::clone(shared));
2985 std::thread::spawn(move || {
2986 let world = World::load();
2987 let judged = world.judge(&key).filter(|(_, t)| t.lane != porch::Lane::SetAside);
2989 let subject = judged.as_ref().map(|(_, t)| t.card.subject.clone()).unwrap_or_default();
2990 let found = judged.and_then(|(path, _)| reading::attachment_from(&crypto::readable(&path)?, u32::try_from(index).ok()?));
2991 let Some((name, bytes)) = found else {
2992 set_status(&qt, tr().text("ui-attachments-closed", None));
2993 return;
2994 };
2995 let name = one_file_name(&name);
2996 let phone = crate::attachments::phone();
2997 if afterwards != Afterwards::Save && crate::attachments::kept_closed(&name, phone) {
3001 set_status(&qt, say("attachment-program", &[("name", name)]));
3002 return;
3003 }
3004 let folder = attachment_cache(&key);
3005 let file = folder.join(&name);
3006 if std::fs::create_dir_all(&folder).and_then(|()| std::fs::write(&file, &bytes)).is_err() {
3007 set_status(&qt, say("scan-error", &[("name", name), ("detail", folder.display().to_string())]));
3008 return;
3009 }
3010 mark_from_mail(&file);
3011 if !phone {
3012 set_status(&qt, say("scan-running", &[("name", name.clone())]));
3013 }
3014 static KEYS: OnceLock<std::collections::hash_map::RandomState> = OnceLock::new();
3016 let content = std::hash::BuildHasher::hash_one(KEYS.get_or_init(Default::default), &bytes);
3017 let known_clean = shared.clean.lock().is_ok_and(|c| c.contains(&content));
3018 let verdict = match crate::attachments::check(phone, known_clean, unchecked) {
3020 crate::attachments::Check::Scan => antivirus::scan(&file),
3021 crate::attachments::Check::Skip | crate::attachments::Check::Unchecked => Verdict::Clean,
3022 };
3023 let line = match verdict {
3024 Verdict::Clean => {
3025 if !unchecked
3026 && !phone
3027 && let Ok(mut clean) = shared.clean.lock()
3028 {
3029 clean.insert(content);
3030 }
3031 match afterwards {
3032 Afterwards::Open if phone => crate::attachments::open(&file, &name),
3034 Afterwards::Open => {
3035 let url = file_url(&file);
3036 let _ = qt.queue(move |sioul| sioul.open_url(QString::from(&url)));
3037 say("scan-clean", &[("name", name)])
3038 }
3039 Afterwards::Paper => {
3040 let kept = crate::papers::keep_checked(&file, &name, &subject);
3041 let _ = std::fs::remove_file(&file);
3042 match kept {
3043 Ok((path, title, kind)) => {
3044 let _ = qt.queue(move |mut sioul| sioul.as_mut().paper_kept(QString::from(&path), QString::from(&title), QString::from(kind)));
3045 say("papers-kept", &[("name", name)])
3046 }
3047 Err(e) => say("scan-error", &[("name", name), ("detail", e)]),
3048 }
3049 }
3050 Afterwards::Save if phone => crate::attachments::save(&qt, &file, &name),
3052 Afterwards::Save => {
3053 let target = free_path(&downloads(), &name);
3054 let saved = std::fs::create_dir_all(downloads()).and_then(|()| std::fs::copy(&file, &target));
3055 let _ = std::fs::remove_file(&file);
3056 match saved {
3057 Ok(_) => {
3058 mark_from_mail(&target);
3059 say("ui-saved", &[("path", target.display().to_string())])
3060 }
3061 Err(e) => say("scan-error", &[("name", name), ("detail", e.to_string())]),
3062 }
3063 }
3064 }
3065 }
3066 Verdict::Infected(threat) => {
3067 let _ = std::fs::remove_file(&file);
3068 say("scan-infected", &[("name", name), ("threat", threat)])
3069 }
3070 Verdict::Unavailable(_) | Verdict::TooBig => {
3073 let _ = std::fs::remove_file(&file);
3074 let what = match afterwards {
3075 Afterwards::Open => 0,
3076 Afterwards::Save => 1,
3077 Afterwards::Paper => 2,
3078 };
3079 let big = verdict == Verdict::TooBig;
3080 let (key, hint, shown) = (key.clone(), antivirus::install_hint(), name.clone());
3081 let _ = qt.queue(move |mut sioul| sioul.as_mut().scan_unavailable(QString::from(&key), index, what, QString::from(&shown), QString::from(&hint), big));
3082 match verdict {
3083 Verdict::Unavailable(problem) => say("scan-unavailable-short", &[("detail", problem)]),
3084 _ => say("scan-too-big", &[("name", name)]),
3085 }
3086 }
3087 };
3088 set_status(&qt, line);
3089 });
3090}
3091
3092#[derive(serde::Serialize)]
3094struct FoundView {
3095 host: String,
3096 port: u16,
3097 security: &'static str,
3098 login: String,
3099 by: String,
3101 hint: String,
3103 help_url: Option<&'static str>,
3105 google: bool,
3108 google_key: bool,
3110}
3111
3112fn message_json(key: &str) -> String {
3115 let world = World::load();
3116 let own = mail::own_addresses(&world.config);
3117 let shown = world.judge(key).and_then(|(path, triaged)| {
3118 let raw = std::fs::read(&path).ok()?;
3119 crypto::learn_autocrypt(&raw, &triaged);
3120 let (opened, protection) = match crypto::open(&raw) {
3121 Some((opened, protection)) => (opened, Some(protection)),
3122 None => (None, None),
3123 };
3124 let mut shown = view::message_from(opened.as_deref().unwrap_or(&raw), &path, &triaged, &own, tr(), protection)?;
3125 shown.role = mail::locate(key).and_then(|(account, file)| sioul_sync::mailbox::folder_of(&account, &file)).map(|f| f.role);
3126 shown.unsubscribe = mail::unsubscribe_view(&world.config, &triaged);
3127 Some(shown)
3128 });
3129 shown.map_or_else(String::new, |m| json(&m))
3130}
3131
3132impl qobject::Sioul {
3133 pub(crate) fn shared(&self) -> Arc<Shared> {
3134 Arc::clone(&self.rust().shared)
3135 }
3136
3137 fn text(&self, id: &QString) -> QString {
3138 QString::from(&tr().text(&id.to_string(), None))
3139 }
3140
3141 fn text_args(&self, id: &QString, args: &QString) -> QString {
3142 let given: serde_json::Map<String, serde_json::Value> = serde_json::from_str(&args.to_string()).unwrap_or_default();
3143 let pairs: Vec<(&str, String)> = given.iter().map(|(k, v)| (k.as_str(), v.as_str().map_or_else(|| v.to_string(), str::to_string))).collect();
3144 QString::from(&say(&id.to_string(), &pairs))
3145 }
3146
3147 fn text_with(&self, id: &QString, name: &QString, value: &QString) -> QString {
3148 QString::from(&say(&id.to_string(), &[(&name.to_string(), value.to_string())]))
3149 }
3150
3151 fn text_counted(&self, id: &QString, n: i32) -> QString {
3152 QString::from(&tr().text(&id.to_string(), Some(&tr().counted(usize::try_from(n).unwrap_or(0)))))
3153 }
3154
3155 fn start(mut self: Pin<&mut Self>) {
3156 end_work_now();
3158 if let Err(e) = config::migrate_sites_at_start(&config_path()) {
3160 eprintln!("{e}");
3161 }
3162 self.as_mut().set_reading(QString::from(&reading_json()));
3163 self.as_mut().set_mode(QString::from(&mode_json()));
3164 crate::outside::started(self.as_mut());
3166 crate::pauses::tick(&self.qt_thread(), &self.shared());
3168 crate::sites::favicon_tick(&self.qt_thread());
3170 update_weather(&self.qt_thread(), &self.shared());
3171 let (qt, shared) = (self.qt_thread(), self.shared());
3172 if let Ok(mut state) = shared.work.lock() {
3174 *state = work::WorkState::load();
3175 }
3176 show(&qt, &shared);
3177 pim::show_pim(&qt, &shared);
3178 work::show_work(&qt, &shared);
3179 start_watchers(&qt, &shared);
3180 if std::env::var_os("SIOUL_GRAB").is_none() || cfg!(feature = "insecure-test-tls") {
3182 pim::start_dav_watchers(&qt, &shared);
3183 crate::github::start(&qt, &shared);
3184 }
3185 if !offline() {
3187 std::thread::spawn(|| {
3188 let _ = antivirus::refresh_signatures();
3189 });
3190 }
3191 forget_opened_attachments();
3193 if offline() {
3196 return;
3197 }
3198 let (qt, shared) = (qt.clone(), Arc::clone(&shared));
3199 std::thread::spawn(move || {
3200 let checked: usize = load_config().mail_sources().iter().map(|s| sioul_sync::verify::verify_folder(&s.folder, false)).sum();
3201 if checked > 0 {
3202 show(&qt, &shared);
3203 }
3204 });
3205 }
3206
3207 fn refresh(self: Pin<&mut Self>) {
3208 show(&self.qt_thread(), &self.shared());
3209 }
3210
3211 fn sync_now(mut self: Pin<&mut Self>) {
3214 let (qt, shared) = (self.qt_thread(), self.shared());
3215 crate::share::exchange(&qt, &shared);
3216 show(&qt, &shared);
3217 pim::show_pim(&qt, &shared);
3218 work::show_work(&qt, &shared);
3219 update_weather(&qt, &shared);
3220 let calendars = pim::start_dav_watchers(&qt, &shared);
3221 crate::github::start(&qt, &shared);
3222 if start_watchers(&qt, &shared) == 0 && calendars == 0 {
3223 self.as_mut().set_status(QString::from(&tr().text("sync-nothing", None)));
3224 return;
3225 }
3226 self.as_mut().set_busy(true);
3227 self.as_mut().set_status(QString::from(&tr().text("ui-refreshing", None)));
3228 }
3229
3230 fn open_anyway(self: Pin<&mut Self>) {
3231 let shared = self.shared();
3232 shared.opened_anyway.store(true, Ordering::Relaxed);
3233 show(&self.qt_thread(), &shared);
3234 }
3235
3236 fn done(mut self: Pin<&mut Self>) {
3237 let shared = self.shared();
3238 let newest = shared.shown.lock().map(|s| s.clone()).unwrap_or_default();
3239 let path = PorchState::default_path();
3240 let mut state = PorchState::load(&path);
3241 state.close(&newest);
3242 let line = match state.save(&path) {
3243 Ok(()) => tr().text(if newest.is_empty() { "done-nothing" } else { "done-closed" }, None),
3244 Err(e) => e,
3245 };
3246 shared.opened_anyway.store(false, Ordering::Relaxed);
3247 self.as_mut().set_status(QString::from(&line));
3248 show(&self.qt_thread(), &shared);
3249 }
3250
3251 fn read_message(self: Pin<&mut Self>, key: &QString) {
3252 let (qt, key) = (self.qt_thread(), key.to_string());
3253 std::thread::spawn(move || {
3254 let reading = std::panic::catch_unwind(|| message_json(&key)).unwrap_or_default();
3256 let invitation = std::panic::catch_unwind(|| pim::invitation(&key)).unwrap_or_default();
3257 let _ = qt.queue(move |mut sioul| sioul.as_mut().message_read(QString::from(&key), QString::from(&reading), QString::from(&invitation)));
3258 });
3259 }
3260
3261 fn open_attachment(mut self: Pin<&mut Self>, key: &QString, index: i32) {
3262 if !crate::attachments::phone() {
3263 self.as_mut().set_status(QString::from(&tr().text("scan-running-any", None)));
3264 }
3265 checked_attachment(&self.qt_thread(), &self.shared(), key.to_string(), index, Afterwards::Open, false);
3266 }
3267
3268 fn save_attachment(mut self: Pin<&mut Self>, key: &QString, index: i32) {
3269 if !crate::attachments::phone() {
3270 self.as_mut().set_status(QString::from(&tr().text("scan-running-any", None)));
3271 }
3272 checked_attachment(&self.qt_thread(), &self.shared(), key.to_string(), index, Afterwards::Save, false);
3273 }
3274
3275 fn attachments_unscanned(&self) -> bool {
3276 crate::attachments::phone()
3277 }
3278
3279 fn attachment_unchecked(self: Pin<&mut Self>, key: &QString, index: i32, what: i32) {
3280 checked_attachment(&self.qt_thread(), &self.shared(), key.to_string(), index, Afterwards::of(what), true);
3281 }
3282
3283 fn keep_attachment_as_paper(mut self: Pin<&mut Self>, key: &QString, index: i32) {
3284 if !crate::attachments::phone() {
3285 self.as_mut().set_status(QString::from(&tr().text("scan-running-any", None)));
3286 }
3287 checked_attachment(&self.qt_thread(), &self.shared(), key.to_string(), index, Afterwards::Paper, false);
3288 }
3289
3290 fn papers(&self) -> QString {
3291 QString::from(&crate::papers::view())
3292 }
3293
3294 fn save_paper(self: Pin<&mut Self>, id: &QString, edit: &QString) -> QString {
3295 QString::from(&crate::papers::save(&id.to_string(), &edit.to_string()))
3296 }
3297
3298 fn remove_paper(self: Pin<&mut Self>, id: &QString) -> QString {
3299 QString::from(&crate::papers::remove(&id.to_string()))
3300 }
3301
3302 fn plan_renewal(self: Pin<&mut Self>, id: &QString) -> QString {
3303 QString::from(&crate::papers::plan_renewal(&self.qt_thread(), &self.shared(), &id.to_string()))
3304 }
3305
3306 fn let_in(mut self: Pin<&mut Self>, address: &QString) {
3307 let address = address.to_string();
3308 let line = match KnownSenders::let_in(&load_config().known_senders_path(), &address) {
3309 Ok(()) => say("ui-let-in-done", &[("address", address)]),
3310 Err(e) => e,
3311 };
3312 self.as_mut().set_status(QString::from(&line));
3313 show(&self.qt_thread(), &self.shared());
3314 }
3315
3316 fn block(self: Pin<&mut Self>, entry: &QString) {
3317 self.set_standing(entry, &QString::from("blocked"));
3318 }
3319
3320 fn block_from(self: Pin<&mut Self>, entry: &QString, key: &QString) {
3321 crate::mail::label_blocked(&key.to_string());
3322 self.block(entry);
3323 }
3324
3325 fn unblock(mut self: Pin<&mut Self>, entry: &QString) {
3326 let line = crate::senders::unblock(&entry.to_string());
3327 self.as_mut().set_status(QString::from(&line));
3328 let (qt, shared) = (self.qt_thread(), self.shared());
3329 show(&qt, &shared);
3330 pim::show_pim(&qt, &shared);
3331 }
3332
3333 fn set_standing(self: Pin<&mut Self>, entry: &QString, standing: &QString) {
3334 let entry = entry.to_string().trim().to_ascii_lowercase();
3335 self.set_standing_of(&QString::from(&serde_json::json!([entry]).to_string()), standing);
3337 }
3338
3339 fn standing(&self, address: &QString) -> QString {
3340 QString::from(porch::Senders::load(&load_config()).who(&address.to_string()).id())
3341 }
3342
3343 fn standing_of(&self, addresses: &QString) -> QString {
3344 QString::from(&crate::senders::standing_json(&addresses.to_string()))
3345 }
3346
3347 fn set_standing_of(mut self: Pin<&mut Self>, addresses: &QString, standing: &QString) {
3348 let line = crate::senders::set_standing_of(&addresses.to_string(), &standing.to_string());
3349 if line.is_empty() {
3350 return;
3351 }
3352 self.as_mut().set_status(QString::from(&line));
3353 let (qt, shared) = (self.qt_thread(), self.shared());
3354 show(&qt, &shared);
3355 pim::show_pim(&qt, &shared);
3356 }
3357
3358 fn person_of(&self, key: &QString) -> QString {
3359 QString::from(&crate::senders::person_json(&key.to_string()))
3360 }
3361
3362 fn set_person(mut self: Pin<&mut Self>, key: &QString, standing: &QString) {
3363 let line = crate::senders::set_person(&key.to_string(), &standing.to_string());
3364 if line.is_empty() {
3365 return;
3366 }
3367 self.as_mut().set_status(QString::from(&line));
3368 let (qt, shared) = (self.qt_thread(), self.shared());
3369 show(&qt, &shared);
3370 pim::show_pim(&qt, &shared);
3371 }
3372
3373 fn add_mail_line(mut self: Pin<&mut Self>, key: &QString, budget: &QString) {
3374 let (key, budget) = (key.to_string(), budget.to_string());
3375 let world = World::load();
3376 let outcome = (|| {
3377 let ledger = world.ledger().ok_or_else(|| tr().text("ui-no-budgets", None))?;
3378 let path = world.ledger_path().ok_or_else(|| tr().text("ui-no-budgets", None))?;
3379 let mail = world.mail_lines(&ledger);
3380 let found = mail.iter().find(|l| l.key == key).ok_or_else(|| tr().text("note-no-amount", None))?;
3381 let line = found.line(&budget).ok_or_else(|| tr().text("note-no-amount", None))?;
3382 let origin = say("budget-origin-mail", &[("subject", found.subject.clone()), ("sender", found.sender.clone()), ("date", Zoned::now().date().to_string())]);
3383 budget::record_line(&path, &line, &origin)?;
3384 Ok::<String, String>(say("ui-line-added", &[("budget", ledger.budget_title(&budget).to_string())]))
3385 })();
3386 let line = outcome.unwrap_or_else(|e| e);
3387 self.as_mut().set_status(QString::from(&line));
3388 show(&self.qt_thread(), &self.shared());
3389 }
3390
3391 fn ignore_mail_line(mut self: Pin<&mut Self>, key: &QString) {
3392 let path = MoneyState::default_path();
3393 let mut state = MoneyState::load(&path);
3394 state.ignored.insert(key.to_string());
3395 let line = match state.save(&path) {
3396 Ok(()) => tr().text("ui-line-ignored", None),
3397 Err(e) => e,
3398 };
3399 self.as_mut().set_status(QString::from(&line));
3400 show(&self.qt_thread(), &self.shared());
3401 }
3402
3403 fn discover(mut self: Pin<&mut Self>, address: &QString) {
3404 let address = address.to_string().trim().to_ascii_lowercase();
3405 let qt = self.qt_thread();
3406 self.as_mut().set_form_busy(true);
3407 self.as_mut().set_form_error(QString::default());
3408 self.as_mut().set_found(QString::default());
3409 std::thread::spawn(move || {
3410 let result = sioul_sync::discover(&address).map(|found| {
3411 let server = say(
3412 "account-server",
3413 &[
3414 ("host", found.host.clone()),
3415 ("port", found.port.to_string()),
3416 ("security", tr().text(&format!("security-{}", found.security.as_str()), None)),
3417 ],
3418 );
3419 let google = found.by == sioul_sync::FoundBy::Google || found.host.eq_ignore_ascii_case(sioul_sync::discover::GOOGLE_IMAP);
3421 json(&FoundView {
3422 by: say("account-found", &[("server", server), ("by", tr().text(found.by.message_id(), None))]),
3423 hint: tr().text(if google { "account-gmail-hint" } else { "account-app-password-hint" }, None),
3424 help_url: google.then_some(sioul_sync::GMAIL_APP_PASSWORDS),
3425 google,
3426 google_key: google && gmail::has_own_key(&address),
3427 host: found.host,
3428 port: found.port,
3429 security: found.security.as_str(),
3430 login: found.username,
3431 })
3432 });
3433 let _ = qt.queue(move |mut sioul| {
3434 sioul.as_mut().set_form_busy(false);
3435 match result {
3436 Ok(found) => sioul.as_mut().set_found(QString::from(&found)),
3437 Err(e) => sioul.as_mut().set_form_error(QString::from(&e.sentence(tr(), &address))),
3438 }
3439 });
3440 });
3441 }
3442
3443 fn add_account(mut self: Pin<&mut Self>, address: &QString, host: &QString, port: i32, security: &QString, login: &QString, password: &QString) {
3444 let (address, host, login, password) = (address.to_string().trim().to_ascii_lowercase(), host.to_string().trim().to_string(), login.to_string().trim().to_string(), password.to_string());
3445 let security = if security.to_string() == "starttls" { Security::Starttls } else { Security::Tls };
3446 let port = u16::try_from(port).ok().filter(|p| *p > 0).unwrap_or(if security == Security::Tls { 993 } else { 143 });
3447 self.as_mut().set_form_busy(true);
3448 self.as_mut().set_form_error(QString::default());
3449 add_mail(&self.qt_thread(), &self.shared(), address, host, port, security, login, Some(password));
3450 }
3451
3452 fn add_mail_like(mut self: Pin<&mut Self>, address: &QString, host: &QString, port: i32, security: &QString, login: &QString) {
3453 let (address, host, login) = (address.to_string().trim().to_ascii_lowercase(), host.to_string().trim().to_string(), login.to_string().trim().to_string());
3454 let security = if security.to_string() == "starttls" { Security::Starttls } else { Security::Tls };
3455 let port = u16::try_from(port).ok().filter(|p| *p > 0).unwrap_or(if security == Security::Tls { 993 } else { 143 });
3456 self.as_mut().set_form_busy(true);
3457 self.as_mut().set_form_error(QString::default());
3458 add_mail(&self.qt_thread(), &self.shared(), address, host, port, security, login, None);
3459 }
3460
3461 fn set_account_enabled(mut self: Pin<&mut Self>, id: &QString, on: bool) -> QString {
3462 let id = id.to_string();
3463 let config = load_config();
3464 let Some(account) = config.every_account().find(|a| a.id == id).cloned() else { return QString::from(&say("account-unknown", &[("id", id)])) };
3465 if let Err(e) = config::set_enabled(&config_path(), &id, on) {
3466 return QString::from(&e);
3467 }
3468 let (qt, shared) = (self.qt_thread(), self.shared());
3469 if account.kind == config::AccountKind::Dav {
3470 sioul_core::vdir::set_account_off(&id, !on);
3472 if on {
3473 pim::start_dav_watchers(&qt, &shared);
3474 } else {
3475 pim::stop_dav_watcher(&shared, &id);
3476 }
3477 } else if on {
3478 start_watchers(&qt, &shared);
3479 } else if let Some(control) = shared.watchers.lock().ok().and_then(|mut w| w.remove(&id)) {
3480 control.stop();
3481 }
3482 self.as_mut().set_status(QString::from(&say(if on { "account-on" } else { "account-off" }, &[("id", id)])));
3483 show(&qt, &shared);
3484 pim::show_pim(&qt, &shared);
3485 mail::show_mail(&qt, &shared);
3486 work::show_work(&qt, &shared);
3488 QString::default()
3489 }
3490
3491 fn scout_account(mut self: Pin<&mut Self>, address: &QString) {
3492 let address = address.to_string().trim().to_ascii_lowercase();
3493 let qt = self.qt_thread();
3494 self.as_mut().set_scouting(true);
3495 self.as_mut().set_scouted(QString::default());
3496 std::thread::spawn(move || {
3497 let config = load_config();
3498 let mine = |kind: config::AccountKind| config.every_account().find(|a| a.address.as_deref().map(str::to_lowercase).as_deref() == Some(address.as_str()) && a.kind == kind).cloned();
3499 let (mail_have, dav_have) = (mine(config::AccountKind::Imap), mine(config::AccountKind::Dav));
3500 let password = dav_have.as_ref().and_then(|a| secret::password(a).ok());
3502 let login = dav_have.as_ref().and_then(|a| a.login().map(str::to_string)).unwrap_or_else(|| address.clone());
3503 let found = sioul_sync::scout::scout(&address, dav_have.as_ref().and_then(|a| a.host.as_deref()), password.as_deref().map(|p| (login.as_str(), p)));
3504 let mail = found.mail.as_ref().map(|m| {
3505 let server = say("account-server", &[("host", m.host.clone()), ("port", m.port.to_string()), ("security", tr().text(&format!("security-{}", m.security.as_str()), None))]);
3506 serde_json::json!({ "host": m.host, "port": m.port, "security": m.security.as_str(), "login": m.username, "line": server, "have": mail_have.as_ref().map(|a| a.id.clone()) })
3507 });
3508 let dav = found.dav.as_ref().map(|url| serde_json::json!({ "url": url, "have": dav_have.as_ref().map(|a| a.id.clone()) }));
3509 let nextcloud = found.nextcloud.as_ref().map(|n| {
3510 let apps: Vec<serde_json::Value> = n
3511 .apps
3512 .iter()
3513 .filter_map(|a| sioul_sync::scout::known_app(a))
3514 .map(|(id, in_sioul)| serde_json::json!({ "id": id, "title": tr().text(&format!("scout-app-{id}"), None), "in_sioul": in_sioul }))
3515 .collect();
3516 serde_json::json!({ "product": n.product, "version": n.version, "server": n.server, "apps": apps, "asked": password.is_some() })
3517 });
3518 let answer = serde_json::json!({ "address": address, "mail": mail, "dav": dav, "nextcloud": nextcloud, "dav_password": password.is_some() }).to_string();
3519 let _ = qt.queue(move |mut sioul| {
3520 sioul.as_mut().set_scouting(false);
3521 sioul.as_mut().set_scouted(QString::from(&answer));
3522 });
3523 });
3524 }
3525
3526 fn add_portal(mut self: Pin<&mut Self>, name: &QString, url: &QString) {
3527 let (id, url) = (config::slug(&name.to_string()), url.to_string().trim().to_string());
3528 let config = load_config();
3529 let outcome = if config.site(&id).is_some() {
3530 Err(say("account-exists", &[("id", id.clone())]))
3531 } else if id.is_empty() || !url.starts_with("https://") {
3532 Err(tr().text("site-https-only", None))
3534 } else {
3535 config::add_site(&config_path(), &id, &name.to_string(), &url).map(|()| say("account-portal-added", &[("id", id.clone()), ("url", url.clone())]))
3536 };
3537 match outcome {
3538 Ok(line) => self.as_mut().set_status(QString::from(&line)),
3539 Err(e) => self.as_mut().set_form_error(QString::from(&e)),
3540 }
3541 show(&self.qt_thread(), &self.shared());
3542 }
3543
3544 fn set_account_password(mut self: Pin<&mut Self>, id: &QString, password: &QString) {
3545 self.as_mut().set_form_busy(true);
3546 give_password(&self.qt_thread(), &self.shared(), id.to_string(), password.to_string());
3547 }
3548
3549 fn remove_account(mut self: Pin<&mut Self>, id: &QString) {
3550 let id = id.to_string();
3551 let shared = self.shared();
3552 if let Some(control) = shared.watchers.lock().ok().and_then(|mut w| w.remove(&id)) {
3553 control.stop();
3554 }
3555 if let Ok(mut statuses) = shared.statuses.lock() {
3556 statuses.remove(&id);
3557 }
3558 let config = load_config();
3559 let line = match config.every_account().find(|a| a.id == id) {
3561 None => say("account-unknown", &[("id", id.clone())]),
3562 Some(account) => {
3563 if account.is_dav() {
3564 pim::stop_dav_watcher(&shared, &id);
3565 }
3566 let shared_password = config.every_account().any(|a| a.id != id && a.host == account.host && a.login() == account.login());
3569 let forgotten = if account.auth.as_deref() == Some("google") {
3570 let purpose = if account.is_dav() { sioul_sync::google::Purpose::Pim } else { sioul_sync::google::Purpose::Mail };
3572 sioul_sync::google::revoke_for(purpose, account.address.as_deref().unwrap_or("")).map_err(|e| e.sentence(tr(), &id))
3573 } else if (account.syncs() || account.is_dav()) && !shared_password {
3574 secret::forget(account).map_err(|e| e.sentence(tr(), &id))
3575 } else {
3576 Ok(())
3577 };
3578 match forgotten.and_then(|()| config::remove_account(&config_path(), &id)) {
3579 Ok(()) => say("account-removed", &[("id", id.clone()), ("path", account.maildir_path().display().to_string())]),
3580 Err(e) => e,
3581 }
3582 }
3583 };
3584 self.as_mut().set_status(QString::from(&line));
3585 show(&self.qt_thread(), &shared);
3586 pim::show_pim(&self.qt_thread(), &shared);
3588 work::show_work(&self.qt_thread(), &shared);
3589 }
3590
3591 fn set_priority(mut self: Pin<&mut Self>, id: &QString, priority: &QString) {
3592 let id = id.to_string();
3593 let line = match Priority::parse(&priority.to_string()) {
3594 Some(priority) => match config::set_priority(&config_path(), &id, priority) {
3595 Ok(()) => say("ui-priority-set", &[("id", id), ("priority", tr().text(&format!("priority-{}", priority.as_str()), None))]),
3596 Err(e) => e,
3597 },
3598 None => return,
3599 };
3600 self.as_mut().set_status(QString::from(&line));
3601 show(&self.qt_thread(), &self.shared());
3602 }
3603
3604 fn open_folder(self: Pin<&mut Self>, account: &QString, folder: &QString, all: bool, query: &QString) {
3605 let shared = self.shared();
3606 if let Ok(mut open) = shared.open_folder.lock() {
3607 *open = Some(mail::OpenFolder { account: account.to_string(), folder: folder.to_string(), all, query: query.to_string() });
3608 }
3609 mail::show_mail(&self.qt_thread(), &shared);
3610 }
3611
3612 fn opened(self: Pin<&mut Self>, key: &QString) {
3613 mail::opened(&self.qt_thread(), &self.shared(), &key.to_string());
3614 }
3615
3616 fn act(self: Pin<&mut Self>, key: &QString, action: &QString, target: &QString) {
3617 mail::act(&self.qt_thread(), &self.shared(), &key.to_string(), &action.to_string(), &target.to_string());
3618 }
3619
3620 fn act_many(self: Pin<&mut Self>, keys: &QString, action: &QString) {
3621 let keys: Vec<String> = serde_json::from_str(&keys.to_string()).unwrap_or_default();
3622 mail::act_many(&self.qt_thread(), &self.shared(), &keys, &action.to_string());
3623 }
3624
3625 fn move_messages(self: Pin<&mut Self>, keys: &QString, account: &QString, folder: &QString) {
3626 let keys: Vec<String> = serde_json::from_str(&keys.to_string()).unwrap_or_default();
3627 mail::move_many(&self.qt_thread(), &self.shared(), &keys, &account.to_string(), &folder.to_string());
3628 }
3629
3630 fn unsubscribe(self: Pin<&mut Self>, key: &QString) -> QString {
3631 let world = World::load();
3632 let triaged = world.judge(&key.to_string()).map(|(_, triaged)| triaged);
3633 QString::from(&mail::unsubscribe(&self.qt_thread(), &self.shared(), &world.config, triaged))
3634 }
3635
3636 fn undo(mut self: Pin<&mut Self>) {
3637 if let Some(draft) = mail::undo(&self.qt_thread(), &self.shared()) {
3638 self.as_mut().compose_requested(QString::from(&draft));
3639 }
3640 }
3641
3642 fn flush(self: Pin<&mut Self>) {
3643 mail::flush(&self.shared());
3644 end_work_now();
3645 securitykey::closing();
3646 }
3647
3648 fn compose(mut self: Pin<&mut Self>, kind: &QString, key: &QString, account: &QString) -> QString {
3649 match mail::compose(&kind.to_string(), &key.to_string(), &account.to_string()) {
3650 Ok(id) => {
3651 mail::show_mail(&self.qt_thread(), &self.shared());
3652 QString::from(&id)
3653 }
3654 Err(e) => {
3655 self.as_mut().set_status(QString::from(&e));
3656 QString::default()
3657 }
3658 }
3659 }
3660
3661 fn draft(&self, id: &QString) -> QString {
3662 QString::from(&mail::draft(&id.to_string()).unwrap_or_default())
3663 }
3664
3665 fn save_draft(self: Pin<&mut Self>, edit: &QString) -> QString {
3666 let line = mail::save_draft(&edit.to_string()).unwrap_or_else(|e| e);
3667 mail::show_mail(&self.qt_thread(), &self.shared());
3668 QString::from(&line)
3669 }
3670
3671 fn attach(self: Pin<&mut Self>, id: &QString, url: &QString) -> QString {
3672 QString::from(&mail::attach(&id.to_string(), &url.to_string()).err().unwrap_or_default())
3673 }
3674
3675 fn detach(mut self: Pin<&mut Self>, id: &QString, index: i32, forwarded: bool) {
3676 if let Err(e) = mail::detach(&id.to_string(), usize::try_from(index).unwrap_or(usize::MAX), forwarded) {
3677 self.as_mut().set_status(QString::from(&e));
3678 }
3679 }
3680
3681 fn preview(&self, markdown: &QString) -> QString {
3682 QString::from(&sioul_core::compose::markdown_html(&markdown.to_string()))
3683 }
3684
3685 fn send(self: Pin<&mut Self>, id: &QString) -> QString {
3686 QString::from(&mail::send(&self.qt_thread(), &self.shared(), &id.to_string()).unwrap_or_default())
3687 }
3688
3689 fn discard(self: Pin<&mut Self>, id: &QString) {
3690 mail::discard(&self.qt_thread(), &self.shared(), &id.to_string());
3691 }
3692
3693 fn draft_closed(self: Pin<&mut Self>, id: &QString) {
3694 mail::closed(&self.qt_thread(), &self.shared(), &id.to_string());
3695 }
3696
3697 fn place(&self, key: &QString) -> QString {
3698 QString::from(&mail::place(&key.to_string()))
3699 }
3700
3701 fn source(&self, key: &QString) -> QString {
3702 QString::from(&mail::source(&key.to_string()))
3703 }
3704
3705 fn set_writing(mut self: Pin<&mut Self>, id: &QString, name: &QString, signature: &QString) {
3706 let id = id.to_string();
3707 let line = match config::set_writing(&config_path(), &id, &name.to_string(), &signature.to_string()) {
3708 Ok(()) => say("ui-writing-saved", &[("id", id)]),
3709 Err(e) => e,
3710 };
3711 self.as_mut().set_status(QString::from(&line));
3712 show(&self.qt_thread(), &self.shared());
3713 }
3714
3715 fn grab_folder(&self) -> QString {
3716 QString::from(&std::env::var("SIOUL_GRAB").unwrap_or_default())
3717 }
3718
3719 fn search_contacts(self: Pin<&mut Self>, query: &QString) {
3720 let shared = self.shared();
3721 if let Ok(mut state) = shared.pim.lock() {
3722 state.query = query.to_string();
3723 }
3724 pim::show_pim(&self.qt_thread(), &shared);
3725 }
3726
3727 fn contact(&self, key: &QString) -> QString {
3728 QString::from(&pim::contact(&key.to_string()))
3729 }
3730
3731 fn save_contact(self: Pin<&mut Self>, key: &QString, edit: &QString) -> QString {
3732 let answer = match pim::save_contact(&self.qt_thread(), &self.shared(), &key.to_string(), &edit.to_string()) {
3733 Ok(key) => serde_json::json!({ "key": key }),
3734 Err(error) => serde_json::json!({ "error": error }),
3735 };
3736 QString::from(&answer.to_string())
3737 }
3738
3739 fn delete_contact(self: Pin<&mut Self>, key: &QString) {
3740 pim::delete_contact(&self.qt_thread(), &self.shared(), &key.to_string());
3741 }
3742
3743 fn show_contacts_category(self: Pin<&mut Self>, category: &QString) {
3744 let shared = self.shared();
3745 if let Ok(mut state) = shared.pim.lock() {
3746 state.category = category.to_string();
3747 }
3748 pim::show_pim(&self.qt_thread(), &shared);
3749 }
3750
3751 fn find_duplicates(self: Pin<&mut Self>) {
3752 crate::duplicates::find(&self.qt_thread(), &self.shared());
3753 }
3754
3755 fn clean_contacts(self: Pin<&mut Self>, keys: &QString) -> QString {
3756 QString::from(&crate::duplicates::clean(&self.qt_thread(), &self.shared(), &keys.to_string()))
3757 }
3758
3759 fn merge_contacts(self: Pin<&mut Self>, lead: &QString, other: &QString) -> QString {
3760 QString::from(&crate::duplicates::merge(&self.qt_thread(), &self.shared(), &lead.to_string(), &other.to_string()))
3761 }
3762
3763 fn not_same_contacts(self: Pin<&mut Self>, first: &QString, second: &QString) -> QString {
3764 QString::from(&crate::duplicates::not_same(&self.qt_thread(), &self.shared(), &first.to_string(), &second.to_string()))
3765 }
3766
3767 fn undo_contacts(self: Pin<&mut Self>, id: &QString) -> QString {
3768 QString::from(&crate::duplicates::undo(&self.qt_thread(), &self.shared(), &id.to_string()))
3769 }
3770
3771 fn show_days(self: Pin<&mut Self>, from: &QString, days: i32, upcoming: bool) {
3772 let shared = self.shared();
3773 if let Ok(mut state) = shared.pim.lock() {
3774 state.from = from.to_string().parse().unwrap_or_else(|_| Zoned::now().date());
3775 state.days = i64::from(days.clamp(1, 366));
3776 state.upcoming = upcoming;
3777 }
3778 pim::show_pim(&self.qt_thread(), &shared);
3779 }
3780
3781 fn calendars(&self) -> QString {
3782 QString::from(&pim::calendars())
3783 }
3784
3785 fn event_form(&self, key: &QString) -> QString {
3786 QString::from(&pim::event(&key.to_string()))
3787 }
3788
3789 fn save_event(self: Pin<&mut Self>, key: &QString, edit: &QString, calendar: &QString) -> QString {
3790 let result = pim::save_event(&self.qt_thread(), &self.shared(), &key.to_string(), &edit.to_string(), &calendar.to_string());
3791 QString::from(&result.err().unwrap_or_default())
3792 }
3793
3794 fn event_reminder(&self, key: &QString, start: f64) -> QString {
3795 QString::from(&crate::remind::of_event(&key.to_string(), start as i64))
3796 }
3797
3798 fn reminder_choices(&self, current: &QString) -> QString {
3799 QString::from(&crate::remind::choices(¤t.to_string()))
3800 }
3801
3802 fn set_event_reminder(self: Pin<&mut Self>, key: &QString, value: &QString) -> QString {
3803 let result = crate::remind::set_event(&self.qt_thread(), &self.shared(), &key.to_string(), &value.to_string());
3804 QString::from(&result.err().unwrap_or_default())
3805 }
3806
3807 fn move_event(self: Pin<&mut Self>, key: &QString, start: f64, new_start: f64, new_end: f64, only_this: bool) -> QString {
3808 QString::from(&pim::move_event(&self.qt_thread(), &self.shared(), &key.to_string(), start as i64, new_start as i64, new_end as i64, only_this))
3809 }
3810
3811 fn delete_event(self: Pin<&mut Self>, key: &QString, start: f64, only_this: bool) {
3812 pim::delete_event(&self.qt_thread(), &self.shared(), &key.to_string(), start as i64, only_this);
3813 }
3814
3815 fn completions(&self, typed: &QString) -> QString {
3816 QString::from(&pim::completions(&typed.to_string()))
3817 }
3818
3819 fn contact_for(&self, address: &QString) -> QString {
3820 QString::from(&pim::contact_for(&address.to_string()))
3821 }
3822
3823 fn add_sender(mut self: Pin<&mut Self>, name: &QString, address: &QString) {
3824 if let Err(e) = pim::add_sender(&self.qt_thread(), &self.shared(), &name.to_string(), &address.to_string()) {
3825 self.as_mut().set_status(QString::from(&e));
3826 }
3827 }
3828
3829 fn answer_invitation(mut self: Pin<&mut Self>, key: &QString, answer: &QString) {
3830 let (qt, shared, key, answer) = (self.qt_thread(), self.shared(), key.to_string(), answer.to_string());
3832 self.as_mut().set_status(QString::from(&tr().text("invitation-answering", None)));
3833 std::thread::spawn(move || {
3834 if let Err(e) = pim::answer(&qt, &shared, &key, &answer) {
3835 tell(&qt, &shared, e);
3836 }
3837 });
3838 }
3839
3840 fn history_weeks(&self) -> i32 {
3841 i32::try_from(load_config().history_weeks.unwrap_or(config::DEFAULT_HISTORY_WEEKS)).unwrap_or(2)
3842 }
3843
3844 fn day_start(&self) -> i32 {
3845 i32::try_from(load_config().agenda.day_start.unwrap_or(7).min(23)).unwrap_or(7)
3846 }
3847
3848 fn set_history(mut self: Pin<&mut Self>, weeks: i32) {
3849 let weeks = u32::try_from(weeks.max(0)).unwrap_or(0);
3850 let line = match config::set_history(&config_path(), weeks) {
3851 Ok(()) if weeks == 0 => tr().text("ui-history-everything", None),
3852 Ok(()) => say("ui-history-set", &[("weeks", weeks.to_string())]),
3853 Err(e) => e,
3854 };
3855 self.as_mut().set_status(QString::from(&line));
3856 let (qt, shared) = (self.qt_thread(), self.shared());
3858 start_watchers(&qt, &shared);
3859 show(&qt, &shared);
3860 pim::show_pim(&qt, &shared);
3861 }
3862
3863 fn add_dav(mut self: Pin<&mut Self>, address: &QString, url: &QString, login: &QString, password: &QString) {
3864 self.as_mut().set_form_busy(true);
3865 self.as_mut().set_form_error(QString::default());
3866 pim::add_account(&self.qt_thread(), &self.shared(), address.to_string(), url.to_string(), login.to_string(), password.to_string());
3867 }
3868
3869 fn add_google(mut self: Pin<&mut Self>, address: &QString, client_id: &QString, client_secret: &QString, again: &QString) {
3870 self.as_mut().set_form_busy(true);
3871 self.as_mut().set_form_error(QString::default());
3872 let again = Some(again.to_string()).filter(|a| !a.is_empty());
3873 pim::add_google_account(&self.qt_thread(), &self.shared(), address.to_string(), client_id.to_string(), client_secret.to_string(), again);
3874 }
3875
3876 fn cancel_google(self: Pin<&mut Self>) {
3877 self.shared().google_stop.store(true, Ordering::Relaxed);
3878 }
3879
3880 fn add_google_mail(mut self: Pin<&mut Self>, address: &QString, client_id: &QString, client_secret: &QString, again: &QString) {
3881 self.as_mut().set_form_busy(true);
3882 self.as_mut().set_form_error(QString::default());
3883 let again = Some(again.to_string()).filter(|a| !a.is_empty());
3884 gmail::add_google_mail(&self.qt_thread(), &self.shared(), address.to_string(), client_id.to_string(), client_secret.to_string(), again);
3885 }
3886
3887 fn pgp_keys(&self) -> QString {
3888 QString::from(&crypto::keys_view())
3889 }
3890
3891 fn pgp_generate(mut self: Pin<&mut Self>, address: &QString) {
3892 let (qt, shared, address) = (self.qt_thread(), self.shared(), address.to_string());
3894 self.as_mut().set_status(QString::from(&tr().text("pgp-making", None)));
3895 std::thread::spawn(move || {
3896 let line = crypto::generate(&address).unwrap_or_else(|e| e);
3897 tell(&qt, &shared, line);
3898 let _ = qt.queue(|mut sioul| sioul.as_mut().keys_changed());
3899 });
3900 }
3901
3902 fn pgp_import(mut self: Pin<&mut Self>, url: &QString, passphrase: &QString) -> QString {
3903 match crypto::import(&url.to_string(), &passphrase.to_string()) {
3904 Ok(line) => {
3905 self.as_mut().set_status(QString::from(&line));
3906 self.as_mut().keys_changed();
3907 QString::default()
3908 }
3909 Err(e) => QString::from(&e),
3910 }
3911 }
3912
3913 fn pgp_export(mut self: Pin<&mut Self>, fingerprint: &QString) {
3914 let line = match crypto::export(&fingerprint.to_string(), &downloads()) {
3915 Ok(path) => say("ui-saved", &[("path", path.display().to_string())]),
3916 Err(e) => e,
3917 };
3918 self.as_mut().set_status(QString::from(&line));
3919 }
3920
3921 fn pgp_save_revocation(mut self: Pin<&mut Self>, fingerprint: &QString) {
3922 let line = match sioul_core::pgp::save_revocation(&fingerprint.to_string(), &downloads()) {
3923 Ok(path) => say("pgp-revocation-saved", &[("path", path.display().to_string())]),
3924 Err(e) => e,
3925 };
3926 self.as_mut().set_status(QString::from(&line));
3927 }
3928
3929 fn pgp_remove(mut self: Pin<&mut Self>, fingerprint: &QString) {
3930 let line = match sioul_core::pgp::remove(&fingerprint.to_string()) {
3931 Ok(()) => tr().text("pgp-removed", None),
3932 Err(e) => e,
3933 };
3934 self.as_mut().set_status(QString::from(&line));
3935 self.as_mut().keys_changed();
3936 }
3937
3938 fn pgp_lookup(mut self: Pin<&mut Self>, draft: &QString) {
3939 let (qt, shared, id) = (self.qt_thread(), self.shared(), draft.to_string());
3940 self.as_mut().set_status(QString::from(&tr().text("pgp-looking", None)));
3941 std::thread::spawn(move || {
3942 let Some(draft) = sioul_core::compose::Draft::by_id(&id) else { return };
3943 tell(&qt, &shared, crypto::lookup(&draft));
3944 let _ = qt.queue(|mut sioul| sioul.as_mut().keys_changed());
3945 });
3946 }
3947
3948 fn security_key_for_draft(&self, id: &QString) -> QString {
3949 QString::from(&securitykey::for_draft(&id.to_string()))
3950 }
3951
3952 fn sign_and_send(self: Pin<&mut Self>, id: &QString, pin: &QString) {
3953 securitykey::sign_and_send(&self.qt_thread(), &self.shared(), &id.to_string(), pin.to_string());
3954 }
3955
3956 fn open_with_security_key(self: Pin<&mut Self>, key: &QString, pin: &QString) {
3957 securitykey::open_message(&self.qt_thread(), &self.shared(), &key.to_string(), pin.to_string());
3958 }
3959
3960 fn security_key_not_now(self: Pin<&mut Self>, context: &QString) {
3961 securitykey::not_now(&context.to_string());
3962 }
3963
3964 fn let_gnupg_release(self: Pin<&mut Self>) -> QString {
3965 QString::from(&securitykey::release())
3966 }
3967
3968 fn security_keys(&self) -> QString {
3969 QString::from(&securitykey::known_view())
3970 }
3971
3972 fn read_security_key(self: Pin<&mut Self>) {
3973 securitykey::read_for_setup(&self.qt_thread());
3974 }
3975
3976 fn find_security_key_certificate(self: Pin<&mut Self>, ident: &QString) {
3977 securitykey::look_for_certificate(&self.qt_thread(), &ident.to_string());
3978 }
3979
3980 fn import_security_key_certificate(self: Pin<&mut Self>, ident: &QString, url: &QString) {
3981 securitykey::import_certificate(&self.qt_thread(), &ident.to_string(), &url.to_string());
3982 }
3983
3984 fn import_security_key_from_gnupg(self: Pin<&mut Self>, ident: &QString) {
3985 securitykey::import_from_gnupg(&self.qt_thread(), &ident.to_string());
3986 }
3987
3988 fn renew_security_key(self: Pin<&mut Self>, ident: &QString) {
3989 securitykey::renew(&self.qt_thread(), &ident.to_string());
3990 }
3991
3992 fn send_security_key_to_keys_openpgp(self: Pin<&mut Self>, ident: &QString) {
3993 securitykey::send_to_keys_openpgp(&self.qt_thread(), &ident.to_string());
3994 }
3995
3996 fn forget_security_key_pin(mut self: Pin<&mut Self>) {
3997 let line = securitykey::forget_pin();
3998 self.as_mut().set_status(QString::from(&line));
3999 self.as_mut().keys_changed();
4000 }
4001
4002 fn stop_using_security_key(self: Pin<&mut Self>, ident: &QString) {
4003 securitykey::stop_using(&self.qt_thread(), &self.shared(), &ident.to_string());
4004 }
4005
4006 fn show_tasks(self: Pin<&mut Self>, by: &QString, done: bool, query: &QString, project_id: &QString) {
4007 work::set_view(&self.qt_thread(), &self.shared(), &by.to_string(), done, &query.to_string(), &project_id.to_string());
4008 }
4009
4010 fn filter_tasks(self: Pin<&mut Self>, kind: &QString, category: &QString) {
4011 work::set_filter(&self.qt_thread(), &self.shared(), &kind.to_string(), &category.to_string());
4012 }
4013
4014 fn refresh_work(self: Pin<&mut Self>) {
4015 work::show_work(&self.qt_thread(), &self.shared());
4016 }
4017
4018 fn show_tasks_anyway(self: Pin<&mut Self>, on: bool) {
4019 work::show_anyway(&self.qt_thread(), &self.shared(), on);
4020 }
4021
4022 fn task(&self, uid: &QString) -> QString {
4023 QString::from(&work::task(&self.shared(), &uid.to_string()))
4024 }
4025
4026 fn new_task_form(&self, from: &QString, key: &QString, start: f64, list: &QString) -> QString {
4027 QString::from(&work::new_form(&self.shared(), &from.to_string(), &key.to_string(), start, &list.to_string()))
4028 }
4029
4030 fn save_task(self: Pin<&mut Self>, uid: &QString, edit: &QString, list: &QString) -> QString {
4031 QString::from(&work::save(&self.qt_thread(), &self.shared(), &uid.to_string(), &edit.to_string(), &list.to_string()))
4032 }
4033
4034 fn capture(&self, line: &QString) -> QString {
4035 QString::from(&work::captured(&self.shared(), &line.to_string()))
4036 }
4037
4038 fn add_task(self: Pin<&mut Self>, line: &QString, parent: &QString, list: &QString) -> QString {
4039 QString::from(&work::add(&self.qt_thread(), &self.shared(), &line.to_string(), &parent.to_string(), &list.to_string()))
4040 }
4041
4042 fn set_task_status(self: Pin<&mut Self>, uid: &QString, status: &QString) -> QString {
4043 QString::from(&work::set_status(&self.qt_thread(), &self.shared(), &uid.to_string(), &status.to_string()))
4044 }
4045
4046 fn set_felt(self: Pin<&mut Self>, uid: &QString, felt: &QString) -> QString {
4047 QString::from(&work::set_felt(&self.qt_thread(), &self.shared(), &uid.to_string(), &felt.to_string()))
4048 }
4049
4050 fn set_waits(self: Pin<&mut Self>, uid: &QString, other: &QString, wait: bool, gap_minutes: i32) -> QString {
4051 QString::from(&work::set_waits(&self.qt_thread(), &self.shared(), &uid.to_string(), &other.to_string(), wait, i64::from(gap_minutes)))
4052 }
4053
4054 fn not_now(self: Pin<&mut Self>, uid: &QString) {
4055 work::not_now(&self.qt_thread(), &self.shared(), &uid.to_string());
4056 }
4057
4058 fn set_weather(self: Pin<&mut Self>, weather: &QString) {
4059 work::set_weather(&self.qt_thread(), &self.shared(), &weather.to_string());
4060 crate::reviews::weather_said(sioul_core::today::Weather::parse(&weather.to_string()));
4062 }
4063
4064 fn set_task_at(self: Pin<&mut Self>, uid: &QString, at: &QString) -> QString {
4065 QString::from(&work::set_at(&self.qt_thread(), &self.shared(), &uid.to_string(), &at.to_string(), 0))
4066 }
4067
4068 fn pin_task(self: Pin<&mut Self>, uid: &QString, at: &QString, minutes: i32) -> QString {
4069 QString::from(&work::set_at(&self.qt_thread(), &self.shared(), &uid.to_string(), &at.to_string(), u32::try_from(minutes).unwrap_or(0)))
4070 }
4071
4072 fn delete_task(self: Pin<&mut Self>, uid: &QString) {
4073 work::delete(&self.qt_thread(), &self.shared(), &uid.to_string());
4074 }
4075
4076 fn search_tasks(&self, query: &QString, except: &QString) -> QString {
4077 QString::from(&work::search(&self.shared(), &query.to_string(), &except.to_string()))
4078 }
4079
4080 fn new_list(self: Pin<&mut Self>, account: &QString, name: &QString) -> QString {
4081 QString::from(&work::new_list(&self.qt_thread(), &self.shared(), &account.to_string(), &name.to_string()))
4082 }
4083
4084 fn list_accounts(&self) -> QString {
4085 QString::from(&work::list_accounts())
4086 }
4087
4088 fn focus_start(self: Pin<&mut Self>, uid: &QString, minutes: i32) {
4089 work::focus_start(&self.qt_thread(), &self.shared(), &uid.to_string(), minutes);
4090 }
4091
4092 fn focus_pause(self: Pin<&mut Self>) {
4093 work::focus_pause(&self.qt_thread(), &self.shared());
4094 }
4095
4096 fn focus_extend(self: Pin<&mut Self>, minutes: i32) {
4097 work::focus_extend(&self.qt_thread(), &self.shared(), minutes);
4098 }
4099
4100 fn focus_stop(self: Pin<&mut Self>, done: bool, note: &QString) -> QString {
4101 QString::from(&work::focus_stop(&self.qt_thread(), &self.shared(), done, ¬e.to_string()))
4102 }
4103
4104 fn search_notes(self: Pin<&mut Self>, query: &QString) {
4105 work::search_notes(&self.qt_thread(), &self.shared(), &query.to_string());
4106 }
4107
4108 fn note(&self, path: &QString) -> QString {
4109 QString::from(&work::note(&self.shared(), &path.to_string()))
4110 }
4111
4112 fn save_note(self: Pin<&mut Self>, path: &QString, text: &QString, stamp: &QString) -> QString {
4113 QString::from(&work::save_note(&self.qt_thread(), &self.shared(), &path.to_string(), &text.to_string(), &stamp.to_string()))
4114 }
4115
4116 fn create_note(self: Pin<&mut Self>, title: &QString, links: &QString) -> QString {
4117 QString::from(&work::create_note(&self.qt_thread(), &self.shared(), &title.to_string(), &links.to_string()))
4118 }
4119
4120 fn related(&self, uri: &QString) -> QString {
4121 QString::from(&work::related(&self.shared(), &uri.to_string()))
4122 }
4123
4124 fn link_things(mut self: Pin<&mut Self>, from: &QString, to: &QString) -> QString {
4125 let line = work::link(&self.qt_thread(), &self.shared(), &from.to_string(), &to.to_string());
4126 self.as_mut().set_status(QString::from(&line));
4127 QString::from(&line)
4128 }
4129
4130 fn unlink_things(mut self: Pin<&mut Self>, from: &QString, to: &QString) -> QString {
4131 let line = work::unlink(&self.qt_thread(), &self.shared(), &from.to_string(), &to.to_string());
4132 self.as_mut().set_status(QString::from(&line));
4133 QString::from(&line)
4134 }
4135
4136 fn search_things(&self, query: &QString, kind: &QString, from: &QString) -> QString {
4137 QString::from(&work::search_things(&self.shared(), &query.to_string(), &kind.to_string(), &from.to_string()))
4138 }
4139
4140 fn uri_of(&self, kind: &QString, id: &QString) -> QString {
4141 QString::from(&work::uri_of(&kind.to_string(), &id.to_string()))
4142 }
4143
4144 fn is_program(&self, name: &QString) -> bool {
4145 crate::attachments::kept_closed(&name.to_string(), crate::attachments::phone())
4146 }
4147
4148 fn first_frame(&self) {
4149 crate::timing("the first frame drawn");
4150 }
4151
4152 fn mark(&self, what: &QString) {
4153 crate::timing(&what.to_string());
4154 }
4155
4156 fn phone_accounts(&self) -> bool {
4157 cfg!(target_os = "android")
4158 }
4159
4160 fn choose_phone_account(self: Pin<&mut Self>) {
4161 #[cfg(target_os = "android")]
4162 {
4163 if let Ok(mut waiting) = PHONE_CHOOSER.lock() {
4164 *waiting = Some(self.qt_thread());
4165 }
4166 unsafe { sioul_android_choose_account() };
4169 }
4170 #[cfg(not(target_os = "android"))]
4171 let _ = self;
4172 }
4173
4174 fn budget_detail(&self, id: &QString, anchor: &QString, step: &QString) -> QString {
4175 let world = World::load();
4176 let today = Zoned::now().date();
4177 let Some(ledger) = world.ledger() else { return QString::default() };
4178 let Some(found) = ledger.budgets.iter().find(|b| b.id == id.to_string()) else { return QString::default() };
4179 let anchor = anchor.to_string().parse().unwrap_or(today);
4180 QString::from(&json(&view::budget_detail(&ledger, found, anchor, &step.to_string(), tr(), today)))
4181 }
4182
4183 fn keep_folder(self: Pin<&mut Self>, account: &QString, folder: &QString, kept: bool) -> QString {
4184 let (id, folder) = (account.to_string(), folder.to_string());
4185 let path = config_path();
4188 let changed = sioul_core::filelock::with_lock(&path, || {
4189 let config = config::Config::load(&path)?;
4190 let Some(account) = config.account(&id).cloned() else { return Ok(None) };
4191 let mut skipped = account.skip_folders.clone();
4192 skipped.retain(|f| f != &folder);
4193 if !kept {
4194 skipped.push(folder.clone());
4195 }
4196 config::set_value(&path, &format!("account.{id}.skip_folders"), &config::SettingValue::Texts(skipped)).map(|()| Some(account))
4197 });
4198 let account = match changed {
4199 Ok(Some(account)) => account,
4200 Ok(None) => return QString::default(),
4201 Err(e) => return QString::from(&e),
4202 };
4203 let (qt, shared) = (self.qt_thread(), self.shared());
4204 std::thread::spawn(move || {
4205 if !kept && let Err(e) = sioul_sync::mailbox::forget_folder(&account, &folder) {
4206 set_status(&qt, e.sentence(tr(), &account.id));
4207 }
4208 if let Some(control) = shared.watchers.lock().ok().and_then(|w| w.get(&account.id).cloned()) {
4209 control.nudge();
4210 }
4211 mail::show_mail(&qt, &shared);
4212 });
4213 QString::default()
4214 }
4215
4216 fn create_folder(self: Pin<&mut Self>, account: &QString, name: &QString) {
4217 let (id, name) = (account.to_string(), name.to_string());
4218 let (qt, shared) = (self.qt_thread(), self.shared());
4219 std::thread::spawn(move || {
4220 let Some(account) = load_config().account(&id).cloned() else { return };
4221 let made = sioul_sync::secret::password(&account).and_then(|p| sioul_sync::mailbox::create_folder(&account, &p, &name));
4222 match made {
4223 Ok(()) => tell(&qt, &shared, say("folder-made", &[("name", name.clone())])),
4224 Err(e) => tell(&qt, &shared, e.sentence(tr(), &account.id)),
4225 }
4226 mail::show_mail(&qt, &shared);
4227 });
4228 }
4229
4230 fn delete_folder(self: Pin<&mut Self>, account: &QString, folder: &QString) {
4231 let (id, folder) = (account.to_string(), folder.to_string());
4232 let (qt, shared) = (self.qt_thread(), self.shared());
4233 std::thread::spawn(move || {
4234 let Some(account) = load_config().account(&id).cloned() else { return };
4235 let gone = sioul_sync::secret::password(&account).and_then(|p| sioul_sync::mailbox::delete_folder(&account, &p, &folder));
4236 match gone {
4237 Ok(()) => tell(&qt, &shared, say("folder-deleted", &[("name", folder.clone())])),
4238 Err(sioul_sync::SyncError::Message(m)) if m == sioul_sync::mailbox::NOT_EMPTY => tell(&qt, &shared, say("folder-not-empty", &[("name", folder.clone())])),
4239 Err(e) => tell(&qt, &shared, e.sentence(tr(), &account.id)),
4240 }
4241 mail::show_mail(&qt, &shared);
4242 });
4243 }
4244
4245 fn trash_note(self: Pin<&mut Self>, path: &QString) -> QString {
4246 QString::from(&work::trash_note(&self.qt_thread(), &self.shared(), &path.to_string()))
4247 }
4248
4249 fn rename_note(self: Pin<&mut Self>, path: &QString, name: &QString) -> QString {
4250 QString::from(&work::rename_note(&self.qt_thread(), &self.shared(), &path.to_string(), &name.to_string()))
4251 }
4252
4253 fn create_note_in(self: Pin<&mut Self>, folder: &QString, title: &QString) -> QString {
4254 QString::from(&work::create_note_in(&self.qt_thread(), &self.shared(), &folder.to_string(), &title.to_string()))
4255 }
4256
4257 fn make_folder(self: Pin<&mut Self>, parent: &QString, name: &QString) -> QString {
4258 QString::from(&work::make_folder(&self.qt_thread(), &self.shared(), &parent.to_string(), &name.to_string()))
4259 }
4260
4261 fn rename_folder(self: Pin<&mut Self>, path: &QString, name: &QString) -> QString {
4262 QString::from(&work::rename_folder(&self.qt_thread(), &self.shared(), &path.to_string(), &name.to_string()))
4263 }
4264
4265 fn remove_folder(self: Pin<&mut Self>, path: &QString) -> QString {
4266 QString::from(&work::remove_folder(&self.qt_thread(), &self.shared(), &path.to_string()))
4267 }
4268
4269 fn remove_project(self: Pin<&mut Self>, id: &QString) -> QString {
4270 QString::from(&crate::projects::remove(&self.qt_thread(), &self.shared(), &id.to_string()))
4271 }
4272
4273 fn save_budget(self: Pin<&mut Self>, id: &QString, edit: &QString) -> QString {
4274 QString::from(&crate::projects::save_budget(&self.qt_thread(), &self.shared(), &id.to_string(), &edit.to_string()))
4275 }
4276
4277 fn letters(&self) -> QString {
4278 QString::from(&crate::letters::view())
4279 }
4280
4281 fn letter_task(self: Pin<&mut Self>, id: &QString) -> QString {
4282 QString::from(&crate::letters::make_task(&self.qt_thread(), &self.shared(), &id.to_string()))
4283 }
4284
4285 fn letter_event(self: Pin<&mut Self>, id: &QString) -> QString {
4286 QString::from(&crate::letters::make_event(&self.qt_thread(), &self.shared(), &id.to_string()))
4287 }
4288
4289 fn letter_done(self: Pin<&mut Self>, id: &QString, project: &QString) -> QString {
4290 QString::from(&crate::letters::done(&id.to_string(), &project.to_string()))
4291 }
4292
4293 fn bank(&self) -> QString {
4294 QString::from(&crate::bank::view())
4295 }
4296
4297 fn import_bank(self: Pin<&mut Self>, file: &QString) -> QString {
4298 let (ok, said) = crate::bank::import(&file.to_string());
4299 show(&self.qt_thread(), &self.shared());
4301 QString::from(&serde_json::json!({ "ok": ok, "said": said }).to_string())
4302 }
4303
4304 fn import_bank_into(self: Pin<&mut Self>, file: &QString, account: &QString) -> QString {
4305 let (ok, said) = crate::bank::import_into(&file.to_string(), &account.to_string());
4306 show(&self.qt_thread(), &self.shared());
4307 QString::from(&serde_json::json!({ "ok": ok, "said": said }).to_string())
4308 }
4309
4310 fn save_bank_account(self: Pin<&mut Self>, id: &QString, edit: &QString) -> QString {
4311 let problem = crate::bank::save_account(&id.to_string(), &edit.to_string());
4312 show(&self.qt_thread(), &self.shared());
4313 QString::from(&problem)
4314 }
4315
4316 fn remove_bank_account(self: Pin<&mut Self>, id: &QString) -> QString {
4317 let problem = crate::bank::remove_account(&id.to_string());
4318 show(&self.qt_thread(), &self.shared());
4319 QString::from(&problem)
4320 }
4321
4322 fn place_movement(self: Pin<&mut Self>, account: &QString, movement: &QString, choice: &QString) -> QString {
4323 let problem = crate::bank::place_movement(&account.to_string(), &movement.to_string(), &choice.to_string());
4324 show(&self.qt_thread(), &self.shared());
4325 QString::from(&problem)
4326 }
4327
4328 fn save_bank_rule(self: Pin<&mut Self>, rule: &QString, edit: &QString) -> QString {
4329 let problem = crate::bank::save_rule(&rule.to_string(), &edit.to_string());
4330 show(&self.qt_thread(), &self.shared());
4331 QString::from(&problem)
4332 }
4333
4334 fn remove_bank_rule(self: Pin<&mut Self>, rule: &QString) -> QString {
4335 let problem = crate::bank::remove_rule(&rule.to_string());
4336 show(&self.qt_thread(), &self.shared());
4337 QString::from(&problem)
4338 }
4339
4340 fn save_reserve(self: Pin<&mut Self>, id: &QString, edit: &QString) -> QString {
4341 let problem = crate::bank::save_reserve(&id.to_string(), &edit.to_string());
4342 show(&self.qt_thread(), &self.shared());
4343 QString::from(&problem)
4344 }
4345
4346 fn contracts(&self) -> QString {
4347 QString::from(&crate::contracts::view())
4348 }
4349
4350 fn save_contract(self: Pin<&mut Self>, id: &QString, edit: &QString) -> QString {
4351 QString::from(&crate::contracts::save(&id.to_string(), &edit.to_string()))
4352 }
4353
4354 fn remove_contract(self: Pin<&mut Self>, id: &QString) -> QString {
4355 QString::from(&crate::contracts::remove(&id.to_string()))
4356 }
4357
4358 fn contract_letter(self: Pin<&mut Self>, id: &QString) -> QString {
4359 QString::from(&crate::contracts::letter(&id.to_string()))
4360 }
4361
4362 fn contract_from(&self, subject: &QString, from: &QString) -> QString {
4363 QString::from(&crate::contracts::from_card(&subject.to_string(), &from.to_string()))
4364 }
4365
4366 fn routines(&self) -> QString {
4367 QString::from(&work::routines(&self.shared()))
4368 }
4369
4370 fn save_routine(self: Pin<&mut Self>, id: &QString, title: &QString, steps: &QString, by_itself: bool) -> QString {
4371 QString::from(&work::save_routine(&id.to_string(), &title.to_string(), &steps.to_string(), by_itself))
4372 }
4373
4374 fn remove_routine(self: Pin<&mut Self>, id: &QString) -> QString {
4375 QString::from(&work::remove_routine(&id.to_string()))
4376 }
4377
4378 fn share_status(&self, folder: &QString) -> QString {
4379 QString::from(&crate::share::status(&folder.to_string()))
4380 }
4381
4382 fn start_sharing(self: Pin<&mut Self>, folder: &QString, passphrase: &QString, again: &QString) -> QString {
4383 let problem = crate::share::start(&folder.to_string(), &passphrase.to_string(), &again.to_string());
4384 if problem.is_empty() {
4385 crate::share::exchange(&self.qt_thread(), &self.shared());
4386 }
4387 QString::from(&problem)
4388 }
4389
4390 fn stop_sharing(self: Pin<&mut Self>) -> QString {
4391 QString::from(&crate::share::stop())
4392 }
4393
4394 fn share_candidates(&self) -> QString {
4395 QString::from(&json(&crate::share::candidates()))
4396 }
4397
4398 fn set_share_projects(self: Pin<&mut Self>, on: bool) -> QString {
4399 let problem = crate::share::set_projects(on);
4400 if problem.is_empty() {
4401 crate::share::exchange(&self.qt_thread(), &self.shared());
4402 }
4403 QString::from(&problem)
4404 }
4405
4406 fn folders_in(&self, path: &QString) -> QString {
4407 QString::from(&crate::share::folders_in(&path.to_string()))
4408 }
4409
4410 fn files_access(&self) -> bool {
4411 #[cfg(target_os = "android")]
4412 {
4413 unsafe { sioul_android_files_access() }
4415 }
4416 #[cfg(not(target_os = "android"))]
4417 true
4418 }
4419
4420 fn ask_files_access(self: Pin<&mut Self>) {
4421 #[cfg(target_os = "android")]
4423 unsafe {
4424 sioul_android_ask_files_access()
4425 };
4426 let _ = self;
4427 }
4428
4429 fn share_now(self: Pin<&mut Self>) {
4430 crate::share::exchange(&self.qt_thread(), &self.shared());
4431 }
4432
4433 fn export_time_csv(mut self: Pin<&mut Self>, project: &QString, from: &QString, to: &QString, target: &QString) -> QString {
4434 match crate::projects::export_time_csv(&self.shared(), &project.to_string(), &from.to_string(), &to.to_string(), &target.to_string()) {
4435 Ok(path) => {
4436 self.as_mut().set_status(QString::from(&say("time-exported", &[("path", path)])));
4437 QString::default()
4438 }
4439 Err(e) => QString::from(&e),
4440 }
4441 }
4442
4443 fn change_line(self: Pin<&mut Self>, uri: &QString, shown: &QString, label: &QString, amount: f64, date: &QString) -> QString {
4444 QString::from(&crate::projects::change_line(&self.qt_thread(), &self.shared(), &uri.to_string(), &shown.to_string(), &label.to_string(), amount, &date.to_string()))
4445 }
4446
4447 fn remove_budget(self: Pin<&mut Self>, what: &QString) -> QString {
4448 QString::from(&crate::projects::remove_budget(&self.qt_thread(), &self.shared(), &what.to_string()))
4449 }
4450
4451 fn remove_line(self: Pin<&mut Self>, uri: &QString, shown: &QString) -> QString {
4452 QString::from(&crate::projects::remove_line(&self.qt_thread(), &self.shared(), &uri.to_string(), &shown.to_string()))
4453 }
4454
4455 fn noise_url(&self, kind: &QString) -> QString {
4456 match sioul_core::sounds::noise_file(&config::cache_dir().join("sounds"), &kind.to_string()) {
4457 Ok(path) => QString::from(&file_url(&path)),
4458 Err(_) => QString::default(),
4459 }
4460 }
4461
4462 fn calm_sounds(&self) -> QString {
4463 QString::from(&work::calm_sounds(&self.shared()))
4464 }
4465
4466 fn move_task(self: Pin<&mut Self>, uid: &QString, list: &QString, confirmed: bool) -> QString {
4467 QString::from(&work::move_task(&self.qt_thread(), &self.shared(), &uid.to_string(), &list.to_string(), confirmed))
4468 }
4469
4470 fn move_contact(self: Pin<&mut Self>, key: &QString, book: &QString, confirmed: bool) -> QString {
4471 QString::from(&pim::move_contact(&self.qt_thread(), &self.shared(), &key.to_string(), &book.to_string(), confirmed))
4472 }
4473
4474 fn address_books(&self) -> QString {
4475 QString::from(&pim::books())
4476 }
4477
4478 fn find_places(self: Pin<&mut Self>, name: &QString) {
4479 let (qt, name) = (self.qt_thread(), name.to_string());
4480 let language = tr().text("qt-locale", None).chars().take(2).collect::<String>();
4481 std::thread::spawn(move || {
4482 let found = sioul_sync::weather::places(&name, &language).map(|p| json(&p)).unwrap_or_else(|e| serde_json::json!({ "error": e.sentence(tr(), "Open-Meteo") }).to_string());
4483 let _ = qt.queue(move |mut sioul| sioul.as_mut().set_places_found(QString::from(&found)));
4484 });
4485 }
4486
4487 fn set_weather_place(mut self: Pin<&mut Self>, name: &QString, latitude: f64, longitude: f64) {
4488 let name = name.to_string();
4489 let path = config_path();
4490 let written = if name.trim().is_empty() {
4491 ["weather.place", "weather.latitude", "weather.longitude"].iter().try_for_each(|k| config::set_value(&path, k, &config::SettingValue::Text(String::new())))
4492 } else {
4493 config::set_value(&path, "weather.place", &config::SettingValue::Text(name))
4494 .and_then(|()| config::set_value(&path, "weather.latitude", &config::SettingValue::Float(latitude)))
4495 .and_then(|()| config::set_value(&path, "weather.longitude", &config::SettingValue::Float(longitude)))
4496 };
4497 if let Err(e) = written {
4498 self.as_mut().set_status(QString::from(&e));
4499 }
4500 let _ = std::fs::remove_file(weather_cache());
4501 self.as_mut().set_forecast(QString::default());
4502 update_weather(&self.qt_thread(), &self.shared());
4503 crate::homecard::weather_seen();
4505 }
4506
4507 fn health_page(&self) -> QString {
4508 QString::from(&crate::health::page())
4509 }
4510
4511 fn save_medicine(self: Pin<&mut Self>, id: &QString, edit: &QString) -> QString {
4512 QString::from(&crate::health::save_medicine(&id.to_string(), &edit.to_string()))
4513 }
4514
4515 fn save_prescription(self: Pin<&mut Self>, id: &QString, edit: &QString) -> QString {
4516 QString::from(&crate::health::save_prescription(&id.to_string(), &edit.to_string()))
4517 }
4518
4519 fn health_for_professional(&self, prescription: &QString) -> QString {
4520 QString::from(&crate::health::for_professional(&prescription.to_string()))
4521 }
4522
4523 fn remove_health(self: Pin<&mut Self>, id: &QString) -> QString {
4524 QString::from(&crate::health::remove(&id.to_string()))
4525 }
4526
4527 fn refilled(self: Pin<&mut Self>, id: &QString) -> QString {
4528 QString::from(&crate::health::refilled(&id.to_string()))
4529 }
4530
4531 fn set_dose_taken(self: Pin<&mut Self>, key: &QString, taken: bool) {
4532 let problem = crate::health::set_taken(&key.to_string(), taken);
4533 if !problem.is_empty() {
4534 tell(&self.qt_thread(), &self.shared(), problem);
4535 }
4536 crate::share::exchange(&self.qt_thread(), &self.shared());
4538 }
4539
4540 fn dose_not_taken(self: Pin<&mut Self>, key: &QString) {
4541 let problem = crate::health::not_taken(&key.to_string());
4542 if !problem.is_empty() {
4543 tell(&self.qt_thread(), &self.shared(), problem);
4544 }
4545 crate::share::exchange(&self.qt_thread(), &self.shared());
4546 }
4547
4548 fn dose_taken_at(self: Pin<&mut Self>, key: &QString, time: &QString) -> QString {
4549 let problem = crate::health::taken_late(&key.to_string(), &time.to_string());
4550 if problem.is_empty() {
4551 crate::share::exchange(&self.qt_thread(), &self.shared());
4552 }
4553 QString::from(&problem)
4554 }
4555
4556 fn dose_info(&self, key: &QString) -> QString {
4557 QString::from(&crate::health::dose_info(&key.to_string()))
4558 }
4559
4560 fn dose_choose(self: Pin<&mut Self>, key: &QString, taken: bool) -> QString {
4561 let problem = crate::health::choose(&key.to_string(), taken);
4562 crate::share::exchange(&self.qt_thread(), &self.shared());
4564 crate::health::show_health(&self.qt_thread(), &self.shared());
4565 QString::from(&problem)
4566 }
4567
4568 fn device_off(self: Pin<&mut Self>, id: &QString, off: bool) -> QString {
4569 let problem = crate::health::device_off(&id.to_string(), off);
4570 crate::health::show_health(&self.qt_thread(), &self.shared());
4571 QString::from(&problem)
4572 }
4573
4574 fn device_forget(self: Pin<&mut Self>, id: &QString) -> QString {
4575 QString::from(&crate::health::device_forget(&id.to_string()))
4576 }
4577
4578 fn needs(&self) -> QString {
4579 QString::from(&crate::health::needs_page())
4580 }
4581
4582 fn save_needs(self: Pin<&mut Self>, edit: &QString, shown: &QString) -> QString {
4583 let problem = crate::health::save_needs(&edit.to_string(), &shown.to_string());
4584 crate::work::show_work(&self.qt_thread(), &self.shared());
4586 QString::from(&problem)
4587 }
4588
4589 fn skip_need(self: Pin<&mut Self>, key: &QString, skip: bool) -> QString {
4590 let problem = crate::health::skip_today(&key.to_string(), skip);
4591 crate::work::show_work(&self.qt_thread(), &self.shared());
4592 QString::from(&problem)
4593 }
4594
4595 fn needs_today(&self) -> QString {
4596 QString::from(&crate::health::needs_today())
4597 }
4598
4599 fn refresh_health(self: Pin<&mut Self>) {
4600 crate::health::show_health(&self.qt_thread(), &self.shared());
4601 }
4602
4603 fn show_health_week(self: Pin<&mut Self>, day: &QString) {
4604 crate::health::show_week(&day.to_string());
4605 crate::health::show_health(&self.qt_thread(), &self.shared());
4606 }
4607
4608 fn drag_need(self: Pin<&mut Self>, edit: &QString) -> QString {
4609 let (qt, shared) = (self.qt_thread(), self.shared());
4610 let problem = crate::health::drag_need(&qt, &shared, &edit.to_string());
4611 work::show_work(&qt, &shared);
4613 crate::health::show_health(&qt, &shared);
4614 QString::from(&problem)
4615 }
4616
4617 fn change_need(self: Pin<&mut Self>, edit: &QString) -> QString {
4618 let problem = crate::health::change_need(&edit.to_string());
4619 crate::work::show_work(&self.qt_thread(), &self.shared());
4621 crate::health::show_health(&self.qt_thread(), &self.shared());
4622 QString::from(&problem)
4623 }
4624
4625 fn health_settings(&self) -> QString {
4626 QString::from(&crate::health::settings_view())
4627 }
4628
4629 fn wake_settings(&self, which: &QString) {
4630 crate::wake::open_settings(&which.to_string());
4631 }
4632
4633 fn try_wake(&self) -> QString {
4634 QString::from(&crate::wake::try_now())
4635 }
4636
4637 fn set_overlap_aside(self: Pin<&mut Self>, key: &QString) -> QString {
4638 let problem = crate::pim::set_overlap_aside(&key.to_string());
4639 pim::show_pim(&self.qt_thread(), &self.shared());
4640 QString::from(&problem)
4641 }
4642
4643 fn move_need(self: Pin<&mut Self>, key: &QString, minutes: i32, time: &QString) -> QString {
4644 let problem = crate::health::move_today(&key.to_string(), i64::from(minutes), &time.to_string());
4645 crate::work::show_work(&self.qt_thread(), &self.shared());
4647 QString::from(&problem)
4648 }
4649
4650 fn needs_later(&self) -> i32 {
4651 i32::try_from(crate::health::later_minutes()).unwrap_or(15)
4652 }
4653
4654 fn missed_doses(&self) -> QString {
4655 QString::from(&crate::health::missed())
4656 }
4657
4658 fn going_away(mut self: Pin<&mut Self>) {
4661 self.as_mut().set_away(true);
4662 AWAY.store(true, Ordering::SeqCst);
4663 std::thread::spawn(crate::alarms::schedule);
4664 std::thread::spawn(crate::eventalarms::schedule);
4666 crate::homecard::now();
4668 crate::share::set_shown(false);
4669 let (qt, shared) = (self.qt_thread(), self.shared());
4670 std::thread::spawn(move || {
4671 crate::mail::quiet_watchers(&shared);
4672 crate::mail::finish_all(&qt, &shared);
4673 crate::share::closing();
4674 crate::share::nudge(&qt, &shared, 0, false);
4676 });
4677 }
4678
4679 fn back_here(mut self: Pin<&mut Self>) {
4680 self.as_mut().set_away(false);
4681 AWAY.store(false, Ordering::SeqCst);
4682 if let Some(key) = crate::alarms::opened() {
4684 self.as_mut().reminder_opened(QString::from("dose"), QString::default(), QString::from(&key));
4685 }
4686 if let Some((kind, key)) = crate::eventalarms::opened() {
4688 self.as_mut().reminder_opened(QString::from(&kind), QString::default(), QString::from(&key));
4689 }
4690 if let Some((kind, uri, key)) = crate::homecard::opened() {
4693 self.as_mut().reminder_opened(QString::from(kind), QString::from(&uri), QString::from(&key));
4694 }
4695 let (qt, shared) = (self.qt_thread(), self.shared());
4697 if shared.views_job.deferred.swap(false, Ordering::SeqCst) | shared.mail_job.deferred.swap(false, Ordering::SeqCst) {
4698 show(&qt, &shared);
4699 }
4700 if shared.pim_job.deferred.swap(false, Ordering::SeqCst) {
4701 pim::show_pim(&qt, &shared);
4702 }
4703 if shared.work_job.deferred.swap(false, Ordering::SeqCst) {
4704 work::show_work(&qt, &shared);
4705 }
4706 if shared.health_job.deferred.swap(false, Ordering::SeqCst) {
4707 crate::health::show_health(&qt, &shared);
4708 }
4709 crate::share::set_shown(true);
4710 crate::outside::take(self.as_mut());
4712 std::thread::spawn(|| crate::outside::publish(true));
4713 crate::pauses::tick(&self.qt_thread(), &self.shared());
4715 let now = jiff::Timestamp::now().as_second();
4716 self.shared().active.store(now, std::sync::atomic::Ordering::Relaxed);
4717 let (qt, shared) = (self.qt_thread(), self.shared());
4718 std::thread::spawn(move || {
4719 crate::devices::window_opened();
4721 let _ = crate::share::keeper("health", sioul_sync::lease::Rule::FollowsYou, now, false);
4722 crate::share::nudge(&qt, &shared, 0, true);
4725 crate::mail::wake_watchers(&shared);
4726 });
4727 }
4728
4729 fn touch(self: Pin<&mut Self>) {
4730 self.shared().active.store(jiff::Timestamp::now().as_second(), std::sync::atomic::Ordering::Relaxed);
4731 }
4732
4733 fn set_health(self: Pin<&mut Self>, key: &QString, value: &QString) -> QString {
4734 QString::from(&crate::health::set_setting(&key.to_string(), &value.to_string()))
4735 }
4736
4737 fn movement_minutes(&self) -> i32 {
4738 crate::health::movement_minutes()
4739 }
4740
4741 fn chat_minute(self: Pin<&mut Self>) -> bool {
4742 crate::health::chat_minute()
4743 }
4744
4745 fn chats_covered(&self) -> bool {
4746 crate::health::chats_covered()
4747 }
4748
4749 fn memo_url(&self) -> QString {
4750 QString::from(&work::memo_url())
4751 }
4752
4753 fn memo_recorded(self: Pin<&mut Self>, url: &QString) -> QString {
4754 let (qt, shared) = (self.qt_thread(), self.shared());
4755 work::show_work(&qt, &shared);
4756 QString::from(&work::note_path_of(&url.to_string()))
4757 }
4758
4759 fn set_notes_tree(self: Pin<&mut Self>, tree: bool) {
4760 work::set_notes_tree(&self.qt_thread(), &self.shared(), tree);
4761 }
4762
4763 fn view_flag(&self, name: &QString) -> bool {
4764 work::view_flag(&self.shared(), &name.to_string())
4765 }
4766
4767 fn set_view_flag(self: Pin<&mut Self>, name: &QString, value: bool) {
4768 work::set_view_flag(&self.shared(), &name.to_string(), value);
4769 }
4770
4771 fn add_movement(mut self: Pin<&mut Self>, edit: &QString) -> QString {
4772 #[derive(serde::Deserialize)]
4773 struct Movement {
4774 kind: String,
4776 budget: String,
4777 label: String,
4778 amount: f64,
4779 #[serde(default)]
4780 date: String,
4781 #[serde(default)]
4783 every: String,
4784 #[serde(default)]
4785 day: i8,
4786 #[serde(default)]
4787 month: Option<i8>,
4788 #[serde(default)]
4789 from: String,
4790 #[serde(default)]
4791 until: String,
4792 #[serde(default)]
4793 estimate: bool,
4794 }
4795 let world = World::load();
4796 let outcome = (|| {
4797 let movement: Movement = serde_json::from_str(&edit.to_string()).map_err(|e| e.to_string())?;
4798 let ledger = world.ledger().ok_or_else(|| tr().text("ui-no-budgets", None))?;
4799 let path = world.ledger_path().ok_or_else(|| tr().text("ui-no-budgets", None))?;
4800 if movement.amount == 0.0 || !movement.amount.is_finite() {
4801 return Err(tr().text("budget-add-bad-amount", None));
4802 }
4803 let today = Zoned::now().date();
4804 let origin = say("budget-origin-hand", &[("date", today.to_string())]);
4805 if movement.kind == "recurring" {
4806 let preset = budget::PresetEdit {
4807 budget: movement.budget.clone(),
4808 label: movement.label.clone(),
4809 amount: movement.amount,
4810 every: movement.every.clone(),
4811 day: movement.day,
4812 month: movement.month,
4813 from: movement.from.clone(),
4814 until: movement.until.clone(),
4815 estimate: movement.estimate,
4816 };
4817 budget::record_preset(&path, &preset, &origin)?;
4818 } else {
4819 let date: jiff::civil::Date = movement.date.parse().unwrap_or(today);
4820 let line = budget::Line {
4821 budget: movement.budget.clone(),
4822 date,
4823 amount: sioul_core::money::Money::from_units(movement.amount),
4824 label: movement.label.trim().to_string(),
4825 planned: date > today,
4826 reserve: None,
4827 links: Vec::new(),
4828 preset: None,
4829 };
4830 budget::record_line(&path, &line, &origin)?;
4831 }
4832 Ok::<String, String>(say("budget-added", &[("budget", ledger.budget_title(&movement.budget).to_string())]))
4833 })();
4834 let (line, problem) = match outcome {
4835 Ok(line) => (line, String::new()),
4836 Err(e) => (e.clone(), e),
4837 };
4838 self.as_mut().set_status(QString::from(&line));
4839 show(&self.qt_thread(), &self.shared());
4840 QString::from(&problem)
4841 }
4842
4843 fn site_list(&self) -> QString {
4844 QString::from(&crate::sites::list())
4845 }
4846
4847 fn site_notified(self: Pin<&mut Self>, id: &QString, title: &QString, text: &QString) {
4848 crate::sites::notified(&self.qt_thread(), &self.shared(), &id.to_string(), &title.to_string(), &text.to_string());
4849 }
4850
4851 fn site_seen(self: Pin<&mut Self>, id: &QString) {
4852 crate::sites::seen(&self.qt_thread(), &id.to_string());
4853 }
4854
4855 fn site_notices(&self) -> QString {
4856 QString::from(&crate::sites::waiting())
4857 }
4858
4859 fn site_for_sender(&self, sender: &QString) -> QString {
4860 QString::from(&crate::sites::announced_by(&sender.to_string()))
4861 }
4862
4863 fn set_site(mut self: Pin<&mut Self>, id: &QString, field: &QString, value: &QString) -> QString {
4864 let (field, value) = (field.to_string(), value.to_string());
4865 let setting = match field.as_str() {
4866 "realtime" | "muted" | "background" | "microphone" | "camera" | "screen" => config::SettingValue::Bool(value == "true"),
4867 "site" | "area" | "name" => config::SettingValue::Text(value.trim().to_string()),
4868 "categories" => config::SettingValue::Texts(serde_json::from_str::<Vec<String>>(&value).unwrap_or_default().into_iter().map(|c| c.trim().to_string()).filter(|c| !c.is_empty()).collect()),
4870 "url" if value.trim().starts_with("https://") => config::SettingValue::Text(value.trim().to_string()),
4872 "url" => return QString::from(&tr().text("site-https-only", None)),
4873 _ => return QString::default(),
4874 };
4875 let result = config::set_value(&config_path(), &format!("site.{}.{field}", id), &setting);
4876 self.as_mut().sites_changed();
4877 QString::from(&result.err().unwrap_or_default())
4878 }
4879
4880 fn site_presets(&self, country: &QString, region: &QString) -> QString {
4881 QString::from(&crate::sites::presets(&country.to_string(), ®ion.to_string()))
4882 }
4883
4884 fn add_site(mut self: Pin<&mut Self>, edit: &QString) -> QString {
4885 let answer = crate::sites::add(&edit.to_string());
4886 self.as_mut().sites_changed();
4887 QString::from(&answer)
4888 }
4889
4890 fn site_presets_tree(&self) -> QString {
4891 QString::from(&crate::sites::presets_tree())
4892 }
4893
4894 fn add_sites(mut self: Pin<&mut Self>, rows: &QString) -> QString {
4895 let answer = crate::sites::add_many(&rows.to_string());
4896 self.as_mut().sites_changed();
4897 QString::from(&answer)
4898 }
4899
4900 fn move_site(mut self: Pin<&mut Self>, id: &QString, delta: i32) -> QString {
4901 let said = crate::sites::move_site(&id.to_string(), i64::from(delta));
4902 self.as_mut().sites_changed();
4903 QString::from(&said)
4904 }
4905
4906 fn remove_site(mut self: Pin<&mut Self>, id: &QString) -> QString {
4907 let problem = crate::sites::remove(&id.to_string());
4908 self.as_mut().sites_changed();
4909 QString::from(&problem)
4910 }
4911
4912 fn google_built_in(&self) -> bool {
4913 sioul_sync::google::built_in().is_some()
4914 }
4915
4916 fn call_devices(&self) -> QString {
4917 QString::from(&serde_json::to_string(&load_config().calls).unwrap_or_default())
4918 }
4919
4920 fn calls_script(&self) -> QString {
4921 QString::from(include_str!("calls.js"))
4922 }
4923
4924 fn set_call_device(mut self: Pin<&mut Self>, which: &QString, name: &QString) -> QString {
4925 let which = which.to_string();
4926 if !matches!(which.as_str(), "camera" | "microphone" | "speaker") {
4927 return QString::default();
4928 }
4929 let name = name.to_string().trim().to_string();
4930 let result = config::set_value(&config_path(), &format!("calls.{which}"), &config::SettingValue::Text(name));
4931 self.as_mut().sites_changed();
4932 QString::from(&result.err().unwrap_or_default())
4933 }
4934
4935 fn bitwarden_state(&self) -> QString {
4936 QString::from(&crate::sites::bitwarden_state(&self.shared()))
4937 }
4938
4939 fn bitwarden_unlock(self: Pin<&mut Self>, password: &QString, provider: i32, code: &QString) -> i32 {
4940 let (shared, password, code) = (self.shared(), password.to_string(), code.to_string());
4941 crate::sites::bitwarden_later(&self.qt_thread(), move || crate::sites::bitwarden_unlock(&shared, &password, provider, &code))
4942 }
4943
4944 fn bitwarden_lock(self: Pin<&mut Self>) {
4945 crate::sites::bitwarden_lock(&self.shared());
4946 }
4947
4948 fn bitwarden_send_code(self: Pin<&mut Self>, password: &QString) -> i32 {
4949 let password = password.to_string();
4950 crate::sites::bitwarden_later(&self.qt_thread(), move || crate::sites::bitwarden_send_code(&password))
4951 }
4952
4953 fn bitwarden_passkey_begin(self: Pin<&mut Self>) -> i32 {
4954 crate::sites::bitwarden_later(&self.qt_thread(), crate::sites::bitwarden_passkey_begin)
4955 }
4956
4957 fn bitwarden_passkey(self: Pin<&mut Self>, answer: &QString) -> i32 {
4958 let (shared, answer) = (self.shared(), answer.to_string());
4959 crate::sites::bitwarden_later(&self.qt_thread(), move || crate::sites::bitwarden_passkey(&shared, &answer))
4960 }
4961
4962 fn bitwarden_search(self: Pin<&mut Self>, url: &QString, site: &QString, user: &QString) -> QString {
4963 QString::from(&crate::sites::bitwarden_search(&self.qt_thread(), &self.shared(), &url.to_string(), &site.to_string(), &user.to_string()))
4964 }
4965
4966 fn bitwarden_site(&self, url: &QString) -> QString {
4967 QString::from(&crate::sites::bitwarden_site(&self.shared(), &url.to_string()))
4968 }
4969
4970 fn bitwarden_login(&self, url: &QString, id: &QString) -> QString {
4971 QString::from(&crate::sites::bitwarden_login(&self.shared(), &url.to_string(), &id.to_string()))
4972 }
4973
4974 fn map_view(&self) -> QString {
4975 QString::from(&crate::map::view(&self.shared()))
4976 }
4977
4978 fn place_of(&self, address: &QString) -> QString {
4979 QString::from(&crate::map::place_of(&address.to_string()))
4980 }
4981
4982 fn locate_addresses(mut self: Pin<&mut Self>, allow: bool) {
4983 if allow && !load_config().map.geocode {
4984 if let Err(e) = config::set_value(&config_path(), "map.geocode", &config::SettingValue::Bool(true)) {
4985 self.as_mut().set_status(QString::from(&e));
4986 return;
4987 }
4988 }
4989 crate::map::locate(&self.qt_thread(), &self.shared());
4990 }
4991
4992 fn refresh_mode(mut self: Pin<&mut Self>) {
4993 let (qt, shared_tick) = (self.qt_thread(), self.shared());
4995 std::thread::spawn(move || crate::health::tick(&qt, &shared_tick));
4996 let (qt_time, shared_time) = (self.qt_thread(), self.shared());
4999 std::thread::spawn(move || crate::timenote::follow(&qt_time, &shared_time));
5000 let qt_spam = self.qt_thread();
5002 std::thread::spawn(move || crate::spam::by_itself(qt_spam));
5003 crate::pauses::tick(&self.qt_thread(), &self.shared());
5005 if *self.as_ref().away() {
5009 if Zoned::now().minute() % 5 == 0 {
5010 crate::share::exchange_tick(&self.qt_thread(), &self.shared());
5011 }
5012 return;
5013 }
5014 crate::homecard::tick();
5016 crate::outside::take(self.as_mut());
5018 let now = Zoned::now();
5020 let (stamp, day) = (now.timestamp().as_second(), i64::from(now.date().year()) * 1000 + i64::from(now.date().day_of_year()));
5021 let shared = self.shared();
5022 let last = shared.planned_at.load(Ordering::Relaxed);
5023 if last == 0 {
5024 shared.planned_at.store(stamp, Ordering::Relaxed);
5025 shared.planned_day.store(day, Ordering::Relaxed);
5026 } else if stamp - last >= REPLAN_EVERY || shared.planned_day.load(Ordering::Relaxed) != day {
5027 if shared.planned_day.load(Ordering::Relaxed) != day {
5029 pim::show_pim(&self.qt_thread(), &shared);
5030 }
5031 shared.planned_at.store(stamp, Ordering::Relaxed);
5032 shared.planned_day.store(day, Ordering::Relaxed);
5033 work::show_work(&self.qt_thread(), &shared);
5034 }
5035 crate::share::exchange_tick(&self.qt_thread(), &self.shared());
5038 crate::share::nudge_tick(&self.qt_thread(), &self.shared());
5039 let shared_invoices = self.shared();
5042 std::thread::spawn(move || crate::projects::keep_invoices(&shared_invoices));
5043 crate::sites::gather_tick(&self.qt_thread(), &self.shared());
5045 crate::sites::favicon_tick(&self.qt_thread());
5047 crate::remind::tick(&self.qt_thread());
5049 crate::mailnote::tick_in_window(crate::mailnote::Window { qt: self.qt_thread(), active: self.shared().active.load(Ordering::Relaxed) });
5051 crate::reviews::tick(&self.qt_thread(), &self.shared());
5053 crate::letters::tick(&self.qt_thread(), &self.shared());
5055 update_weather(&self.qt_thread(), &self.shared());
5057 let line = mode_json();
5058 if self.as_ref().mode().to_string() != line {
5059 let was_quiet = self.as_ref().mode().to_string().contains("\"quiet\":true");
5060 self.as_mut().set_mode(QString::from(&line));
5061 if was_quiet != line.contains("\"quiet\":true") {
5063 let (qt, shared) = (self.qt_thread(), self.shared());
5064 show(&qt, &shared);
5065 work::show_work(&qt, &shared);
5066 }
5067 }
5068 }
5069
5070 fn work_a_while(self: Pin<&mut Self>, minutes: i32) {
5071 let until = Zoned::now().timestamp().as_second() + i64::from(minutes.max(1)) * 60;
5072 self.change_overrides(|o| {
5073 o.rest_until = None;
5074 o.work_until = Some(until);
5075 });
5076 }
5077
5078 pub(crate) fn done_for_the_day(mut self: Pin<&mut Self>) -> QString {
5079 let (qt, shared) = (self.qt_thread(), self.shared());
5080 let closed = work::closing(&shared);
5081 let path = sioul_core::quiet::Overrides::default_path();
5082 let previous = sioul_core::quiet::Overrides::load(&path);
5083 let mut next = previous.clone();
5084 next.work_until = None;
5085 next.rest_until = Some(closed.back.timestamp().as_second());
5086 next.rest_from = Some(Zoned::now().timestamp().as_second());
5087 next.first_step = Some(closed.first.text.clone()).filter(|s| !s.is_empty());
5088 next.first_step_task = Some(closed.first.uid.clone()).filter(|s| !s.is_empty());
5089 next.first_step_day = Some(closed.back.date());
5090 if let Err(e) = next.save(&path) {
5091 self.as_mut().set_status(QString::from(&e));
5092 return QString::default();
5093 }
5094 if sioul_core::timelog::running().is_some() {
5096 work::focus_stop(&qt, &shared, false, "");
5097 }
5098 let screen = json(&closed.closing);
5099 mail::rest(&qt, &shared, previous, next, serde_json::from_str::<serde_json::Value>(&screen).ok().and_then(|v| v["put_away"].as_str().map(str::to_string)).unwrap_or_default());
5100 self.as_mut().set_mode(QString::from(&mode_json()));
5101 show(&qt, &shared);
5102 work::show_work(&qt, &shared);
5103 QString::from(&screen)
5104 }
5105
5106 fn day_review(&self, kind: &QString) -> QString {
5107 QString::from(&crate::reviews::view(&kind.to_string()))
5108 }
5109
5110 fn close_review(self: Pin<&mut Self>, kind: &QString, felt: &QString, mix: &QString, memo: &QString) -> QString {
5111 crate::reviews::close(self, &kind.to_string(), &felt.to_string(), &mix.to_string(), &memo.to_string())
5112 }
5113
5114 fn day_review_line(&self, date: &QString) -> QString {
5115 QString::from(&crate::reviews::line(&date.to_string()))
5116 }
5117
5118 fn set_first_step(self: Pin<&mut Self>, text: &QString) {
5119 let text = text.to_string().trim().to_string();
5120 self.change_overrides(|o| {
5121 if o.first_step.as_deref() != Some(text.as_str()) {
5122 o.first_step_task = None;
5124 o.first_step = Some(text).filter(|t| !t.is_empty());
5125 }
5126 });
5127 }
5128
5129 fn clear_first_step(self: Pin<&mut Self>) {
5130 self.change_overrides(|o| {
5131 o.first_step = None;
5132 o.first_step_task = None;
5133 });
5134 }
5135
5136 fn usual_hours(self: Pin<&mut Self>) {
5137 self.change_overrides(|o| {
5138 o.work_until = None;
5139 o.rest_until = None;
5140 o.work_now = None;
5141 });
5142 }
5143
5144 fn set_work_now(self: Pin<&mut Self>, on: bool) {
5145 let config = load_config();
5146 let work: Vec<_> = config.windows.iter().filter(|w| w.kind() == "work").cloned().collect();
5147 let until = sioul_core::quiet::end_of_next_workday(&work, &config.time_off, &Zoned::now()).timestamp().as_second();
5148 self.change_overrides(|o| {
5149 o.work_until = None;
5151 if on {
5152 o.rest_until = None;
5153 o.work_now = Some(until);
5154 } else {
5155 o.work_now = None;
5156 }
5157 });
5158 }
5159
5160 fn set_free_time(self: Pin<&mut Self>, on: bool) -> QString {
5161 crate::pauses::set_free(self, on)
5162 }
5163
5164 fn keep_usual_end(self: Pin<&mut Self>) {
5165 crate::pauses::keep_usual_end(self);
5166 }
5167
5168 fn set_free_nothing(self: Pin<&mut Self>, on: bool) {
5169 crate::pauses::set_free_nothing(self, on);
5170 }
5171
5172 fn pause_now(self: Pin<&mut Self>) {
5173 crate::pauses::press(self);
5174 }
5175
5176 fn come_back(self: Pin<&mut Self>) -> QString {
5177 crate::pauses::come_back(self)
5178 }
5179
5180 fn lighten_tomorrow(self: Pin<&mut Self>) {
5181 crate::pauses::lighten_tomorrow(self);
5182 }
5183
5184 fn forget_pause(self: Pin<&mut Self>) {
5185 crate::pauses::forget(self);
5186 }
5187
5188 fn pause_screen(&self) -> QString {
5189 QString::from(&crate::pauses::screen())
5190 }
5191
5192 fn pause_setup(&self) -> QString {
5193 QString::from(&crate::pauses::setup())
5194 }
5195
5196 fn open_dnd(&self, key: &QString) {
5197 crate::dnd::open(&key.to_string());
5198 }
5199
5200 fn dnd_toggle(self: Pin<&mut Self>, on: bool, minutes: i32) {
5201 crate::everywhere::toggle(self, on, minutes);
5202 }
5203
5204 fn dnd_setup(&self) -> QString {
5205 QString::from(&crate::everywhere::setup())
5206 }
5207
5208 fn dnd_change(self: Pin<&mut Self>, verb: &QString, json: &QString) -> QString {
5209 let (answer, changed) = crate::everywhere::change(&verb.to_string(), &json.to_string());
5210 if changed {
5212 crate::share::exchange(&self.qt_thread(), &self.shared());
5213 }
5214 QString::from(&answer)
5215 }
5216
5217 fn app_notes_setup(&self) -> QString {
5218 QString::from(&crate::appnotes::setup())
5219 }
5220
5221 fn app_notes_change(&self, verb: &QString, json: &QString) -> QString {
5222 QString::from(&crate::appnotes::change(&verb.to_string(), &json.to_string()))
5223 }
5224
5225 fn calls_through(mut self: Pin<&mut Self>, on: bool, minutes: i32) {
5226 if let Err(e) = crate::calls::press(on, minutes) {
5227 self.as_mut().set_status(QString::from(&e));
5228 return;
5229 }
5230 let (qt, shared) = (self.qt_thread(), self.shared());
5232 std::thread::spawn(move || {
5233 crate::calls::refresh(true);
5234 let _ = qt.queue(|mut sioul| sioul.as_mut().set_mode(QString::from(&mode_json())));
5235 crate::share::exchange(&qt, &shared);
5236 });
5237 }
5238
5239 fn calls_view(&self) -> QString {
5240 QString::from(&crate::calls::view())
5241 }
5242
5243 fn calls_action(self: Pin<&mut Self>, verb: &QString, json: &QString) -> QString {
5244 let answer = crate::calls::act(&verb.to_string(), &json.to_string());
5245 if answer.contains("\"shared\":true") {
5247 let (qt, shared) = (self.qt_thread(), self.shared());
5248 std::thread::spawn(move || {
5249 crate::calls::refresh(true);
5250 crate::share::exchange(&qt, &shared);
5251 });
5252 }
5253 QString::from(&answer)
5254 }
5255
5256 fn calls_setup(&self) -> QString {
5257 QString::from(&crate::calls::setup())
5258 }
5259
5260 fn phone_messages(self: Pin<&mut Self>, verb: &QString, json: &QString) -> QString {
5261 let (answer, shared) = crate::phonemsgs::call(&verb.to_string(), &json.to_string());
5262 if shared {
5264 crate::share::exchange(&self.qt_thread(), &self.shared());
5265 }
5266 QString::from(&answer)
5267 }
5268
5269 fn texts(self: Pin<&mut Self>, verb: &QString, json: &QString) -> QString {
5271 let (answer, shared) = crate::texts::call(&verb.to_string(), &json.to_string());
5272 if shared {
5274 crate::share::exchange(&self.qt_thread(), &self.shared());
5275 }
5276 QString::from(&answer)
5277 }
5278
5279 fn reaches_view(&self) -> QString {
5280 QString::from(&crate::reaches::view(self.shared().realtime.load(Ordering::Relaxed)))
5281 }
5282
5283 fn reaches_set(self: Pin<&mut Self>, row: &QString, words: &QString, shown: &QString) -> QString {
5284 let problem = crate::reaches::set_row(&row.to_string(), &words.to_string(), &shown.to_string());
5285 if problem.is_empty() {
5286 self.attention_changed();
5287 }
5288 QString::from(&problem)
5289 }
5290
5291 fn reaches_preset(self: Pin<&mut Self>, id: &QString) -> QString {
5292 let problem = crate::reaches::preset(&id.to_string());
5293 if problem.is_empty() {
5294 self.attention_changed();
5295 }
5296 QString::from(&problem)
5297 }
5298
5299 fn person_sheet(&self, key: &QString, addresses: &QString) -> QString {
5300 QString::from(&crate::reaches::person(&key.to_string(), &addresses.to_string()))
5301 }
5302
5303 fn person_sheet_change(mut self: Pin<&mut Self>, key: &QString, addresses: &QString, verb: &QString, value: &QString) -> QString {
5304 let answer = crate::reaches::person_change(&key.to_string(), &addresses.to_string(), &verb.to_string(), &value.to_string());
5305 let line = serde_json::from_str::<serde_json::Value>(&answer).ok().and_then(|a| a["line"].as_str().map(str::to_string)).unwrap_or_default();
5306 if !line.is_empty() {
5307 self.as_mut().set_status(QString::from(&line));
5308 }
5309 let (qt, shared) = (self.qt_thread(), self.shared());
5311 show(&qt, &shared);
5312 pim::show_pim(&qt, &shared);
5313 std::thread::spawn(move || {
5314 crate::calls::refresh(true);
5315 crate::everywhere::apply();
5316 crate::share::exchange(&qt, &shared);
5317 });
5318 QString::from(&answer)
5319 }
5320
5321 fn attention_changed(mut self: Pin<&mut Self>) {
5325 self.as_mut().set_mode(QString::from(&mode_json()));
5326 let (qt, shared) = (self.qt_thread(), self.shared());
5327 show(&qt, &shared);
5328 work::show_work(&qt, &shared);
5329 std::thread::spawn(move || {
5330 crate::calls::refresh(true);
5331 crate::everywhere::apply();
5332 crate::share::exchange(&qt, &shared);
5333 });
5334 }
5335
5336 fn calls_setup_change(self: Pin<&mut Self>, verb: &QString, json: &QString) -> QString {
5337 QString::from(&crate::calls::setup_change(&verb.to_string(), &json.to_string()))
5338 }
5339
5340 fn change_overrides(mut self: Pin<&mut Self>, change: impl FnOnce(&mut sioul_core::quiet::Overrides)) {
5342 let path = sioul_core::quiet::Overrides::default_path();
5343 let mut overrides = sioul_core::quiet::Overrides::load(&path);
5344 change(&mut overrides);
5345 let said = overrides.save(&path).err().unwrap_or_default();
5347 self.as_mut().set_status(QString::from(&said));
5348 self.as_mut().set_mode(QString::from(&mode_json()));
5349 let (qt, shared) = (self.qt_thread(), self.shared());
5350 show(&qt, &shared);
5351 work::show_work(&qt, &shared);
5352 }
5353
5354 fn project_rows(&self) -> QString {
5355 QString::from(&crate::projects::rows(&self.shared()))
5356 }
5357
5358 fn project_page(&self, id: &QString) -> QString {
5359 QString::from(&crate::projects::page(&self.shared(), &id.to_string()))
5360 }
5361
5362 fn save_project(self: Pin<&mut Self>, id: &QString, edit: &QString) -> QString {
5363 QString::from(&crate::projects::save(&self.qt_thread(), &self.shared(), &id.to_string(), &edit.to_string()))
5364 }
5365
5366 fn time_page(&self, period: &QString, anchor: &QString, project: &QString) -> QString {
5367 QString::from(&crate::projects::time(&self.shared(), &period.to_string(), &anchor.to_string(), &project.to_string()))
5368 }
5369
5370 fn note_time(self: Pin<&mut Self>, edit: &QString) -> QString {
5371 QString::from(&crate::projects::note_time(&self.qt_thread(), &self.shared(), &edit.to_string()))
5372 }
5373
5374 fn stopped(&self) -> QString {
5375 QString::from(&crate::work::stopped(&self.shared()))
5376 }
5377
5378 fn set_stopped(self: Pin<&mut Self>, text: &QString) -> QString {
5379 QString::from(&crate::work::set_stopped(&self.qt_thread(), &self.shared(), &text.to_string()))
5380 }
5381
5382 fn change_time(self: Pin<&mut Self>, key: &QString, edit: &QString) -> QString {
5383 QString::from(&crate::projects::change_time(&self.qt_thread(), &self.shared(), &key.to_string(), &edit.to_string()))
5384 }
5385
5386 fn task_choices(&self, keep: &QString) -> QString {
5387 QString::from(&crate::projects::task_choices(&self.shared(), &keep.to_string()))
5388 }
5389
5390 fn remove_time(self: Pin<&mut Self>, key: &QString) -> QString {
5391 QString::from(&crate::projects::remove_time(&self.qt_thread(), &self.shared(), &key.to_string()))
5392 }
5393
5394 fn take_invoices(self: Pin<&mut Self>) -> QString {
5395 QString::from(&crate::projects::take_invoices(&self.shared()))
5396 }
5397
5398 fn make_invoice(self: Pin<&mut Self>, id: &QString) -> QString {
5399 QString::from(&crate::projects::make_invoice(&self.qt_thread(), &self.shared(), &id.to_string()))
5400 }
5401
5402 fn invoice_again(&self, number: &QString) -> QString {
5403 QString::from(&crate::projects::invoice_again(&number.to_string()))
5404 }
5405
5406 fn set_invoice_paid(self: Pin<&mut Self>, number: &QString, paid: bool) -> QString {
5407 QString::from(&crate::projects::set_paid(&self.qt_thread(), &self.shared(), &number.to_string(), paid))
5408 }
5409
5410 fn make_linked(self: Pin<&mut Self>, kind: &QString, from: &QString, key: &QString, start: f64) -> QString {
5411 QString::from(&work::make_linked(&self.qt_thread(), &self.shared(), &kind.to_string(), &from.to_string(), &key.to_string(), start))
5412 }
5413
5414 fn task_from_mail(self: Pin<&mut Self>, key: &QString) -> QString {
5415 QString::from(&work::task_from_mail(&self.qt_thread(), &self.shared(), &key.to_string()))
5416 }
5417
5418 fn note_from_mail(self: Pin<&mut Self>, key: &QString) -> QString {
5419 QString::from(&work::note_from_mail(&self.qt_thread(), &self.shared(), &key.to_string()))
5420 }
5421
5422 fn note_from_event(self: Pin<&mut Self>, key: &QString, start: f64) -> QString {
5423 QString::from(&work::note_from_event(&self.qt_thread(), &self.shared(), &key.to_string(), start))
5424 }
5425
5426 fn task_from_event(self: Pin<&mut Self>, key: &QString, start: f64) -> QString {
5427 QString::from(&work::task_from_event(&self.qt_thread(), &self.shared(), &key.to_string(), start))
5428 }
5429
5430 fn task_from_line(self: Pin<&mut Self>, path: &QString, line: i32) -> QString {
5431 QString::from(&work::task_from_line(&self.qt_thread(), &self.shared(), &path.to_string(), line))
5432 }
5433
5434 fn draft_for_task(self: Pin<&mut Self>, uid: &QString) -> QString {
5435 QString::from(&work::draft_for_task(&self.qt_thread(), &self.shared(), &uid.to_string()))
5436 }
5437
5438 fn mail_note(self: Pin<&mut Self>, path: &QString) -> QString {
5439 QString::from(&work::mail_note(&self.qt_thread(), &self.shared(), &path.to_string()))
5440 }
5441
5442 fn settings(&self, view: &QString) -> QString {
5443 let config = load_config();
5444 let lists: Vec<(String, String)> = sioul_core::tasks::lists().into_iter().filter(|c| !c.read_only).map(|c| (format!("{}/{}", c.account, c.id), c.label(&config, tr()))).collect();
5445 let store = config.notes_root_path().and_then(|root| ProjectStore::load(&root).ok());
5446 let mut rows = sioul_core::settings::for_view(&view.to_string(), &config, tr(), &lists, store.as_ref());
5447 if view.to_string() == "tasks" {
5449 let categories = work::categories_in_use(&self.shared());
5450 if !categories.is_empty() {
5451 let at = rows.iter().position(|r| r.key == "tasks.kind").map_or(rows.len(), |i| i + 1);
5452 rows.insert(at, sioul_core::settings::categories(tr(), categories));
5453 }
5454 }
5455 for row in rows.iter_mut().filter(|r| r.key == "ai_key") {
5457 row.value = config::SettingValue::Bool(sioul_sync::shield_ai::api_key().is_some());
5458 }
5459 for row in rows.iter_mut().filter(|r| r.key == "github_token") {
5460 row.value = config::SettingValue::Bool(sioul_sync::github::token().is_some());
5461 }
5462 for row in rows.iter_mut().filter(|r| r.key == "reminders_closed") {
5464 row.value = config::SettingValue::Bool(crate::remind::background());
5465 }
5466 if cfg!(target_os = "android") {
5468 for row in rows.iter_mut().filter(|r| r.key == "reminders.before_event") {
5469 row.help = format!("{} {}", row.help, tr().text(if crate::alarms::exact() { "set-reminders-before-exact" } else { "set-reminders-before-inexact" }, None));
5470 }
5471 }
5472 for row in rows.iter_mut().filter(|r| r.key == "passwords_shown") {
5473 row.value = config::SettingValue::Bool(work::view_flag(&self.shared(), "passwords-shown"));
5474 }
5475 if cfg!(target_os = "android") {
5478 rows.retain(|r| r.key != "screen_colours" && r.key != "calmer_colours");
5479 }
5480 #[cfg(not(target_os = "android"))]
5481 for row in rows.iter_mut().filter(|r| r.key == "screen_colours") {
5482 use crate::colour::Said;
5483 let said = match crate::colour::said() {
5484 Said::Converted(name) if !name.is_empty() => say("colour-said-converted", &[("profile", name)]),
5485 Said::Converted(_) => tr().text("colour-said-converted-unnamed", None),
5486 Said::Desktop => tr().text("colour-said-desktop", None),
5487 Said::Plain => tr().text("colour-said-plain", None),
5488 Said::Off | Said::Unknown => String::new(),
5489 };
5490 if !said.is_empty() {
5491 row.help = format!("{} {said}", row.help);
5492 }
5493 }
5494 if crate::homecard::has_setting()
5496 && let Some(at) = rows.iter().position(|r| r.key == "passwords_shown")
5497 {
5498 let row = crate::homecard::setting(&rows[at].group, &rows[at].section, !work::view_flag(&self.shared(), crate::homecard::PLAIN));
5499 rows.insert(at + 1, row);
5500 }
5501 QString::from(&json(&rows))
5502 }
5503
5504 fn set_setting(self: Pin<&mut Self>, key: &QString, value: &QString) -> QString {
5505 self.change_setting(key, &QString::default(), value)
5506 }
5507
5508 fn change_setting(mut self: Pin<&mut Self>, key: &QString, shown: &QString, value: &QString) -> QString {
5509 let key = key.to_string();
5510 let shown: Option<config::SettingValue> = match Some(shown.to_string()).filter(|s| !s.is_empty()).map(|s| serde_json::from_str(&s)) {
5513 Some(Ok(shown)) => Some(shown),
5514 Some(Err(e)) => return QString::from(&format!("{key}: {e}")),
5515 None => None,
5516 };
5517 let parsed: Result<config::SettingValue, String> = serde_json::from_str(&value.to_string()).map_err(|e| e.to_string());
5518 let result = parsed.and_then(|v| match (key.as_str(), v) {
5519 ("ai_key", config::SettingValue::Text(secret)) => sioul_sync::shield_ai::save_api_key(&secret).map_err(|e| e.sentence(tr(), "Anthropic")),
5521 ("github_token", config::SettingValue::Text(secret)) => sioul_sync::github::save_token(&secret).map_err(|e| e.sentence(tr(), "GitHub")),
5522 ("task_categories", config::SettingValue::Rename(change)) => work::rename_category(&self.qt_thread(), &self.shared(), &change.from, &change.to),
5523 ("reminders_closed", config::SettingValue::Bool(on)) => crate::remind::set_background(on),
5524 ("passwords_shown", config::SettingValue::Bool(on)) => {
5525 work::set_view_flag(&self.shared(), "passwords-shown", on);
5526 Ok(())
5527 }
5528 ("home_card_details", config::SettingValue::Bool(on)) => {
5529 work::set_view_flag(&self.shared(), crate::homecard::PLAIN, !on);
5530 crate::homecard::now();
5531 Ok(())
5532 }
5533 (_, v) => sioul_core::settings::change(&config_path(), &key, shown.as_ref(), &v),
5534 });
5535 if let Err(e) = result {
5536 return QString::from(&e);
5537 }
5538 let (qt, shared) = (self.qt_thread(), self.shared());
5540 if let Some(project) = key.strip_prefix("project.").and_then(|k| k.strip_suffix(".routes")) {
5541 retie_project(project.to_string());
5542 }
5543 if key == "collections"
5545 && let Some(account) = serde_json::from_str::<serde_json::Value>(&value.to_string()).ok().and_then(|v| v["from"].as_str().and_then(|f| f.split('/').nth(1)).map(str::to_string))
5546 {
5547 pim::nudge(&shared, &account);
5548 }
5549 if key.starts_with("reading.") || key == "theme" || key == "places_named" || key == "screen_colours" || key == "calmer_colours" {
5551 self.as_mut().set_reading(QString::from(&reading_json()));
5552 }
5553 if key.contains("fetch_minutes") {
5554 update_paces(&shared);
5555 }
5556 if key.starts_with("window") || key == "time_off" {
5558 self.as_mut().set_mode(QString::from(&mode_json()));
5559 }
5560 if key.starts_with("mail.") {
5561 mail::show_mail(&qt, &shared);
5562 }
5563 if key.starts_with("github") {
5565 if load_config().github.enabled {
5566 crate::github::start(&qt, &shared);
5567 } else {
5568 crate::github::stop(&shared);
5569 }
5570 }
5571 show(&qt, &shared);
5572 pim::show_pim(&qt, &shared);
5573 work::show_work(&qt, &shared);
5574 QString::default()
5575 }
5576
5577 fn forced_theme(&self) -> QString {
5578 QString::from(&std::env::var("SIOUL_THEME").unwrap_or_default())
5579 }
5580
5581 fn set_realtime_mode(mut self: Pin<&mut Self>, on: bool) {
5582 let shared = self.shared();
5583 shared.realtime.store(on, Ordering::Relaxed);
5584 shared.opened_anyway.store(on || shared.opened_anyway.load(Ordering::Relaxed), Ordering::Relaxed);
5585 update_paces(&shared);
5586 self.as_mut().set_realtime(on);
5587 let line = tr().text(if on { "realtime-on" } else { "realtime-off" }, None);
5588 self.as_mut().set_status(QString::from(&line));
5589 show(&self.qt_thread(), &shared);
5590 }
5591
5592 fn grab_phone(&self) -> bool {
5593 std::env::var_os("SIOUL_GRAB_PHONE").is_some()
5594 }
5595
5596 fn grab_steps(&self) -> QString {
5597 let steps = std::env::var("SIOUL_GRAB_STEPS").unwrap_or_else(|_| "pages".into());
5598 if cfg!(feature = "insecure-test-tls") || steps == "demo" || steps == "phone" || steps == "drag" || (["taskform", "review", "site-open", "site-quit", "site-during", "site-share", "site-devices", "site-colour", "rail", "pauses", "blocks", "unsubscribe", "attention", "line", "share-panel", "share-send", "spam", "mail-search", "mail-filters", "security-key", "health-gpg", "calls", "words", "movetask", "compose", "texts", "ai", "health", "tiles", "attachments", "projects-file", "memory", "bitwarden-wait", "tray", "notes-shared"].contains(&steps.as_str()) && offline()) {
5641 return QString::from(&steps);
5642 }
5643 QString::from("pages")
5644 }
5645}
5646
5647impl qobject::Sioul {
5649 fn set_notes_carried(self: Pin<&mut Self>, carried: bool) -> QString {
5650 QString::from(&crate::share::set_notes_carried(carried))
5651 }
5652
5653 fn set_share_part(self: Pin<&mut Self>, part: &QString, on: bool) -> QString {
5654 let problem = crate::share::set_part(&part.to_string(), on);
5655 if problem.is_empty() {
5656 crate::share::exchange(&self.qt_thread(), &self.shared());
5657 }
5658 QString::from(&problem)
5659 }
5660
5661 fn share_history(self: Pin<&mut Self>, filter: &QString) {
5662 let (qt, filter) = (self.qt_thread(), filter.to_string());
5663 std::thread::spawn(move || {
5664 let listed = crate::share::history(&filter);
5665 let _ = qt.queue(move |mut sioul| sioul.as_mut().share_listed(QString::from(&listed)));
5666 });
5667 }
5668
5669 fn share_put_back_preview(&self, part: &QString, file: &QString, stamp: &QString) -> QString {
5670 QString::from(&crate::share::put_back_preview(&part.to_string(), &file.to_string(), &stamp.to_string()))
5671 }
5672
5673 fn share_put_back(self: Pin<&mut Self>, part: &QString, file: &QString, stamp: &QString) {
5674 let (qt, shared) = (self.qt_thread(), self.shared());
5675 let (part, file, stamp) = (part.to_string(), file.to_string(), stamp.to_string());
5676 std::thread::spawn(move || {
5678 let problem = if part == sioul_sync::history::ACCOUNTS {
5680 match crate::share::put_back_item(&file, &stamp) {
5681 Ok(account) => {
5682 crate::pim::nudge(&shared, &account);
5683 String::new()
5684 }
5685 Err(e) => e,
5686 }
5687 } else {
5688 crate::share::put_back(&part, &file, &stamp)
5689 };
5690 let said = if problem.is_empty() { crate::share::put_back_said(&file, &stamp) } else { String::new() };
5691 let store = crate::share::store_of(&file);
5692 let done = problem.clone();
5693 let _ = qt.queue(move |mut sioul| {
5694 if !said.is_empty() {
5695 sioul.as_mut().set_status(QString::from(&said));
5696 sioul.as_mut().shared_in(QString::from(&store));
5698 }
5699 sioul.as_mut().share_put_back_done(QString::from(&done));
5700 });
5701 if problem.is_empty() {
5702 crate::work::show_work(&qt, &shared);
5703 crate::share::exchange(&qt, &shared);
5704 }
5705 });
5706 }
5707
5708 fn share_confirm_gone(self: Pin<&mut Self>, store: &QString) -> QString {
5709 let problem = crate::share::confirm_gone(&store.to_string());
5710 if problem.is_empty() {
5711 crate::share::exchange(&self.qt_thread(), &self.shared());
5712 }
5713 QString::from(&problem)
5714 }
5715
5716 fn share_estimate(self: Pin<&mut Self>, part: &QString) {
5717 let (qt, part) = (self.qt_thread(), part.to_string());
5718 std::thread::spawn(move || {
5719 let counted = crate::share::estimate(&part);
5720 let _ = qt.queue(move |mut sioul| sioul.as_mut().share_estimated(QString::from(&counted)));
5721 });
5722 }
5723
5724 fn spam_status(&self) -> QString {
5725 QString::from(&crate::spam::status())
5726 }
5727
5728 fn spam_train(self: Pin<&mut Self>) -> QString {
5729 QString::from(&crate::spam::train(self.qt_thread(), self.shared()))
5730 }
5731
5732 fn spam_stop(self: Pin<&mut Self>) {
5733 crate::spam::stop(self.qt_thread());
5734 }
5735
5736 fn mail_filters(&self) -> QString {
5737 QString::from(&crate::filters::view())
5738 }
5739
5740 fn set_mail_filters(self: Pin<&mut Self>, filters: &QString, shown: &QString) -> QString {
5741 QString::from(&crate::filters::save(&filters.to_string(), &shown.to_string()))
5742 }
5743
5744 fn mail_filter_said(&self, filter: &QString) -> QString {
5745 QString::from(&crate::filters::said(&filter.to_string()))
5746 }
5747
5748 fn mail_filter_try(self: Pin<&mut Self>, filter: &QString) {
5749 crate::filters::try_one(self.qt_thread(), filter.to_string());
5750 }
5751
5752 fn mail_filters_preview(self: Pin<&mut Self>) {
5753 crate::filters::preview(self.qt_thread());
5754 }
5755
5756 fn mail_filters_run(self: Pin<&mut Self>) -> QString {
5757 QString::from(&crate::filters::run(&self.qt_thread(), &self.shared()))
5758 }
5759
5760 fn new_mail_filter(self: Pin<&mut Self>, given: &QString) -> QString {
5761 QString::from(&crate::filters::new_from(&given.to_string()))
5762 }
5763
5764 fn set_share_backup(self: Pin<&mut Self>, on: bool) -> QString {
5765 let problem = crate::share::set_backup(on);
5766 if problem.is_empty() {
5768 crate::share::exchange(&self.qt_thread(), &self.shared());
5769 }
5770 QString::from(&problem)
5771 }
5772
5773 fn set_share_send(self: Pin<&mut Self>, on: bool) -> QString {
5774 let problem = crate::share::set_send(on);
5775 if problem.is_empty() && on {
5777 crate::share::exchange(&self.qt_thread(), &self.shared());
5778 }
5779 QString::from(&problem)
5780 }
5781 fn share_send_again(self: Pin<&mut Self>) {
5782 crate::share::send_again(&self.qt_thread(), &self.shared());
5783 }
5784 fn share_backup_place(self: Pin<&mut Self>, place: &QString) -> QString {
5785 let problem = crate::share::set_backup_place(&place.to_string());
5786 if problem.is_empty() {
5787 crate::share::exchange(&self.qt_thread(), &self.shared());
5788 }
5789 QString::from(&problem)
5790 }
5791
5792 fn start_sharing_on_server(self: Pin<&mut Self>, account: &QString, place: &QString, passphrase: &QString, again: &QString) {
5793 crate::share::start_on_server(&self.qt_thread(), &self.shared(), account.to_string(), place.to_string(), passphrase.to_string(), again.to_string());
5794 }
5795}
5796
5797#[cfg(test)]
5798mod tests {
5799 use super::*;
5800
5801 #[test]
5802 fn file_addresses_reach_the_file() {
5803 assert_eq!(file_url(Path::new("/tmp/Réunion #2 100%.pdf")), "file:///tmp/R%C3%A9union%20%232%20100%25.pdf");
5804 assert_eq!(local_path(&file_url(Path::new("/tmp/Réunion #2 100%.pdf"))), PathBuf::from("/tmp/Réunion #2 100%.pdf"));
5805 assert_eq!(local_path("file://localhost/tmp/a%20b"), PathBuf::from("/tmp/a b"));
5806 assert_eq!(local_path("/tmp/plain"), PathBuf::from("/tmp/plain"));
5807 assert_eq!(windows_slashed(r"C:\Users\me\a b.pdf"), "/C:/Users/me/a b.pdf");
5809 assert_eq!(windows_slashed(r"\\?\C:\Users\me"), "/C:/Users/me");
5810 assert_eq!(windows_slashed(r"\\server\share\a"), "//server/share/a");
5811 assert_eq!(windows_slashed(r"\\?\UNC\server\share\a"), "//server/share/a");
5812 assert_eq!(windows_path("/C:/Users/me/a b.pdf"), r"C:\Users\me\a b.pdf");
5813 assert_eq!(windows_path("server/share/a"), r"\\server\share\a");
5814 }
5815
5816 #[test]
5817 fn programs_are_not_opened() {
5818 for name in ["setup.exe", "Facture.PDF.exe", "invoice.exe.", "invoice.exe ", "run.sh", "x.desktop", "a.lnk", "b.js", "c.terminal", "d.iso"] {
5819 assert!(sioul_core::links::is_program(name), "{name}");
5820 }
5821 for name in ["facture.pdf", "photo.jpeg", "notes.txt", "archive.zip", "exe", "README"] {
5822 assert!(!sioul_core::links::is_program(name), "{name}");
5823 }
5824 }
5825
5826 #[test]
5827 fn new_sentences_read_in_both_languages() {
5828 for language in ["en", "fr"] {
5829 let tr = Translator::new(language);
5830 for id in ["attachment-program", "budget-origin-mail", "budget-origin-hand", "note-outside-notes", "setting-unknown-key", "reminders-entry-name", "reminders-entry-comment", "bank-rule-gone", "budget-line-gone", "mail-no-such-folder", "mail-not-in-accounts", "mail-move-needs-to", "list-read-only-mark", "import-bad-key"] {
5831 assert_ne!(tr.text(id, None), id, "{language}: {id}");
5832 }
5833 }
5834 }
5835
5836 #[test]
5837 fn attachment_names_stay_in_their_folder() {
5838 assert_eq!(one_file_name("a/b\\c.pdf"), "a_b_c.pdf");
5839 assert_eq!(one_file_name(".."), "attachment");
5840 assert_eq!(one_file_name(""), "attachment");
5841 if cfg!(windows) {
5842 assert_eq!(one_file_name("C:evil.txt"), "C_evil.txt");
5843 assert_eq!(one_file_name("con.txt"), "_con.txt");
5844 } else {
5845 assert_eq!(one_file_name("C:evil.txt"), "C:evil.txt");
5846 }
5847 }
5848}