Skip to main content

mps/commands/
mod.rs

1pub mod open;
2pub mod list;
3pub mod append;
4pub mod edit;
5pub mod delete;
6pub mod search;
7pub mod stats;
8pub mod tags;
9pub mod export;
10pub mod git;
11pub mod update;
12pub mod config_cmd;
13pub mod notify;
14pub mod daemon;
15pub mod meta_cmd;
16
17use colored::Colorize;
18use indexmap::IndexMap;
19use crate::elements::{Element, ElementKind};
20use crate::ref_resolver::RefResolver;
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, .. } => {
49            data.duration_str()
50                .map(|d| format!("({}) ", d.yellow()))
51                .unwrap_or_default()
52        }
53        Element::Reminder { data, .. } => {
54            data.at.as_deref()
55                .map(|t| format!("({}) ", t.magenta()))
56                .unwrap_or_default()
57        }
58        Element::Character { data, .. } => {
59            data.name.as_deref()
60                .map(|n| format!("({}) ", n.bold()))
61                .unwrap_or_default()
62        }
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() { return false; }
85    if let Some(ref tf) = opts.type_filter {
86        if el.sign() != tf { return false; }
87    }
88    if let Some(ref tag) = opts.tag_filter {
89        if !el.tags().iter().any(|t| t == tag) { return false; }
90    }
91    if let Some(ref sf) = opts.status_filter {
92        // --status only applies to tasks; all other types are excluded
93        match el {
94            Element::Task { data, .. } => {
95                if data.status_str() != sf { return false; }
96            }
97            _ => return false,
98        }
99    }
100    if let Some(ref nf) = opts.name_filter {
101        // --name only applies to characters; all other types are excluded
102        match el {
103            Element::Character { data, .. } => {
104                if data.name.as_deref().map(|n| n.to_lowercase()) != Some(nf.to_lowercase()) {
105                    return false;
106                }
107            }
108            _ => return false,
109        }
110    }
111    true
112}
113
114/// Print one element line. If `ref_str` is Some, it is shown left-justified before the badge.
115pub fn print_element(el: &Element, depth: usize, ref_str: Option<&str>) {
116    let indent    = "  ".repeat(depth + 1);
117    let badge     = type_badge(&el.kind());
118    let extra     = element_extra(el);
119    let body_line = el.body_str().trim().lines().next().unwrap_or("").trim();
120    let tags      = tags_str(el.tags());
121
122    if let Some(r) = ref_str {
123        print!("{}{}  ", indent, format!("{:<12}", r).white());
124    } else {
125        print!("{}", indent);
126    }
127    println!("{} {}{}{}", badge, extra, body_line, tags);
128}
129
130/// Renders elements_hash as indented tree, ordered by ref-path.
131/// The synthetic root @mps wrapper is skipped (depth ≤ root_depth).
132/// @mps group headers are shown only when they have visible children.
133/// Returns count of non-MpsGroup elements printed.
134///
135/// `resolver` enables human-readable refs in the display (pass None to suppress).
136/// `show_refs` controls whether the ref column is printed.
137pub fn print_tree(
138    elements:  &IndexMap<String, Element>,
139    opts:      &DisplayOpts,
140    resolver:  Option<&RefResolver>,
141    show_refs: bool,
142) -> usize {
143    if elements.is_empty() { return 0; }
144
145    let mut sorted: Vec<(&String, &Element)> = elements.iter().collect();
146    sorted.sort_by(|(a, _), (b, _)| {
147        let a_parts: Vec<u64> = a.split('.').filter_map(|s| s.parse().ok()).collect();
148        let b_parts: Vec<u64> = b.split('.').filter_map(|s| s.parse().ok()).collect();
149        a_parts.cmp(&b_parts)
150    });
151
152    let root_depth = sorted.first().map(|(k, _)| k.split('.').count()).unwrap_or(1);
153    let mut shown = 0usize;
154
155    for (ref_key, el) in &sorted {
156        let depth = ref_key.split('.').count();
157        if depth <= root_depth { continue; }
158        let render_depth = depth - root_depth - 1;
159
160        // Resolve human ref (falls back to epoch ref if resolver absent or key unmapped)
161        let human_ref: String = resolver
162            .and_then(|r| r.to_human(ref_key))
163            .map(|s| s.to_string())
164            .unwrap_or_else(|| ref_key.to_string());
165
166        if el.is_mps_group() {
167            let prefix = format!("{}.", ref_key);
168            let any_visible = sorted.iter().any(|(k, v)| {
169                k.starts_with(&prefix) && !v.is_mps_group() && visible(v, opts)
170            });
171            if !any_visible { continue; }
172
173            let indent = "  ".repeat(render_depth + 1);
174            if show_refs {
175                print!("{}{}  ", indent, format!("{:<12}", &human_ref).white());
176            } else {
177                print!("{}", indent);
178            }
179            println!("{}", "[@mps]".white());
180        } else {
181            if !visible(el, opts) { continue; }
182            let ref_str = if show_refs { Some(human_ref.as_str()) } else { None };
183            print_element(el, render_depth, ref_str);
184            shown += 1;
185        }
186    }
187
188    shown
189}