cfait 1.1.8

Powerful, fast and elegant task / TODO manager. (GUI & TUI, CalDAV & local)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
// SPDX-License-Identifier: GPL-3.0-or-later
// File: ./src/gui/state.rs
// Manages the application state for the GUI (Iced).

use crate::client::RustyClient;
use crate::config::{AppTheme, Config, LogLevel};
use crate::context::AppContext;
use crate::gui::icon;
use crate::model::{Alarm, CalendarListEntry, Task as TodoTask};
use crate::store::TaskStore;
use crate::system::SystemEvent;
use iced::widget::text_editor;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use strum::IntoEnumIterator;
use tokio::sync::mpsc;

#[derive(PartialEq, Clone, Copy, Debug, Default)]
pub enum AppState {
    #[default]
    Loading,
    Onboarding,
    Active,
    Settings,
    Help(crate::help::HelpTab, u8),
}

#[derive(Default, PartialEq, Clone, Copy, Debug)]
pub enum SidebarMode {
    #[default]
    Calendars,
    Categories,
    Locations,
    Journal,
    Goals,
}

#[derive(PartialEq, Clone, Copy, Debug, Default)]
pub enum Focus {
    #[default]
    MainList,
    Sidebar,
    SearchInput,
    AddTaskInput,
}

#[derive(Debug, Clone, Copy)]
pub enum ResizeDirection {
    North,
    South,
    East,
    West,
    NorthEast,
    NorthWest,
    SouthEast,
    SouthWest,
}

pub struct GuiApp {
    pub core_config: Config,
    pub state: AppState,
    pub ctx: Arc<dyn AppContext>,
    pub store: TaskStore,
    pub controller: crate::controller::TaskController,
    pub session: crate::model::SessionState,
    pub tasks: Vec<crate::store::TaskListItem>,
    pub calendars: Vec<CalendarListEntry>,
    pub client: Option<RustyClient>,
    pub tag_aliases: HashMap<String, Vec<String>>,
    pub bg_tx: Option<tokio::sync::mpsc::Sender<crate::gui::async_ops::WorkerCommand>>,

    // Cached Sidebar Data (computed once, not in view())
    pub cached_categories: Vec<crate::store::AggregateItem>,
    pub cached_locations: Vec<crate::store::AggregateItem>,
    pub cached_journal_pages: Vec<crate::store::JournalPageItem>,

    // --- Stable ID Cache ---
    // Maps Task UID -> Iced Widget ID. Ensures the View and Update loops use the exact same ID instance.
    pub task_ids: HashMap<String, iced::widget::Id>,

    // UI State
    pub sidebar_mode: SidebarMode,
    pub active_cal_href: Option<String>,
    pub hidden_calendars: HashSet<String>,
    pub disabled_calendars: HashSet<String>,
    pub yanked_uid: Option<String>,
    pub yank_lock_active: bool,

    pub hovered_tag_uid: Option<String>,

    // Track selected task for highlighting
    pub selected_uid: Option<String>,

    pub active_focus: Focus,
    pub sidebar_selection_idx: usize,
    pub journal_date: chrono::NaiveDate,
    pub journal_editor_content: text_editor::Content,
    pub journal_editing_href: Option<String>,
    pub journal_editing_uid: Option<String>,
    pub journal_title_input: String,
    pub journal_debounce_version: usize,
    /// Whether the Journal tab has been opened at least once since app start.
    /// The first visit defaults to today's daily note; later visits restore
    /// whatever page/date the user left the tab on.
    pub journal_initialized: bool,

    // Preferences
    pub hide_completed: bool,
    pub strikethrough_completed: bool,
    pub hide_fully_completed_tags: bool,
    pub hide_aliases_in_sidebar: bool,
    pub show_inline_descriptions: bool,
    pub sort_cutoff_days: Option<u32>,
    pub sort_standard_by_priority: bool,
    pub paused_sort_behavior: crate::config::PausedSortBehavior,
    pub sort_tiebreak_recent: bool,
    pub sort_preset: crate::config::SortPreset,
    pub current_theme: AppTheme,

