1use ratatui::{
2 buffer::Buffer,
3 layout::Rect,
4 style::{Color, Modifier},
5 widgets::Widget,
6};
7use std::sync::OnceLock;
8use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
9
10static BIN_NAME: OnceLock<&'static str> = OnceLock::new();
22
23pub fn set_bin_name(name: &'static str) {
27 let _ = BIN_NAME.set(name);
28}
29
30pub fn bin_name() -> &'static str {
34 BIN_NAME.get().copied().unwrap_or("vta")
35}
36
37static FULL_DISPLAY: AtomicBool = AtomicBool::new(false);
46
47pub fn set_full_display(enabled: bool) {
50 FULL_DISPLAY.store(enabled, Ordering::Relaxed);
51}
52
53pub fn is_full_display() -> bool {
56 FULL_DISPLAY.load(Ordering::Relaxed)
57}
58
59pub fn print_full_entry(pairs: &[(&str, &str)]) {
66 let widest = pairs.iter().map(|(l, _)| l.len()).max().unwrap_or(0);
67 for (label, value) in pairs {
68 let pad = " ".repeat(widest.saturating_sub(label.len()));
69 println!(" {label}:{pad} {DIM}{value}{RESET}");
70 }
71 println!();
72}
73
74pub fn print_full_list_title(title: &str, count: usize) {
77 println!();
78 println!("{BOLD}{title} ({count}){RESET}");
79 println!();
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum OutputFormat {
93 Human,
94 Json,
95}
96
97static OUTPUT_FORMAT: AtomicU8 = AtomicU8::new(0); pub fn set_output_format(format: OutputFormat) {
102 OUTPUT_FORMAT.store(
103 match format {
104 OutputFormat::Human => 0,
105 OutputFormat::Json => 1,
106 },
107 Ordering::Relaxed,
108 );
109}
110
111pub fn output_format() -> OutputFormat {
113 if OUTPUT_FORMAT.load(Ordering::Relaxed) == 1 {
114 OutputFormat::Json
115 } else {
116 OutputFormat::Human
117 }
118}
119
120#[must_use]
124pub fn is_json_output() -> bool {
125 output_format() == OutputFormat::Json
126}
127
128pub fn print_json<T: serde::Serialize>(value: &T) -> Result<(), serde_json::Error> {
133 let text = serde_json::to_string_pretty(value)?;
134 println!("{text}");
135 Ok(())
136}
137
138pub const BOLD: &str = "\x1b[1m";
141pub const DIM: &str = "\x1b[2m";
142pub const GREEN: &str = "\x1b[32m";
143pub const RED: &str = "\x1b[31m";
144pub const CYAN: &str = "\x1b[36m";
145pub const YELLOW: &str = "\x1b[33m";
146pub const RESET: &str = "\x1b[0m";
147
148pub fn print_cli_error(err: &(dyn std::error::Error + 'static)) {
162 use vta_sdk::error::VtaError;
163 if let Some(vta_err) = err.downcast_ref::<VtaError>() {
164 match vta_err {
165 VtaError::Auth(msg) => {
166 eprintln!("{RED}\u{2717}{RESET} Authentication failed: {msg}");
167 eprintln!(
168 " {DIM}Token may be expired. Try `pnm setup` to re-authenticate, or check \
169 that the VTA's `/auth` endpoint is reachable.{RESET}"
170 );
171 }
172 VtaError::Forbidden(msg) => {
173 eprintln!("{RED}\u{2717}{RESET} Forbidden: {msg}");
174 eprintln!(
175 " {DIM}Your role or context access doesn't permit this operation. \
176 Inspect with `pnm acl get <your-did>`.{RESET}"
177 );
178 }
179 VtaError::NotFound(msg) => {
180 eprintln!("{RED}\u{2717}{RESET} Not found: {msg}");
181 }
182 VtaError::Conflict(msg) => {
183 eprintln!("{RED}\u{2717}{RESET} Conflict: {msg}");
184 }
185 VtaError::Gone(msg) => {
186 let bin = bin_name();
187 eprintln!("{RED}\u{2717}{RESET} Resource is gone: {msg}");
188 eprintln!(
189 " {DIM}This usually means the bootstrap carve-out has already been used. \
190 For a second admin, run `{bin} bootstrap provision-request` from the new \
191 operator's host and have an existing admin run \
192 `{bin} bootstrap provision-integration` against this VTA.{RESET}"
193 );
194 }
195 VtaError::Validation(msg) => {
196 eprintln!("{RED}\u{2717}{RESET} Invalid request: {msg}");
197 }
198 VtaError::Network(e) => {
199 eprintln!("{RED}\u{2717}{RESET} Network error: {e}");
200 eprintln!(" {DIM}Is the VTA reachable? Check its URL with `pnm vta info`.{RESET}");
201 }
202 VtaError::Server { status, body } => {
203 eprintln!("{RED}\u{2717}{RESET} Server error (HTTP {status}): {body}");
204 eprintln!(
205 " {DIM}This is a VTA-side failure. Check server logs or contact the operator.{RESET}"
206 );
207 }
208 VtaError::UnsupportedTransport(msg) => {
209 eprintln!("{RED}\u{2717}{RESET} Unsupported transport: {msg}");
210 eprintln!(
211 " {DIM}This operation requires a specific transport (REST or DIDComm). \
212 Check which mode your CLI is in and whether the endpoint supports it.{RESET}"
213 );
214 }
215 VtaError::DidcommTransport(msg) => {
216 eprintln!("{RED}\u{2717}{RESET} DIDComm transport error: {msg}");
217 eprintln!(
218 " {DIM}Mediator or peer unreachable. Retry after checking mediator \
219 connectivity.{RESET}"
220 );
221 }
222 VtaError::DidcommRemote { code, comment } => {
223 eprintln!("{RED}\u{2717}{RESET} Remote error ({code}): {comment}");
224 }
225 VtaError::Protocol(msg) => {
226 eprintln!("{RED}\u{2717}{RESET} Protocol error: {msg}");
227 }
228 VtaError::LastServiceRefused => {
230 let bin = bin_name();
231 eprintln!(
232 "{RED}\u{2717}{RESET} Refused: would leave the VTA with no advertised services."
233 );
234 eprintln!(
235 " {DIM}At least one transport (REST or DIDComm) must remain advertised. \
236 Enable the other transport first via `{bin} services <kind> enable …`, \
237 then retry.{RESET}"
238 );
239 }
240 VtaError::ServiceNotPresent => {
241 let bin = bin_name();
242 eprintln!("{RED}\u{2717}{RESET} Service is not present.");
243 eprintln!(
244 " {DIM}The service kind isn't currently enabled. Use `{bin} services \
245 <kind> enable …` to bring it online before updating, disabling, or rolling \
246 it back.{RESET}"
247 );
248 }
249 VtaError::ServiceAlreadyEnabled => {
250 let bin = bin_name();
251 eprintln!("{RED}\u{2717}{RESET} Service is already enabled.");
252 eprintln!(
253 " {DIM}Use `{bin} services <kind> update …` to change its configuration, \
254 or `{bin} services <kind> disable` to remove it.{RESET}"
255 );
256 }
257 VtaError::MediatorHandshakeFailed { reason } => {
258 eprintln!("{RED}\u{2717}{RESET} Mediator handshake failed: {reason}");
259 eprintln!(
260 " {DIM}Confirm the mediator DID is correct and the mediator is reachable. \
261 The reason above is the specific cause from the handshake protocol.{RESET}"
262 );
263 }
264 VtaError::DrainTtlOutOfBounds {
265 min,
266 max,
267 requested,
268 } => {
269 eprintln!(
270 "{RED}\u{2717}{RESET} Drain TTL {requested}s is outside the allowed range \
271 [{min}s, {max}s]."
272 );
273 eprintln!(
274 " {DIM}Pick a value within those bounds. The minimum applies when the \
275 command is delivered over DIDComm transport (so the listener stays up long \
276 enough for the response).{RESET}"
277 );
278 }
279 VtaError::NoPriorMutation => {
280 let bin = bin_name();
281 eprintln!("{RED}\u{2717}{RESET} No prior mutation to roll back.");
282 eprintln!(
283 " {DIM}Use `{bin} services <kind> {{enable,update,disable}} …` directly \
284 instead of rollback.{RESET}"
285 );
286 }
287 other => eprintln!("{RED}\u{2717}{RESET} Error: {other}"),
288 }
289 return;
290 }
291 eprintln!("{RED}\u{2717}{RESET} Error: {err}");
292 let mut source = err.source();
293 while let Some(s) = source {
294 eprintln!(" {DIM}caused by: {s}{RESET}");
295 source = s.source();
296 }
297}
298
299pub fn print_widget(widget: impl Widget, height: u16) {
302 let width = ratatui::crossterm::terminal::size().map_or(120, |(w, _)| w);
303 let area = Rect::new(0, 0, width, height);
304 let mut buf = Buffer::empty(area);
305 widget.render(area, &mut buf);
306
307 let mut out = String::new();
308 for y in 0..height {
309 let mut cur_fg = Color::Reset;
310 let mut cur_bg = Color::Reset;
311 let mut cur_mod = Modifier::empty();
312
313 for x in 0..width {
314 let cell = &buf[(x, y)];
315 if cell.skip {
316 continue;
317 }
318
319 if cell.fg != cur_fg || cell.bg != cur_bg || cell.modifier != cur_mod {
320 out.push_str("\x1b[0m");
321 push_ansi_fg(&mut out, cell.fg);
322 push_ansi_bg(&mut out, cell.bg);
323 push_ansi_mod(&mut out, cell.modifier);
324 cur_fg = cell.fg;
325 cur_bg = cell.bg;
326 cur_mod = cell.modifier;
327 }
328
329 out.push_str(cell.symbol());
330 }
331 out.push_str("\x1b[0m\n");
332 }
333
334 print!("{out}");
335}
336
337pub fn push_ansi_fg(out: &mut String, color: Color) {
338 use std::fmt::Write as _;
339 match color {
340 Color::Reset => {}
341 Color::Black => out.push_str("\x1b[30m"),
342 Color::Red => out.push_str("\x1b[31m"),
343 Color::Green => out.push_str("\x1b[32m"),
344 Color::Yellow => out.push_str("\x1b[33m"),
345 Color::Blue => out.push_str("\x1b[34m"),
346 Color::Magenta => out.push_str("\x1b[35m"),
347 Color::Cyan => out.push_str("\x1b[36m"),
348 Color::Gray => out.push_str("\x1b[37m"),
349 Color::DarkGray => out.push_str("\x1b[90m"),
350 Color::LightRed => out.push_str("\x1b[91m"),
351 Color::LightGreen => out.push_str("\x1b[92m"),
352 Color::LightYellow => out.push_str("\x1b[93m"),
353 Color::LightBlue => out.push_str("\x1b[94m"),
354 Color::LightMagenta => out.push_str("\x1b[95m"),
355 Color::LightCyan => out.push_str("\x1b[96m"),
356 Color::White => out.push_str("\x1b[97m"),
357 Color::Rgb(r, g, b) => {
358 let _ = write!(out, "\x1b[38;2;{r};{g};{b}m");
359 }
360 Color::Indexed(i) => {
361 let _ = write!(out, "\x1b[38;5;{i}m");
362 }
363 }
364}
365
366pub fn push_ansi_bg(out: &mut String, color: Color) {
367 use std::fmt::Write as _;
368 match color {
369 Color::Reset => {}
370 Color::Black => out.push_str("\x1b[40m"),
371 Color::Red => out.push_str("\x1b[41m"),
372 Color::Green => out.push_str("\x1b[42m"),
373 Color::Yellow => out.push_str("\x1b[43m"),
374 Color::Blue => out.push_str("\x1b[44m"),
375 Color::Magenta => out.push_str("\x1b[45m"),
376 Color::Cyan => out.push_str("\x1b[46m"),
377 Color::Gray => out.push_str("\x1b[47m"),
378 Color::DarkGray => out.push_str("\x1b[100m"),
379 Color::LightRed => out.push_str("\x1b[101m"),
380 Color::LightGreen => out.push_str("\x1b[102m"),
381 Color::LightYellow => out.push_str("\x1b[103m"),
382 Color::LightBlue => out.push_str("\x1b[104m"),
383 Color::LightMagenta => out.push_str("\x1b[105m"),
384 Color::LightCyan => out.push_str("\x1b[106m"),
385 Color::White => out.push_str("\x1b[107m"),
386 Color::Rgb(r, g, b) => {
387 let _ = write!(out, "\x1b[48;2;{r};{g};{b}m");
388 }
389 Color::Indexed(i) => {
390 let _ = write!(out, "\x1b[48;5;{i}m");
391 }
392 }
393}
394
395pub fn push_ansi_mod(out: &mut String, modifier: Modifier) {
396 if modifier.contains(Modifier::BOLD) {
397 out.push_str("\x1b[1m");
398 }
399 if modifier.contains(Modifier::DIM) {
400 out.push_str("\x1b[2m");
401 }
402 if modifier.contains(Modifier::ITALIC) {
403 out.push_str("\x1b[3m");
404 }
405 if modifier.contains(Modifier::UNDERLINED) {
406 out.push_str("\x1b[4m");
407 }
408 if modifier.contains(Modifier::REVERSED) {
409 out.push_str("\x1b[7m");
410 }
411 if modifier.contains(Modifier::CROSSED_OUT) {
412 out.push_str("\x1b[9m");
413 }
414}
415
416pub fn print_section(title: &str) {
417 let pad = 46usize.saturating_sub(title.len());
418 println!(
419 "\n{DIM}──{RESET} {BOLD}{title}{RESET} {DIM}{}{RESET}",
420 "─".repeat(pad)
421 );
422}