use crate::model::item::{Task, TaskStatus};
use chrono::Utc;
pub fn random_session_example() -> String {
const DURATIONS: &[&str] = &["30m", "1h", "2h", "6h", "14:00-15:30", "09:00-10:15"];
DURATIONS[fastrand::usize(..DURATIONS.len())].to_string()
}
pub trait TaskDisplay {
fn to_smart_string(&self) -> String;
fn format_duration_short(&self, store: Option<&crate::store::TaskStore>) -> String;
fn checkbox_symbol(&self) -> &'static str;
fn is_paused(&self) -> bool;
}
pub fn random_related_icon(uid1: &str, uid2: &str) -> char {
let (first, second) = if uid1 < uid2 {
(uid1, uid2)
} else {
(uid2, uid1)
};
let hash: u32 = first
.bytes()
.chain(second.bytes())
.fold(0u32, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u32));
match hash % 3 {
0 => '\u{f0a5a}',
1 => '\u{f0a5e}',
_ => '\u{f02e8}',
}
}
impl TaskDisplay for Task {
fn is_paused(&self) -> bool {
self.status == TaskStatus::NeedsAction
&& ((self.percent_complete.unwrap_or(0) > 0
&& self.percent_complete.unwrap_or(0) < 100)
|| self.time_spent_seconds > 0
|| !self.sessions.is_empty())
}
fn checkbox_symbol(&self) -> &'static str {
if self.is_paused() {
return "[‖]";
}
match self.status {
TaskStatus::Completed => "[✔]",
TaskStatus::Cancelled => "[✘]",
TaskStatus::InProcess => "[▶]",
TaskStatus::NeedsAction => "[ ]",
}
}
fn format_duration_short(&self, store: Option<&crate::store::TaskStore>) -> String {
if let Some(goal) = &self.goal {
let current = if let Some(s) = store {
s.calculate_goal_progress(&format!("task:{}", self.uid), goal)
} else {
0 };
let (c_str, t_str) = crate::model::parser::format_goal_duration(current, goal.target);
return format!("[ {} / {}/{}]", c_str, t_str, goal.interval.format_short());
}
let now_ts = Utc::now().timestamp();
let total_seconds = if let Some(s) = store {
s.get_aggregated_time_seconds(&self.uid)
} else {
let current_session = self
.last_started_at
.map(|start| (now_ts - start).max(0) as u64)
.unwrap_or(0);
self.time_spent_seconds + current_session
};
let total_mins = (total_seconds / 60) as u32;
let combined_time_str = if let Some(min) = self.estimated_duration {
let max = self.estimated_duration_max.unwrap_or(min).max(min);
if total_mins > 0 || self.last_started_at.is_some() {
let (c_str, max_str) = crate::model::parser::format_goal_duration(total_mins, max);
let est_display = if max > min {
let (_, min_str) = crate::model::parser::format_goal_duration(total_mins, min);
format!("~{}-{}", min_str, max_str)
} else {
format!("~{}", max_str)
};
format!("{} / {}", c_str, est_display)
} else {
if max > min {
format!(
"~{}-{}",
crate::model::parser::format_duration_compact(min),
crate::model::parser::format_duration_compact(max)
)
} else {
format!("~{}", crate::model::parser::format_duration_compact(min))
}
}
} else {
if total_mins > 0 || self.last_started_at.is_some() {
crate::model::parser::format_duration_compact(total_mins)
} else {
String::new()
}
};
let pc_str = if !self.status.is_done() && self.percent_complete.unwrap_or(0) > 0 {
if let Some(pc) = self.percent_complete {
format!("{}%", pc)
} else {
String::new()
}
} else {
String::new()
};
if !pc_str.is_empty() && !combined_time_str.is_empty() {
format!("[{}] | {}", pc_str, combined_time_str)
} else if !pc_str.is_empty() {
format!("[{}]", pc_str)
} else if !combined_time_str.is_empty() {
format!("[{}]", combined_time_str)
} else {
String::new()
}
}
fn to_smart_string(&self) -> String {
use crate::model::item::AlarmTrigger;
use chrono::{Duration, Local};
let mut s = if self.is_journal {
format!(
"[[{}]]",
crate::model::parser::escape_summary(&self.summary)
)
} else {
crate::model::parser::escape_summary(&self.summary)
};
if self.is_note && !self.is_journal {
if s.is_empty() {
s = "-".to_string();
} else {
s = format!("- {}", s);
}
}
if self.priority > 0 {
s.push_str(&format!(" !{}", self.priority));
}
for loc in &self.locations {
s.push_str(&format!(" @@{}", crate::model::parser::quote_value(loc)));
}
if let Some(u) = &self.url {
s.push_str(&format!(" url:{}", crate::model::parser::quote_value(u)));
}
if let Some(g) = &self.geo {
s.push_str(&format!(" geo:{}", crate::model::parser::quote_value(g)));
}
if let Some(min) = self.estimated_duration {
let fmt_val = |m: u32| -> String {
if m.is_multiple_of(525600) {
format!("{}y", m / 525600)
} else if m.is_multiple_of(43200) {
format!("{}mo", m / 43200)
} else if m.is_multiple_of(10080) {
format!("{}w", m / 10080)
} else if m.is_multiple_of(1440) {
format!("{}d", m / 1440)
} else if m.is_multiple_of(60) {
format!("{}h", m / 60)
} else {
format!("{}m", m)
}
};
if let Some(max) = self.estimated_duration_max {
if max > min {
s.push_str(&format!(" ~{}-{}", fmt_val(min), fmt_val(max)));
} else {
s.push_str(&format!(" ~{}", fmt_val(min)));
}
} else {
s.push_str(&format!(" ~{}", fmt_val(min)));
}
}
if let Some(r) = &self.rrule {
let is_relative = self
.unmapped_properties
.iter()
.any(|p| p.key == "X-CFAIT-RECUR-FROM-COMPLETION");
let pretty = crate::model::parser::prettify_recurrence(r, is_relative);
s.push_str(&format!(" {}", pretty));
}
for ex in &self.exdates {
s.push_str(&format!(" except {}", ex.format_smart()));
}
for alarm in &self.alarms {
if alarm.is_snooze() || alarm.acknowledged.is_some() {
continue;
}
match alarm.trigger {
AlarmTrigger::Relative(offset) => {
let mins = -offset;
if mins > 0 {
if mins % 10080 == 0 {
s.push_str(&format!(" rem:{}w", mins / 10080));
} else if mins % 1440 == 0 {
s.push_str(&format!(" rem:{}d", mins / 1440));
} else if mins % 60 == 0 {
s.push_str(&format!(" rem:{}h", mins / 60));
} else {
s.push_str(&format!(" rem:{}m", mins));
}
} else {
s.push_str(&format!(" rem:{}m", mins));
}
}
AlarmTrigger::Absolute(dt) => {
let local = dt.with_timezone(&Local);
let now = Local::now();
let task_date = self
.due
.as_ref()
.or(self.dtstart.as_ref())
.map(|d| d.to_date_naive());
if Some(local.date_naive()) == task_date
|| local.date_naive() == now.date_naive()
{
s.push_str(&format!(" rem:{}", local.format("%H:%M")));
} else if local.date_naive() == now.date_naive() + Duration::days(1) {
s.push_str(&format!(" rem:tomorrow {}", local.format("%H:%M")));
} else {
s.push_str(&format!(" rem:{}", local.format("%Y-%m-%d %H:%M")));
}
}
}
}
for cat in &self.categories {
s.push_str(&format!(" #{}", crate::model::parser::quote_value(cat)));
}
if let Some(create_event) = self.create_event {
s.push_str(if create_event { " +cal" } else { " -cal" });
}
if self.pinned {
s.push_str(" is:pinned");
}
if self.manual_block {
let block_str = rust_i18n::t!("search_is_blocked");
if block_str == "search_is_blocked" || block_str.is_empty() {
s.push_str(" is:blocked");
} else {
s.push_str(&format!(
" {}",
block_str.split(',').next().unwrap_or("is:blocked").trim()
));
}
}
if self.permanent {
let perm_str = rust_i18n::t!("parser_is_permanent");
if perm_str == "parser_is_permanent" || perm_str.is_empty() {
s.push_str(" is:permanent");
} else {
s.push_str(&format!(
" {}",
perm_str.split(',').next().unwrap_or("is:permanent").trim()
));
}
}
if let Some(goal) = &self.goal {
let type_str = if goal.goal_type == crate::config::GoalType::Duration {
crate::model::parser::format_duration_compact(goal.target)
} else {
goal.target.to_string()
};
s.push_str(&format!(
" goal:{}/{}",
type_str,
goal.interval.format_short()
));
}
if let (Some(start), Some(due)) = (&self.dtstart, &self.due) {
if start == due {
s.push_str(&format!(" ^@{}", start.format_smart()));
} else if let (
crate::model::DateType::Specific(s_dt),
crate::model::DateType::Specific(d_dt),
) = (start, due)
{
let s_loc = s_dt.with_timezone(&chrono::Local);
let d_loc = d_dt.with_timezone(&chrono::Local);
if s_loc.date_naive() == d_loc.date_naive() {
s.push_str(&format!(
" ^@{} {}-{}",
s_loc.format("%Y-%m-%d"),
s_loc.format("%H:%M"),
d_loc.format("%H:%M")
));
} else {
s.push_str(&format!(" ^{}", start.format_smart()));
s.push_str(&format!(" @{}", due.format_smart()));
}
} else {
s.push_str(&format!(" ^{}", start.format_smart()));
s.push_str(&format!(" @{}", due.format_smart()));
}
} else {
if let Some(start) = &self.dtstart {
s.push_str(&format!(" ^{}", start.format_smart()));
}
if let Some(d) = &self.due {
s.push_str(&format!(" @{}", d.format_smart()));
}
}
if let Some(comp) = self.completion_date() {
let local = comp.with_timezone(&chrono::Local);
s.push_str(&format!(" done:{}", local.format("%Y-%m-%d %H:%M")));
} else if let Some(pc) = self.percent_complete
&& pc > 0
{
s.push_str(&format!(" done:{}%", pc));
}
s
}
}