    // Store the resolved random theme for this session
    pub resolved_random_theme: AppTheme,

    // Filter State
    pub filter_min_duration: Option<u32>,
    pub filter_max_duration: Option<u32>,
    pub filter_include_unset_duration: bool,

    pub quick_filter_term: String,
    pub quick_filter_icon: String,
    pub show_quick_filter: bool,
    pub show_calendars_tab: bool,
    pub show_tags_tab: bool,
    pub show_locations_tab: bool,
    pub show_goals_tab: bool,
    pub show_journal_tab: bool,
    pub blur_when_unfocused: bool,
    pub is_window_focused: bool,
    pub cached_goals_progress: HashMap<String, (u32, Vec<f32>)>,
    pub cached_task_goals: Vec<(String, String, crate::config::Goal, u32, Vec<f32>)>,
    pub sidebar_is_hidden: bool,
    pub sort_collections_by_size: bool,
    pub ob_quick_filter_term_input: String,
    pub ob_quick_filter_icon_input: String,

    // Inputs - Main
    pub input_value: text_editor::Content,
    pub description_value: text_editor::Content,
    pub search_value: text_editor::Content,
    pub search_debounce_version: usize,
    pub search_highlight_regex: Option<std::rc::Rc<regex::Regex>>,
    pub editing_uid: Option<String>,
    pub editing_tree_uid: Option<String>,
    pub creating_child_of: Option<String>,
    pub moving_task_uid: Option<String>,
    pub moving_task_is_tree: bool,
    pub move_target_idx: usize,
    pub child_lock_active: bool,
    pub creating_with_desc: bool,
    pub new_task_title: String,
    pub expanded_tasks: HashSet<String>,
    pub help_expanded_sections: HashSet<String>,
    pub unsynced_changes: bool,
    pub unsynced_tooltip: String,
    pub last_sync_failed: bool,

    // Session UI state
    pub adding_session_uid: Option<String>,
    pub editing_session_idx: Option<usize>,
    pub session_input: iced::widget::text_editor::Content,
    pub show_all_sessions: HashSet<String>,

    // Computed State (Persisted for view borrowing)
    pub current_placeholder: String,
    pub search_placeholder: String,
    pub notes_placeholder: String,

    // UI Visuals
    pub location_tab_icon: char,
    pub random_icon: char, // NEW
    pub goal_icon: char,
    pub focus_icon: char,
    pub journal_icon: char,
    pub create_journal_icon: char,

    // Inputs - Settings (Aliases)
    pub alias_input_key: String,
    pub alias_input_values: String,
    pub editing_alias_key: Option<String>,

    // Inputs - Settings (Goals)
    pub goal_input_key: String,
    pub goal_input_type: crate::config::GoalType,
    pub goal_input_target: String,
    pub goal_input_amount: String,
    pub goal_input_unit: crate::config::IntervalUnit,
    pub editing_goal_key: Option<String>,

    pub ob_trash_retention_input: String,
    pub trash_retention_days: u32,

    pub ob_default_duration_goal_mins_input: String,
    pub sessions_count_as_completions: bool,

    // System
    pub loading: bool,
    pub error_msg: Option<String>,
    pub info_msg: Option<String>,
    pub info_msg_version: usize,
    /// Monotonically incremented on every user-initiated store mutation.
    /// Used to detect if edits happened during an async refresh load.
    pub edit_generation: u64,
    /// Snapshot of edit_generation when an async local refresh was started.
    /// Compared on LocalLoaded to decide whether a full store replace is safe.
    pub pending_refresh_generation: u64,

    // Onboarding / Config
    pub ob_url: String,
    pub ob_user: String,
    pub ob_pass: String,
    pub ob_password_visible: bool,
    pub ob_default_cal: Option<String>,
    pub ob_sort_days_input: String,
    pub ob_insecure: bool,
    pub ob_tls_client_cert_path: String,
    pub ob_tls_client_key_path: String,
    /// If true, the config file exists but is invalid. We must block overwrites.
    pub config_was_corrupted: bool,

