├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── backtracetk.toml ├── examples ├── assert_failed.rs ├── panic_macro.rs └── unwrap_result.rs ├── macros ├── Cargo.lock ├── Cargo.toml └── src │ └── lib.rs ├── screenshot.png └── src ├── config.rs ├── lib.rs ├── main.rs ├── partial.rs └── render.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anstream" 16 | version = "0.6.14" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" 19 | dependencies = [ 20 | "anstyle", 21 | "anstyle-parse", 22 | "anstyle-query", 23 | "anstyle-wincon", 24 | "colorchoice", 25 | "is_terminal_polyfill", 26 | "utf8parse", 27 | ] 28 | 29 | [[package]] 30 | name = "anstyle" 31 | version = "1.0.7" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" 34 | 35 | [[package]] 36 | name = "anstyle-parse" 37 | version = "0.2.4" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" 40 | dependencies = [ 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-query" 46 | version = "1.1.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" 49 | dependencies = [ 50 | "windows-sys 0.52.0", 51 | ] 52 | 53 | [[package]] 54 | name = "anstyle-wincon" 55 | version = "3.0.3" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" 58 | dependencies = [ 59 | "anstyle", 60 | "windows-sys 0.52.0", 61 | ] 62 | 63 | [[package]] 64 | name = "anyhow" 65 | version = "1.0.86" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" 68 | 69 | [[package]] 70 | name = "backtracetk" 71 | version = "0.1.0" 72 | dependencies = [ 73 | "anstream", 74 | "anstyle", 75 | "anyhow", 76 | "clap", 77 | "home", 78 | "macros", 79 | "regex", 80 | "serde", 81 | "termion", 82 | "toml", 83 | ] 84 | 85 | [[package]] 86 | name = "bitflags" 87 | version = "1.3.2" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 90 | 91 | [[package]] 92 | name = "bitflags" 93 | version = "2.6.0" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 96 | 97 | [[package]] 98 | name = "clap" 99 | version = "4.5.8" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "84b3edb18336f4df585bc9aa31dd99c036dfa5dc5e9a2939a722a188f3a8970d" 102 | dependencies = [ 103 | "clap_builder", 104 | "clap_derive", 105 | ] 106 | 107 | [[package]] 108 | name = "clap_builder" 109 | version = "4.5.8" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "c1c09dd5ada6c6c78075d6fd0da3f90d8080651e2d6cc8eb2f1aaa4034ced708" 112 | dependencies = [ 113 | "anstream", 114 | "anstyle", 115 | "clap_lex", 116 | "strsim", 117 | "terminal_size", 118 | ] 119 | 120 | [[package]] 121 | name = "clap_derive" 122 | version = "4.5.8" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" 125 | dependencies = [ 126 | "heck", 127 | "proc-macro2", 128 | "quote", 129 | "syn", 130 | ] 131 | 132 | [[package]] 133 | name = "clap_lex" 134 | version = "0.7.1" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" 137 | 138 | [[package]] 139 | name = "colorchoice" 140 | version = "1.0.1" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" 143 | 144 | [[package]] 145 | name = "equivalent" 146 | version = "1.0.1" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 149 | 150 | [[package]] 151 | name = "errno" 152 | version = "0.3.9" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 155 | dependencies = [ 156 | "libc", 157 | "windows-sys 0.52.0", 158 | ] 159 | 160 | [[package]] 161 | name = "hashbrown" 162 | version = "0.14.5" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 165 | 166 | [[package]] 167 | name = "heck" 168 | version = "0.5.0" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 171 | 172 | [[package]] 173 | name = "home" 174 | version = "0.5.9" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" 177 | dependencies = [ 178 | "windows-sys 0.52.0", 179 | ] 180 | 181 | [[package]] 182 | name = "indexmap" 183 | version = "2.2.6" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" 186 | dependencies = [ 187 | "equivalent", 188 | "hashbrown", 189 | ] 190 | 191 | [[package]] 192 | name = "is_terminal_polyfill" 193 | version = "1.70.0" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" 196 | 197 | [[package]] 198 | name = "libc" 199 | version = "0.2.155" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 202 | 203 | [[package]] 204 | name = "libredox" 205 | version = "0.0.2" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "3af92c55d7d839293953fcd0fda5ecfe93297cfde6ffbdec13b41d99c0ba6607" 208 | dependencies = [ 209 | "bitflags 2.6.0", 210 | "libc", 211 | "redox_syscall", 212 | ] 213 | 214 | [[package]] 215 | name = "linux-raw-sys" 216 | version = "0.4.14" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 219 | 220 | [[package]] 221 | name = "macros" 222 | version = "0.1.0" 223 | dependencies = [ 224 | "proc-macro2", 225 | "quote", 226 | "syn", 227 | "synstructure", 228 | ] 229 | 230 | [[package]] 231 | name = "memchr" 232 | version = "2.7.4" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 235 | 236 | [[package]] 237 | name = "numtoa" 238 | version = "0.1.0" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" 241 | 242 | [[package]] 243 | name = "proc-macro2" 244 | version = "1.0.86" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 247 | dependencies = [ 248 | "unicode-ident", 249 | ] 250 | 251 | [[package]] 252 | name = "quote" 253 | version = "1.0.36" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 256 | dependencies = [ 257 | "proc-macro2", 258 | ] 259 | 260 | [[package]] 261 | name = "redox_syscall" 262 | version = "0.4.1" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 265 | dependencies = [ 266 | "bitflags 1.3.2", 267 | ] 268 | 269 | [[package]] 270 | name = "redox_termios" 271 | version = "0.1.3" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "20145670ba436b55d91fc92d25e71160fbfbdd57831631c8d7d36377a476f1cb" 274 | 275 | [[package]] 276 | name = "regex" 277 | version = "1.10.5" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" 280 | dependencies = [ 281 | "aho-corasick", 282 | "memchr", 283 | "regex-automata", 284 | "regex-syntax", 285 | ] 286 | 287 | [[package]] 288 | name = "regex-automata" 289 | version = "0.4.7" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" 292 | dependencies = [ 293 | "aho-corasick", 294 | "memchr", 295 | "regex-syntax", 296 | ] 297 | 298 | [[package]] 299 | name = "regex-syntax" 300 | version = "0.8.4" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" 303 | 304 | [[package]] 305 | name = "rustix" 306 | version = "0.38.34" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" 309 | dependencies = [ 310 | "bitflags 2.6.0", 311 | "errno", 312 | "libc", 313 | "linux-raw-sys", 314 | "windows-sys 0.52.0", 315 | ] 316 | 317 | [[package]] 318 | name = "serde" 319 | version = "1.0.203" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" 322 | dependencies = [ 323 | "serde_derive", 324 | ] 325 | 326 | [[package]] 327 | name = "serde_derive" 328 | version = "1.0.203" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" 331 | dependencies = [ 332 | "proc-macro2", 333 | "quote", 334 | "syn", 335 | ] 336 | 337 | [[package]] 338 | name = "serde_spanned" 339 | version = "0.6.6" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" 342 | dependencies = [ 343 | "serde", 344 | ] 345 | 346 | [[package]] 347 | name = "strsim" 348 | version = "0.11.1" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 351 | 352 | [[package]] 353 | name = "syn" 354 | version = "2.0.69" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "201fcda3845c23e8212cd466bfebf0bd20694490fc0356ae8e428e0824a915a6" 357 | dependencies = [ 358 | "proc-macro2", 359 | "quote", 360 | "unicode-ident", 361 | ] 362 | 363 | [[package]] 364 | name = "synstructure" 365 | version = "0.13.1" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 368 | dependencies = [ 369 | "proc-macro2", 370 | "quote", 371 | "syn", 372 | ] 373 | 374 | [[package]] 375 | name = "terminal_size" 376 | version = "0.3.0" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" 379 | dependencies = [ 380 | "rustix", 381 | "windows-sys 0.48.0", 382 | ] 383 | 384 | [[package]] 385 | name = "termion" 386 | version = "4.0.2" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "1ccce68e518d1173e80876edd54760b60b792750d0cab6444a79101c6ea03848" 389 | dependencies = [ 390 | "libc", 391 | "libredox", 392 | "numtoa", 393 | "redox_termios", 394 | ] 395 | 396 | [[package]] 397 | name = "toml" 398 | version = "0.8.14" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" 401 | dependencies = [ 402 | "serde", 403 | "serde_spanned", 404 | "toml_datetime", 405 | "toml_edit", 406 | ] 407 | 408 | [[package]] 409 | name = "toml_datetime" 410 | version = "0.6.6" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" 413 | dependencies = [ 414 | "serde", 415 | ] 416 | 417 | [[package]] 418 | name = "toml_edit" 419 | version = "0.22.14" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" 422 | dependencies = [ 423 | "indexmap", 424 | "serde", 425 | "serde_spanned", 426 | "toml_datetime", 427 | "winnow", 428 | ] 429 | 430 | [[package]] 431 | name = "unicode-ident" 432 | version = "1.0.12" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 435 | 436 | [[package]] 437 | name = "utf8parse" 438 | version = "0.2.2" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 441 | 442 | [[package]] 443 | name = "windows-sys" 444 | version = "0.48.0" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 447 | dependencies = [ 448 | "windows-targets 0.48.5", 449 | ] 450 | 451 | [[package]] 452 | name = "windows-sys" 453 | version = "0.52.0" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 456 | dependencies = [ 457 | "windows-targets 0.52.6", 458 | ] 459 | 460 | [[package]] 461 | name = "windows-targets" 462 | version = "0.48.5" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 465 | dependencies = [ 466 | "windows_aarch64_gnullvm 0.48.5", 467 | "windows_aarch64_msvc 0.48.5", 468 | "windows_i686_gnu 0.48.5", 469 | "windows_i686_msvc 0.48.5", 470 | "windows_x86_64_gnu 0.48.5", 471 | "windows_x86_64_gnullvm 0.48.5", 472 | "windows_x86_64_msvc 0.48.5", 473 | ] 474 | 475 | [[package]] 476 | name = "windows-targets" 477 | version = "0.52.6" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 480 | dependencies = [ 481 | "windows_aarch64_gnullvm 0.52.6", 482 | "windows_aarch64_msvc 0.52.6", 483 | "windows_i686_gnu 0.52.6", 484 | "windows_i686_gnullvm", 485 | "windows_i686_msvc 0.52.6", 486 | "windows_x86_64_gnu 0.52.6", 487 | "windows_x86_64_gnullvm 0.52.6", 488 | "windows_x86_64_msvc 0.52.6", 489 | ] 490 | 491 | [[package]] 492 | name = "windows_aarch64_gnullvm" 493 | version = "0.48.5" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 496 | 497 | [[package]] 498 | name = "windows_aarch64_gnullvm" 499 | version = "0.52.6" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 502 | 503 | [[package]] 504 | name = "windows_aarch64_msvc" 505 | version = "0.48.5" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 508 | 509 | [[package]] 510 | name = "windows_aarch64_msvc" 511 | version = "0.52.6" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 514 | 515 | [[package]] 516 | name = "windows_i686_gnu" 517 | version = "0.48.5" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 520 | 521 | [[package]] 522 | name = "windows_i686_gnu" 523 | version = "0.52.6" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 526 | 527 | [[package]] 528 | name = "windows_i686_gnullvm" 529 | version = "0.52.6" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 532 | 533 | [[package]] 534 | name = "windows_i686_msvc" 535 | version = "0.48.5" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 538 | 539 | [[package]] 540 | name = "windows_i686_msvc" 541 | version = "0.52.6" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 544 | 545 | [[package]] 546 | name = "windows_x86_64_gnu" 547 | version = "0.48.5" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 550 | 551 | [[package]] 552 | name = "windows_x86_64_gnu" 553 | version = "0.52.6" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 556 | 557 | [[package]] 558 | name = "windows_x86_64_gnullvm" 559 | version = "0.48.5" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 562 | 563 | [[package]] 564 | name = "windows_x86_64_gnullvm" 565 | version = "0.52.6" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 568 | 569 | [[package]] 570 | name = "windows_x86_64_msvc" 571 | version = "0.48.5" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 574 | 575 | [[package]] 576 | name = "windows_x86_64_msvc" 577 | version = "0.52.6" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 580 | 581 | [[package]] 582 | name = "winnow" 583 | version = "0.6.13" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" 586 | dependencies = [ 587 | "memchr", 588 | ] 589 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "backtracetk" 4 | version = "0.1.0" 5 | 6 | [dependencies] 7 | anstream = "0.6.14" 8 | anstyle = "1.0.7" 9 | anyhow = "1.0.86" 10 | clap = { version = "4.5.8", features = ["derive", "wrap_help"] } 11 | home = "0.5.9" 12 | macros = { path = "macros" } 13 | regex = "1.10.5" 14 | serde = { version = "1.0.203", features = ["derive"] } 15 | termion = "4.0.2" 16 | toml = "0.8.14" 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Backtracetk 2 | 3 | Backtracetk is a command-line tool that prints colorized Rust backtraces without needing extra dependencies. 4 | It works by capturing the output of a Rust binary, detecting anything that looks like a backtrace, and then printing it with colors to make it easier on the eyes. 5 | Additionally, it displays code snippets if available in the filesystem and offers configurable options to hide specific frames. 6 | 7 | Backtracetk is useful in situations where you can't or don't want to add runtime dependencies. 8 | It is thus more "dynamic", allowing you to run the process many times (assuming it's cheap to do so) and adjust the output accordingly without the need to recompile your code. 9 | 10 | If you're ok with adding dependencies, consider looking at [color-eyre](https://crates.io/crates/color-eyre) or [color-backtrace](https://crates.io/crates/color-backtrace). 11 | 12 | I've only tested this on Linux and primarily within a single project. 13 | If you try it and encounter any issues, please share the output of your process. 14 | 15 | ## Installation 16 | 17 | ```bash 18 | cargo install --git https://github.com/nilehmann/backtracetk 19 | ``` 20 | 21 | ## Screenshot 22 | 23 | ![Screenshot](./screenshot.png) 24 | 25 | ## Usage 26 | 27 | ```bash 28 | $ backtracetk --help 29 | Print colorized Rust backtraces by capturing the output of an external process 30 | 31 | Usage: backtracetk [OPTIONS] [CMD]... 32 | 33 | Arguments: 34 | [CMD]... 35 | 36 | Options: 37 | --print-config Print the current detected configuration 38 | --print-default-config Print the default configuration used when no configuration files are detected 39 | -h, --help Print help 40 | 41 | ``` 42 | 43 | ### Configuration 44 | 45 | Backtracetk can be configured using a TOML file named `backtracetk.toml` or `.backtracetk.toml`. 46 | It searches for a *global* configuration file in your home directory and a *local* configuration file in the parent directories starting from the current working directory. The local configuration will override the global configuration where they overlap. 47 | 48 | Below is a sample configuration: 49 | 50 | ```toml 51 | # Backtracetk Configuration File 52 | 53 | # `style` sets the backtrace detail level. 54 | # Options: 55 | # - "short" (default): Sets `RUST_BACKTRACE=1` 56 | # - "full": Sets `RUST_BACKTRACE=full` 57 | style = "short" 58 | 59 | # `echo` controls whether backtracetk echoes captured lines. 60 | # - true (default): Captured lines are printed as they are read 61 | # - false: Suppresses output until the program exits 62 | echo = true 63 | 64 | # `env` allows specifying additional environment variables for the child process. 65 | [env] 66 | CLICOLOR_FORCE = "1" # e.g., try forcing ANSI colors 67 | RUST_LIB_BACKTRACE = "0" # e.g., disable lib backtrace 68 | 69 | # `hyperlinks` configures the mission of hyperlinks for file paths in the backtrace output. 70 | [hyperlinks] 71 | enabled = true # Enable or disable hyperlinking. 72 | url = "vscode://file${FILE_PATH}:${LINE}:${COLUMN}" # Template for generating file links. 73 | 74 | # `hide` sections define rules to exclude specific frames from the backtrace output. 75 | # Frames can be hidden based on regex patterns or ranges between start and end patterns. 76 | 77 | # Hide frames matching a specific regex pattern. 78 | [[hide]] 79 | pattern = "panic_macro::fn2" # Regex pattern to match frames for exclusion. 80 | 81 | # Hide frames within a range defined by start and end regex patterns. 82 | [[hide]] 83 | begin = "core::panicking" # Start pattern. 84 | end = "rust_begin_unwind" # End pattern (optional). If omitted, hides all subsequent frames. 85 | ``` 86 | -------------------------------------------------------------------------------- /backtracetk.toml: -------------------------------------------------------------------------------- 1 | # Sample Configuration File 2 | 3 | # `style` sets the backtrace detail level. 4 | # Options: 5 | # - "short" (default): Sets `RUST_BACKTRACE=1` 6 | # - "full": Sets `RUST_BACKTRACE=full` 7 | style = "short" 8 | 9 | # `echo` controls whether backtracetk echoes captured lines. 10 | # - true (default): Captured lines are printed as they are read 11 | # - false: Suppresses output until the program exits 12 | echo = true 13 | 14 | # `env` allows specifying additional environment variables for the child process. 15 | [env] 16 | CLICOLOR_FORCE = "1" # e.g., try forcing ANSI colors 17 | RUST_LIB_BACKTRACE = "0" # e.g., disable lib backtrace 18 | 19 | # `hyperlinks` configures the mission of hyperlinks for file paths in the backtrace output. 20 | [hyperlinks] 21 | enabled = true # Enable or disable hyperlinking. 22 | url = "vscode://file${FILE_PATH}:${LINE}:${COLUMN}" # Template for generating file links. 23 | 24 | # `hide` sections define rules to exclude specific frames from the backtrace output. 25 | # Frames can be hidden based on regex patterns or ranges between start and end patterns. 26 | 27 | # Hide frames matching a specific regex pattern. 28 | [[hide]] 29 | pattern = "panic_macro::fn2" # Regex pattern to match frames for exclusion. 30 | 31 | # Hide frames within a range defined by start and end regex patterns. 32 | [[hide]] 33 | begin = "core::panicking" # Start pattern. 34 | end = "rust_begin_unwind" # End pattern (optional). If omitted, hides all subsequent frames. 35 | -------------------------------------------------------------------------------- /examples/assert_failed.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | assert_eq!(1, 2); 3 | } 4 | -------------------------------------------------------------------------------- /examples/panic_macro.rs: -------------------------------------------------------------------------------- 1 | fn fn3() { 2 | let blah = 123; 3 | panic!("{}", blah); 4 | } 5 | 6 | fn fn2() { 7 | fn3(); 8 | } 9 | 10 | fn main() { 11 | fn2(); 12 | } 13 | -------------------------------------------------------------------------------- /examples/unwrap_result.rs: -------------------------------------------------------------------------------- 1 | fn fn3() { 2 | fn4(); // Source printing at start the of a file ... 3 | } 4 | 5 | fn fn2() { 6 | // sdfsdf 7 | let _dead = 1 + 4; 8 | fn3(); 9 | let _fsdf = "sdfsdf"; 10 | let _fsgg = 2 + 5; 11 | } 12 | 13 | fn fn1() { 14 | fn2(); 15 | } 16 | 17 | fn main() { 18 | fn1(); 19 | } 20 | 21 | fn fn4() { 22 | // Source printing at the end of a file 23 | "x".parse::().unwrap(); 24 | } 25 | -------------------------------------------------------------------------------- /macros/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "macros" 7 | version = "0.1.0" 8 | dependencies = [ 9 | "proc-macro2", 10 | "quote", 11 | "syn", 12 | "synstructure", 13 | ] 14 | 15 | [[package]] 16 | name = "proc-macro2" 17 | version = "1.0.86" 18 | source = "registry+https://github.com/rust-lang/crates.io-index" 19 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 20 | dependencies = [ 21 | "unicode-ident", 22 | ] 23 | 24 | [[package]] 25 | name = "quote" 26 | version = "1.0.36" 27 | source = "registry+https://github.com/rust-lang/crates.io-index" 28 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 29 | dependencies = [ 30 | "proc-macro2", 31 | ] 32 | 33 | [[package]] 34 | name = "syn" 35 | version = "2.0.69" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "201fcda3845c23e8212cd466bfebf0bd20694490fc0356ae8e428e0824a915a6" 38 | dependencies = [ 39 | "proc-macro2", 40 | "quote", 41 | "unicode-ident", 42 | ] 43 | 44 | [[package]] 45 | name = "synstructure" 46 | version = "0.13.1" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 49 | dependencies = [ 50 | "proc-macro2", 51 | "quote", 52 | "syn", 53 | ] 54 | 55 | [[package]] 56 | name = "unicode-ident" 57 | version = "1.0.12" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 60 | -------------------------------------------------------------------------------- /macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "macros" 4 | version = "0.1.0" 5 | 6 | [lib] 7 | proc-macro = true 8 | 9 | [dependencies] 10 | proc-macro2 = "1.0.86" 11 | quote = "1.0.36" 12 | syn = "2.0.69" 13 | synstructure = "0.13.1" 14 | -------------------------------------------------------------------------------- /macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use quote::quote; 3 | use syn::{Data, Error, Ident}; 4 | use synstructure::Structure; 5 | 6 | synstructure::decl_derive!([Partial, attributes(complete)] => partial_derive); 7 | 8 | synstructure::decl_derive!([Complete] => complete_derive); 9 | 10 | synstructure::decl_derive!([Partialize] => partialize_derive); 11 | 12 | fn partial_derive(s: Structure) -> TokenStream { 13 | partial_derive_inner(s).unwrap_or_else(|err| err.to_compile_error()) 14 | } 15 | 16 | fn partial_derive_inner(s: Structure) -> syn::Result { 17 | let data = check_is_struct("Partial", &s)?; 18 | 19 | let complete_ident = find_complete_attr(&s.ast().attrs)?.parse_args::()?; 20 | 21 | let merge_with_body: TokenStream = iter_fields(data) 22 | .map(|(f, _)| quote! { #f: other.#f.merge_with(self.#f), }) 23 | .collect(); 24 | 25 | let into_complete_body: TokenStream = iter_fields(data) 26 | .map(|(f, _)| quote! { #f: self.#f.into_complete(), }) 27 | .collect(); 28 | 29 | Ok(s.gen_impl(quote! { 30 | gen impl crate::partial::Partial for @Self { 31 | type Complete = #complete_ident; 32 | 33 | fn merge_with(mut self, other: Self) -> Self { 34 | Self { #merge_with_body } 35 | } 36 | 37 | fn into_complete(self) -> Self::Complete { 38 | #complete_ident { #into_complete_body } 39 | } 40 | } 41 | })) 42 | } 43 | 44 | fn find_complete_attr(attrs: &[syn::Attribute]) -> syn::Result<&syn::Attribute> { 45 | attrs 46 | .iter() 47 | .find(|attr| attr.path().is_ident("complete")) 48 | .ok_or_else(|| Error::new(Span::call_site(), "missing complete attr")) 49 | } 50 | 51 | fn complete_derive(s: Structure) -> TokenStream { 52 | s.gen_impl(quote! { 53 | gen impl crate::partial::Complete for @Self { 54 | type Partial = Option; 55 | 56 | fn into_partial(self) -> Option { 57 | Some(self) 58 | } 59 | } 60 | }) 61 | } 62 | 63 | fn partialize_derive(s: Structure) -> TokenStream { 64 | partialize_derive_inner(s).unwrap_or_else(|err| err.to_compile_error()) 65 | } 66 | 67 | fn partialize_derive_inner(s: Structure) -> syn::Result { 68 | let data = check_is_struct("Partialize", &s)?; 69 | 70 | let ident = &s.ast().ident; 71 | let partial_ident = Ident::new(&format!("Partial{ident}"), Span::call_site()); 72 | let partial_fields: TokenStream = iter_fields(data) 73 | .map(|(f, ty)| quote! { #f: <#ty as crate::partial::Complete>::Partial, }) 74 | .collect(); 75 | let into_partial_fields: TokenStream = iter_fields(data) 76 | .map(|(f, ty)| quote! { #f: <#ty as crate::partial::Complete>::into_partial(self.#f), }) 77 | .collect(); 78 | 79 | let complete_impl = s.gen_impl(quote! { 80 | gen impl crate::partial::Complete for @Self { 81 | type Partial = #partial_ident; 82 | 83 | fn into_partial(self) -> #partial_ident { 84 | #partial_ident { #into_partial_fields } 85 | } 86 | } 87 | }); 88 | 89 | Ok(quote! { 90 | #[derive(Default, Deserialize, macros::Partial)] 91 | #[complete(#ident)] 92 | #[serde(default)] 93 | pub struct #partial_ident { 94 | #partial_fields 95 | } 96 | 97 | #complete_impl 98 | }) 99 | } 100 | 101 | fn iter_fields(data: &syn::DataStruct) -> impl Iterator { 102 | data.fields 103 | .iter() 104 | .enumerate() 105 | .map(|(i, fld)| match fld.ident.as_ref() { 106 | Some(ident) => (ident.clone(), &fld.ty), 107 | None => (Ident::new(&format!("{i}"), Span::call_site()), &fld.ty), 108 | }) 109 | } 110 | 111 | fn check_is_struct<'a>(trait_: &str, s: &'a Structure) -> syn::Result<&'a syn::DataStruct> { 112 | match &s.ast().data { 113 | Data::Struct(data) => Ok(data), 114 | _ => Err(Error::new_spanned( 115 | s.ast(), 116 | format!("{trait_} can only be derived for structs"), 117 | )), 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilehmann/backtracetk/2dfc9d9a554ff8fb672e4cf357dd08d652b30b84/screenshot.png -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use core::fmt; 2 | 3 | use std::{ 4 | collections::HashMap, 5 | fs, 6 | io::Read, 7 | path::{Path, PathBuf}, 8 | }; 9 | 10 | use macros::{Complete, Partialize}; 11 | use regex::Regex; 12 | use serde::{ser::SerializeMap, Deserialize, Serialize}; 13 | 14 | use crate::partial::{Complete, Partial}; 15 | 16 | #[derive(Serialize, Partialize, Debug)] 17 | pub struct Config { 18 | pub style: BacktraceStyle, 19 | pub echo: Echo, 20 | pub hyperlinks: HyperLinks, 21 | pub env: HashMap, 22 | pub hide: Vec, 23 | } 24 | 25 | impl Config { 26 | pub fn read() -> anyhow::Result { 27 | PartialConfig::read().map(PartialConfig::into_complete) 28 | } 29 | } 30 | 31 | impl fmt::Display for Config { 32 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 33 | write!(f, "{}", toml::to_string_pretty(self).unwrap()) 34 | } 35 | } 36 | 37 | impl Default for Config { 38 | fn default() -> Self { 39 | Self { 40 | style: Default::default(), 41 | hide: vec![Hide::Range { 42 | begin: Regex::new("core::panicking::panic_explicit").unwrap(), 43 | end: None, 44 | }], 45 | env: Default::default(), 46 | echo: Default::default(), 47 | hyperlinks: Default::default(), 48 | } 49 | } 50 | } 51 | 52 | #[derive(Serialize, Partialize, Debug)] 53 | pub struct HyperLinks { 54 | pub enabled: bool, 55 | pub url: String, 56 | } 57 | 58 | impl HyperLinks { 59 | pub fn render(&self, file: &str, line: usize, col: usize) -> String { 60 | self.url 61 | .replace("${LINE}", &format!("{line}")) 62 | .replace("${COLUMN}", &format!("{col}")) 63 | .replace("${FILE_PATH}", file) 64 | } 65 | } 66 | 67 | impl Default for HyperLinks { 68 | fn default() -> Self { 69 | Self { 70 | enabled: false, 71 | url: r"file://${FILE_PATH}".to_string(), 72 | } 73 | } 74 | } 75 | 76 | #[derive(Clone, Copy, Serialize, Deserialize, Complete, Default, Debug)] 77 | #[serde(from = "bool")] 78 | #[serde(into = "bool")] 79 | pub enum Echo { 80 | #[default] 81 | True, 82 | False, 83 | } 84 | 85 | impl From for Echo { 86 | fn from(b: bool) -> Self { 87 | if b { 88 | Echo::True 89 | } else { 90 | Echo::False 91 | } 92 | } 93 | } 94 | 95 | impl Into for Echo { 96 | fn into(self) -> bool { 97 | match self { 98 | Echo::True => true, 99 | Echo::False => false, 100 | } 101 | } 102 | } 103 | 104 | #[derive(Clone, Copy, Serialize, Deserialize, Debug, Default, Complete)] 105 | #[serde(rename_all = "lowercase")] 106 | pub enum BacktraceStyle { 107 | #[default] 108 | Short, 109 | Full, 110 | } 111 | 112 | impl BacktraceStyle { 113 | pub fn env_var_str(&self) -> &'static str { 114 | match self { 115 | BacktraceStyle::Short => "1", 116 | BacktraceStyle::Full => "full", 117 | } 118 | } 119 | } 120 | 121 | #[derive(Debug)] 122 | pub enum Hide { 123 | Pattern { pattern: Regex }, 124 | Range { begin: Regex, end: Option }, 125 | } 126 | 127 | const PATTERN: &str = "pattern"; 128 | const BEGIN: &str = "begin"; 129 | const END: &str = "end"; 130 | 131 | // Unfortunately we have to implement our own deserializer. 132 | // See https://github.com/toml-rs/toml/issues/748 and https://github.com/toml-rs/toml/issues/535 133 | impl<'de> Deserialize<'de> for Hide { 134 | fn deserialize(deserializer: D) -> Result 135 | where 136 | D: serde::Deserializer<'de>, 137 | { 138 | use serde::de::Error; 139 | 140 | struct Visitor; 141 | impl<'de> serde::de::Visitor<'de> for Visitor { 142 | type Value = Hide; 143 | 144 | fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 145 | write!( 146 | f, 147 | "a map with wither the field `{PATTERN}`, or the fields `{BEGIN}` and optionally `{END}`" 148 | ) 149 | } 150 | 151 | fn visit_map(self, mut map: A) -> Result 152 | where 153 | A: serde::de::MapAccess<'de>, 154 | { 155 | let re = |s: &str| Regex::new(s).map_err(|e| Error::custom(&e.to_string())); 156 | let mut entries = HashMap::::default(); 157 | while let Some((k, v)) = map.next_entry::()? { 158 | entries.insert(k, v); 159 | } 160 | 161 | if entries.contains_key(PATTERN) && entries.contains_key(BEGIN) { 162 | return Err(Error::custom(format!( 163 | "cannot use `{PATTERN}` and `{BEGIN}` toghether" 164 | ))); 165 | } 166 | if let Some(pattern) = entries.remove(PATTERN) { 167 | let pattern = re(&pattern)?; 168 | Ok(Hide::Pattern { pattern }) 169 | } else if let Some(begin) = entries.remove(BEGIN) { 170 | let begin = re(&begin)?; 171 | let end = entries.remove(END).as_deref().map(re).transpose()?; 172 | Ok(Hide::Range { begin, end }) 173 | } else { 174 | Err(Error::custom(format!( 175 | "missing field `{PATTERN}` or `{BEGIN}`" 176 | ))) 177 | } 178 | } 179 | } 180 | deserializer.deserialize_map(Visitor) 181 | } 182 | } 183 | 184 | impl Serialize for Hide { 185 | fn serialize(&self, serializer: S) -> Result 186 | where 187 | S: serde::Serializer, 188 | { 189 | let mut m = serializer.serialize_map(None)?; 190 | match self { 191 | Hide::Pattern { pattern } => m.serialize_entry(PATTERN, pattern.as_str())?, 192 | Hide::Range { begin, end } => { 193 | m.serialize_entry(BEGIN, begin.as_str())?; 194 | if let Some(end) = end { 195 | m.serialize_entry(END, end.as_str())?; 196 | } 197 | } 198 | } 199 | m.end() 200 | } 201 | } 202 | 203 | impl PartialConfig { 204 | fn read() -> anyhow::Result { 205 | let config = PartialConfig::find_home_file() 206 | .map(PartialConfig::parse_file) 207 | .transpose()? 208 | .unwrap_or_else(|| Config::default().into_partial()); 209 | let Some(local_path) = PartialConfig::find_local_file() else { 210 | return Ok(config); 211 | }; 212 | Ok(config.merge_with(PartialConfig::parse_file(local_path)?)) 213 | } 214 | 215 | fn parse_file(path: PathBuf) -> anyhow::Result { 216 | let mut contents = String::new(); 217 | let mut file = fs::File::open(path)?; 218 | file.read_to_string(&mut contents)?; 219 | let config = toml::from_str(&contents)?; 220 | Ok(config) 221 | } 222 | 223 | fn find_home_file() -> Option { 224 | let home_dir = home::home_dir()?; 225 | PartialConfig::find_file_in(&home_dir) 226 | } 227 | 228 | fn find_local_file() -> Option { 229 | let mut path = std::env::current_dir().unwrap(); 230 | loop { 231 | if let Some(file) = PartialConfig::find_file_in(&path) { 232 | return Some(file); 233 | } 234 | if !path.pop() { 235 | return None; 236 | } 237 | } 238 | } 239 | 240 | fn find_file_in(dir: &Path) -> Option { 241 | for name in ["backtracetk.toml", ".backtracetk.toml"] { 242 | let file = dir.join(name); 243 | if file.exists() { 244 | return Some(file); 245 | } 246 | } 247 | None 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | mod partial; 3 | mod render; 4 | 5 | use regex::Regex; 6 | 7 | pub struct Backtrace { 8 | pub frames: Vec, 9 | pub panic_info: Option, 10 | } 11 | 12 | pub struct PanicInfo { 13 | pub thread: String, 14 | pub at: String, 15 | pub message: Vec, 16 | } 17 | 18 | pub struct Frame { 19 | pub function: String, 20 | pub frameno: u32, 21 | pub source_info: Option, 22 | } 23 | 24 | pub struct SourceInfo { 25 | pub file: String, 26 | pub lineno: usize, 27 | pub colno: usize, 28 | } 29 | 30 | pub struct Parser { 31 | panic_regex: Regex, 32 | function_regex: Regex, 33 | source_regex: Regex, 34 | lines: Vec, 35 | } 36 | 37 | enum ParsedLine { 38 | /// A line reporting a panic, e.g., 39 | /// ```ignore 40 | /// thread 'rustc' panicked at /rustc/b3aa8e7168a3d940122db3561289ffbf3f587262/compiler/rustc_errors/src/lib.rs:1651:9: 41 | /// ``` 42 | ThreadPanic { thread: String, at: String }, 43 | /// The begining of a trace starts with `stack backtrace:` 44 | BacktraceStart, 45 | /// The "header" of a frame containing the frame number and the function's name, e.g., 46 | /// ```ignore 47 | /// 28: rustc_middle::ty::context::tls::enter_context` 48 | /// ``` 49 | BacktraceHeader { function: String, frameno: u32 }, 50 | /// Line containing source information about a frame, e.g., 51 | /// ```ignore 52 | /// at /rustc/b3aa8e7168a3d940122db3561289ffbf3f587262/compiler/rustc_middle/src/ty/context/tls.rs:79:9 53 | /// ``` 54 | BacktraceSource(SourceInfo), 55 | /// A line that doesn't match any of the previous patterns 56 | Other(String), 57 | } 58 | 59 | impl Parser { 60 | pub fn new() -> Parser { 61 | let panic_regex = 62 | Regex::new(r"^thread\s+'(?P[^']+)'\spanicked\s+at\s+(?P.+)").unwrap(); 63 | let function_regex = 64 | Regex::new(r"^\s+(?P\d+):\s+((\w+)\s+-\s+)?(?P.+)").unwrap(); 65 | let source_regex = 66 | Regex::new(r"^\s+at\s+(?P[^:]+):(?P\d+):(?P\d+)").unwrap(); 67 | Parser { 68 | panic_regex, 69 | function_regex, 70 | source_regex, 71 | lines: vec![], 72 | } 73 | } 74 | 75 | pub fn parse_line(&mut self, line: String) { 76 | let parsed = if line.eq_ignore_ascii_case("stack backtrace:") { 77 | ParsedLine::BacktraceStart 78 | } else if let Some(captures) = self.panic_regex.captures(&line) { 79 | let thread = captures.name("thread").unwrap().as_str().to_string(); 80 | let at = captures.name("at").unwrap().as_str().to_string(); 81 | ParsedLine::ThreadPanic { thread, at } 82 | } else if let Some(captures) = self.function_regex.captures(&line) { 83 | let frameno = captures.name("frameno").unwrap().as_str().to_string(); 84 | let function = captures.name("function").unwrap().as_str().to_string(); 85 | ParsedLine::BacktraceHeader { 86 | function, 87 | frameno: frameno.parse().unwrap(), 88 | } 89 | } else if let Some(captures) = self.source_regex.captures(&line) { 90 | let file = captures.name("file").unwrap().as_str().to_string(); 91 | let lineno = captures.name("lineno").unwrap().as_str(); 92 | let colno = captures.name("colno").unwrap().as_str(); 93 | ParsedLine::BacktraceSource(SourceInfo { 94 | file, 95 | lineno: lineno.parse().unwrap(), 96 | colno: colno.parse().unwrap(), 97 | }) 98 | } else { 99 | ParsedLine::Other(line) 100 | }; 101 | self.lines.push(parsed) 102 | } 103 | 104 | pub fn into_backtraces(self) -> Vec { 105 | let mut backtraces = vec![]; 106 | let mut frames = vec![]; 107 | let mut lines = self.lines.into_iter().peekable(); 108 | let mut panic_info = None; 109 | let mut in_panic_info = false; 110 | while let Some(line) = lines.next() { 111 | match line { 112 | ParsedLine::ThreadPanic { thread, at } => { 113 | in_panic_info = true; 114 | panic_info = Some(PanicInfo { 115 | thread, 116 | at, 117 | message: vec![], 118 | }); 119 | } 120 | ParsedLine::Other(line) => { 121 | if let Some(panic_info) = &mut panic_info { 122 | if in_panic_info { 123 | panic_info.message.push(line); 124 | } 125 | } 126 | } 127 | ParsedLine::BacktraceStart => { 128 | in_panic_info = false; 129 | if !frames.is_empty() { 130 | backtraces.push(Backtrace { 131 | frames: std::mem::take(&mut frames), 132 | panic_info: std::mem::take(&mut panic_info), 133 | }); 134 | } 135 | } 136 | ParsedLine::BacktraceHeader { function, frameno } => { 137 | in_panic_info = false; 138 | let source_info = lines 139 | .next_if(|line| matches!(line, ParsedLine::BacktraceSource(..))) 140 | .and_then(|line| { 141 | if let ParsedLine::BacktraceSource(source_info) = line { 142 | Some(source_info) 143 | } else { 144 | None 145 | } 146 | }); 147 | frames.push(Frame { 148 | function, 149 | frameno, 150 | source_info, 151 | }) 152 | } 153 | ParsedLine::BacktraceSource(..) => { 154 | // This case is in theory never reached because source lines should be consumed 155 | // in the `BacktraceHeader` case. 156 | in_panic_info = false; 157 | } 158 | } 159 | } 160 | if !frames.is_empty() { 161 | backtraces.push(Backtrace { frames, panic_info }); 162 | } 163 | backtraces 164 | } 165 | } 166 | 167 | pub trait FrameFilter { 168 | fn should_hide(&mut self, frame: &Frame) -> bool; 169 | } 170 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::io::{BufRead, BufReader}; 2 | use std::process::{Command, Stdio}; 3 | 4 | use backtracetk::config::{self, Config, Echo}; 5 | use backtracetk::{Frame, FrameFilter}; 6 | use clap::Parser; 7 | use regex::Regex; 8 | 9 | /// Print colorized Rust backtraces by capturing the output of an external process. 10 | #[derive(clap::Parser)] 11 | #[command(max_term_width = 110, arg_required_else_help = true)] 12 | struct Args { 13 | #[arg(trailing_var_arg(true))] 14 | cmd: Vec, 15 | 16 | /// Print the current detected configuration 17 | #[arg(long)] 18 | print_config: bool, 19 | 20 | /// Print the default configuration used when no configuratoin files are detected 21 | #[arg(long)] 22 | print_default_config: bool, 23 | } 24 | 25 | fn main() -> anyhow::Result<()> { 26 | let mut args = Args::parse(); 27 | 28 | if args.print_default_config { 29 | println!("{}", Config::default()); 30 | std::process::exit(0); 31 | } 32 | 33 | let config = Config::read()?; 34 | 35 | if args.print_config { 36 | println!("{config}"); 37 | std::process::exit(0); 38 | } 39 | 40 | let mut env_vars = vec![("RUST_BACKTRACE", config.style.env_var_str())]; 41 | 42 | for (k, v) in &config.env { 43 | env_vars.push((k, v)); 44 | } 45 | 46 | println!("$ {}", args.cmd.join(" ")); 47 | 48 | let child = match Command::new(args.cmd.remove(0)) 49 | .args(args.cmd) 50 | .stderr(Stdio::piped()) 51 | .envs(env_vars) 52 | .spawn() 53 | { 54 | Ok(child) => child, 55 | Err(err) => { 56 | eprintln!("Error: command exited with non-zero code: `{err}`"); 57 | std::process::exit(2); 58 | } 59 | }; 60 | 61 | let mut parser = backtracetk::Parser::new(); 62 | let stderr = child.stderr.expect("failed to open stderr"); 63 | for line in BufReader::new(stderr).lines() { 64 | let line = line?; 65 | if let Echo::True = config.echo { 66 | anstream::eprintln!("{line}"); 67 | } 68 | parser.parse_line(line); 69 | } 70 | 71 | for backtrace in parser.into_backtraces() { 72 | backtrace.render(&config, &mut Filters::new(&config)); 73 | } 74 | 75 | Ok(()) 76 | } 77 | 78 | pub struct Filters<'a> { 79 | filters: Vec>, 80 | } 81 | 82 | impl<'a> Filters<'a> { 83 | fn new(config: &'a Config) -> Self { 84 | let mut filters = vec![]; 85 | for filter in &config.hide { 86 | filters.push(filter.into()) 87 | } 88 | Self { filters } 89 | } 90 | } 91 | 92 | impl FrameFilter for Filters<'_> { 93 | fn should_hide(&mut self, frame: &Frame) -> bool { 94 | self.filters 95 | .iter_mut() 96 | .any(|filter| filter.do_match(&frame.function)) 97 | } 98 | } 99 | 100 | enum Filter<'a> { 101 | Pattern(&'a Regex), 102 | Range { 103 | begin: &'a Regex, 104 | end: Option<&'a Regex>, 105 | inside: bool, 106 | }, 107 | } 108 | 109 | impl Filter<'_> { 110 | fn do_match(&mut self, s: &str) -> bool { 111 | match self { 112 | Filter::Pattern(regex) => regex.is_match(s), 113 | Filter::Range { begin, end, inside } => { 114 | if *inside { 115 | let Some(end) = end else { return true }; 116 | *inside = !end.is_match(s); 117 | true 118 | } else { 119 | *inside = begin.is_match(s); 120 | *inside 121 | } 122 | } 123 | } 124 | } 125 | } 126 | 127 | impl<'a> From<&'a config::Hide> for Filter<'a> { 128 | fn from(value: &'a config::Hide) -> Self { 129 | match value { 130 | config::Hide::Pattern { pattern } => Filter::Pattern(pattern), 131 | config::Hide::Range { begin, end } => Filter::Range { 132 | begin, 133 | end: end.as_ref(), 134 | inside: false, 135 | }, 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/partial.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | hash::{BuildHasher, Hash}, 4 | }; 5 | 6 | pub trait Partial { 7 | type Complete; 8 | 9 | fn merge_with(self, other: Self) -> Self; 10 | 11 | fn into_complete(self) -> Self::Complete; 12 | } 13 | 14 | impl Partial for Option 15 | where 16 | T: Default, 17 | { 18 | type Complete = T; 19 | 20 | fn merge_with(self, other: Self) -> Self { 21 | other.or(self) 22 | } 23 | 24 | fn into_complete(self) -> Self::Complete { 25 | self.unwrap_or_default() 26 | } 27 | } 28 | 29 | impl Partial for Vec { 30 | type Complete = Vec; 31 | 32 | fn merge_with(mut self, other: Self) -> Self { 33 | self.extend(other); 34 | self 35 | } 36 | 37 | fn into_complete(self) -> Self::Complete { 38 | self 39 | } 40 | } 41 | 42 | impl Partial for HashMap 43 | where 44 | K: Eq + Hash, 45 | S: BuildHasher, 46 | { 47 | type Complete = HashMap; 48 | 49 | fn merge_with(mut self, other: Self) -> Self { 50 | self.extend(other); 51 | self 52 | } 53 | 54 | fn into_complete(self) -> Self::Complete { 55 | self 56 | } 57 | } 58 | 59 | pub trait Complete { 60 | type Partial: Partial; 61 | 62 | fn into_partial(self) -> Self::Partial; 63 | } 64 | 65 | impl Complete for Vec { 66 | type Partial = Vec; 67 | 68 | fn into_partial(self) -> Self::Partial { 69 | self 70 | } 71 | } 72 | 73 | impl Complete for HashMap 74 | where 75 | K: Eq + Hash, 76 | S: BuildHasher, 77 | { 78 | type Partial = HashMap; 79 | 80 | fn into_partial(self) -> Self::Partial { 81 | self 82 | } 83 | } 84 | 85 | impl Complete for bool { 86 | type Partial = Option; 87 | 88 | fn into_partial(self) -> Self::Partial { 89 | Some(self) 90 | } 91 | } 92 | 93 | impl Complete for String { 94 | type Partial = Option; 95 | 96 | fn into_partial(self) -> Self::Partial { 97 | Some(self) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/render.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt, 3 | fs::File, 4 | io::{self, BufRead}, 5 | path::Path, 6 | }; 7 | 8 | use anstyle::{AnsiColor, Color, Reset, Style}; 9 | 10 | use crate::{config::Config, Backtrace, Frame, FrameFilter, PanicInfo, SourceInfo}; 11 | 12 | const GREEN: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Green))); 13 | const CYAN: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Cyan))); 14 | const RED: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Red))); 15 | const BOLD: Style = Style::new().bold(); 16 | const RESET: Reset = Reset; 17 | 18 | impl Backtrace { 19 | pub fn render(&self, config: &Config, filter: &mut impl FrameFilter) { 20 | let frameno_width = self.compute_frameno_width(); 21 | let lineno_width = self.compute_lineno_width(); 22 | let total_width = self.compute_width(frameno_width); 23 | let cx = RenderCtxt { 24 | config, 25 | frameno_width, 26 | lineno_width, 27 | total_width, 28 | }; 29 | cx.render_backtrace(self, filter) 30 | } 31 | } 32 | 33 | struct RenderCtxt<'a> { 34 | config: &'a Config, 35 | frameno_width: usize, 36 | lineno_width: usize, 37 | total_width: usize, 38 | } 39 | 40 | impl<'a> RenderCtxt<'a> { 41 | fn render_backtrace(&self, backtrace: &Backtrace, filter: &mut impl FrameFilter) { 42 | if backtrace.frames.is_empty() { 43 | return; 44 | } 45 | anstream::eprintln!("\n{:━^width$}", " BACKTRACE ", width = self.total_width); 46 | 47 | let mut hidden = 0; 48 | for frame in backtrace.frames.iter().rev() { 49 | if filter.should_hide(frame) { 50 | hidden += 1; 51 | } else { 52 | self.print_hidden_frames_message(hidden); 53 | self.render_frame(frame); 54 | hidden = 0; 55 | } 56 | } 57 | self.print_hidden_frames_message(hidden); 58 | 59 | if let Some(panic_info) = &backtrace.panic_info { 60 | self.render_panic_info(panic_info); 61 | } 62 | 63 | eprintln!(); 64 | } 65 | 66 | fn print_hidden_frames_message(&self, hidden: u32) { 67 | let msg = match hidden { 68 | 0 => return, 69 | 1 => format!(" ({hidden} frame hidden) "), 70 | _ => format!(" ({hidden} frames hidden) "), 71 | }; 72 | anstream::eprintln!("{CYAN}{msg:┄^width$}{RESET}", width = self.total_width); 73 | } 74 | 75 | fn render_frame(&self, frame: &Frame) { 76 | anstream::eprintln!( 77 | "{:>width$}: {GREEN}{}{RESET}", 78 | frame.frameno, 79 | frame.function, 80 | width = self.frameno_width 81 | ); 82 | 83 | if let Some(source_info) = &frame.source_info { 84 | self.render_source_info(source_info); 85 | let _ = self.render_code_snippet(source_info); 86 | } 87 | } 88 | 89 | fn render_source_info(&self, source_info: &SourceInfo) { 90 | let text = format!( 91 | "{}:{}:{}", 92 | source_info.file, source_info.lineno, source_info.colno 93 | ); 94 | if self.config.hyperlinks.enabled { 95 | if let Some(encoded) = encode_file_path_for_url(&source_info.file) { 96 | let url = 97 | self.config 98 | .hyperlinks 99 | .render(&encoded, source_info.lineno, source_info.colno); 100 | anstream::eprintln!("{} at {}", self.frameno_padding(), Link::new(text, url)); 101 | return; 102 | } 103 | } 104 | anstream::eprintln!("{} at {text}", self.frameno_padding()) 105 | } 106 | 107 | fn render_code_snippet(&self, source_info: &SourceInfo) -> io::Result<()> { 108 | let path = Path::new(&source_info.file); 109 | if path.exists() { 110 | let file = File::open(path)?; 111 | let reader = io::BufReader::new(file); 112 | for (i, line) in viewport(reader, source_info)? { 113 | if i == source_info.lineno { 114 | anstream::eprint!("{BOLD}"); 115 | } 116 | anstream::eprintln!( 117 | "{} {i:>width$} | {line}", 118 | self.frameno_padding(), 119 | width = self.lineno_width 120 | ); 121 | if i == source_info.lineno { 122 | anstream::eprint!("{RESET}"); 123 | } 124 | } 125 | } 126 | Ok(()) 127 | } 128 | 129 | fn frameno_padding(&self) -> Padding { 130 | Padding(self.frameno_width) 131 | } 132 | 133 | fn render_panic_info(&self, panic_info: &PanicInfo) { 134 | anstream::eprint!("{RED}"); 135 | anstream::eprintln!( 136 | "thread '{}' panickd at {}", 137 | panic_info.thread, 138 | panic_info.at 139 | ); 140 | for line in &panic_info.message { 141 | anstream::eprintln!("{line}"); 142 | } 143 | anstream::eprint!("{RESET}"); 144 | } 145 | } 146 | 147 | fn viewport( 148 | reader: io::BufReader, 149 | source_info: &SourceInfo, 150 | ) -> io::Result> { 151 | reader 152 | .lines() 153 | .enumerate() 154 | .skip(source_info.lineno.saturating_sub(2)) 155 | .take(5) 156 | .map(|(i, line)| Ok((i + 1, line?))) 157 | .collect() 158 | } 159 | 160 | impl Backtrace { 161 | fn compute_lineno_width(&self) -> usize { 162 | // This is assuming we have 2 more lines in the file, if we don't, in the worst case we will 163 | // print an unnecesary extra space for each line number. 164 | self.frames 165 | .iter() 166 | .flat_map(|f| &f.source_info) 167 | .map(|source_info| source_info.lineno + 3) 168 | .max() 169 | .unwrap_or(1) 170 | .ilog10() as usize 171 | } 172 | 173 | fn compute_frameno_width(&self) -> usize { 174 | self.frames.len().ilog10() as usize + 1 175 | } 176 | 177 | fn compute_width(&self, frameno_width: usize) -> usize { 178 | let term_size = termion::terminal_size().unwrap_or((80, 0)).0 as usize; 179 | self.frames 180 | .iter() 181 | .map(|f| f.width(frameno_width)) 182 | .max() 183 | .unwrap_or(80) 184 | .min(term_size) 185 | } 186 | } 187 | 188 | impl Frame { 189 | fn width(&self, frameno_width: usize) -> usize { 190 | usize::max( 191 | frameno_width + 2 + self.function.len(), 192 | self.source_info 193 | .as_ref() 194 | .map(|s| s.width(frameno_width)) 195 | .unwrap_or(0), 196 | ) 197 | } 198 | } 199 | 200 | impl SourceInfo { 201 | /// Width without considering the source code snippet 202 | fn width(&self, frameno_width: usize) -> usize { 203 | frameno_width + self.file.len() + (self.lineno.ilog10() + self.colno.ilog10()) as usize + 9 204 | } 205 | } 206 | 207 | struct Padding(usize); 208 | 209 | impl std::fmt::Display for Padding { 210 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 211 | for _ in 0..self.0 { 212 | write!(f, " ")?; 213 | } 214 | Ok(()) 215 | } 216 | } 217 | 218 | struct Link { 219 | text: String, 220 | url: String, 221 | } 222 | 223 | impl Link { 224 | fn new(text: String, url: String) -> Self { 225 | Self { text, url } 226 | } 227 | } 228 | 229 | impl fmt::Display for Link { 230 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 231 | write!( 232 | f, 233 | "\u{1b}]8;;{}\u{1b}\\{}\u{1b}]8;;\u{1b}\\", 234 | self.url, self.text 235 | ) 236 | } 237 | } 238 | 239 | fn encode_file_path_for_url(path: &str) -> Option { 240 | println!("{path:?}"); 241 | let path = Path::new(path).canonicalize().ok()?; 242 | println!("{path:?}"); 243 | Some(format!("{}", path.display())) 244 | } 245 | --------------------------------------------------------------------------------