1pub mod append;
2pub mod serve;
3pub mod config_cmd;
4pub mod daemon;
5pub mod delete;
6pub mod edit;
7pub mod export;
8pub mod git;
9pub mod list;
10pub mod meta_cmd;
11pub mod notify;
12pub mod open;
13pub mod search;
14pub mod stats;
15pub mod tags;
16pub mod update;
17
18use crate::elements::{Element, ElementKind};
19use crate::ref_resolver::RefResolver;
20use colored::Colorize;
21use indexmap::IndexMap;
22
23pub fn type_badge(kind: &ElementKind) -> String {
26 match kind {
27 ElementKind::Task => "[task]".green().bold().to_string(),
28 ElementKind::Note => "[note]".cyan().bold().to_string(),
29 ElementKind::Log => "[log]".yellow().bold().to_string(),
30 ElementKind::Reminder => "[reminder]".magenta().bold().to_string(),
31 ElementKind::MpsGroup => "[@mps]".white().to_string(),
32 ElementKind::Character => "[character]".blue().bold().to_string(),
33 ElementKind::Unknown => "[unknown]".white().to_string(),
34 }
35}
36
37pub fn element_extra(el: &Element) -> String {
39 match el {
40 Element::Task { data, .. } => {
41 let s = data.status_str();
42 let colored = if data.is_done() {
43 s.green().to_string()
44 } else {
45 s.yellow().to_string()
46 };
47 format!("({}) ", colored)
48 }
49 Element::Log { data, .. } => data
50 .duration_str()
51 .map(|d| format!("({}) ", d.yellow()))
52 .unwrap_or_default(),
53 Element::Reminder { data, .. } => data
54 .at
55 .as_deref()
56 .map(|t| format!("({}) ", t.magenta()))
57 .unwrap_or_default(),
58 Element::Character { data, .. } => data
59 .name
60 .as_deref()
61 .map(|n| format!("({}) ", n.bold()))
62 .unwrap_or_default(),
63 _ => String::new(),
64 }
65}
66
67pub fn tags_str(tags: &[String]) -> String {
68 if tags.is_empty() {
69 String::new()
70 } else {
71 format!(" {}", format!("[{}]", tags.join(", ")).white())
72 }
73}
74
75pub struct DisplayOpts {
77 pub type_filter: Option<String>,
78 pub tag_filter: Option<String>,
79 pub status_filter: Option<String>,
80 pub name_filter: Option<String>,
81}
82
83pub fn visible(el: &Element, opts: &DisplayOpts) -> bool {
84 if el.is_unknown() {
85 return false;
86 }
87 if let Some(ref tf) = opts.type_filter {
88 if el.sign() != tf {
89 return false;
90 }
91 }
92 if let Some(ref tag) = opts.tag_filter {
93 if !el.tags().iter().any(|t| t == tag) {
94 return false;
95 }
96 }
97 if let Some(ref sf) = opts.status_filter {
98 match el {
100 Element::Task { data, .. } => {
101 if data.status_str() != sf {
102 return false;
103 }
104 }
105 _ => return false,
106 }
107 }
108 if let Some(ref nf) = opts.name_filter {
109 match el {
111 Element::Character { data, .. } => {
112 if data.name.as_deref().map(|n| n.to_lowercase()) != Some(nf.to_lowercase()) {
113 return false;
114 }
115 }
116 _ => return false,
117 }
118 }
119 true
120}
121
122pub fn print_element(el: &Element, depth: usize, ref_str: Option<&str>) {
124 let indent = " ".repeat(depth + 1);
125 let badge = type_badge(&el.kind());
126 let extra = element_extra(el);
127 let body_line = el.body_str().trim().lines().next().unwrap_or("").trim();
128 let tags = tags_str(el.tags());
129
130 if let Some(r) = ref_str {
131 print!("{}{} ", indent, format!("{:<12}", r).white());
132 } else {
133 print!("{}", indent);
134 }
135 println!("{} {}{}{}", badge, extra, body_line, tags);
136}
137
138pub fn print_tree(
146 elements: &IndexMap<String, Element>,
147 opts: &DisplayOpts,
148 resolver: Option<&RefResolver>,
149 show_refs: bool,
150) -> usize {
151 if elements.is_empty() {
152 return 0;
153 }
154
155 let mut sorted: Vec<(&String, &Element)> = elements.iter().collect();
156 sorted.sort_by(|(a, _), (b, _)| {
157 let a_parts: Vec<u64> = a.split('.').filter_map(|s| s.parse().ok()).collect();
158 let b_parts: Vec<u64> = b.split('.').filter_map(|s| s.parse().ok()).collect();
159 a_parts.cmp(&b_parts)
160 });
161
162 let root_depth = sorted
163 .first()
164 .map(|(k, _)| k.split('.').count())
165 .unwrap_or(1);
166 let mut shown = 0usize;
167
168 for (ref_key, el) in &sorted {
169 let depth = ref_key.split('.').count();
170 if depth <= root_depth {
171 continue;
172 }
173 let render_depth = depth - root_depth - 1;
174
175 let human_ref: String = resolver
177 .and_then(|r| r.to_human(ref_key))
178 .map(|s| s.to_string())
179 .unwrap_or_else(|| ref_key.to_string());
180
181 if el.is_mps_group() {
182 let prefix = format!("{}.", ref_key);
183 let any_visible = sorted
184 .iter()
185 .any(|(k, v)| k.starts_with(&prefix) && !v.is_mps_group() && visible(v, opts));
186 if !any_visible {
187 continue;
188 }
189
190 let indent = " ".repeat(render_depth + 1);
191 if show_refs {
192 print!("{}{} ", indent, format!("{:<12}", &human_ref).white());
193 } else {
194 print!("{}", indent);
195 }
196 println!("{}", "[@mps]".white());
197 } else {
198 if !visible(el, opts) {
199 continue;
200 }
201 let ref_str = if show_refs {
202 Some(human_ref.as_str())
203 } else {
204 None
205 };
206 print_element(el, render_depth, ref_str);
207 shown += 1;
208 }
209 }
210
211 shown
212}