    // Local Calendar Management
    pub local_cals_editing: Vec<CalendarListEntry>,
    pub remote_cals_editing: Vec<CalendarListEntry>,
    pub color_picker_active_href: Option<String>,
    pub temp_color: iced::Color,
    pub scrollable_id: iced::widget::Id,
    pub sidebar_scrollable_id: iced::widget::Id,

    // Window Resizing State
    pub resize_direction: Option<ResizeDirection>,
    pub current_window_size: iced::Size,
    pub resize_debounce_version: usize,
    pub ob_urgent_days_input: String,
    pub ob_urgent_prio_input: String,
    pub ob_default_priority_input: String,
    pub ob_start_grace_input: String,
    pub urgent_days: u32,
    pub urgent_prio: u8,
    pub default_priority: u8,
    pub start_grace_period_days: u32,
    pub alarm_tx: Option<mpsc::Sender<SystemEvent>>, // Send tasks to actor
    pub ringing_tasks: Vec<(TodoTask, Alarm)>,       // Stack of firing alarms

    // Snooze Custom Input
    pub snooze_custom_input: String,

    // ICS Import Dialog State
    pub ics_import_dialog_open: bool,
    pub ics_import_file_path: Option<String>,
    pub ics_import_content: Option<String>,
    pub ics_import_selected_calendar: Option<String>,
    pub ics_import_task_count: Option<usize>,

    // Double click tracking
    pub last_click: Option<(std::time::Instant, String)>, // Added
    pub last_title_click: Option<std::time::Instant>,

    pub pinned_actions: Vec<crate::config::TaskAction>,
    pub active_context_menu: Option<(String, bool, iced::Point)>, // (UID, is_full_menu, pt)

    // Config cache (New fields)
    // Optional selected language for the GUI. `None` => use system default.
    pub language: Option<String>,
    pub auto_reminders: bool,
    pub default_reminder_time: String,
    pub snooze_short_mins: u32,
    pub snooze_long_mins: u32,
    pub create_events_for_tasks: bool,
    pub delete_events_on_completion: bool,
    pub deleting_events: bool,

    // Settings input buffers for duration strings
    pub ob_snooze_short_input: String,
    pub ob_snooze_long_input: String,
    pub ob_auto_refresh_input: String, // Added

    // Advanced Settings Inputs
    pub show_advanced_settings: bool,
    pub ob_max_done_roots_input: String,
    pub ob_max_done_subtasks_input: String,

    pub show_priority_numbers: bool,
    pub sync_settings: bool,

    // Logging level
    pub log_level: LogLevel,

    // Force Server-Side Decorations
    pub force_ssd: bool,

    // ADDED: Auto Refresh
    pub auto_refresh_interval_mins: u32,
    pub first_day_of_week: crate::config::FirstDayOfWeek,
    pub journal_date_input: String,

    // ADDED: UI Scale (for global zooming)
    pub ui_scale: f32,

    pub undo_history: crate::journal::UndoHistory,

    pub input_history: crate::model::session::TextHistory,
    pub desc_history: crate::model::session::TextHistory,
    pub last_edited_field: u8, // 0 for main input, 1 for description
    pub editor_maximized: bool,
}

impl GuiApp {
    pub fn get_sidebar_len(&self) -> usize {
        match self.sidebar_mode {
            SidebarMode::Calendars => self.get_filtered_calendars().len(),
            SidebarMode::Categories => self.cached_categories.len(),
            SidebarMode::Locations => self.cached_locations.len(),
            SidebarMode::Journal => 31,
            SidebarMode::Goals => self.core_config.goals.len(),
        }
    }

