Skip to main content

mps/commands/
mod.rs

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
23// ── Display helpers ────────────────────────────────────────────────────────────
24
25pub 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
37/// Extra info shown between badge and body: status for tasks, duration for logs, at for reminders.
38pub 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
75/// Visibility filter: type, tag, and status (status excludes non-task elements).
76pub 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        // --status only applies to tasks; all other types are excluded
99        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        // --name only applies to characters; all other types are excluded
110        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
122/// Print one element line. If `ref_str` is Some, it is shown left-justified before the badge.
123pub 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
138/// Renders elements_hash as indented tree, ordered by ref-path.
139/// The synthetic root @mps wrapper is skipped (depth ≤ root_depth).
140/// @mps group headers are shown only when they have visible children.
141/// Returns count of non-MpsGroup elements printed.
142///
143/// `resolver` enables human-readable refs in the display (pass None to suppress).
144/// `show_refs` controls whether the ref column is printed.
145pub 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        // Resolve human ref (falls back to epoch ref if resolver absent or key unmapped)
176        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}