Skip to main content

track_core/
terminal_ui.rs

1use std::env;
2use std::io::{self, IsTerminal};
3
4use owo_colors::OwoColorize;
5
6#[derive(Clone, Copy)]
7pub enum SummaryTone {
8    Success,
9    Info,
10}
11
12#[derive(Clone, Copy)]
13pub enum ValueTone {
14    Plain,
15    Path,
16    PriorityHigh,
17    PriorityMedium,
18    PriorityLow,
19    StatusOpen,
20    StatusClosed,
21}
22
23pub fn format_summary(
24    title: &str,
25    tone: SummaryTone,
26    rows: &[(&str, String, ValueTone)],
27) -> String {
28    let mut lines = vec![style_title(title, tone)];
29    let label_width = rows
30        .iter()
31        .map(|(label, _, _)| label.len())
32        .max()
33        .unwrap_or(0);
34
35    for (label, value, value_tone) in rows {
36        lines.push(format_summary_row(label, value, *value_tone, label_width));
37    }
38
39    lines.join("\n")
40}
41
42pub fn format_note(label: &str, value: &str) -> String {
43    let raw_label = format!("{label:<8}");
44    if should_color() {
45        format!("  {}  {}", raw_label.bold().blue(), value.dimmed())
46    } else {
47        format!("  {raw_label}  {value}")
48    }
49}
50
51pub fn format_prompt_label(label: &str, default_value: Option<&str>) -> String {
52    match default_value.filter(|value| !value.trim().is_empty()) {
53        Some(default_value) => format!("{label} [{default_value}]"),
54        None => label.to_owned(),
55    }
56}
57
58fn format_summary_row(
59    label: &str,
60    value: &str,
61    value_tone: ValueTone,
62    label_width: usize,
63) -> String {
64    let raw_label = format!("{label:<label_width$}", label_width = label_width);
65    let rendered_label = if should_color() {
66        raw_label.bold().blue().to_string()
67    } else {
68        raw_label
69    };
70
71    format!("  {rendered_label}  {}", style_value(value, value_tone))
72}
73
74fn style_title(title: &str, tone: SummaryTone) -> String {
75    if !should_color() {
76        return title.to_owned();
77    }
78
79    match tone {
80        SummaryTone::Success => title.bold().green().to_string(),
81        SummaryTone::Info => title.bold().cyan().to_string(),
82    }
83}
84
85fn style_value(value: &str, tone: ValueTone) -> String {
86    if !should_color() {
87        return value.to_owned();
88    }
89
90    match tone {
91        ValueTone::Plain => value.to_owned(),
92        ValueTone::Path => value.cyan().to_string(),
93        ValueTone::PriorityHigh => value.bold().red().to_string(),
94        ValueTone::PriorityMedium => value.bold().yellow().to_string(),
95        ValueTone::PriorityLow => value.bold().green().to_string(),
96        ValueTone::StatusOpen => value.bold().green().to_string(),
97        ValueTone::StatusClosed => value.bold().magenta().to_string(),
98    }
99}
100
101fn should_color() -> bool {
102    env::var_os("NO_COLOR").is_none() && io::stdout().is_terminal()
103}
104
105#[cfg(test)]
106mod tests {
107    use super::{format_note, format_prompt_label, format_summary, SummaryTone, ValueTone};
108
109    #[test]
110    fn renders_plain_prompt_labels_with_defaults() {
111        assert_eq!(
112            format_prompt_label("Model file", Some("~/.models/parser.gguf")),
113            "Model file [~/.models/parser.gguf]"
114        );
115    }
116
117    #[test]
118    fn renders_plain_note_without_a_terminal() {
119        assert_eq!(
120            format_note("Enter", "keep current values"),
121            "  Enter     keep current values"
122        );
123    }
124
125    #[test]
126    fn renders_plain_summary_without_a_terminal() {
127        let rendered = format_summary(
128            "Created task",
129            SummaryTone::Success,
130            &[
131                ("Project", "project-x".to_owned(), ValueTone::Plain),
132                (
133                    "File",
134                    "~/.track/issues/project-x/open/task.md".to_owned(),
135                    ValueTone::Path,
136                ),
137            ],
138        );
139
140        assert_eq!(
141            rendered,
142            "Created task\n  Project  project-x\n  File     ~/.track/issues/project-x/open/task.md"
143        );
144    }
145}