    pub fn get_sidebar_action(&self, is_enter: bool) -> Option<crate::gui::message::Message> {
        let idx = self.sidebar_selection_idx;
        match self.sidebar_mode {
            SidebarMode::Calendars => self.get_filtered_calendars().get(idx).map(|c| {
                if is_enter {
                    crate::gui::message::Message::SelectCalendar(c.href.clone())
                } else {
                    crate::gui::message::Message::ToggleCalendarVisibility(
                        c.href.clone(),
                        self.hidden_calendars.contains(&c.href),
                    )
                }
            }),
            SidebarMode::Categories => self
                .cached_categories
                .get(idx)
                .map(|c| crate::gui::message::Message::CategoryToggled(c.full_key.clone())),
            SidebarMode::Locations => self
                .cached_locations
                .get(idx)
                .map(|c| crate::gui::message::Message::LocationToggled(c.full_key.clone())),
            SidebarMode::Goals => {
                let mut keys: Vec<_> = self.core_config.goals.keys().cloned().collect();
                keys.sort();
                keys.get(idx).and_then(|key| {
                    if key.starts_with('#') {
                        Some(crate::gui::message::Message::JumpToTag(
                            key.trim_start_matches('#').to_string(),
                        ))
                    } else if key.starts_with("@@") {
                        Some(crate::gui::message::Message::JumpToLocation(
                            key.trim_start_matches("@@").to_string(),
                        ))
                    } else {
                        None
                    }
                })
            }
            SidebarMode::Journal => None,
        }
    }

    /// Resolve the collection (calendar href) the journal editor should bind to
    /// for the given date: an explicitly chosen journal collection, then the
    /// active (write) collection, then the first visible journal-supporting
    /// calendar (preferring one that already has an entry for `date`), finally
    /// the local calendar. Mirrors the resolution used by the journal main pane
    /// so the editor and the highlighted collection button stay in sync.
    pub fn resolve_journal_href(&self, date: chrono::NaiveDate) -> String {
        if let Some(h) = self
            .journal_editing_href
            .clone()
            .or_else(|| self.active_cal_href.clone())
        {
            return h;
        }
        let mut visible: Vec<&CalendarListEntry> = self
            .calendars
            .iter()
            .filter(|c| {
                let supports = if c.href.starts_with("local://") {
                    true
                } else {
                    c.supports_vjournal.unwrap_or(false)
                };
                supports
                    && !self.hidden_calendars.contains(&c.href)
                    && !self.disabled_calendars.contains(&c.href)
                    && c.href != crate::storage::LOCAL_TRASH_HREF
                    && c.href != "local://recovery"
            })
            .collect();
        visible.sort_by_key(|c| {
            if self.store.get_journal_entry(&c.href, date).is_some() {
                0
            } else {
                1
            }
        });
        visible
            .first()
            .map(|c| c.href.clone())
            .unwrap_or_else(|| crate::storage::LOCAL_CALENDAR_HREF.to_string())
    }

    /// Whether a collection is present and currently visible (not hidden or
    /// disabled, not the trash/recovery pseudo-calendars).
    pub fn collection_visible(&self, href: &str) -> bool {
        self.calendars.iter().any(|c| {
            c.href == href
                && !self.hidden_calendars.contains(&c.href)
                && !self.disabled_calendars.contains(&c.href)
                && c.href != crate::storage::LOCAL_TRASH_HREF
                && c.href != "local://recovery"
        })
    }

    pub fn sort_calendars(&mut self) {
        let order = self.core_config.collection_order.clone();
        let sort_by_size = self.sort_collections_by_size;
        let mut sizes = HashMap::new();
        if sort_by_size {
            for cal in &self.calendars {
                let count = self
                    .store
                    .calendars
                    .get(&cal.href)
                    .map(|m| m.len())
                    .unwrap_or(0);
                sizes.insert(cal.href.clone(), count);
            }
        }
        self.calendars.sort_by(|a, b| {
            if sort_by_size {
                let count_a = sizes.get(&a.href).unwrap_or(&0);
                let count_b = sizes.get(&b.href).unwrap_or(&0);
                crate::model::compare_calendars_with_size(
                    &a.href, &a.name, *count_a, &b.href, &b.name, *count_b, &order,
                )
            } else {
                crate::model::compare_calendars(&a.href, &a.name, &b.href, &b.name, &order)
            }
        });
    }

