Skip to main content

mps/commands/
mod.rs

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