1use crate::config::ResolvedConfig;
2use crate::config::layered::ConfigWarning;
3use crate::core::effect::Effect;
4use crate::git::atomize::AtomicResult;
5use crate::git::branch::BranchState;
6use owo_colors::OwoColorize;
7use std::io::{self, Write};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum OutputMode {
11 Human,
12 Json,
13 Quiet,
14}
15
16pub struct Printer {
17 pub mode: OutputMode,
18 pub verbosity: u8,
19}
20
21impl Printer {
22 pub fn new(json: bool, quiet: bool, verbosity: u8) -> Self {
23 let mode = if json {
24 OutputMode::Json
25 } else if quiet {
26 OutputMode::Quiet
27 } else {
28 OutputMode::Human
29 };
30 Self { mode, verbosity }
31 }
32
33 pub fn print_commit_results(&self, results: &[AtomicResult], dry_run: bool) {
34 match self.mode {
35 OutputMode::Quiet => {}
36 OutputMode::Json => {
37 let output = serde_json::json!({
38 "dry_run": dry_run,
39 "results": results.iter().map(|r| {
40 serde_json::json!({
41 "component": r.component,
42 "branch": r.branch,
43 "commit": r.commit_id.to_string(),
44 "files": r.files.iter().map(|p| p.display().to_string()).collect::<Vec<_>>(),
45 "created": r.created,
46 })
47 }).collect::<Vec<_>>(),
48 });
49 println!("{}", serde_json::to_string_pretty(&output).unwrap());
50 }
51 OutputMode::Human => {
52 let mut out = io::stdout().lock();
53 for r in results {
54 let prefix = if dry_run { "would " } else { "" };
55 let action = if r.created { "create" } else { "update" };
56 let short_id = r.commit_id.to_string();
57 let short_id = short_id.get(..8).unwrap_or(&short_id);
58
59 let _ = writeln!(
60 out,
61 "{} [{}] {} → {} ({}{}, {} file{})",
62 "✓".green(),
63 r.component.cyan(),
64 short_id.dimmed(),
65 r.branch.bold(),
66 prefix,
67 action,
68 r.files.len(),
69 if r.files.len() == 1 { "" } else { "s" }
70 );
71
72 if self.verbosity > 0 {
73 for f in &r.files {
74 let _ = writeln!(out, " {}", f.display().dimmed());
75 }
76 }
77 }
78 }
79 }
80 }
81
82 pub fn print_status(
83 &self,
84 components: &[(String, Vec<std::path::PathBuf>, BranchState, String)],
85 ) {
86 match self.mode {
87 OutputMode::Quiet => {}
88 OutputMode::Json => {
89 let output: Vec<_> = components
90 .iter()
91 .map(|(name, files, state, branch)| {
92 serde_json::json!({
93 "component": name,
94 "branch": branch,
95 "state": format!("{state:?}"),
96 "file_count": files.len(),
97 })
98 })
99 .collect();
100 println!("{}", serde_json::to_string_pretty(&output).unwrap());
101 }
102 OutputMode::Human => {
103 let mut out = io::stdout().lock();
104 for (name, files, state, branch) in components {
105 let state_str = match state {
106 BranchState::Missing => "missing".yellow().to_string(),
107 BranchState::Current => "current".green().to_string(),
108 BranchState::FastForward { .. } => "ahead".cyan().to_string(),
109 BranchState::Diverged { .. } => "diverged".red().to_string(),
110 };
111 let _ = writeln!(
112 out,
113 " {} {} ({}, {} file{})",
114 name.bold(),
115 branch.dimmed(),
116 state_str,
117 files.len(),
118 if files.len() == 1 { "" } else { "s" }
119 );
120 }
121 }
122 }
123 }
124
125 pub fn print_validate_ok(&self) {
126 match self.mode {
127 OutputMode::Quiet => {}
128 OutputMode::Json => println!(r#"{{"valid": true}}"#),
129 OutputMode::Human => println!("{} configuration is valid", "✓".green()),
130 }
131 }
132
133 pub fn print_init(&self, path: &std::path::Path) {
134 match self.mode {
135 OutputMode::Quiet => {}
136 OutputMode::Json => {
137 println!(
138 "{}",
139 serde_json::json!({"created": path.display().to_string()})
140 );
141 }
142 OutputMode::Human => {
143 println!("{} created {}", "✓".green(), path.display());
144 }
145 }
146 }
147
148 pub fn print_error(&self, err: &crate::core::Error) {
149 match self.mode {
150 OutputMode::Quiet => {}
151 OutputMode::Json => {
152 println!("{}", serde_json::json!({"error": err.to_string()}));
153 }
154 OutputMode::Human => {
155 eprintln!("{} {}", "✗".red(), err);
156 }
157 }
158 }
159
160 pub fn print_validate_error(&self, err: &crate::core::Error) {
161 match self.mode {
162 OutputMode::Quiet => {}
163 OutputMode::Json => {
164 println!(
165 "{}",
166 serde_json::json!({"valid": false, "error": err.to_string()})
167 );
168 }
169 OutputMode::Human => {
170 eprintln!("{} {}", "✗".red(), err);
171 }
172 }
173 }
174
175 pub fn print_config_provenance(&self, resolved: &ResolvedConfig) {
176 match self.mode {
177 OutputMode::Quiet => {}
178 OutputMode::Json => {
179 let config = serde_json::json!({
180 "config": {
181 "base_branch": {
182 "value": resolved.base_branch.value,
183 "source": resolved.base_branch.source.label(),
184 },
185 "branch_template": {
186 "value": resolved.branch_template.value,
187 "source": resolved.branch_template.source.label(),
188 },
189 "unmatched_files": {
190 "value": resolved.unmatched_files.value.to_string(),
191 "source": resolved.unmatched_files.source.label(),
192 },
193 "default_commit_type": {
194 "value": resolved.default_commit_type.value,
195 "source": resolved.default_commit_type.source.label(),
196 },
197 }
198 });
199 println!("{}", serde_json::to_string_pretty(&config).unwrap());
200 }
201 OutputMode::Human => {
202 let mut out = io::stdout().lock();
203 let _ = writeln!(out, "{}", "Settings:".bold());
204 let _ = writeln!(
205 out,
206 " {:<20} = {:<30} ({})",
207 "base_branch",
208 resolved.base_branch.value,
209 resolved.base_branch.source.label().dimmed()
210 );
211 let _ = writeln!(
212 out,
213 " {:<20} = {:<30} ({})",
214 "branch_template",
215 resolved.branch_template.value,
216 resolved.branch_template.source.label().dimmed()
217 );
218 let _ = writeln!(
219 out,
220 " {:<20} = {:<30} ({})",
221 "unmatched_files",
222 resolved.unmatched_files.value.to_string(),
223 resolved.unmatched_files.source.label().dimmed()
224 );
225 if let Some(ref ct) = resolved.default_commit_type.value {
226 let _ = writeln!(
227 out,
228 " {:<20} = {:<30} ({})",
229 "default_commit_type",
230 ct,
231 resolved.default_commit_type.source.label().dimmed()
232 );
233 }
234 let _ = writeln!(out);
235 }
236 }
237 }
238
239 pub fn print_config_warning(&self, warning: &ConfigWarning) {
240 match self.mode {
241 OutputMode::Quiet => {}
242 OutputMode::Json => {
243 println!("{}", serde_json::json!({"warning": warning.message}));
244 }
245 OutputMode::Human => {
246 eprintln!("{} {}", "⚠".yellow(), warning.message);
247 }
248 }
249 }
250
251 pub fn print_effect_preview(&self, effect: &Effect) {
252 match self.mode {
253 OutputMode::Quiet => {}
254 OutputMode::Json => {
255 let desc = match effect {
256 Effect::RefTransaction { edits, .. } => {
257 serde_json::json!({
258 "effect": "ref_transaction",
259 "dry_run": true,
260 "refs": edits.iter().map(|e| {
261 serde_json::json!({
262 "ref": e.ref_name,
263 "component": e.component,
264 "action": if e.created { "create" } else { "update" },
265 })
266 }).collect::<Vec<_>>(),
267 })
268 }
269 Effect::Push { remote, branches } => {
270 serde_json::json!({
271 "effect": "push",
272 "dry_run": true,
273 "remote": remote,
274 "branches": branches,
275 })
276 }
277 Effect::WriteFile {
278 path,
279 content,
280 structured,
281 } => {
282 let content_value = structured
283 .clone()
284 .unwrap_or_else(|| serde_json::Value::String(content.clone()));
285 serde_json::json!({
286 "effect": "write_file",
287 "dry_run": true,
288 "path": path.display().to_string(),
289 "content": content_value,
290 })
291 }
292 };
293 println!("{}", serde_json::to_string_pretty(&desc).unwrap());
294 }
295 OutputMode::Human => {
296 let mut out = io::stdout().lock();
297 match effect {
298 Effect::RefTransaction { edits, .. } => {
299 for e in edits {
300 let action = if e.created { "create" } else { "update" };
301 let branch = e
302 .ref_name
303 .strip_prefix("refs/heads/")
304 .unwrap_or(&e.ref_name);
305 let _ = writeln!(
306 out,
307 " {} would {} branch {}",
308 "▸".dimmed(),
309 action,
310 branch.bold()
311 );
312 }
313 }
314 Effect::Push { remote, branches } => {
315 let _ = writeln!(
316 out,
317 " {} would push {} branch{} to {}",
318 "▸".dimmed(),
319 branches.len(),
320 if branches.len() == 1 { "" } else { "es" },
321 remote.bold()
322 );
323 }
324 Effect::WriteFile { path, content, .. } => {
325 let _ = writeln!(
326 out,
327 " {} would create {}",
328 "▸".dimmed(),
329 path.display().bold()
330 );
331 if self.verbosity > 0 || content.len() < 4096 {
332 let _ = writeln!(out);
333 for line in content.lines() {
334 let _ = writeln!(out, " {}", line.dimmed());
335 }
336 }
337 }
338 }
339 }
340 }
341 }
342}