use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Priority {
Low,
Medium,
High,
}
impl Priority {
pub fn label(&self) -> &'static str {
match self {
Priority::Low => "Low",
Priority::Medium => "Med",
Priority::High => "High",
}
}
pub fn rank(&self) -> u8 {
match self {
Priority::High => 3,
Priority::Medium => 2,
Priority::Low => 1,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum TaskStatus {
Pending,
InProgress,
Done,
}
impl TaskStatus {
pub fn label(&self) -> &'static str {
match self {
TaskStatus::Pending => "Pending",
TaskStatus::InProgress => "In Progress",
TaskStatus::Done => "Done",
}
}
pub fn short_label(&self) -> &'static str {
match self {
TaskStatus::Pending => "Todo",
TaskStatus::InProgress => "Active",
TaskStatus::Done => "Done",
}
}
pub fn icon(&self) -> &'static str {
match self {
TaskStatus::Pending => "○",
TaskStatus::InProgress => "◉",
TaskStatus::Done => "✓",
}
}
pub fn bracket_marker(&self) -> &'static str {
match self {
TaskStatus::Pending => "[ ]",
TaskStatus::InProgress => "[~]",
TaskStatus::Done => "[x]",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Task {
pub id: u64,
pub title: String,
pub notes: String,
pub priority: Priority,
pub status: TaskStatus,
pub estimated_minutes: u32,
pub created_at: DateTime<Utc>,
pub completed_at: Option<DateTime<Utc>>,
pub actual_minutes: u32,
pub sessions: u32,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub due_date: Option<String>,
#[serde(default)]
pub today: bool,
#[serde(default)]
pub sort_order: u32,
}
impl Task {
pub fn new(id: u64, title: String) -> Self {
Self {
id,
title,
notes: String::new(),
priority: Priority::Medium,
status: TaskStatus::Pending,
estimated_minutes: 25,
created_at: Utc::now(),
completed_at: None,
actual_minutes: 0,
sessions: 0,
tags: Vec::new(),
due_date: None,
today: false,
sort_order: id as u32,
}
}
pub fn is_overdue(&self) -> bool {
if self.status == TaskStatus::Done {
return false;
}
let Some(ref due) = self.due_date else {
return false;
};
let today = chrono::Local::now().format("%Y-%m-%d").to_string();
due.as_str() < today.as_str()
}
pub fn progress_ratio(&self) -> f64 {
if self.estimated_minutes == 0 {
return 0.0;
}
(self.actual_minutes as f64 / self.estimated_minutes as f64).clamp(0.0, 1.0)
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum TimerState {
Idle,
Running,
Paused,
Finished,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum TimerMode {
Focus,
ShortBreak,
LongBreak,
Custom,
}
impl TimerMode {
pub fn label(&self) -> &'static str {
match self {
TimerMode::Focus => "FOCUS",
TimerMode::ShortBreak => "SHORT BREAK",
TimerMode::LongBreak => "LONG BREAK",
TimerMode::Custom => "CUSTOM",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FocusSessionRecord {
pub date: String,
pub minutes: u32,
pub task_id: Option<u64>,
pub mode: TimerMode,
pub completed_at: DateTime<Utc>,
}
#[derive(Debug, Clone)]
pub struct StoredSession {
pub id: i64,
pub record: FocusSessionRecord,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "kebab-case")]
pub enum EmptyQueueBehavior {
#[default]
FreeFocus,
PauseTimer,
AskEachTime,
}
impl EmptyQueueBehavior {
pub fn label(&self) -> &'static str {
match self {
EmptyQueueBehavior::FreeFocus => "Free focus",
EmptyQueueBehavior::PauseTimer => "Pause timer",
EmptyQueueBehavior::AskEachTime => "Ask each time",
}
}
pub fn next(self) -> Self {
match self {
EmptyQueueBehavior::FreeFocus => EmptyQueueBehavior::PauseTimer,
EmptyQueueBehavior::PauseTimer => EmptyQueueBehavior::AskEachTime,
EmptyQueueBehavior::AskEachTime => EmptyQueueBehavior::FreeFocus,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "kebab-case")]
pub enum EstimateCompleteBehavior {
#[default]
Nudge,
None,
AutoDone,
}
impl EstimateCompleteBehavior {
pub fn label(&self) -> &'static str {
match self {
EstimateCompleteBehavior::Nudge => "Nudge",
EstimateCompleteBehavior::None => "Off",
EstimateCompleteBehavior::AutoDone => "Auto-done",
}
}
pub fn next(self) -> Self {
match self {
EstimateCompleteBehavior::Nudge => EstimateCompleteBehavior::None,
EstimateCompleteBehavior::None => EstimateCompleteBehavior::AutoDone,
EstimateCompleteBehavior::AutoDone => EstimateCompleteBehavior::Nudge,
}
}
}
fn default_focus_minutes() -> u32 {
25
}
fn default_short_break() -> u32 {
5
}
fn default_long_break() -> u32 {
15
}
fn default_long_every() -> u32 {
4
}
fn default_true() -> bool {
true
}
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum ThemeVariant {
Dark,
Light,
Polaris,
#[default]
Matrix,
}
impl ThemeVariant {
pub fn label(&self) -> &'static str {
match self {
ThemeVariant::Dark => "Dark",
ThemeVariant::Light => "Light",
ThemeVariant::Polaris => "Polaris",
ThemeVariant::Matrix => "Matrix",
}
}
pub fn next(self) -> Self {
match self {
ThemeVariant::Dark => ThemeVariant::Light,
ThemeVariant::Light => ThemeVariant::Polaris,
ThemeVariant::Polaris => ThemeVariant::Matrix,
ThemeVariant::Matrix => ThemeVariant::Dark,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppData {
pub tasks: Vec<Task>,
pub total_focus_minutes: u32,
pub total_sessions: u32,
pub streak_days: u32,
pub last_session_date: Option<String>,
pub daily_goal_minutes: u32,
pub sound_enabled: bool,
pub auto_start_breaks: bool,
pub auto_start_focus: bool,
pub next_id: u64,
#[serde(default)]
pub today_focus_minutes: u32,
#[serde(default)]
pub today_date: Option<String>,
#[serde(default = "default_focus_minutes")]
pub focus_minutes: u32,
#[serde(default = "default_short_break")]
pub short_break_minutes: u32,
#[serde(default = "default_long_break")]
pub long_break_minutes: u32,
#[serde(default = "default_long_every")]
pub long_break_every: u32,
#[serde(default)]
pub session_history: Vec<FocusSessionRecord>,
#[serde(default = "default_true")]
pub auto_pick_task: bool,
#[serde(default)]
pub auto_advance_task: bool,
#[serde(default)]
pub theme: ThemeVariant,
#[serde(default)]
pub active_task_id: Option<u64>,
#[serde(default = "default_true")]
pub notify_on_finish: bool,
#[serde(default)]
pub goal_streak_days: u32,
#[serde(default)]
pub last_goal_date: Option<String>,
#[serde(default)]
pub empty_queue_behavior: EmptyQueueBehavior,
#[serde(default)]
pub log_breaks: bool,
#[serde(default)]
pub estimate_complete: EstimateCompleteBehavior,
}
impl Default for AppData {
fn default() -> Self {
Self {
tasks: Vec::new(),
total_focus_minutes: 0,
total_sessions: 0,
streak_days: 0,
last_session_date: None,
daily_goal_minutes: 120,
sound_enabled: true,
auto_start_breaks: false,
auto_start_focus: false,
next_id: 1,
today_focus_minutes: 0,
today_date: None,
focus_minutes: 25,
short_break_minutes: 5,
long_break_minutes: 15,
long_break_every: 4,
session_history: Vec::new(),
auto_pick_task: true,
auto_advance_task: false,
theme: ThemeVariant::Matrix,
active_task_id: None,
notify_on_finish: true,
goal_streak_days: 0,
last_goal_date: None,
empty_queue_behavior: EmptyQueueBehavior::FreeFocus,
log_breaks: false,
estimate_complete: EstimateCompleteBehavior::Nudge,
}
}
}