├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── pretty_gay_panic.png ├── scripts ├── citest └── test ├── src ├── lib.rs └── main.rs └── tests └── test_panic.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | checks: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - name: All checks 21 | run: sh scripts/citest 22 | 23 | coverage: 24 | 25 | runs-on: ubuntu-latest 26 | 27 | container: 28 | image: xd009642/tarpaulin:0.22.0-nightly 29 | 30 | options: --security-opt seccomp=unconfined 31 | 32 | steps: 33 | - uses: actions/checkout@v3 34 | 35 | - name: Generate code coverage 36 | run: | 37 | cargo +nightly tarpaulin --doc --tests --timeout 120 --out Xml 38 | 39 | - name: Upload to codecov.io 40 | uses: codecov/codecov-action@v2 41 | with: 42 | fail_ci_if_error: true 43 | 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | 4 | # Added by cargo 5 | # 6 | # already existing elements were commented out 7 | 8 | #/target 9 | /Cargo.lock 10 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gay_panic" 3 | version = "1.1.0" 4 | authors = ["Vi "] 5 | edition = "2024" 6 | description = "A Rust panic handler, but make it gay." 7 | 8 | homepage = "https://github.com/mistodon/gay_panic" 9 | repository = "https://github.com/mistodon/gay_panic" 10 | readme = "README.md" 11 | keywords = ["panic", "backtrace", "terminal"] 12 | categories = ["development-tools"] 13 | license = "CC0-1.0" 14 | 15 | [dependencies] 16 | color_space = "0.5" 17 | owo-colors = "4.2" 18 | sashimi = "0.1" 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gay_panic 2 | 3 | A Rust panic handler, but make it gay. 4 | 5 | 6 | [![Docs.rs](https://docs.rs/gay_panic/badge.svg)](https://docs.rs/gay_panic/1.1.0/gay_panic/) 7 | [![Crates.io](https://img.shields.io/crates/v/gay_panic.svg)](https://crates.io/crates/gay_panic) 8 | 9 | 10 | 11 | A panic handler that shows pretty backtraces: 12 | 13 | ```rust 14 | fn main() { 15 | use gay_panic::Config; 16 | 17 | gay_panic::init_with(Config { 18 | call_previous_hook: false, 19 | force_capture_backtrace: true, 20 | }); 21 | } 22 | ``` 23 | 24 | ![Rainbow backtrace](./pretty_gay_panic.png) 25 | -------------------------------------------------------------------------------- /pretty_gay_panic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mistodon/gay_panic/279fafe8eefb28b8ccadb45a2404bf29cbdd2893/pretty_gay_panic.png -------------------------------------------------------------------------------- /scripts/citest: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eux 4 | 5 | cargo check 6 | cargo test 7 | cargo fmt -- --check 8 | cargo clippy -- -D clippy::all 9 | cargo build 10 | -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eux 4 | set -o pipefail 5 | 6 | sh scripts/citest 7 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A Rust panic handler, but make it gay. 2 | 3 | use owo_colors::{AnsiColors, OwoColorize, Rgb, colors}; 4 | use sashimi::{LineBasedRules, Parser}; 5 | 6 | use std::{ 7 | backtrace::*, 8 | collections::HashMap, 9 | panic::{self, PanicInfo}, 10 | path::PathBuf, 11 | }; 12 | 13 | type Result = std::result::Result>; 14 | 15 | fn parse_int(parser: &mut Parser) -> Result { 16 | let n = parser.skip_matching(|ch| b"0123456789".contains(&ch)); 17 | let s = std::str::from_utf8(n)?; 18 | Ok(s.parse()?) 19 | } 20 | 21 | fn show_info(info: &PanicInfo) -> Result<()> { 22 | let thread = std::thread::current() 23 | .name() 24 | .map(str::to_owned) 25 | .unwrap_or_else(|| "unknown".to_owned()); 26 | 27 | let message = info.payload().downcast_ref::<&str>(); 28 | let loc = info.location().unwrap(); 29 | let at_sep = if message.is_some() { " at " } else { "" }; 30 | eprintln!( 31 | "\nthread '{thread}' panicked{at_sep}'{message}', {fname}:{line}:{col}:\n", 32 | thread = thread.fg::(), 33 | at_sep = at_sep, 34 | message = message.unwrap_or(&"").fg::(), 35 | fname = loc.file().fg::(), 36 | line = loc.line().fg::(), 37 | col = loc.column().fg::(), 38 | ); 39 | 40 | Ok(()) 41 | } 42 | 43 | fn show_backtrace(info: &PanicInfo, crate_name: String, backtrace: &Backtrace) -> Result<()> { 44 | show_info(info)?; 45 | 46 | let source = backtrace.to_string(); 47 | if source.starts_with("disabled backtrace") { 48 | eprintln!("{}", source.fg::()); 49 | return Ok(()); 50 | } 51 | 52 | let frame_count = source.lines().count() / 2; 53 | 54 | let parser = &mut Parser::::new(&source); 55 | parser.skip_whitespace(); 56 | 57 | let crate_colors = [ 58 | AnsiColors::BrightGreen, 59 | AnsiColors::BrightCyan, 60 | AnsiColors::Cyan, 61 | AnsiColors::BrightBlue, 62 | AnsiColors::BrightMagenta, 63 | ]; 64 | let mut crate_count = 0; 65 | let mut crate_colormap = HashMap::new(); 66 | crate_colormap.insert("std".to_owned(), AnsiColors::BrightRed); 67 | crate_colormap.insert("rust_begin_unwind".to_owned(), AnsiColors::BrightRed); 68 | crate_colormap.insert("core".to_owned(), AnsiColors::Blue); 69 | crate_colormap.insert(crate_name, AnsiColors::BrightYellow); 70 | 71 | while !parser.finished() { 72 | let frame_number = parse_int(parser)?; 73 | parser.expect(b":")?; 74 | let callpath = parse_path(parser)?; 75 | 76 | let frame_col = { 77 | use color_space::{Hsv, Rgb}; 78 | 79 | let hue = frame_number as f64 / frame_count as f64; 80 | let frame_col = Hsv::new(hue * 360., 0.9, 1.); 81 | Rgb::from(frame_col) 82 | }; 83 | 84 | let frame_col = Rgb(frame_col.r as u8, frame_col.g as u8, frame_col.b as u8); 85 | 86 | if parser.finished() { 87 | eprintln!( 88 | " {frame_number: >2}. {crate_name}", 89 | frame_number = frame_number.color(frame_col), 90 | crate_name = callpath.first.color(frame_col), 91 | ); 92 | break; 93 | } 94 | 95 | parser.expect(b"at")?; 96 | 97 | let filename = 98 | unsafe { std::str::from_utf8_unchecked(parser.skip_matching(|ch| ch != b':')) }; 99 | let path = PathBuf::from(&filename); 100 | let components = path.components().collect::>(); 101 | let fname = components 102 | .last() 103 | .unwrap() 104 | .as_os_str() 105 | .to_string_lossy() 106 | .into_owned(); 107 | let modname = if components.len() > 1 && fname == "mod.rs" { 108 | components.get(components.len() - 2) 109 | } else { 110 | None 111 | }; 112 | let fsep = if modname.is_some() { "/" } else { "" }; 113 | let modname = modname 114 | .map(|x| x.as_os_str().to_string_lossy().into_owned()) 115 | .unwrap_or_default(); 116 | 117 | parser.expect(b":")?; 118 | let line_num = parse_int(parser)?; 119 | parser.expect(b":")?; 120 | let col_num = parse_int(parser)?; 121 | 122 | let [crate_name, sep1, scope, sep2, fn_name] = callpath.parts(); 123 | 124 | let crate_col = crate_colormap 125 | .entry(crate_name.to_owned()) 126 | .or_insert_with(|| { 127 | let col = crate_colors[crate_count]; 128 | crate_count = (crate_count + 1) % crate_colors.len(); 129 | col 130 | }); 131 | 132 | eprintln!( 133 | " {frame_number: >2}. {crate_name}{sep1}{scope}{sep2}{fn_name}\n at {modname}{fsep}{fname}:{line}:{col}\n", 134 | frame_number = frame_number.color(frame_col), 135 | crate_name = crate_name.color(*crate_col), 136 | sep1 = sep1, 137 | scope = scope.fg::(), 138 | sep2 = sep2, 139 | fn_name = fn_name.fg::(), 140 | modname = modname.fg::(), 141 | fsep = fsep.fg::(), 142 | fname = fname.fg::(), 143 | line = line_num.fg::(), 144 | col = col_num.fg::(), 145 | ); 146 | 147 | parser.expect(b"\n")?; 148 | } 149 | 150 | Ok(()) 151 | } 152 | 153 | struct CallPath { 154 | first: String, 155 | scope: Option, 156 | last: Option, 157 | } 158 | 159 | impl CallPath { 160 | fn from_components(components: Vec) -> Self { 161 | let first = components.first().unwrap().to_owned(); 162 | let mut scope = None; 163 | let mut last = None; 164 | 165 | let mut weird_fn_name = false; 166 | 167 | if components.len() > 1 { 168 | let name = components.last().unwrap().to_owned(); 169 | weird_fn_name = name.starts_with('{') || name.starts_with('<'); 170 | last = Some(name); 171 | } 172 | 173 | if components.len() > 2 { 174 | let name = components.get(components.len() - 2).unwrap().to_owned(); 175 | let in_struct = name.chars().next().unwrap().is_uppercase(); 176 | 177 | if in_struct || weird_fn_name { 178 | scope = Some(name); 179 | } 180 | } 181 | 182 | CallPath { first, scope, last } 183 | } 184 | 185 | fn parts(&self) -> [&str; 5] { 186 | [ 187 | &self.first, 188 | if self.scope.is_some() || self.last.is_some() { 189 | " :: " 190 | } else { 191 | "" 192 | }, 193 | self.scope.as_deref().unwrap_or(""), 194 | if self.scope.is_some() { "::" } else { "" }, 195 | self.last.as_deref().unwrap_or(""), 196 | ] 197 | } 198 | } 199 | 200 | fn parse_path(parser: &mut Parser) -> Result { 201 | fn s(b: &[u8]) -> String { 202 | String::from_utf8_lossy(b).into_owned() 203 | } 204 | 205 | let mut parts = vec![]; 206 | let mut first = true; 207 | while first || parser.skip(b"::").is_some() { 208 | if let Some(bytes) = parser.skip(b"{{closure}}") { 209 | parts.push(s(bytes)); 210 | } else if parser.check(b"<").is_some() { 211 | parts.push(s(parser.skip_around(b'<')?)); 212 | } else { 213 | let mut ident = s(parser.expect_ident()?); 214 | if parser.check(b"<").is_some() { 215 | let generics = parser.skip_around(b'<')?; 216 | ident.push_str(&s(generics)); 217 | } 218 | if parser.check(b"{").is_some() { 219 | let shim = parser.skip_around(b'{')?; 220 | ident.push_str(&s(shim)); 221 | } 222 | parts.push(ident); 223 | } 224 | 225 | first = false; 226 | } 227 | 228 | parser.expect(b"\n")?; 229 | 230 | Ok(CallPath::from_components(parts)) 231 | } 232 | 233 | /// Configuration for the panic handler. 234 | /// 235 | /// # Examples 236 | /// ```rust 237 | /// use gay_panic::Config; 238 | /// 239 | /// assert_eq!( 240 | /// Config::default(), 241 | /// Config { 242 | /// call_previous_hook: false, 243 | /// force_capture_backtrace: false, 244 | /// } 245 | /// ); 246 | /// ``` 247 | #[derive(Debug, Default, Clone, PartialEq, Eq)] 248 | pub struct Config { 249 | /// If `true`, the existing/default panic handler will be called after `gay_panic`'s 250 | /// executes. 251 | pub call_previous_hook: bool, 252 | 253 | /// If `true`, a backtrace will be displayed regardless of environment variables set. 254 | pub force_capture_backtrace: bool, 255 | } 256 | 257 | /// Replaces the current panic hook with a more colorful one. Allows some configuration. 258 | /// 259 | /// # Examples 260 | /// 261 | /// ```rust 262 | /// use gay_panic::Config; 263 | /// 264 | /// gay_panic::init_with(Config { 265 | /// call_previous_hook: false, 266 | /// force_capture_backtrace: true, 267 | /// }); 268 | /// ``` 269 | pub fn init_with(config: Config) { 270 | let crate_name = PathBuf::from(module_path!()) 271 | .iter() 272 | .next() 273 | .unwrap() 274 | .to_string_lossy() 275 | .into_owned(); 276 | let hook = panic::take_hook(); 277 | panic::set_hook(Box::new(move |info| { 278 | let backtrace = if config.force_capture_backtrace { 279 | Backtrace::force_capture() 280 | } else { 281 | Backtrace::capture() 282 | }; 283 | match show_backtrace(info, crate_name.clone(), &backtrace) { 284 | Ok(()) => (), 285 | Err(e) => eprintln!("Error formatting backtrace: {e}"), 286 | } 287 | 288 | if config.call_previous_hook { 289 | hook(info); 290 | } 291 | })); 292 | } 293 | 294 | /// Replaces the current panic hook with a more colorful one. Uses default `Config`. 295 | /// 296 | /// # Examples 297 | /// 298 | /// ```rust 299 | /// gay_panic::init(); 300 | /// ``` 301 | pub fn init() { 302 | init_with(Config::default()); 303 | } 304 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | gay_panic::init_with(gay_panic::Config { 3 | call_previous_hook: false, 4 | force_capture_backtrace: true, 5 | }); 6 | 7 | let x: Option<()> = None; 8 | x.map(|_| std::fs::read_to_string("").unwrap()).unwrap(); 9 | } 10 | -------------------------------------------------------------------------------- /tests/test_panic.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | #[should_panic] // NOTE: Comment out to view output 3 | fn test_panic() { 4 | gay_panic::init_with(gay_panic::Config { 5 | call_previous_hook: false, 6 | force_capture_backtrace: true, 7 | }); 8 | 9 | let x: Option<()> = None; 10 | x.map(|_| std::fs::read_to_string("").unwrap()).unwrap(); 11 | } 12 | --------------------------------------------------------------------------------