cfait 1.0.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
// SPDX-License-Identifier: GPL-3.0-or-later
// File: ./src/gui/state.rs
// Manages the application state for the GUI (Iced).

rust_i18n::i18n!("../locales", fallback = "en");

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, // NEW
}

#[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>,

    // --- 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,

    // Preferences
    pub hide_completed: bool,
    pub strikethrough_completed: bool,
    pub hide_fully_completed_tags: bool,
    pub hide_aliases_in_sidebar: bool,
    pub sort_cutoff_months: Option<u32>,
    pub sort_standard_by_priority: 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 sidebar_is_hidden: 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 editing_uid: Option<String>,
    pub creating_child_of: Option<String>,
    pub moving_task_uid: Option<String>,
    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 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

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

    // System
    pub loading: bool,
    pub error_msg: Option<String>,

    // 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_months_input: String,
    pub ob_insecure: bool,
    /// 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 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 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,

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

impl GuiApp {
    pub fn sort_calendars(&mut self) {
        self.calendars.sort_by_key(|c| {
            if c.href == "local://recovery" {
                1
            } else if c.href == crate::storage::LOCAL_TRASH_HREF {
                2
            } else {
                0
            }
        });
    }

    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) -> Vec<&CalendarListEntry> {
        self.calendars
            .iter()
            .filter(|c| {
                c.href != task_calendar_href
                    && !self.disabled_calendars.contains(&c.href)
                    && c.href != crate::storage::LOCAL_TRASH_HREF
                    && c.href != "local://recovery"
            })
            .collect()
    }
}

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())];

        // 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(),

            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,

            hovered_tag_uid: None,

            hide_completed: false,
            hide_fully_completed_tags: true,
            hide_aliases_in_sidebar: true,
            sort_cutoff_months: Some(2),
            sort_standard_by_priority: false,
            sort_preset: crate::config::SortPreset::default(),
            ob_sort_months_input: "2".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,
            sidebar_is_hidden: false,
            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,
            editing_uid: None,
            creating_child_of: None,
            moving_task_uid: None,
            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,
            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,
            alias_input_key: String::new(),
            alias_input_values: String::new(),
            editing_alias_key: None,
            ob_trash_retention_input: "14".to_string(),
            trash_retention_days: 14,

            loading: true,
            error_msg: None,
            ob_url: String::new(),
            ob_user: String::new(),
            ob_pass: String::new(),
            ob_password_visible: false,
            ob_default_cal: None,
            ob_insecure: false,
            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(800.0, 600.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: "09: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,

            // Default UI scale
            ui_scale: 1.0,

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

            last_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,
        }
    }
}