use std::path::PathBuf;
use rich::{ColorSystem, Console};
use rich_ext::diagnostic::{
Diagnostic, DiagnosticInfo, Level, Location, SourceSnippet, Suggestion,
};
use rich_ext::event::EventView;
use rich_ext::hyperlink::Hyperlinker;
use rich_ext::stacktrace;
struct Shots {
dir: Option<PathBuf>,
}
impl Shots {
fn from_args() -> Self {
let args: Vec<String> = std::env::args().skip(1).collect();
let dir = args
.iter()
.position(|arg| arg == "--svg")
.and_then(|i| args.get(i + 1))
.map(PathBuf::from);
Shots { dir }
}
fn shot(&self, name: &str, width: usize, body: impl FnOnce(&Console)) {
match &self.dir {
None => {
let console = Console::new();
console.print_str(&format!("[dim]── {name} ──[/]"));
body(&console);
}
Some(dir) => {
let console = Console::builder()
.width(width)
.force_terminal(true)
.color_system(Some(ColorSystem::Truecolor))
.build();
let id = format!("guide_diagnostics-{name}");
let svg = console.export_svg("rich-ext", &id, body);
std::fs::create_dir_all(dir).expect("create the SVG directory");
let path = dir.join(format!("{id}.svg"));
std::fs::write(&path, svg).expect("write the SVG");
eprintln!("wrote {}", path.display());
}
}
}
}
#[derive(Debug, thiserror::Error)]
enum ConfigError {
#[error("port {0:?} is not a number")]
BadPort(String),
#[error("cannot read the config")]
Read(#[from] std::io::Error),
}
impl DiagnosticInfo for ConfigError {
fn code(&self) -> Option<String> {
Some(match self {
ConfigError::BadPort(_) => "C001".into(),
ConfigError::Read(_) => "C002".into(),
})
}
fn code_url(&self) -> Option<String> {
self.code()
.map(|code| format!("https://acme.dev/errors/{code}"))
}
fn help(&self) -> Option<String> {
matches!(self, ConfigError::BadPort(_)).then(|| "use a number such as 8080".into())
}
fn location(&self) -> Option<Location> {
Some(Location::new("app.toml", Some(2), None))
}
}
use rich_ext::stacktrace::{Frame, Language, Parsers, StackTrace, TraceParser};
struct TinyParser;
impl TraceParser for TinyParser {
fn detect(&self, text: &str) -> bool {
text.starts_with("tiny error: ")
}
fn parse(&self, text: &str) -> Option<StackTrace> {
let mut lines = text.lines();
let mut trace = StackTrace::new(Language::Other("tiny".into()));
trace.kind = Some("tiny error".into());
trace.message = lines.next()?.strip_prefix("tiny error: ").map(Into::into);
for line in lines {
let (function, place) = line.trim().strip_prefix("at ")?.split_once(" (")?;
let (path, line) = place.trim_end_matches(')').rsplit_once(':')?;
trace.frames.push(Frame {
function: Some(function.into()),
path: Some(path.into()),
line: line.parse().ok(),
..Frame::default()
});
}
trace.frames.reverse(); Some(trace)
}
}
fn install_panic_hook() {
let console = Console::builder()
.force_terminal(std::io::IsTerminal::is_terminal(&std::io::stderr()))
.build();
std::panic::set_hook(Box::new(stacktrace::panic_hook(console)));
}
const PYTHON_TRACE: &str = r#"Traceback (most recent call last):
File "/srv/app/app.py", line 6, in main
load()
File "/srv/app/app.py", line 3, in load
return {}["port"]
~~^^^^^^^^
KeyError: 'port'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/srv/app/app.py", line 11, in outer
main()
File "/srv/app/app.py", line 8, in main
raise ValueError("bad config") from e
ValueError: bad config
"#;
const RUST_PANIC: &str = "thread 'main' panicked at src/main.rs:14:10:
port must be a number: ParseIntError { kind: InvalidDigit }
stack backtrace:
0: __rustc::rust_begin_unwind
at /rustc/0000000000000000000000000000000000000000/library/std/src/panicking.rs:679:5
1: core::panicking::panic_fmt
at /rustc/0000000000000000000000000000000000000000/library/core/src/panicking.rs:80:14
2: core::result::unwrap_failed
at /rustc/0000000000000000000000000000000000000000/library/core/src/result.rs:1870:5
3: app::parse_port
at ./src/main.rs:14:10
4: app::main
at ./src/main.rs:3:16
5: core::ops::function::FnOnce::call_once
at /rustc/0000000000000000000000000000000000000000/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
";
fn main() {
if std::env::args().any(|arg| arg == "--panic") {
install_panic_hook();
let port: u16 = "eighty".parse().expect("port must be a number");
println!("{port}");
}
let shots = Shots::from_args();
shots.shot("quickstart", 70, |console| {
let diagnostic = Diagnostic::error("mismatched types")
.code("E0308")
.location(Location::new("src/main.rs", Some(12), Some(5)))
.cause("expected `u16`, found `&str`");
console.print(&diagnostic);
});
shots.shot("levels", 70, |console| {
console.print(&Diagnostic::error("cannot open `app.toml`"));
console.print(&Diagnostic::warning("`timeout` is deprecated"));
console.print(&Diagnostic::new("using 4 worker threads").level(Level::Info));
console.print(&Diagnostic::new("defaults came from /etc/acme").level(Level::Note));
console.print(&Diagnostic::new("run `acme check` to validate").level(Level::Help));
console.print(&Diagnostic::new("config rejected").code("CFG"));
});
shots.shot("snippet", 70, |console| {
let source = "[server]\nport = invalid\nhost = \"localhost\"\n";
let value = source.find("invalid").unwrap();
let key = source.find("port").unwrap();
let snippet = SourceSnippet::new("config.toml".into(), source.into(), value..value + 7, 1)
.expect("span on UTF-8 boundaries")
.primary_label("not a number")
.secondary(key..key + 4, "for this key")
.expect("span on UTF-8 boundaries");
let fix = Suggestion::replace("for example", source, value..value + 7, "8080")
.expect("span on UTF-8 boundaries");
let diagnostic = Diagnostic::error("invalid endpoint")
.code("CFG001")
.code_url("https://acme.dev/errors/CFG001")
.location(snippet.location())
.cause("port must be numeric")
.snippet(snippet)
.note("ports below 1024 need privileges")
.help("use a port from 1 to 65535")
.suggestion(fix)
.hyperlinker(Hyperlinker::new())
.view(EventView::Expanded); console.print(&diagnostic);
});
shots.shot("info", 70, |console| {
let error = ConfigError::BadPort("eighty".into());
console.print(&error.to_diagnostic().view(EventView::Expanded));
let io = std::io::Error::new(std::io::ErrorKind::NotFound, "no such file");
console.print(&ConfigError::from(io).to_diagnostic());
});
shots.shot("anyhow", 70, |console| {
use anyhow::Context;
let error = std::fs::read_to_string("/etc/acme/missing.toml")
.context("reading the config")
.context("starting the server")
.unwrap_err();
console.print(&Diagnostic::from_anyhow(&error, 8));
});
shots.shot("trace-python", 70, |console| {
let trace = stacktrace::parse(PYTHON_TRACE).expect("a Python traceback");
assert_eq!(trace.chain().count(), 2); let origin = trace.origin().expect("an application frame");
assert_eq!(origin.function.as_deref(), Some("main"));
console.print(&trace);
});
shots.shot("trace-rust", 70, |console| {
let trace = stacktrace::parse(RUST_PANIC).expect("a Rust panic");
console.print(&trace);
let _all = trace
.render_options()
.show_library(true)
.hyperlinker(Hyperlinker::disabled());
});
shots.shot("trace-custom", 70, |console| {
let parsers = Parsers::new().with_parser(TinyParser);
let text =
"tiny error: out of cheese\n at brew (src/pot.tiny:9)\n at main (src/main.tiny:2)";
let trace = parsers.parse(text).expect("TinyParser detects it");
let diagnostic = Diagnostic::error("worker crashed")
.trace(trace)
.view(EventView::Expanded); console.print(&diagnostic);
});
shots.shot("dashboard", 70, |console| {
use rich_ext::dashboard::DiagnosticsDashboard;
let at = |path: &str, line, column| Location::new(path, Some(line), Some(column));
let mut dashboard = DiagnosticsDashboard::new().top_codes(3);
dashboard
.push(
Diagnostic::error("mismatched types")
.code("E0308")
.location(at("src/main.rs", 12, 5)),
)
.push(
Diagnostic::warning("unused variable `x`")
.code("W1")
.location(at("src/main.rs", 3, 9)),
)
.push(
Diagnostic::warning("unused import")
.code("W1")
.location(at("src/lib.rs", 1, 5)),
)
.push(Diagnostic::new("see the migration guide").level(Level::Note));
console.print(&dashboard);
});
}