use crate::context::AppContext;
use crate::storage::LocalStorage;
use anyhow::{Error, Result};
use chrono;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::fs;
use strum::EnumIter;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, EnumIter)]
pub enum LogLevel {
#[default]
Error,
Warn,
Info,
Debug,
Trace,
}
impl fmt::Display for LogLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LogLevel::Error => write!(f, "Error"),
LogLevel::Warn => write!(f, "Warn"),
LogLevel::Info => write!(f, "Info"),
LogLevel::Debug => write!(f, "Debug"),
LogLevel::Trace => write!(f, "Trace"),
}
}
}
impl LogLevel {
pub const ALL: &'static [LogLevel] = &[
LogLevel::Error,
LogLevel::Warn,
LogLevel::Info,
LogLevel::Debug,
LogLevel::Trace,
];
pub fn to_level_filter(&self) -> log::LevelFilter {
match self {
LogLevel::Error => log::LevelFilter::Error,
LogLevel::Warn => log::LevelFilter::Warn,
LogLevel::Info => log::LevelFilter::Info,
LogLevel::Debug => log::LevelFilter::Debug,
LogLevel::Trace => log::LevelFilter::Trace,
}
}
}
fn default_true() -> bool {
true
}
fn default_cutoff() -> Option<u32> {
Some(2)
}
const CURRENT_CONFIG_VERSION: u32 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TaskAction {
OpenUrl,
ToggleDetails,
CompleteAndShift,
ToggleTimer,
StopTimer,
AddSession,
IncreasePriority,
DecreasePriority,
Edit,
Yank,
CreateSubtask,
DuplicateTree,
Promote,
Move,
Cancel,
Delete,
DeleteTree,
OpenLocations,
OpenCoordinates,
ExtractSubtasks,
TogglePin,
}
impl TaskAction {
pub const ALL: &'static [TaskAction] = &[
TaskAction::OpenUrl, TaskAction::OpenCoordinates, TaskAction::OpenLocations, TaskAction::ToggleDetails,
TaskAction::CompleteAndShift,
TaskAction::ToggleTimer,
TaskAction::StopTimer,
TaskAction::AddSession,
TaskAction::IncreasePriority,
TaskAction::DecreasePriority,
TaskAction::Edit,
TaskAction::Yank,
TaskAction::CreateSubtask,
TaskAction::ExtractSubtasks,
TaskAction::TogglePin,
TaskAction::DuplicateTree,
TaskAction::Promote,
TaskAction::Move,
TaskAction::Cancel,
TaskAction::Delete,
TaskAction::DeleteTree,
];
pub fn label(&self) -> String {
match self {
TaskAction::ExtractSubtasks => rust_i18n::t!("action_extract_subtasks").to_string(),
TaskAction::CompleteAndShift => rust_i18n::t!("action_complete_and_shift").to_string(),
TaskAction::ToggleDetails => rust_i18n::t!("show_details").to_string(),
TaskAction::ToggleTimer => rust_i18n::t!("start_task").to_string(),
TaskAction::StopTimer => rust_i18n::t!("stop_reset").to_string(),
TaskAction::AddSession => rust_i18n::t!("help_metadata_log_time").to_string(),
TaskAction::IncreasePriority => rust_i18n::t!("increase_priority").to_string(),
TaskAction::DecreasePriority => rust_i18n::t!("menu_decrease_prio").to_string(),
TaskAction::Edit => rust_i18n::t!("edit").to_string(),
TaskAction::Yank => rust_i18n::t!("yank_copy_id").to_string(),
TaskAction::CreateSubtask => rust_i18n::t!("create_subtask").to_string(),
TaskAction::DuplicateTree => rust_i18n::t!("duplicate_task").to_string(),
TaskAction::Promote => rust_i18n::t!("promote_remove_parent").to_string(),
TaskAction::Move => rust_i18n::t!("menu_move").to_string(),
TaskAction::Cancel => rust_i18n::t!("cancel").to_string(),
TaskAction::Delete => rust_i18n::t!("delete").to_string(),
TaskAction::DeleteTree => rust_i18n::t!("delete_task_tree").to_string(),
TaskAction::OpenLocations => rust_i18n::t!("action_open_locations").to_string(),
TaskAction::OpenCoordinates => rust_i18n::t!("open_coordinates").to_string(),
TaskAction::OpenUrl => rust_i18n::t!("open_url").to_string(),
TaskAction::TogglePin => rust_i18n::t!("action_toggle_pin").to_string(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, EnumIter)]
pub enum SortPreset {
#[default]
UrgentStartedDue,
UrgentDueStarted,
StartedUrgentDue,
}
impl fmt::Display for SortPreset {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SortPreset::UrgentStartedDue => write!(f, "Urgent > Started > Due Soon"),
SortPreset::UrgentDueStarted => write!(f, "Urgent > Due Soon > Started"),
SortPreset::StartedUrgentDue => write!(f, "Started > Urgent > Due Soon"),
}
}
}
impl std::str::FromStr for SortPreset {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"Urgent > Started > Due Soon" => Ok(SortPreset::UrgentStartedDue),
"Urgent > Due Soon > Started" => Ok(SortPreset::UrgentDueStarted),
"Started > Urgent > Due Soon" => Ok(SortPreset::StartedUrgentDue),
_ => Err(()),
}
}
}
fn default_pinned_actions() -> Vec<TaskAction> {
vec![
TaskAction::OpenUrl, TaskAction::OpenCoordinates,
TaskAction::ToggleDetails,
TaskAction::ToggleTimer,
TaskAction::IncreasePriority,
TaskAction::DecreasePriority,
TaskAction::Cancel,
TaskAction::Edit,
TaskAction::Yank,
TaskAction::CreateSubtask,
]
}
pub fn set_locale_with_fallback(locale: &str) {
let available = rust_i18n::available_locales!();
if available.iter().any(|l| l == locale) {
rust_i18n::set_locale(locale);
} else if let Some(primary) = locale.split(&['_', '-'][..]).next() {
if available.iter().any(|l| l == primary) {
rust_i18n::set_locale(primary);
} else {
rust_i18n::set_locale(locale);
}
} else {
rust_i18n::set_locale(locale);
}
}
pub fn init_locale(ctx: &dyn crate::context::AppContext) {
let config = Config::load(ctx).unwrap_or_default();
if let Some(lang) = config.language {
set_locale_with_fallback(&lang);
} else if let Some(sys_lang) = sys_locale::get_locale() {
set_locale_with_fallback(&sys_lang);
}
}
fn default_urgent_days() -> u32 {
1
}
fn default_urgent_prio() -> u8 {
1
}
fn default_enable_local_mode() -> bool {
true
}
fn default_start_grace_period() -> u32 {
1
}
fn default_priority() -> u8 {
5
}
fn default_auto_remind() -> bool {
true
}
fn default_remind_time() -> String {
"08:00".to_string()
}
fn default_snooze_1() -> u32 {
60
}
fn default_snooze_2() -> u32 {
1440
}
fn default_create_events() -> bool {
false
}
fn default_delete_events_on_completion() -> bool {
false
}
fn default_refresh_interval() -> u32 {
30
}
fn default_max_done_roots() -> usize {
20
}
fn default_ui_scale() -> f32 {
1.0
}
fn default_max_done_subtasks() -> usize {
5
}
fn default_quick_filter_term() -> String {
"is:ready".to_string()
}
fn default_quick_filter_icon() -> String {
"f0fa9".to_string()
}
fn default_trash_retention() -> u32 {
14
}
fn default_log_level() -> LogLevel {
LogLevel::Info
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, EnumIter)]
pub enum AppTheme {
#[default]
RustyDark,
Random,
Light,
Dark,
Dracula,
Nord,
SolarizedLight,
SolarizedDark,
GruvboxLight,
GruvboxDark,
CatppuccinLatte,
CatppuccinFrappe,
CatppuccinMacchiato,
CatppuccinMocha,
TokyoNight,
TokyoNightStorm,
TokyoNightLight,
KanagawaWave,
KanagawaDragon,
KanagawaLotus,
Moonfly,
Nightfly,
Oxocarbon,
Ferra,
}
impl AppTheme {
pub fn is_dark(&self) -> bool {
!matches!(
self,
AppTheme::Light
| AppTheme::SolarizedLight
| AppTheme::GruvboxLight
| AppTheme::CatppuccinLatte
| AppTheme::TokyoNightLight
)
}
}
impl fmt::Display for AppTheme {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppTheme::RustyDark => write!(f, "Rusty Dark"),
AppTheme::Random => write!(f, "Random"),
AppTheme::Light => write!(f, "Light"),
AppTheme::Dark => write!(f, "Dark"),
AppTheme::Dracula => write!(f, "Dracula"),
AppTheme::Nord => write!(f, "Nord"),
AppTheme::SolarizedLight => write!(f, "Solarized Light"),
AppTheme::SolarizedDark => write!(f, "Solarized Dark"),
AppTheme::GruvboxLight => write!(f, "Gruvbox Light"),
AppTheme::GruvboxDark => write!(f, "Gruvbox Dark"),
AppTheme::CatppuccinLatte => write!(f, "Catppuccin Latte"),
AppTheme::CatppuccinFrappe => write!(f, "Catppuccin Frappé"),
AppTheme::CatppuccinMacchiato => write!(f, "Catppuccin Macchiato"),
AppTheme::CatppuccinMocha => write!(f, "Catppuccin Mocha"),
AppTheme::TokyoNight => write!(f, "Tokyo Night"),
AppTheme::TokyoNightStorm => write!(f, "Tokyo Night Storm"),
AppTheme::TokyoNightLight => write!(f, "Tokyo Night Light"),
AppTheme::KanagawaWave => write!(f, "Kanagawa Wave"),
AppTheme::KanagawaDragon => write!(f, "Kanagawa Dragon"),
AppTheme::KanagawaLotus => write!(f, "Kanagawa Lotus"),
AppTheme::Moonfly => write!(f, "Moonfly"),
AppTheme::Nightfly => write!(f, "Nightfly"),
AppTheme::Oxocarbon => write!(f, "Oxocarbon"),
AppTheme::Ferra => write!(f, "Ferra"),
}
}
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct Config {
#[serde(default)]
pub config_version: u32,
pub url: String,
pub username: String,
#[serde(skip_serializing, default)]
pub password: String,
#[serde(default)]
pub allow_insecure_certs: bool,
#[serde(default)]
pub disabled_calendars: Vec<String>,
pub default_calendar: Option<String>,
#[serde(default = "default_enable_local_mode")]
pub enable_local_mode: bool,
#[serde(default)]
pub hide_completed: bool,
#[serde(default)]
pub strikethrough_completed: bool,
#[serde(default = "default_true")]
pub hide_fully_completed_tags: bool,
#[serde(default = "default_true")]
pub hide_aliases_in_sidebar: bool,
#[serde(default = "default_ui_scale")]
pub ui_scale: f32,
#[serde(default = "default_cutoff")]
pub sort_cutoff_months: Option<u32>,
#[serde(default)]
pub sort_standard_by_priority: bool,
#[serde(default)]
pub sort_preset: SortPreset,
#[serde(default)]
pub theme: AppTheme,
#[serde(default)]
pub language: Option<String>,
#[serde(default = "default_urgent_days")]
pub urgent_days_horizon: u32,
#[serde(default = "default_urgent_prio")]
pub urgent_priority_threshold: u8,
#[serde(default = "default_priority")]
pub default_priority: u8,
#[serde(default = "default_start_grace_period")]
pub start_grace_period_days: u32,
#[serde(default = "default_auto_remind")]
pub auto_reminders: bool,
#[serde(default = "default_remind_time")]
pub default_reminder_time: String,
#[serde(default = "default_snooze_1")]
pub snooze_short_mins: u32,
#[serde(default = "default_snooze_2")]
pub snooze_long_mins: u32,
#[serde(default = "default_create_events")]
pub create_events_for_tasks: bool,
#[serde(default = "default_delete_events_on_completion")]
pub delete_events_on_completion: bool,
#[serde(default = "default_refresh_interval")]
pub auto_refresh_interval_mins: u32,
#[serde(default = "default_trash_retention")]
pub trash_retention_days: u32,
#[serde(default = "default_max_done_roots")]
pub max_done_roots: usize,
#[serde(default = "default_max_done_subtasks")]
pub max_done_subtasks: usize,
#[serde(default = "default_true")]
pub show_ongoing_notifications: bool,
#[serde(default = "default_true")]
pub show_priority_numbers: bool,
#[serde(default = "default_pinned_actions")]
pub pinned_actions: Vec<TaskAction>,
#[serde(default = "default_quick_filter_term")]
pub quick_filter_term: String,
#[serde(default = "default_quick_filter_icon")]
pub quick_filter_icon: String,
#[serde(default = "default_true")]
pub show_quick_filter: bool,
#[serde(default)]
pub sidebar_is_hidden: bool,
#[serde(default)]
pub description_editor: String,
#[serde(default = "default_log_level")]
pub log_level: LogLevel,
#[serde(default)]
pub hidden_calendars: Vec<String>,
#[serde(default)]
pub tag_aliases: HashMap<String, Vec<String>>,
#[serde(default)]
pub expanded_tags: Vec<String>,
#[serde(default)]
pub expanded_locations: Vec<String>,
#[serde(default)]
pub expanded_done_groups: Vec<String>,
#[serde(default = "default_true")]
pub sync_settings: bool,
#[serde(default)]
pub settings_updated_at: i64,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
pub struct SyncableConfig {
#[serde(default)]
pub tag_aliases: HashMap<String, Vec<String>>,
#[serde(default)]
pub hide_completed: bool,
#[serde(default)]
pub hide_fully_completed_tags: bool,
#[serde(default)]
pub hide_aliases_in_sidebar: bool,
#[serde(default)]
pub sort_cutoff_months: Option<u32>,
#[serde(default)]
pub sort_standard_by_priority: bool,
#[serde(default)]
pub sort_preset: SortPreset,
#[serde(default = "default_urgent_days")]
pub urgent_days_horizon: u32,
#[serde(default = "default_urgent_prio")]
pub urgent_priority_threshold: u8,
#[serde(default = "default_priority")]
pub default_priority: u8,
#[serde(default = "default_start_grace_period")]
pub start_grace_period_days: u32,
#[serde(default = "default_auto_remind")]
pub auto_reminders: bool,
#[serde(default = "default_remind_time")]
pub default_reminder_time: String,
#[serde(default = "default_snooze_1")]
pub snooze_short_mins: u32,
#[serde(default = "default_snooze_2")]
pub snooze_long_mins: u32,
#[serde(default = "default_create_events")]
pub create_events_for_tasks: bool,
#[serde(default = "default_delete_events_on_completion")]
pub delete_events_on_completion: bool,
#[serde(default = "default_trash_retention")]
pub trash_retention_days: u32,
#[serde(default = "default_max_done_roots")]
pub max_done_roots: usize,
#[serde(default = "default_max_done_subtasks")]
pub max_done_subtasks: usize,
#[serde(default = "default_true")]
pub show_ongoing_notifications: bool,
#[serde(default = "default_true")]
pub show_priority_numbers: bool,
#[serde(default = "default_pinned_actions")]
pub pinned_actions: Vec<TaskAction>,
#[serde(default = "default_quick_filter_term")]
pub quick_filter_term: String,
#[serde(default = "default_quick_filter_icon")]
pub quick_filter_icon: String,
#[serde(default = "default_true")]
pub show_quick_filter: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct SettingsPayload {
#[serde(default)]
pub updated_at: i64,
#[serde(default)]
pub config: SyncableConfig,
}
impl Default for Config {
fn default() -> Self {
Self {
config_version: CURRENT_CONFIG_VERSION,
url: String::new(),
username: String::new(),
password: String::new(),
default_calendar: None,
enable_local_mode: true,
allow_insecure_certs: false,
hidden_calendars: Vec::new(),
disabled_calendars: Vec::new(),
hide_completed: false,
hide_fully_completed_tags: true,
hide_aliases_in_sidebar: true,
ui_scale: 1.0,
sort_cutoff_months: Some(2),
sort_standard_by_priority: false,
sort_preset: SortPreset::default(),
tag_aliases: HashMap::new(),
language: None,
theme: AppTheme::default(),
urgent_days_horizon: 1,
urgent_priority_threshold: 1,
default_priority: 5,
start_grace_period_days: 1,
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,
auto_refresh_interval_mins: 30,
trash_retention_days: 14,
strikethrough_completed: false,
max_done_roots: 20,
max_done_subtasks: 5,
show_ongoing_notifications: true,
show_priority_numbers: true,
pinned_actions: default_pinned_actions(),
quick_filter_term: default_quick_filter_term(),
quick_filter_icon: default_quick_filter_icon(),
show_quick_filter: true,
sidebar_is_hidden: false,
description_editor: String::new(),
log_level: default_log_level(),
expanded_tags: Vec::new(),
expanded_locations: Vec::new(),
expanded_done_groups: Vec::new(),
sync_settings: true,
settings_updated_at: 0,
}
}
}
impl Config {
pub fn get_syncable(&self) -> SyncableConfig {
SyncableConfig {
tag_aliases: self.tag_aliases.clone(),
hide_completed: self.hide_completed,
hide_fully_completed_tags: self.hide_fully_completed_tags,
hide_aliases_in_sidebar: self.hide_aliases_in_sidebar,
sort_cutoff_months: self.sort_cutoff_months,
sort_standard_by_priority: self.sort_standard_by_priority,
sort_preset: self.sort_preset,
urgent_days_horizon: self.urgent_days_horizon,
urgent_priority_threshold: self.urgent_priority_threshold,
default_priority: self.default_priority,
start_grace_period_days: self.start_grace_period_days,
auto_reminders: self.auto_reminders,
default_reminder_time: self.default_reminder_time.clone(),
snooze_short_mins: self.snooze_short_mins,
snooze_long_mins: self.snooze_long_mins,
create_events_for_tasks: self.create_events_for_tasks,
delete_events_on_completion: self.delete_events_on_completion,
trash_retention_days: self.trash_retention_days,
max_done_roots: self.max_done_roots,
max_done_subtasks: self.max_done_subtasks,
show_ongoing_notifications: self.show_ongoing_notifications,
show_priority_numbers: self.show_priority_numbers,
pinned_actions: self.pinned_actions.clone(),
quick_filter_term: self.quick_filter_term.clone(),
quick_filter_icon: self.quick_filter_icon.clone(),
show_quick_filter: self.show_quick_filter,
}
}
pub fn apply_syncable(&mut self, sync: SyncableConfig) {
self.tag_aliases = sync.tag_aliases;
self.hide_completed = sync.hide_completed;
self.hide_fully_completed_tags = sync.hide_fully_completed_tags;
self.hide_aliases_in_sidebar = sync.hide_aliases_in_sidebar;
self.sort_cutoff_months = sync.sort_cutoff_months;
self.sort_standard_by_priority = sync.sort_standard_by_priority;
self.sort_preset = sync.sort_preset;
self.urgent_days_horizon = sync.urgent_days_horizon;
self.urgent_priority_threshold = sync.urgent_priority_threshold;
self.default_priority = sync.default_priority;
self.start_grace_period_days = sync.start_grace_period_days;
self.auto_reminders = sync.auto_reminders;
self.default_reminder_time = sync.default_reminder_time;
self.snooze_short_mins = sync.snooze_short_mins;
self.snooze_long_mins = sync.snooze_long_mins;
self.create_events_for_tasks = sync.create_events_for_tasks;
self.delete_events_on_completion = sync.delete_events_on_completion;
self.trash_retention_days = sync.trash_retention_days;
self.max_done_roots = sync.max_done_roots;
self.max_done_subtasks = sync.max_done_subtasks;
self.show_ongoing_notifications = sync.show_ongoing_notifications;
self.show_priority_numbers = sync.show_priority_numbers;
self.pinned_actions = sync.pinned_actions;
self.quick_filter_term = sync.quick_filter_term;
self.quick_filter_icon = sync.quick_filter_icon;
self.show_quick_filter = sync.show_quick_filter;
}
pub fn update_sync_timestamp_if_changed(&mut self, old: &Config) {
if self.get_syncable() != old.get_syncable() {
self.settings_updated_at = chrono::Utc::now().timestamp();
}
}
pub fn load(ctx: &dyn AppContext) -> Result<Self> {
let path = ctx.get_config_file_path()?;
if !path.exists() {
return Err(anyhow::anyhow!("Config file not found"));
}
let contents = fs::read_to_string(&path).map_err(|e| {
anyhow::anyhow!("Failed to read config file '{}': {}", path.display(), e)
})?;
let mut config: Config = toml::from_str(&contents).map_err(|e| {
anyhow::anyhow!("Failed to parse config file '{}': {}", path.display(), e)
})?;
if config.config_version == 0 {
if !config.pinned_actions.contains(&TaskAction::OpenCoordinates) {
config.pinned_actions.insert(0, TaskAction::OpenCoordinates);
}
if !config.pinned_actions.contains(&TaskAction::OpenUrl) {
config.pinned_actions.insert(0, TaskAction::OpenUrl);
}
config.config_version = 1;
}
Ok(config)
}
pub fn load_with_credentials(ctx: &dyn AppContext) -> Result<Self> {
let mut config = Self::load(ctx)?;
let user_key = if config.username.is_empty() {
"default"
} else {
&config.username
};
match keyring_core::Entry::new("cfait", user_key) {
Ok(entry) => {
if !config.password.is_empty() {
if let Err(err) = entry.set_password(&config.password) {
log::warn!(
"Failed to migrate password into keyring for user '{}': {}",
user_key,
err
);
}
} else {
match entry.get_password() {
Ok(pw) => {
config.password = pw;
}
Err(keyring_core::Error::NoEntry) => {}
Err(err) => {
log::warn!(
"Failed to load password from keyring for user '{}': {}",
user_key,
err
);
}
}
}
}
Err(err) => {
log::warn!(
"Failed to initialize keyring entry for user '{}': {}",
user_key,
err
);
}
}
Ok(config)
}
pub fn is_missing_config_error(err: &Error) -> bool {
if err.to_string().contains("Config file not found") {
return true;
}
if let Some(io_err) = err.downcast_ref::<std::io::Error>()
&& io_err.kind() == std::io::ErrorKind::NotFound
{
return true;
}
for cause in err.chain() {
if let Some(io_err) = cause.downcast_ref::<std::io::Error>()
&& io_err.kind() == std::io::ErrorKind::NotFound
{
return true;
}
}
false
}
pub fn save(&self, ctx: &dyn AppContext) -> Result<()> {
let path = ctx.get_config_file_path()?;
LocalStorage::with_lock(&path, || {
let toml_str = toml::to_string_pretty(self)?;
let documented_toml = Self::inject_documentation(&toml_str);
LocalStorage::atomic_write(&path, documented_toml)?;
Ok(())
})?;
Ok(())
}
pub fn save_with_credentials(&self, ctx: &dyn AppContext) -> Result<()> {
let user_key = if self.username.is_empty() {
"default"
} else {
&self.username
};
match keyring_core::Entry::new("cfait", user_key) {
Ok(entry) => {
if !self.password.is_empty() {
if let Err(err) = entry.set_password(&self.password) {
log::warn!(
"Failed to save password to keyring for user '{}': {}",
user_key,
err
);
}
} else if let Err(err) = entry.delete_credential() {
if !matches!(err, keyring_core::Error::NoEntry) {
log::warn!(
"Failed to delete keyring credential for user '{}': {}",
user_key,
err
);
}
}
}
Err(err) => {
log::warn!(
"Failed to initialize keyring entry for user '{}': {}",
user_key,
err
);
}
}
self.save(ctx)
}
pub fn get_path_string(ctx: &dyn AppContext) -> Result<String> {
let path = ctx.get_config_file_path()?;
Ok(path.to_string_lossy().to_string())
}
fn inject_documentation(raw_toml: &str) -> String {
let mut out = String::with_capacity(raw_toml.len() + 2048);
out.push_str("# Cfait Configuration\n\n");
out.push_str("# --- Connection Settings ---\n");
for line in raw_toml.lines() {
let trimmed = line.trim();
if trimmed.starts_with("default_calendar =") {
out.push_str("\n# --- UI & Behavior ---\n");
} else if trimmed.starts_with("sort_cutoff_months =") {
out.push_str("\n# --- Sorting & Ranking Logic ---\n");
} else if trimmed.starts_with("auto_reminders =") {
out.push_str("\n# --- Notifications & Reminders ---\n");
} else if trimmed.starts_with("create_events_for_tasks =") {
out.push_str("\n# --- Calendar Integration (VEVENT Sync) ---\n");
} else if trimmed.starts_with("max_done_roots =") {
out.push_str("\n# --- Advanced Settings ---\n");
} else if trimmed.starts_with("[tag_aliases]") {
out.push_str("\n# --- Aliases (Global Templates) ---\n");
out.push_str("# Map shortcuts to sets of tags/locations/priorities.\n");
out.push_str("# Example: \"#gardening\" = [\"#fun\", \"@@home\"]\n");
}
if trimmed.starts_with("url =") {
out.push_str("# URL: The full address to your CalDAV server endpoint.\n");
out.push_str(line);
} else if trimmed.starts_with("allow_insecure_certs =") {
out.push_str(line);
out.push_str(
" # Boolean: Set true to bypass SSL verification (e.g. self-signed certs).",
);
} else if trimmed.starts_with("sync_settings =") {
out.push_str(line);
out.push_str(
" # Boolean: If true, sync settings and aliases as a hidden task on the server.",
);
} else if trimmed.starts_with("disabled_calendars =") {
out.push_str("# List of calendar HREFs (strings) to completely disable/ignore.\n");
out.push_str(line);
} else if trimmed.starts_with("default_calendar =") {
out.push_str(line);
out.push_str(
" # String: Target for new tasks. HREF or 'local://default' when local mode is enabled.",
);
} else if trimmed.starts_with("enable_local_mode =") {
out.push_str(line);
out.push_str(
" # Boolean: If false, TUI local/offline calendars are hidden and new tasks target remote calendars only.",
);
} else if trimmed.starts_with("hide_completed =") {
out.push_str(line);
out.push_str(" # Boolean: If true, Completed/Cancelled tasks are hidden globally.");
} else if trimmed.starts_with("strikethrough_completed =") {
out.push_str(line);
out.push_str(" # Boolean: Apply strikethrough styling to completed task titles.");
} else if trimmed.starts_with("hide_fully_completed_tags =") {
out.push_str(line);
out.push_str(" # Boolean: Hide tags in sidebar if all their tasks are completed.");
} else if trimmed.starts_with("hide_aliases_in_sidebar =") {
out.push_str(line);
out.push_str(" # Boolean: Hide shorthand aliases from the sidebar (showing only their targets).");
} else if trimmed.starts_with("ui_scale =") {
out.push_str(line);
out.push_str(
" # Float: Global UI scale factor (0.5–3.0). Ctrl+/Ctrl-/scroll to change.",
);
} else if trimmed.starts_with("theme =") {
out.push_str(line);
out.push_str(" # String: App Theme (RustyDark, Light, Dark, etc). In the TUI, light themes adapt text contrast for light terminal backgrounds.");
} else if trimmed.starts_with("sort_cutoff_months =") {
out.push_str(line);
out.push_str(
" # Integer/None: Tasks due beyond this many months are ranked lower.",
);
} else if trimmed.starts_with("urgent_days_horizon =") {
out.push_str(line);
out.push_str(
" # Integer: Tasks due within this many days are considered 'Urgent'.",
);
} else if trimmed.starts_with("urgent_priority_threshold =") {
out.push_str(line);
out.push_str(" # Integer (1-9): Priorities <= this value are 'Urgent'. (1=High)");
} else if trimmed.starts_with("default_priority =") {
out.push_str(line);
out.push_str(" # Integer (1-9): Default priority for new tasks. 0 maps to this.");
} else if trimmed.starts_with("start_grace_period_days =") {
out.push_str(line);
out.push_str(
" # Integer: Future tasks appear in the list this many days before start.",
);
} else if trimmed.starts_with("auto_reminders =") {
out.push_str(line);
out.push_str(
" # Boolean: Auto-remind on Due/Start dates if no explicit alarms exist.",
);
} else if trimmed.starts_with("default_reminder_time =") {
out.push_str(line);
out.push_str(" # String (HH:MM): Default time for date-only auto-reminders.");
} else if trimmed.starts_with("snooze_short_mins =") {
out.push_str(line);
out.push_str(" # Integer: Minutes for the 'Short Snooze' button.");
} else if trimmed.starts_with("snooze_long_mins =") {
out.push_str(line);
out.push_str(" # Integer: Minutes for the 'Long Snooze' button.");
} else if trimmed.starts_with("auto_refresh_interval_mins =") {
out.push_str(line);
out.push_str(" # Integer: Background sync interval in minutes. 0 to disable.");
} else if trimmed.starts_with("create_events_for_tasks =") {
out.push_str(line);
out.push_str(
" # Boolean: Create companion VEVENTs on server for tasks with dates.",
);
} else if trimmed.starts_with("delete_events_on_completion =") {
out.push_str(line);
out.push_str(" # Boolean: Remove the companion VEVENT when task is completed.");
} else if trimmed.starts_with("max_done_roots =") {
out.push_str(line);
out.push_str(
" # Integer: Limit completed root tasks shown before 'Expand' button.",
);
} else if trimmed.starts_with("max_done_subtasks =") {
out.push_str(line);
out.push_str(
" # Integer: Limit completed subtasks shown in a parent before 'Expand'.",
);
} else if trimmed.starts_with("show_ongoing_notifications =") {
out.push_str(line);
out.push_str(" # Boolean: Display ongoing timer notification for active tasks.");
} else if trimmed.starts_with("quick_filter_term =") {
out.push_str(line);
out.push_str(" # String: The search term toggled by the quick filter button.");
} else if trimmed.starts_with("quick_filter_icon =") {
out.push_str(line);
out.push_str(" # String: Hex code or character for the quick filter button icon.");
} else if trimmed.starts_with("show_quick_filter =") {
out.push_str(line);
out.push_str(" # Boolean: Display the quick filter button in the search bar.");
} else if trimmed.starts_with("sidebar_is_hidden =") {
out.push_str(line);
out.push_str(" # Boolean: Hide the left sidebar collections panel.");
} else if trimmed.starts_with("description_editor =") {
out.push_str(line);
out.push_str(" # String: Editor for task descriptions. Empty uses $VISUAL/$EDITOR. 'builtin' forces internal editor.");
} else if trimmed.starts_with("show_priority_numbers =") {
out.push_str(line);
out.push_str(" # Boolean: Render priority numbers (!X) visually next to tags.");
} else if trimmed.starts_with("hidden_calendars =") {
out.push_str("# List of calendar HREFs currently toggled 'off' in the sidebar.\n");
out.push_str(line);
} else if trimmed.starts_with("expanded_tags =") {
out.push_str("\n# --- UI Memory State ---\n");
out.push_str("# Arrays remembering which tree folders are currently expanded.\n");
out.push_str(line);
} else if trimmed.starts_with("trash_retention_days =") {
out.push_str(line);
out.push_str(" # Integer: Days to keep deleted items in local trash before permanent delete. 0 disables trash.");
} else if trimmed.starts_with("log_level =") {
out.push_str(line);
out.push_str(" # String: Logging verbosity level (Error, Warn, Info, Debug, Trace). Applies to both log file and terminal.");
} else if trimmed.starts_with("config_version =") {
out.push_str(
"# Internal version for configuration migrations. Do not edit manually.",
);
out.push_str(line);
} else {
out.push_str(line);
}
out.push('\n');
}
out
}
}