Skip to main content

mps/commands/
mod.rs

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