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