use crate::core::{Finding, Severity};
use console::{style, Color, Style, Term};
use indicatif::{ProgressBar, ProgressStyle};
use std::fmt::Display;
pub mod chars {
pub const CHECK: &str = "\u{2713}"; pub const CROSS: &str = "\u{2717}"; pub const BULLET: &str = "\u{2022}"; pub const ARROW: &str = "\u{2192}"; pub const DIAMOND: &str = "\u{25C6}"; pub const LINE: &str = "\u{2500}"; pub const PIPE: &str = "\u{2502}"; pub const CORNER: &str = "\u{2514}"; pub const TEE: &str = "\u{251C}"; }
fn severity_color(severity: &Severity) -> Color {
match severity {
Severity::Critical => Color::Magenta,
Severity::High => Color::Red,
Severity::Medium => Color::Yellow,
Severity::Low => Color::Cyan,
Severity::Info => Color::Blue,
}
}
pub struct UI {
term: Term,
pub verbose: bool,
pub quiet: bool,
pub json: bool,
pub compact: bool,
width: usize,
}
impl UI {
pub fn new(verbose: bool, quiet: bool, json: bool, compact: bool) -> Self {
let term = Term::stderr();
let width = term.size().1 as usize;
let width = if width < 40 { 80 } else { width };
Self {
term,
verbose,
quiet,
json,
compact,
width,
}
}
pub fn from_config(cfg: &super::OutputConfig) -> Self {
Self::new(cfg.verbose, cfg.quiet, cfg.json, cfg.compact)
}
fn divider_string(&self) -> String {
chars::LINE.repeat(self.width.min(50))
}
fn should_decorate(&self) -> bool {
!self.quiet && !self.json && !self.compact
}
pub fn header(&self, title: &str) {
if !self.should_decorate() {
return;
}
println!("{}", style(format!("SecureGit {title}")).bold());
println!("{}", style(self.divider_string()).dim());
}
pub fn section(&self, label: &str) {
if !self.should_decorate() {
return;
}
println!();
println!(" {}", style(label).bold());
}
pub fn divider(&self) {
if !self.should_decorate() {
return;
}
println!(" {}", style(self.divider_string()).dim());
}
pub fn field(&self, label: &str, value: impl Display) {
if !self.should_decorate() {
return;
}
println!(" {:>12} {}", style(label).dim(), value);
}
pub fn field_styled(&self, label: &str, value: impl Display, color: Color) {
if !self.should_decorate() {
return;
}
println!(
" {:>12} {}",
style(label).dim(),
Style::new().fg(color).apply_to(value)
);
}
pub fn success(&self, msg: impl Display) {
if self.json || self.compact {
return;
}
println!(
" {} {}",
style(chars::CHECK).green().bold(),
style(msg).green()
);
}
pub fn error(&self, msg: impl Display) {
if self.json {
return;
}
eprintln!(
" {} {}",
style(chars::CROSS).red().bold(),
style(msg).red()
);
}
pub fn warning(&self, msg: impl Display) {
if self.json || self.compact {
return;
}
println!(
" {} {}",
style(chars::BULLET).yellow().bold(),
style(msg).yellow()
);
}
pub fn info(&self, msg: impl Display) {
if !self.should_decorate() {
return;
}
println!(" {} {}", style(chars::ARROW).cyan(), msg);
}
pub fn debug(&self, msg: impl Display) {
if !self.verbose || !self.should_decorate() {
return;
}
println!(" {}", style(msg).dim());
}
pub fn result_banner(&self, ok: bool, msg: &str, fields: &[(&str, String)]) {
if !self.should_decorate() {
return;
}
println!();
if ok {
self.success(msg);
} else {
self.error(msg);
}
if !fields.is_empty() {
println!();
for (label, value) in fields {
self.field(label, value);
}
}
}
pub fn list_item(&self, text: impl Display) {
if !self.should_decorate() {
return;
}
println!(" {} {}", style(chars::BULLET).dim(), text);
}
pub fn status_item(&self, ok: bool, text: impl Display) {
if !self.should_decorate() {
return;
}
if ok {
println!(
" {} {}",
style(chars::CHECK).green(),
style(text).green()
);
} else {
println!(" {} {}", style(chars::CROSS).red(), style(text).red());
}
}
pub fn tree_item(&self, last: bool, text: impl Display) {
if !self.should_decorate() {
return;
}
let connector = if last { chars::CORNER } else { chars::TEE };
println!(
" {}{} {}",
style(connector).dim(),
style(chars::LINE).dim(),
text
);
}
pub fn tree_continuation(&self, text: impl Display) {
if !self.should_decorate() {
return;
}
println!(" {} {}", style(chars::PIPE).dim(), style(text).dim());
}
pub fn log_entry(
&self,
hash: &str,
summary: &str,
author: &str,
time_ago: &str,
is_last: bool,
) {
if !self.should_decorate() {
return;
}
println!(
" {} {} {} {}",
style(chars::DIAMOND).cyan().bold(),
style(hash).yellow(),
summary,
style(format!("({time_ago})")).dim()
);
let connector = if is_last { " " } else { chars::PIPE };
println!(
" {} {}",
style(connector).dim(),
style(format!("Author: {author}")).dim()
);
if !is_last {
println!(" {}", style(chars::PIPE).dim());
}
}
pub fn log_oneline(&self, hash: &str, summary: &str) {
if !self.should_decorate() {
return;
}
println!(" {} {}", style(hash).yellow(), summary);
}
pub fn branch_item(&self, name: &str, hash: &str, summary: &str, current: bool) {
if !self.should_decorate() {
return;
}
let marker = if current {
style(chars::ARROW).green().bold().to_string()
} else {
" ".to_string()
};
let name_style = if current {
style(format!("{name:<20}")).green().bold()
} else {
style(format!("{name:<20}"))
};
println!(
" {} {} {} {}",
marker,
name_style,
style(hash).yellow().dim(),
style(summary).dim()
);
}
pub fn severity_row(
&self,
critical: usize,
high: usize,
medium: usize,
low: usize,
info: usize,
) {
if !self.should_decorate() {
return;
}
let tag = |label: &str, count: usize, color: Color| -> String {
let s = Style::new().fg(color).bold();
format!(
"{} {}",
s.apply_to(format!("[{label}]")),
s.apply_to(count)
)
};
println!(
" {} {} {} {} {}",
tag("CRITICAL", critical, Color::Magenta),
tag("HIGH", high, Color::Red),
tag("MEDIUM", medium, Color::Yellow),
tag("LOW", low, Color::Cyan),
tag("INFO", info, Color::Blue),
);
}
pub fn finding(&self, finding: &Finding) {
if !self.should_decorate() {
return;
}
let color = severity_color(&finding.severity);
println!();
println!(
" {} {}",
Style::new()
.fg(color)
.bold()
.apply_to(format!("[{}]", finding.severity)),
style(&finding.title).bold()
);
if let Some(path) = &finding.file_path {
let loc = if let Some(line) = finding.line_start {
format!("{}:{}", path.display(), line)
} else {
path.display().to_string()
};
println!(" {} {}", style("File").dim(), loc);
}
if !finding.evidence.is_empty() {
for ev in &finding.evidence {
println!(" {} {}", style("Match").dim(), style(ev).dim());
}
}
}
pub fn spinner(&self, message: &str) -> ProgressBar {
if self.quiet || self.json {
return ProgressBar::hidden();
}
let pb = ProgressBar::new_spinner();
pb.set_style(
ProgressStyle::default_spinner()
.tick_strings(&["[= ]", "[ = ]", "[ = ]", "[ =]", "[ = ]", "[ = ]"])
.template(" {spinner} {msg}")
.unwrap(),
);
pb.set_message(message.to_string());
pb.enable_steady_tick(std::time::Duration::from_millis(100));
pb
}
pub fn progress_bar(&self, total: u64, message: &str) -> ProgressBar {
if self.quiet || self.json {
return ProgressBar::hidden();
}
let pb = ProgressBar::new(total);
pb.set_style(
ProgressStyle::default_bar()
.template(" {msg}\n [{bar:40}] {pos}/{len} {eta}")
.unwrap()
.progress_chars("= "),
);
pb.set_message(message.to_string());
pb
}
pub fn transfer_bar(&self, total_bytes: u64) -> ProgressBar {
if self.quiet || self.json {
return ProgressBar::hidden();
}
let pb = ProgressBar::new(total_bytes);
pb.set_style(
ProgressStyle::default_bar()
.template(" [{bar:40}] {bytes}/{total_bytes} ({bytes_per_sec})")
.unwrap()
.progress_chars("= "),
);
pb
}
pub fn finish_progress(&self, pb: &ProgressBar, msg: &str) {
pb.finish_and_clear();
if !msg.is_empty() && self.should_decorate() {
self.success(msg);
}
}
pub fn raw(&self, text: impl Display) {
println!("{text}");
}
pub fn json_out(&self, value: &serde_json::Value) {
if self.json {
println!("{value}");
}
}
pub fn blank(&self) {
if !self.should_decorate() {
return;
}
println!();
}
pub fn flush(&self) {
let _ = self.term.flush();
}
}
pub fn print_finding(finding: &Finding) {
let sev_color = severity_color(&finding.severity);
println!();
println!(
"{} {}",
Style::new()
.fg(sev_color)
.bold()
.apply_to(format!("[{}]", finding.severity)),
style(&finding.title).bold()
);
if let Some(path) = &finding.file_path {
print!(" File: {}", path.display());
if let Some(line) = finding.line_start {
print!(":{}", line);
}
println!();
}
if !finding.description.is_empty() {
println!(" {}", finding.description);
}
if !finding.evidence.is_empty() {
println!(" Evidence:");
for evidence in &finding.evidence {
println!(" {}", style(evidence).dim());
}
}
}
pub fn print_summary(
total: usize,
critical: usize,
high: usize,
medium: usize,
low: usize,
info: usize,
) {
println!();
println!("{}", style("Scan Summary").bold());
println!(" Total findings: {}", total);
if critical > 0 {
println!(" Critical: {}", style(critical).magenta().bold());
}
if high > 0 {
println!(" High: {}", style(high).red().bold());
}
if medium > 0 {
println!(" Medium: {}", style(medium).yellow());
}
if low > 0 {
println!(" Low: {}", style(low).cyan());
}
if info > 0 {
println!(" Info: {}", style(info).blue());
}
println!();
}