    pub fn get_filtered_calendars(&self) -> Vec<&CalendarListEntry> {
        self.calendars
            .iter()
            .filter(|c| !self.disabled_calendars.contains(&c.href))
            .filter(|c| {
                if c.href == crate::storage::LOCAL_TRASH_HREF || c.href == "local://recovery" {
                    self.store
                        .calendars
                        .get(&c.href)
                        .is_some_and(|map| !map.is_empty())
                } else {
                    true
                }
            })
            .collect()
    }

    pub fn get_task_at_index(&self, idx: usize) -> Option<&TodoTask> {
        match self.tasks.get(idx) {
            Some(crate::store::TaskListItem::Task(t)) => Some(t),
            _ => None,
        }
    }

    pub fn find_task_index_by_uid(&self, uid: &str) -> Option<usize> {
        self.tasks.iter().position(|item| {
            if let crate::store::TaskListItem::Task(t) = item {
                t.uid == uid
            } else {
                false
            }
        })
    }

    pub fn get_move_targets(
        &self,
        task_calendar_href: &str,
        include_current: bool,
    ) -> Vec<&CalendarListEntry> {
        self.calendars
            .iter()
            .filter(|c| {
                (include_current || c.href != task_calendar_href)
                    && !self.disabled_calendars.contains(&c.href)
                    && c.href != crate::storage::LOCAL_TRASH_HREF
                    && c.href != "local://recovery"
            })
            .collect()
    }

    pub fn verify_sidebar_mode(&mut self) {
        let valid = match self.sidebar_mode {
            SidebarMode::Calendars => self.show_calendars_tab,
            SidebarMode::Categories => self.show_tags_tab,
            SidebarMode::Locations => self.show_locations_tab,
            SidebarMode::Goals => self.show_goals_tab,
            SidebarMode::Journal => self.show_journal_tab,
        };
        if !valid {
            self.sidebar_mode = if self.show_calendars_tab {
                SidebarMode::Calendars
            } else if self.show_tags_tab {
                SidebarMode::Categories
            } else if self.show_locations_tab {
                SidebarMode::Locations
            } else if self.show_goals_tab {
                SidebarMode::Goals
            } else {
                SidebarMode::Journal
            };
        }
    }
}

