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>>,
pub cached_categories: Vec<crate::store::AggregateItem>,
pub cached_locations: Vec<crate::store::AggregateItem>,
pub cached_journal_pages: Vec<crate::store::JournalPageItem>,
pub task_ids: HashMap<String, iced::widget::Id>,
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>,
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,
pub journal_initialized: bool,
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,
pub resolved_random_theme: AppTheme,
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,
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,
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>,
pub current_placeholder: String,
pub search_placeholder: String,
pub notes_placeholder: String,
pub location_tab_icon: char,
pub random_icon: char, pub goal_icon: char,
pub focus_icon: char,
pub journal_icon: char,
pub create_journal_icon: char,
pub alias_input_key: String,
pub alias_input_values: String,
pub editing_alias_key: Option<String>,
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,
pub loading: bool,
pub error_msg: Option<String>,
pub info_msg: Option<String>,
pub info_msg_version: usize,
pub edit_generation: u64,
pub pending_refresh_generation: u64,
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,
pub config_was_corrupted: bool,
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,
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>>, pub ringing_tasks: Vec<(TodoTask, Alarm)>,
pub snooze_custom_input: String,
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>,
pub last_click: Option<(std::time::Instant, String)>, pub last_title_click: Option<std::time::Instant>,
pub pinned_actions: Vec<crate::config::TaskAction>,
pub active_context_menu: Option<(String, bool, iced::Point)>,
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,
pub ob_snooze_short_input: String,
pub ob_snooze_long_input: String,
pub ob_auto_refresh_input: String,
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,
pub log_level: LogLevel,
pub force_ssd: bool,
pub auto_refresh_interval_mins: u32,
pub first_day_of_week: crate::config::FirstDayOfWeek,
pub journal_date_input: String,
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, 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,
}
}
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())
}
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 {
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())];
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())];
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,
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(),
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,
}
}
}