use crate::cli::Cli;
use comfy_table::{
modifiers::UTF8_ROUND_CORNERS, presets::UTF8_FULL, Attribute, Cell, Color, ContentArrangement,
Table,
};
use libgrite_core::GriteError;
use regex::Regex;
use serde::Serialize;
#[derive(Serialize)]
pub struct JsonResponse<T: Serialize> {
pub schema_version: u32,
pub ok: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<T>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<JsonError>,
}
#[derive(Serialize)]
pub struct JsonError {
pub code: String,
pub message: String,
#[serde(skip_serializing_if = "serde_json::Value::is_null")]
pub details: serde_json::Value,
}
pub fn output_success<T: Serialize>(cli: &Cli, data: T) {
if cli.json {
let response = JsonResponse {
schema_version: 1,
ok: true,
data: Some(data),
error: None,
};
match serde_json::to_string_pretty(&response) {
Ok(json) => println!("{}", json),
Err(e) => eprintln!("error: failed to serialize response: {}", e),
}
} else if !cli.quiet {
match serde_json::to_string_pretty(&data) {
Ok(json) => println!("{}", json),
Err(e) => eprintln!("error: failed to serialize output: {}", e),
}
}
}
pub fn output_error(cli: &Cli, err: &GriteError) {
if cli.json {
let suggestions = err.suggestions();
let details = if suggestions.is_empty() {
serde_json::Value::Null
} else {
serde_json::json!({ "suggestions": suggestions })
};
let response: JsonResponse<()> = JsonResponse {
schema_version: 1,
ok: false,
data: None,
error: Some(JsonError {
code: err.error_code().to_string(),
message: err.to_string(),
details,
}),
};
match serde_json::to_string_pretty(&response) {
Ok(json) => eprintln!("{}", json),
Err(e) => eprintln!("error: failed to serialize error response: {}", e),
}
} else {
eprintln!("error: {}", err);
let suggestions = err.suggestions();
if !suggestions.is_empty() {
eprintln!();
eprintln!("Suggestions:");
for suggestion in suggestions {
eprintln!(" - {}", suggestion);
}
}
}
}
pub fn print_human(cli: &Cli, msg: &str) {
if !cli.json && !cli.quiet {
println!("{}", msg);
}
}
fn strip_markdown(input: &str) -> String {
let re_link = Regex::new(r"\[([^\]]+)\]\([^)]+\)").unwrap();
let re_bold = Regex::new(r"\*\*(.+?)\*\*").unwrap();
let re_italic = Regex::new(r"(^|[^*])\*([^*]+?)\*([^*]|$)").unwrap();
let re_code = Regex::new(r"`([^`]+)`").unwrap();
let re_heading = Regex::new(r"^#+\s+").unwrap();
let s = re_link.replace_all(input, "$1");
let s = re_bold.replace_all(&s, "$1");
let s = re_italic.replace_all(&s, "$1");
let s = re_code.replace_all(&s, "$1");
re_heading.replace_all(&s, "").to_string()
}
pub struct IssueRow {
pub id: String,
pub state: String,
pub title: String,
pub created_ts: u64,
}
fn format_local_date(ts_ms: u64) -> String {
let secs = (ts_ms / 1000) as i64;
chrono::DateTime::from_timestamp(secs, 0)
.map(|dt| {
dt.with_timezone(&chrono::Local)
.format("%b %-e, %y %-l:%M %P")
.to_string()
})
.unwrap_or_else(|| ts_ms.to_string())
}
pub fn format_issue_table(issues: &[IssueRow]) -> String {
let mut table = Table::new();
table
.load_preset(UTF8_FULL)
.apply_modifier(UTF8_ROUND_CORNERS)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(vec!["ID", "Status", "Created", "Title"]);
for issue in issues {
let state_cell = match issue.state.as_str() {
"open" => Cell::new(&issue.state)
.fg(Color::Blue)
.add_attribute(Attribute::Bold),
_ => Cell::new(&issue.state).fg(Color::DarkYellow),
};
let title = strip_markdown(&issue.title);
table.add_row(vec![
Cell::new(&issue.id[..8.min(issue.id.len())]),
state_cell,
Cell::new(format_local_date(issue.created_ts)),
Cell::new(title),
]);
}
let open_count = issues.iter().filter(|i| i.state == "open").count();
let closed_count = issues.len() - open_count;
let mut table_str = table.to_string();
if !issues.is_empty() {
table_str.push_str(&format!(
"\n{} issues total ({} open, {} closed)",
issues.len(),
open_count,
closed_count,
));
}
table_str
}