Skip to main content

mps/commands/
mod.rs

1pub mod append;
2pub mod config_cmd;
3pub mod daemon;
4pub mod delete;
5pub mod edit;
6pub mod export;
7pub mod git;
8pub mod list;
9pub mod meta_cmd;
10pub mod notify;
11pub mod open;
12pub mod search;
13pub mod stats;
14pub mod tags;
15pub mod update;
16
17use crate::elements::{Element, ElementKind};
18use crate::ref_resolver::RefResolver;
19use colored::Colorize;
20use indexmap::IndexMap;
21
22// ── Display helpers ────────────────────────────────────────────────────────────
23
24pub fn type_badge(kind: &ElementKind) -> String {
25    match kind {
26        ElementKind::Task => "[task]".green().bold().to_string(),
27        ElementKind::Note => "[note]".cyan().bold().to_string(),
28        ElementKind::Log => "[log]".yellow().bold().to_string(),
29        ElementKind::Reminder => "[reminder]".magenta().bold().to_string(),
30        ElementKind::MpsGroup => "[@mps]".white().to_string(),
31        ElementKind::Character => "[character]".blue().bold().to_string(),
32        ElementKind::Unknown => "[unknown]".white().to_string(),
33    }
34}
35
36/// Extra info shown between badge and body: status for tasks, duration for logs, at for reminders.
37pub fn element_extra(el: &Element) -> String {
38    match el {
39        Element::Task { data, .. } => {
40            let s = data.status_str();
41            let colored = if data.is_done() {
42                s.green().to_string()
43            } else {
44                s.yellow().to_string()
45            };
46            format!("({}) ", colored)
47        }
48        Element::Log { data, .. } => data
49            .duration_str()
50            .map(|d| format!("({}) ", d.yellow()))
51            .unwrap_or_default(),
52        Element::Reminder { data, .. } => data
53            .at
54            .as_deref()
55            .map(|t| format!("({}) ", t.magenta()))
56            .unwrap_or_default(),
57        Element::Character { data, .. } => data
58            .name
59            .as_deref()
60            .map(|n| format!("({}) ", n.bold()))
61            .unwrap_or_default(),
62        _ => String::new(),
63    }
64}
65
66pub fn tags_str(tags: &[String]) -> String {
67    if tags.is_empty() {
68        String::new()
69    } else {
70        format!(" {}", format!("[{}]", tags.join(", ")).white())
71    }
72}
73
74/// Visibility filter: type, tag, and status (status excludes non-task elements).
75pub struct DisplayOpts {
76    pub type_filter: Option<String>,
77    pub tag_filter: Option<String>,
78    pub status_filter: Option<String>,
79    pub name_filter: Option<String>,
80}
81
82pub fn visible(el: &Element, opts: &DisplayOpts) -> bool {
83    if el.is_unknown() {
84        return false;
85    }
86    if let Some(ref tf) = opts.type_filter {
87        if el.sign() != tf {
88            return false;
89        }
90    }
91    if let Some(ref tag) = opts.tag_filter {
92        if !el.tags().iter().any(|t| t == tag) {
93            return false;
94        }
95    }
96    if let Some(ref sf) = opts.status_filter {
97        // --status only applies to tasks; all other types are excluded
98        match el {
99            Element::Task { data, .. } => {
100                if data.status_str() != sf {
101                    return false;
102                }
103            }
104            _ => return false,
105        }
106    }
107    if let Some(ref nf) = opts.name_filter {
108        // --name only applies to characters; all other types are excluded
109        match el {
110            Element::Character { data, .. } => {
111                if data.name.as_deref().map(|n| n.to_lowercase()) != Some(nf.to_lowercase()) {
112                    return false;
113                }
114            }
115            _ => return false,
116        }
117    }
118    true
119}
120
121/// Print one element line. If `ref_str` is Some, it is shown left-justified before the badge.
122pub fn print_element(el: &Element, depth: usize, ref_str: Option<&str>) {
123    let indent = "  ".repeat(depth + 1);
124    let badge = type_badge(&el.kind());
125    let extra = element_extra(el);
126    let body_line = el.body_str().trim().lines().next().unwrap_or("").trim();
127    let tags = tags_str(el.tags());
128
129    if let Some(r) = ref_str {
130        print!("{}{}  ", indent, format!("{:<12}", r).white());
131    } else {
132        print!("{}", indent);
133    }
134    println!("{} {}{}{}", badge, extra, body_line, tags);
135}
136
137/// Renders elements_hash as indented tree, ordered by ref-path.
138/// The synthetic root @mps wrapper is skipped (depth ≤ root_depth).
139/// @mps group headers are shown only when they have visible children.
140/// Returns count of non-MpsGroup elements printed.
141///
142/// `resolver` enables human-readable refs in the display (pass None to suppress).
143/// `show_refs` controls whether the ref column is printed.
144pub fn print_tree(
145    elements: &IndexMap<String, Element>,
146    opts: &DisplayOpts,
147    resolver: Option<&RefResolver>,
148    show_refs: bool,
149) -> usize {
150    if elements.is_empty() {
151        return 0;
152    }
153
154    let mut sorted: Vec<(&String, &Element)> = elements.iter().collect();
155    sorted.sort_by(|(a, _), (b, _)| {
156        let a_parts: Vec<u64> = a.split('.').filter_map(|s| s.parse().ok()).collect();
157        let b_parts: Vec<u64> = b.split('.').filter_map(|s| s.parse().ok()).collect();
158        a_parts.cmp(&b_parts)
159    });
160
161    let root_depth = sorted
162        .first()
163        .map(|(k, _)| k.split('.').count())
164        .unwrap_or(1);
165    let mut shown = 0usize;
166
167    for (ref_key, el) in &sorted {
168        let depth = ref_key.split('.').count();
169        if depth <= root_depth {
170            continue;
171        }
172        let render_depth = depth - root_depth - 1;
173
174        // Resolve human ref (falls back to epoch ref if resolver absent or key unmapped)
175        let human_ref: String = resolver
176            .and_then(|r| r.to_human(ref_key))
177            .map(|s| s.to_string())
178            .unwrap_or_else(|| ref_key.to_string());
179
180        if el.is_mps_group() {
181            let prefix = format!("{}.", ref_key);
182            let any_visible = sorted
183                .iter()
184                .any(|(k, v)| k.starts_with(&prefix) && !v.is_mps_group() && visible(v, opts));
185            if !any_visible {
186                continue;
187            }
188
189            let indent = "  ".repeat(render_depth + 1);
190            if show_refs {
191                print!("{}{}  ", indent, format!("{:<12}", &human_ref).white());
192            } else {
193                print!("{}", indent);
194            }
195            println!("{}", "[@mps]".white());
196        } else {
197            if !visible(el, opts) {
198                continue;
199            }
200            let ref_str = if show_refs {
201                Some(human_ref.as_str())
202            } else {
203                None
204            };
205            print_element(el, render_depth, ref_str);
206            shown += 1;
207        }
208    }
209
210    shown
211}