impl Default for GuiApp {
    fn default() -> Self {
        // Randomize Location Icon
        let loc_icons = [
            icon::LOCATION,
            icon::EARTH_ASIA,
            icon::EARTH_AMERICAS,
            icon::EARTH_AFRICA,
            icon::EARTH_GENERIC,
            icon::PLANET,
            icon::GALAXY,
            icon::ISLAND,
            icon::COMPASS,
            icon::MOUNTAINS,
            icon::GLOBE,
            icon::GLOBEMODEL,
            icon::MOON,
        ];

        let mut rng = fastrand::Rng::new();

        let location_tab_icon = loc_icons[rng.usize(..loc_icons.len())];

        // Pick initial random icon for the random-jump button
        let random_icon =
            crate::gui::icon::RANDOM_ICONS[rng.usize(..crate::gui::icon::RANDOM_ICONS.len())];

        let goal_icon =
            crate::gui::icon::GOAL_ICONS[rng.usize(..crate::gui::icon::GOAL_ICONS.len())];

        let focus_icon =
            crate::gui::icon::FOCUS_ICONS[rng.usize(..crate::gui::icon::FOCUS_ICONS.len())];

        let journal_icon =
            crate::gui::icon::JOURNAL_ICONS[rng.usize(..crate::gui::icon::JOURNAL_ICONS.len())];

        let create_journal_icon = crate::gui::icon::CREATE_JOURNAL_ICONS
            [rng.usize(..crate::gui::icon::CREATE_JOURNAL_ICONS.len())];

        // Select a random theme (excluding Random itself)
        let themes: Vec<AppTheme> = AppTheme::iter()
            .filter(|&t| t != AppTheme::Random)
            .collect();
        let resolved_random_theme = if !themes.is_empty() {
            themes[rng.usize(..themes.len())]
        } else {
            AppTheme::RustyDark
        };

        let ctx = Arc::new(crate::context::StandardContext::new(None));
        let store = TaskStore::new(ctx.clone());
        let client = Arc::new(tokio::sync::Mutex::new(None));
        let controller = crate::controller::TaskController::new(
            Arc::new(tokio::sync::Mutex::new(store.clone())),
            client,
            ctx.clone(),
        );
        Self {
            core_config: Config::default(),
            ctx: ctx.clone(),
            state: AppState::Loading,
            store,
            controller,
            session: crate::model::SessionState::default(),
            tasks: vec![],
            calendars: vec![],
            client: None,
            tag_aliases: HashMap::new(),
            bg_tx: None,

            cached_categories: Vec::new(),
            cached_locations: Vec::new(),
            cached_journal_pages: Vec::new(),

            task_ids: HashMap::new(),

            sidebar_mode: SidebarMode::Calendars,
            active_cal_href: None,
            hidden_calendars: HashSet::new(),
            disabled_calendars: HashSet::new(),
            yanked_uid: None,
            yank_lock_active: false,
            selected_uid: None,

            active_focus: Focus::MainList,
            sidebar_selection_idx: 0,
            journal_date: chrono::Local::now().date_naive(),
            journal_editor_content: text_editor::Content::new(),
            journal_editing_href: None,
            journal_editing_uid: None,
            journal_title_input: String::new(),
            journal_debounce_version: 0,
            journal_initialized: false,

            hovered_tag_uid: None,

            hide_completed: false,
            hide_fully_completed_tags: true,
            hide_aliases_in_sidebar: true,
            show_inline_descriptions: true,
            sort_cutoff_days: Some(30),
            sort_standard_by_priority: false,
            paused_sort_behavior: crate::config::PausedSortBehavior::default(),
            sort_tiebreak_recent: Config::default().sort_tiebreak_recent,
            sort_preset: crate::config::SortPreset::default(),
            ob_sort_days_input: "30".to_string(),
            current_theme: AppTheme::default(),
            resolved_random_theme,

            filter_min_duration: None,
            filter_max_duration: None,
            filter_include_unset_duration: true,

            quick_filter_term: "is:ready".to_string(),
            quick_filter_icon: "f0fa9".to_string(),
            show_quick_filter: true,
            show_calendars_tab: true,
            show_tags_tab: true,
            show_locations_tab: true,
            show_goals_tab: true,
            show_journal_tab: true,
            blur_when_unfocused: false,
            is_window_focused: true,
            cached_goals_progress: HashMap::new(),
            cached_task_goals: Vec::new(),
            sidebar_is_hidden: false,
            sort_collections_by_size: true,
            ob_quick_filter_term_input: "is:ready".to_string(),
            ob_quick_filter_icon_input: "f0fa9".to_string(),

            input_value: text_editor::Content::new(),
            description_value: text_editor::Content::new(),
            search_value: text_editor::Content::new(),
            search_debounce_version: 0,
            search_highlight_regex: None,
            editing_uid: None,
            editing_tree_uid: None,
            creating_child_of: None,
            moving_task_uid: None,
            moving_task_is_tree: false,
            move_target_idx: 0,
            child_lock_active: false,
            creating_with_desc: false,
            new_task_title: String::new(),
            expanded_tasks: HashSet::new(),
            help_expanded_sections: HashSet::new(),
            unsynced_changes: false,
            unsynced_tooltip: String::new(),
            last_sync_failed: false,

            // Session UI defaults
            adding_session_uid: None,
            editing_session_idx: None,
            session_input: iced::widget::text_editor::Content::new(),
            show_all_sessions: HashSet::new(),

            current_placeholder: rust_i18n::t!("new_task_prompt").to_string(),
            search_placeholder: rust_i18n::t!("search_placeholder").to_string(),
            notes_placeholder: rust_i18n::t!("notes_placeholder").to_string(),

            location_tab_icon,
            random_icon,
            goal_icon,
            focus_icon,
            journal_icon,
            create_journal_icon,
            alias_input_key: String::new(),
            alias_input_values: String::new(),
            editing_alias_key: None,
            goal_input_key: String::new(),
            goal_input_type: crate::config::GoalType::Count,
            goal_input_target: String::new(),
            goal_input_amount: "1".to_string(),
            goal_input_unit: crate::config::IntervalUnit::Weeks,
            editing_goal_key: None,
            ob_trash_retention_input: "14".to_string(),
            trash_retention_days: 14,

            ob_default_duration_goal_mins_input: "60".to_string(),
            sessions_count_as_completions: false,

            loading: true,
            error_msg: None,
            info_msg: None,
            info_msg_version: 0,
            edit_generation: 0,
            pending_refresh_generation: 0,
            ob_url: String::new(),
            ob_user: String::new(),
            ob_pass: String::new(),
            ob_password_visible: false,
            ob_default_cal: None,
            ob_insecure: false,
            ob_tls_client_cert_path: String::new(),
            ob_tls_client_key_path: String::new(),
            config_was_corrupted: false,

            local_cals_editing: vec![],
            remote_cals_editing: vec![],
            color_picker_active_href: None,
            temp_color: iced::Color::WHITE,
            scrollable_id: iced::widget::Id::unique(),
            sidebar_scrollable_id: iced::widget::Id::unique(),

            resize_direction: None,
            current_window_size: iced::Size::new(1024.0, 768.0),
            resize_debounce_version: 0,
            ob_urgent_days_input: "1".to_string(),
            ob_urgent_prio_input: "1".to_string(),
            ob_default_priority_input: "5".to_string(),
            ob_start_grace_input: "1".to_string(),
            urgent_days: 1,
            urgent_prio: 1,
            default_priority: 5,
            start_grace_period_days: 1,
            alarm_tx: None,
            ringing_tasks: Vec::new(),
            snooze_custom_input: String::new(),

            language: None,
            auto_reminders: true,
            default_reminder_time: "08:00".to_string(),
            snooze_short_mins: 60,
            snooze_long_mins: 1440,
            create_events_for_tasks: false,
            delete_events_on_completion: false,
            strikethrough_completed: false,
            deleting_events: false,
            ob_snooze_short_input: "1h".to_string(),
            ob_snooze_long_input: "1d".to_string(),
            ob_auto_refresh_input: "30m".to_string(),

            show_advanced_settings: false,
            ob_max_done_roots_input: "20".to_string(),
            ob_max_done_subtasks_input: "5".to_string(),
            show_priority_numbers: true,
            sync_settings: true,
            log_level: LogLevel::Info,

            force_ssd: {
                #[cfg(target_os = "windows")]
                {
                    let info = os_info::get();
                    matches!(info.os_type(), os_info::Type::Windows)
                        && info.version().to_string().starts_with("10")
                }
                #[cfg(not(target_os = "windows"))]
                {
                    false
                }
            },

            auto_refresh_interval_mins: 30,
            first_day_of_week: crate::config::FirstDayOfWeek::default(),
            journal_date_input: String::new(),

            // Default UI scale
            ui_scale: 1.0,

            pinned_actions: crate::config::Config::default().pinned_actions,
            active_context_menu: None,

            last_click: None,
            last_title_click: None,

            ics_import_dialog_open: false,
            ics_import_file_path: None,
            ics_import_content: None,
            ics_import_selected_calendar: None,
            ics_import_task_count: None,

            undo_history: crate::journal::UndoHistory::new(),

            input_history: crate::model::session::TextHistory::default(),
            desc_history: crate::model::session::TextHistory::default(),
            last_edited_field: 0,
            editor_maximized: false,
        }
    }
}