1use standout_render::{render_with_output, OutputMode, Renderer, Theme};
8
9use crate::Result;
10
11const THEME_YAML: &str = r#"
16pack-name:
17 bold: true
18 fg: blue
19
20filename:
21 fg: white
22
23handler-symbol:
24 bold: true
25 fg: yellow
26
27description:
28 dim: true
29
30deployed:
31 fg: green
32
33pending:
34 fg: magenta
35
36error:
37 fg: red
38 bold: true
39
40warning:
41 fg: yellow
42
43message:
44 fg: cyan
45
46dim:
47 dim: true
48
49header:
50 bold: true
51
52dry-run:
53 fg: yellow
54 italic: true
55"#;
56
57pub const TEMPLATE_PACK_STATUS: &str = r#"{% if message %}[message]{{ message }}[/message]
61{% endif %}{% if dry_run %}[dry-run] (dry run — no changes made)[/dry-run]
62{% endif %}{% for pack in packs %}[pack-name]{{ pack.name }}[/pack-name]
63{% for file in pack.files %} {{ file.name | col(24) }} [handler-symbol]{{ file.symbol }}[/handler-symbol] [description]{{ file.description | col(30) }}[/description] [{{ file.status }}]{{ file.status_label }}[/{{ file.status }}]
64{% endfor %}
65{% endfor %}"#;
66
67pub const TEMPLATE_LIST: &str = r#"{% for pack in packs %}{{ pack.name }}{% if pack.ignored %} [dim](ignored)[/dim]{% endif %}
69{% endfor %}"#;
70
71pub const TEMPLATE_MESSAGE: &str = r#"{% if message %}[message]{{ message }}[/message]
73{% endif %}{% for line in details %} {{ line }}
74{% endfor %}"#;
75
76pub fn create_theme() -> Theme {
80 Theme::from_yaml(THEME_YAML).expect("built-in theme YAML must be valid")
81}
82
83pub fn create_renderer() -> Renderer {
85 let theme = create_theme();
86 let mut renderer = Renderer::new(theme).expect("renderer creation must succeed");
87 renderer
88 .add_template("pack-status", TEMPLATE_PACK_STATUS)
89 .unwrap();
90 renderer.add_template("list", TEMPLATE_LIST).unwrap();
91 renderer.add_template("message", TEMPLATE_MESSAGE).unwrap();
92 renderer
93}
94
95pub fn render<T: serde::Serialize>(
100 template_name: &str,
101 data: &T,
102 mode: OutputMode,
103) -> Result<String> {
104 if matches!(mode, OutputMode::Json) {
105 return serde_json::to_string_pretty(data)
106 .map_err(|e| crate::DodotError::Other(format!("JSON serialization failed: {e}")));
107 }
108
109 let theme = create_theme();
110 let template = match template_name {
111 "pack-status" => TEMPLATE_PACK_STATUS,
112 "list" => TEMPLATE_LIST,
113 "message" => TEMPLATE_MESSAGE,
114 other => {
115 return Err(crate::DodotError::Other(format!(
116 "unknown template: {other}"
117 )))
118 }
119 };
120
121 render_with_output(template, data, &theme, mode)
122 .map_err(|e| crate::DodotError::Other(format!("render failed: {e}")))
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn theme_parses_without_error() {
131 let _theme = create_theme();
132 }
133
134 #[test]
135 fn renderer_creates_with_all_templates() {
136 let _renderer = create_renderer();
137 }
138
139 #[test]
140 fn render_pack_status_text_mode() {
141 use serde::Serialize;
142
143 #[derive(Serialize)]
144 struct Data {
145 message: Option<String>,
146 dry_run: bool,
147 packs: Vec<Pack>,
148 }
149 #[derive(Serialize)]
150 struct Pack {
151 name: String,
152 files: Vec<File>,
153 }
154 #[derive(Serialize)]
155 struct File {
156 name: String,
157 symbol: String,
158 description: String,
159 status: String,
160 status_label: String,
161 }
162
163 let data = Data {
164 message: None,
165 dry_run: false,
166 packs: vec![Pack {
167 name: "vim".into(),
168 files: vec![File {
169 name: "vimrc".into(),
170 symbol: "➞".into(),
171 description: "~/.vimrc".into(),
172 status: "deployed".into(),
173 status_label: "deployed".into(),
174 }],
175 }],
176 };
177
178 let output = render("pack-status", &data, OutputMode::Text).unwrap();
179 assert!(output.contains("vim"));
180 assert!(output.contains("vimrc"));
181 assert!(output.contains("deployed"));
182 }
183
184 #[test]
185 fn json_mode_produces_json() {
186 use serde::Serialize;
187
188 #[derive(Serialize)]
189 struct Data {
190 name: String,
191 }
192
193 let data = Data {
194 name: "test".into(),
195 };
196
197 let output = render("list", &data, OutputMode::Json).unwrap();
198 assert!(output.contains("\"name\""));
199 assert!(output.contains("\"test\""));
200 }
201}