├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── example.png ├── rustfmt.toml └── src ├── app.rs ├── banners.rs ├── body.rs ├── main.rs └── regex_input.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 = "ahash" 7 | version = "0.8.11" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 10 | dependencies = [ 11 | "cfg-if", 12 | "once_cell", 13 | "version_check", 14 | "zerocopy", 15 | ] 16 | 17 | [[package]] 18 | name = "aho-corasick" 19 | version = "1.1.3" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 22 | dependencies = [ 23 | "memchr", 24 | ] 25 | 26 | [[package]] 27 | name = "allocator-api2" 28 | version = "0.2.18" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" 31 | 32 | [[package]] 33 | name = "autocfg" 34 | version = "1.3.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 37 | 38 | [[package]] 39 | name = "bitflags" 40 | version = "2.5.0" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" 43 | 44 | [[package]] 45 | name = "cassowary" 46 | version = "0.3.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 49 | 50 | [[package]] 51 | name = "castaway" 52 | version = "0.2.3" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" 55 | dependencies = [ 56 | "rustversion", 57 | ] 58 | 59 | [[package]] 60 | name = "cfg-if" 61 | version = "1.0.0" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 64 | 65 | [[package]] 66 | name = "compact_str" 67 | version = "0.8.0" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644" 70 | dependencies = [ 71 | "castaway", 72 | "cfg-if", 73 | "itoa", 74 | "rustversion", 75 | "ryu", 76 | "static_assertions", 77 | ] 78 | 79 | [[package]] 80 | name = "crossterm" 81 | version = "0.28.1" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" 84 | dependencies = [ 85 | "bitflags", 86 | "crossterm_winapi", 87 | "mio", 88 | "parking_lot", 89 | "rustix", 90 | "signal-hook", 91 | "signal-hook-mio", 92 | "winapi", 93 | ] 94 | 95 | [[package]] 96 | name = "crossterm_winapi" 97 | version = "0.9.1" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 100 | dependencies = [ 101 | "winapi", 102 | ] 103 | 104 | [[package]] 105 | name = "either" 106 | version = "1.12.0" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" 109 | 110 | [[package]] 111 | name = "errno" 112 | version = "0.3.9" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 115 | dependencies = [ 116 | "libc", 117 | "windows-sys", 118 | ] 119 | 120 | [[package]] 121 | name = "hashbrown" 122 | version = "0.14.5" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 125 | dependencies = [ 126 | "ahash", 127 | "allocator-api2", 128 | ] 129 | 130 | [[package]] 131 | name = "heck" 132 | version = "0.5.0" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 135 | 136 | [[package]] 137 | name = "hermit-abi" 138 | version = "0.3.9" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 141 | 142 | [[package]] 143 | name = "instability" 144 | version = "0.3.2" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c" 147 | dependencies = [ 148 | "quote", 149 | "syn", 150 | ] 151 | 152 | [[package]] 153 | name = "itertools" 154 | version = "0.12.1" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 157 | dependencies = [ 158 | "either", 159 | ] 160 | 161 | [[package]] 162 | name = "itertools" 163 | version = "0.13.0" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 166 | dependencies = [ 167 | "either", 168 | ] 169 | 170 | [[package]] 171 | name = "itoa" 172 | version = "1.0.11" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 175 | 176 | [[package]] 177 | name = "libc" 178 | version = "0.2.155" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 181 | 182 | [[package]] 183 | name = "linux-raw-sys" 184 | version = "0.4.14" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 187 | 188 | [[package]] 189 | name = "lock_api" 190 | version = "0.4.12" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 193 | dependencies = [ 194 | "autocfg", 195 | "scopeguard", 196 | ] 197 | 198 | [[package]] 199 | name = "log" 200 | version = "0.4.21" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 203 | 204 | [[package]] 205 | name = "lru" 206 | version = "0.12.3" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" 209 | dependencies = [ 210 | "hashbrown", 211 | ] 212 | 213 | [[package]] 214 | name = "memchr" 215 | version = "2.7.2" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" 218 | 219 | [[package]] 220 | name = "mio" 221 | version = "1.0.2" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" 224 | dependencies = [ 225 | "hermit-abi", 226 | "libc", 227 | "log", 228 | "wasi", 229 | "windows-sys", 230 | ] 231 | 232 | [[package]] 233 | name = "once_cell" 234 | version = "1.19.0" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 237 | 238 | [[package]] 239 | name = "parking_lot" 240 | version = "0.12.3" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 243 | dependencies = [ 244 | "lock_api", 245 | "parking_lot_core", 246 | ] 247 | 248 | [[package]] 249 | name = "parking_lot_core" 250 | version = "0.9.10" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 253 | dependencies = [ 254 | "cfg-if", 255 | "libc", 256 | "redox_syscall", 257 | "smallvec", 258 | "windows-targets", 259 | ] 260 | 261 | [[package]] 262 | name = "paste" 263 | version = "1.0.15" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 266 | 267 | [[package]] 268 | name = "proc-macro2" 269 | version = "1.0.85" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" 272 | dependencies = [ 273 | "unicode-ident", 274 | ] 275 | 276 | [[package]] 277 | name = "quote" 278 | version = "1.0.36" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 281 | dependencies = [ 282 | "proc-macro2", 283 | ] 284 | 285 | [[package]] 286 | name = "ratatui" 287 | version = "0.28.0" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "5ba6a365afbe5615999275bea2446b970b10a41102500e27ce7678d50d978303" 290 | dependencies = [ 291 | "bitflags", 292 | "cassowary", 293 | "compact_str", 294 | "crossterm", 295 | "instability", 296 | "itertools 0.13.0", 297 | "lru", 298 | "paste", 299 | "strum", 300 | "strum_macros", 301 | "unicode-segmentation", 302 | "unicode-truncate", 303 | "unicode-width", 304 | ] 305 | 306 | [[package]] 307 | name = "redox_syscall" 308 | version = "0.5.1" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" 311 | dependencies = [ 312 | "bitflags", 313 | ] 314 | 315 | [[package]] 316 | name = "regect" 317 | version = "0.2.4" 318 | dependencies = [ 319 | "ratatui", 320 | "regex", 321 | "tui-textarea", 322 | ] 323 | 324 | [[package]] 325 | name = "regex" 326 | version = "1.10.4" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" 329 | dependencies = [ 330 | "aho-corasick", 331 | "memchr", 332 | "regex-automata", 333 | "regex-syntax", 334 | ] 335 | 336 | [[package]] 337 | name = "regex-automata" 338 | version = "0.4.6" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" 341 | dependencies = [ 342 | "aho-corasick", 343 | "memchr", 344 | "regex-syntax", 345 | ] 346 | 347 | [[package]] 348 | name = "regex-syntax" 349 | version = "0.8.3" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" 352 | 353 | [[package]] 354 | name = "rustix" 355 | version = "0.38.34" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" 358 | dependencies = [ 359 | "bitflags", 360 | "errno", 361 | "libc", 362 | "linux-raw-sys", 363 | "windows-sys", 364 | ] 365 | 366 | [[package]] 367 | name = "rustversion" 368 | version = "1.0.17" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" 371 | 372 | [[package]] 373 | name = "ryu" 374 | version = "1.0.18" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 377 | 378 | [[package]] 379 | name = "scopeguard" 380 | version = "1.2.0" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 383 | 384 | [[package]] 385 | name = "signal-hook" 386 | version = "0.3.17" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 389 | dependencies = [ 390 | "libc", 391 | "signal-hook-registry", 392 | ] 393 | 394 | [[package]] 395 | name = "signal-hook-mio" 396 | version = "0.2.4" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" 399 | dependencies = [ 400 | "libc", 401 | "mio", 402 | "signal-hook", 403 | ] 404 | 405 | [[package]] 406 | name = "signal-hook-registry" 407 | version = "1.4.2" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 410 | dependencies = [ 411 | "libc", 412 | ] 413 | 414 | [[package]] 415 | name = "smallvec" 416 | version = "1.13.2" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 419 | 420 | [[package]] 421 | name = "static_assertions" 422 | version = "1.1.0" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 425 | 426 | [[package]] 427 | name = "strum" 428 | version = "0.26.2" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" 431 | dependencies = [ 432 | "strum_macros", 433 | ] 434 | 435 | [[package]] 436 | name = "strum_macros" 437 | version = "0.26.4" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" 440 | dependencies = [ 441 | "heck", 442 | "proc-macro2", 443 | "quote", 444 | "rustversion", 445 | "syn", 446 | ] 447 | 448 | [[package]] 449 | name = "syn" 450 | version = "2.0.66" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" 453 | dependencies = [ 454 | "proc-macro2", 455 | "quote", 456 | "unicode-ident", 457 | ] 458 | 459 | [[package]] 460 | name = "tui-textarea" 461 | version = "0.6.1" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "29c07084342a575cea919eea996b9658a358c800b03d435df581c1d7c60e065a" 464 | dependencies = [ 465 | "crossterm", 466 | "ratatui", 467 | "unicode-width", 468 | ] 469 | 470 | [[package]] 471 | name = "unicode-ident" 472 | version = "1.0.12" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 475 | 476 | [[package]] 477 | name = "unicode-segmentation" 478 | version = "1.11.0" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" 481 | 482 | [[package]] 483 | name = "unicode-truncate" 484 | version = "1.0.0" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "5a5fbabedabe362c618c714dbefda9927b5afc8e2a8102f47f081089a9019226" 487 | dependencies = [ 488 | "itertools 0.12.1", 489 | "unicode-width", 490 | ] 491 | 492 | [[package]] 493 | name = "unicode-width" 494 | version = "0.1.13" 495 | source = "registry+https://github.com/rust-lang/crates.io-index" 496 | checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" 497 | 498 | [[package]] 499 | name = "version_check" 500 | version = "0.9.4" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 503 | 504 | [[package]] 505 | name = "wasi" 506 | version = "0.11.0+wasi-snapshot-preview1" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 509 | 510 | [[package]] 511 | name = "winapi" 512 | version = "0.3.9" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 515 | dependencies = [ 516 | "winapi-i686-pc-windows-gnu", 517 | "winapi-x86_64-pc-windows-gnu", 518 | ] 519 | 520 | [[package]] 521 | name = "winapi-i686-pc-windows-gnu" 522 | version = "0.4.0" 523 | source = "registry+https://github.com/rust-lang/crates.io-index" 524 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 525 | 526 | [[package]] 527 | name = "winapi-x86_64-pc-windows-gnu" 528 | version = "0.4.0" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 531 | 532 | [[package]] 533 | name = "windows-sys" 534 | version = "0.52.0" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 537 | dependencies = [ 538 | "windows-targets", 539 | ] 540 | 541 | [[package]] 542 | name = "windows-targets" 543 | version = "0.52.5" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 546 | dependencies = [ 547 | "windows_aarch64_gnullvm", 548 | "windows_aarch64_msvc", 549 | "windows_i686_gnu", 550 | "windows_i686_gnullvm", 551 | "windows_i686_msvc", 552 | "windows_x86_64_gnu", 553 | "windows_x86_64_gnullvm", 554 | "windows_x86_64_msvc", 555 | ] 556 | 557 | [[package]] 558 | name = "windows_aarch64_gnullvm" 559 | version = "0.52.5" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 562 | 563 | [[package]] 564 | name = "windows_aarch64_msvc" 565 | version = "0.52.5" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 568 | 569 | [[package]] 570 | name = "windows_i686_gnu" 571 | version = "0.52.5" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 574 | 575 | [[package]] 576 | name = "windows_i686_gnullvm" 577 | version = "0.52.5" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 580 | 581 | [[package]] 582 | name = "windows_i686_msvc" 583 | version = "0.52.5" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 586 | 587 | [[package]] 588 | name = "windows_x86_64_gnu" 589 | version = "0.52.5" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 592 | 593 | [[package]] 594 | name = "windows_x86_64_gnullvm" 595 | version = "0.52.5" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 598 | 599 | [[package]] 600 | name = "windows_x86_64_msvc" 601 | version = "0.52.5" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 604 | 605 | [[package]] 606 | name = "zerocopy" 607 | version = "0.7.34" 608 | source = "registry+https://github.com/rust-lang/crates.io-index" 609 | checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" 610 | dependencies = [ 611 | "zerocopy-derive", 612 | ] 613 | 614 | [[package]] 615 | name = "zerocopy-derive" 616 | version = "0.7.34" 617 | source = "registry+https://github.com/rust-lang/crates.io-index" 618 | checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" 619 | dependencies = [ 620 | "proc-macro2", 621 | "quote", 622 | "syn", 623 | ] 624 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "regect" 3 | version = "0.2.5" 4 | edition = "2021" 5 | description = "A cli tool to quickly test regular expressions" 6 | license = "MIT OR Apache-2.0" 7 | repository = "https://github.com/kloki/regect" 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | ratatui = "0.28.0" 12 | regex = "1.10.4" 13 | tui-textarea = "0.6.1" 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Example](./example.png) 2 | 3 | # Regect 4 | 5 | regex 101 like cli tool 6 | 7 | # Run 8 | 9 | ```bash 10 | regect 11 | ``` 12 | 13 | # Input and Output 14 | 15 | ```bash 16 | cat input.txt | regect > filtered_output.txt 17 | ``` 18 | 19 | # Install 20 | 21 | ```bash 22 | cargo install regect --locked 23 | ``` 24 | -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kloki/regect/15f79947814d3f464588ae134b6cd2e27283b9bf/example.png -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | group_imports = "StdExternalCrate" 2 | reorder_imports = true 3 | imports_granularity = "Crate" 4 | reorder_modules = true 5 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use ratatui::{ 4 | backend::Backend, 5 | crossterm, 6 | layout::{Constraint, Direction, Layout}, 7 | Frame, Terminal, 8 | }; 9 | use tui_textarea::{Input, Key}; 10 | 11 | use crate::{ 12 | banners::{footer, header, help}, 13 | body::{captures, substitution, TestInput}, 14 | regex_input::{RegexInput, SubstitutionInput}, 15 | }; 16 | 17 | #[derive(Clone, Copy)] 18 | enum Mode { 19 | Match, 20 | Substitution, 21 | } 22 | 23 | #[derive(Clone, Copy)] 24 | enum EditMode { 25 | Regex, 26 | Substitution, 27 | Body, 28 | } 29 | 30 | enum InfoMode { 31 | QuickReference, 32 | Captures, 33 | } 34 | 35 | enum Action { 36 | Continue, 37 | Quit, 38 | ReturnValue(String), 39 | } 40 | 41 | pub struct App<'a> { 42 | mode: Mode, 43 | edit_mode: EditMode, 44 | info_mode: InfoMode, 45 | regex_input: RegexInput<'a>, 46 | sub_input: SubstitutionInput<'a>, 47 | body: TestInput<'a>, 48 | } 49 | 50 | impl App<'_> { 51 | pub fn new(prefill_input: Option>) -> Self { 52 | let mut body = TestInput::new(); 53 | if let Some(input) = prefill_input { 54 | for line in &input { 55 | body.textarea.insert_str(line); 56 | body.textarea.insert_newline(); 57 | } 58 | } 59 | 60 | Self { 61 | mode: Mode::Substitution, 62 | edit_mode: EditMode::Regex, 63 | info_mode: InfoMode::Captures, 64 | regex_input: RegexInput::new(), 65 | sub_input: SubstitutionInput::new(), 66 | body, 67 | } 68 | } 69 | 70 | pub fn run(&mut self, term: &mut Terminal) -> io::Result> { 71 | loop { 72 | term.draw(|f| self.draw(f))?; 73 | match self.handle_input()? { 74 | Action::Quit => return Ok(None), 75 | Action::ReturnValue(s) => return Ok(Some(s)), 76 | Action::Continue => {} 77 | } 78 | } 79 | } 80 | fn draw(&self, f: &mut Frame) { 81 | match self.mode { 82 | Mode::Match => self.draw_match(f), 83 | Mode::Substitution => self.draw_substitution(f), 84 | } 85 | } 86 | 87 | fn draw_match(&self, f: &mut Frame) { 88 | let layout = Layout::default() 89 | .direction(Direction::Vertical) 90 | .constraints(vec![ 91 | Constraint::Length(2), 92 | Constraint::Length(3), 93 | Constraint::Fill(1), 94 | Constraint::Fill(1), 95 | Constraint::Length(1), 96 | ]) 97 | .split(f.area()); 98 | f.render_widget(header(), layout[0]); 99 | f.render_widget(footer(), layout[4]); 100 | 101 | match self.edit_mode { 102 | EditMode::Body => { 103 | f.render_widget(self.regex_input.unfocused(), layout[1]); 104 | f.render_widget(&self.body.textarea, layout[2]); 105 | } 106 | _ => { 107 | f.render_widget(&self.regex_input.textarea, layout[1]); 108 | f.render_widget( 109 | self.body.highlighted_body(self.regex_input.current_regex()), 110 | layout[2], 111 | ); 112 | } 113 | } 114 | 115 | match self.info_mode { 116 | InfoMode::QuickReference => f.render_widget(help(), layout[3]), 117 | InfoMode::Captures => f.render_widget( 118 | captures(self.regex_input.current_regex(), self.body.body()), 119 | layout[3], 120 | ), 121 | } 122 | } 123 | 124 | fn draw_substitution(&self, f: &mut Frame) { 125 | let layout = Layout::default() 126 | .direction(Direction::Vertical) 127 | .constraints(vec![ 128 | Constraint::Length(2), 129 | Constraint::Length(3), 130 | Constraint::Fill(1), 131 | Constraint::Fill(1), 132 | Constraint::Fill(1), 133 | Constraint::Length(1), 134 | ]) 135 | .split(f.area()); 136 | 137 | let input_layout = Layout::default() 138 | .direction(Direction::Horizontal) 139 | .constraints(vec![Constraint::Fill(1), Constraint::Fill(1)]) 140 | .split(layout[1]); 141 | f.render_widget(header(), layout[0]); 142 | f.render_widget(footer(), layout[5]); 143 | 144 | match self.edit_mode { 145 | EditMode::Body => { 146 | f.render_widget(self.regex_input.unfocused(), input_layout[0]); 147 | f.render_widget(self.sub_input.unfocused(), input_layout[1]); 148 | f.render_widget(&self.body.textarea, layout[2]); 149 | } 150 | EditMode::Regex => { 151 | f.render_widget(&self.regex_input.textarea, input_layout[0]); 152 | f.render_widget(self.sub_input.unfocused(), input_layout[1]); 153 | f.render_widget( 154 | self.body.highlighted_body(self.regex_input.current_regex()), 155 | layout[2], 156 | ); 157 | } 158 | EditMode::Substitution => { 159 | f.render_widget(self.regex_input.unfocused(), input_layout[0]); 160 | f.render_widget(&self.sub_input.textarea, input_layout[1]); 161 | f.render_widget( 162 | self.body.highlighted_body(self.regex_input.current_regex()), 163 | layout[2], 164 | ); 165 | } 166 | } 167 | f.render_widget( 168 | substitution( 169 | self.body.body(), 170 | self.regex_input.current_regex(), 171 | self.sub_input.current_substitution(), 172 | ), 173 | layout[3], 174 | ); 175 | 176 | match self.info_mode { 177 | InfoMode::QuickReference => f.render_widget(help(), layout[4]), 178 | InfoMode::Captures => f.render_widget( 179 | captures(self.regex_input.current_regex(), self.body.body()), 180 | layout[4], 181 | ), 182 | } 183 | } 184 | 185 | fn toggle_edit_mode(&mut self) { 186 | match (self.edit_mode, self.mode) { 187 | (EditMode::Regex, Mode::Match) => self.edit_mode = EditMode::Body, 188 | (EditMode::Regex, Mode::Substitution) => self.edit_mode = EditMode::Substitution, 189 | (EditMode::Substitution, _) => self.edit_mode = EditMode::Body, 190 | (EditMode::Body, _) => self.edit_mode = EditMode::Regex, 191 | } 192 | } 193 | fn toggle_info_mode(&mut self) { 194 | match self.info_mode { 195 | InfoMode::QuickReference => self.info_mode = InfoMode::Captures, 196 | InfoMode::Captures => self.info_mode = InfoMode::QuickReference, 197 | } 198 | } 199 | 200 | fn toggle_mode(&mut self) { 201 | match self.mode { 202 | Mode::Match => self.mode = Mode::Substitution, 203 | Mode::Substitution => { 204 | self.mode = Mode::Match; 205 | if let EditMode::Substitution = self.edit_mode { 206 | self.edit_mode = EditMode::Regex; 207 | } 208 | } 209 | } 210 | } 211 | fn handle_input(&mut self) -> io::Result { 212 | match (crossterm::event::read()?.into(), self.edit_mode) { 213 | ( 214 | Input { 215 | key: Key::Char('q'), 216 | ctrl: true, 217 | .. 218 | }, 219 | _, 220 | ) => return Ok(Action::Quit), 221 | 222 | ( 223 | Input { 224 | key: Key::Char('e'), 225 | ctrl: true, 226 | .. 227 | }, 228 | _, 229 | ) => return Ok(Action::ReturnValue(self.regex_input.current_regex_str())), 230 | ( 231 | Input { 232 | key: Key::Char('o'), 233 | ctrl: true, 234 | .. 235 | }, 236 | _, 237 | ) => match self.regex_input.current_regex() { 238 | None => return Ok(Action::ReturnValue(self.body.body())), 239 | Some(reg) => { 240 | return Ok(Action::ReturnValue( 241 | reg.replace_all( 242 | &self.body.body(), 243 | self.sub_input.current_substitution().to_string(), 244 | ) 245 | .to_string(), 246 | )) 247 | } 248 | }, 249 | (Input { key: Key::Tab, .. }, _) => self.toggle_edit_mode(), 250 | ( 251 | Input { 252 | key: Key::Char('h'), 253 | ctrl: true, 254 | .. 255 | }, 256 | _, 257 | ) => self.toggle_info_mode(), 258 | ( 259 | Input { 260 | key: Key::Char('x'), 261 | ctrl: true, 262 | .. 263 | }, 264 | _, 265 | ) => self.toggle_mode(), 266 | (input, EditMode::Body) => { 267 | self.body.textarea.input(input); 268 | } 269 | (input, EditMode::Regex) => { 270 | if self.regex_input.textarea.input(input) { 271 | self.regex_input.validate() 272 | } 273 | } 274 | (input, EditMode::Substitution) => { 275 | self.sub_input.textarea.input(input); 276 | } 277 | } 278 | Ok(Action::Continue) 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /src/banners.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | style::{Color, Style}, 3 | widgets::{Block, BorderType, Borders, Paragraph, Widget}, 4 | }; 5 | const HEADER: &str = r"┏┓┏┓┏┓┏┓┏╋ 6 | ┛ ┗ ┗┫┗ ┗┗ 7 | "; 8 | const FOOTER: &str = 9 | r"^x toggle match/substitution, ^e export regex, ^o export output, ^h quick reference, ^q quit"; 10 | 11 | const HELP: &str = r" 12 | Match Quantifiers Groups & Substitution 13 | . any char except \n x* zero or more of x (exp) numbered capture group 14 | \d digit x+ one or more of x (?exp) named capture group 15 | \D not digit x? zero or one of x (?:exp) non-capturing group 16 | \s whitespace x{n,m} at least n x and (?flags) set flags within current group 17 | \S not whitespace at most m x (?flags:exp) set flags for exp (non-capturing) 18 | \w word character x{n,} at least n x i case-insensitive 19 | \W not word character x{n} exactly n x m multi-line mode: 20 | \n new line ^ and $ match begin/end of line 21 | [xyz] matching either $0 Complete match 22 | x, y or z (union). $1 Contents of the first group 23 | [^xyz] matching all except $foo Contents of the group named foo 24 | x, y and z. 25 | [a-z] matching in range a-z. 26 | ^ match begin haystack 27 | $ the end of a haystack 28 | "; 29 | 30 | pub fn header() -> impl Widget { 31 | Paragraph::new(HEADER) 32 | .centered() 33 | .style(Style::default().fg(Color::Cyan)) 34 | } 35 | pub fn footer() -> impl Widget { 36 | Paragraph::new(FOOTER).right_aligned() 37 | } 38 | 39 | pub fn help() -> impl Widget { 40 | Paragraph::new(HELP).block( 41 | Block::new() 42 | .border_type(BorderType::Rounded) 43 | .border_style(Style::default()) 44 | .borders(Borders::ALL) 45 | .title("Quick Reference"), 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /src/body.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | layout::Constraint, 3 | style::{Color, Style}, 4 | text::{Line, Span, Text}, 5 | widgets::{Block, BorderType, Borders, Paragraph, Row, Table, Widget}, 6 | }; 7 | use regex::Regex; 8 | use tui_textarea::TextArea; 9 | 10 | pub fn get_color(index: usize) -> Color { 11 | match index % 5 { 12 | 0 => Color::Green, 13 | 1 => Color::Blue, 14 | 2 => Color::Yellow, 15 | 3 => Color::Cyan, 16 | _ => Color::Magenta, 17 | } 18 | } 19 | pub struct TestInput<'a> { 20 | pub textarea: TextArea<'a>, 21 | } 22 | 23 | impl TestInput<'_> { 24 | pub fn new() -> Self { 25 | let mut textarea = TextArea::default(); 26 | textarea.set_style(Style::default().fg(Color::LightGreen)); 27 | textarea.set_block( 28 | Block::default() 29 | .border_type(BorderType::Rounded) 30 | .borders(Borders::ALL) 31 | .title("Input"), 32 | ); 33 | Self { textarea } 34 | } 35 | 36 | pub fn body(&self) -> String { 37 | self.textarea.lines().join("\n").to_string() 38 | } 39 | pub fn highlighted_body(&self, current_regex: Option) -> impl Widget + '_ { 40 | fn append_match(part: &str, lines: &mut Vec>, style: Style) { 41 | let mut last = lines.len() - 1; 42 | if !part.contains('\n') { 43 | lines[last].push(Span::styled(part.to_owned(), style)); 44 | return; 45 | } 46 | 47 | for p in part.split('\n') { 48 | lines[last].push(Span::styled(p.to_owned(), style)); 49 | last += 1; 50 | lines.push(vec![]); 51 | } 52 | 53 | lines.pop(); 54 | } 55 | let body = self.textarea.lines().join("\n"); 56 | let mut text = Text::default(); 57 | if let Some(regex) = current_regex { 58 | let mut lines: Vec> = vec![vec![]]; 59 | 60 | let mut current_index = 0; 61 | 62 | for (i, re_match) in regex.find_iter(&body).enumerate() { 63 | append_match( 64 | &body[current_index..re_match.start()], 65 | &mut lines, 66 | Style::default(), 67 | ); 68 | append_match( 69 | &body[re_match.start()..re_match.end()], 70 | &mut lines, 71 | Style::default().fg(Color::Black).bg(get_color(i)), 72 | ); 73 | current_index = re_match.end(); 74 | } 75 | append_match(&body[current_index..], &mut lines, Style::default()); 76 | for line in lines { 77 | text.push_line(Line::from(line)); 78 | } 79 | } else { 80 | text = body.into(); 81 | }; 82 | 83 | Paragraph::new(text).block( 84 | Block::new() 85 | .border_type(BorderType::Rounded) 86 | .border_style(Style::default().fg(Color::Gray)) 87 | .borders(Borders::ALL) 88 | .title("Input"), 89 | ) 90 | } 91 | } 92 | 93 | pub fn captures(reg: Option, body: String) -> impl Widget { 94 | if let Some(reg) = reg { 95 | let mut rows: Vec = vec![]; 96 | let names = reg 97 | .capture_names() 98 | .enumerate() 99 | .map(|(i, x)| match x { 100 | Some(name) => name.to_string(), 101 | None => i.to_string(), 102 | }) 103 | .collect::>(); 104 | 105 | let widths = vec![Constraint::Fill(1); names.len()]; 106 | 107 | for (i, cap) in reg.captures_iter(&body).enumerate() { 108 | rows.push( 109 | Row::new(cap.iter().map(|sub| match sub { 110 | Some(sub) => sub.as_str().to_string(), 111 | None => "".to_string(), 112 | })) 113 | .style(Style::default().fg(get_color(i))), 114 | ) 115 | } 116 | 117 | Table::new(rows, widths) 118 | .column_spacing(1) 119 | .header(Row::new(names).bottom_margin(1)) 120 | .block( 121 | Block::new() 122 | .border_type(BorderType::Rounded) 123 | .border_style(Style::default().fg(Color::Gray)) 124 | .borders(Borders::ALL) 125 | .title("Captures"), 126 | ) 127 | } else { 128 | let rows: Vec = vec![]; 129 | let widths: Vec = vec![]; 130 | Table::new(rows, widths).block( 131 | Block::new() 132 | .border_type(BorderType::Rounded) 133 | .border_style(Style::default().fg(Color::Gray)) 134 | .borders(Borders::ALL) 135 | .title("Captures"), 136 | ) 137 | } 138 | } 139 | 140 | pub fn substitution(body: String, reg: Option, substitution: String) -> impl Widget { 141 | let body = match reg { 142 | Some(regex) => regex.replace_all(&body, &substitution).to_string(), 143 | None => body, 144 | }; 145 | Paragraph::new(body).block( 146 | Block::new() 147 | .border_type(BorderType::Rounded) 148 | .border_style(Style::default().fg(Color::Gray)) 149 | .borders(Borders::ALL) 150 | .title("Output"), 151 | ) 152 | } 153 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io, 3 | io::{BufWriter, IsTerminal}, 4 | }; 5 | 6 | use ratatui::crossterm::{ 7 | self, 8 | event::{DisableMouseCapture, EnableMouseCapture}, 9 | terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, 10 | }; 11 | use ratatui::{backend::CrosstermBackend, Terminal}; 12 | 13 | mod app; 14 | mod banners; 15 | mod body; 16 | mod regex_input; 17 | 18 | fn read_from_stdin() -> Option> { 19 | let input = io::stdin(); 20 | if !input.is_terminal() { 21 | Some(input.lines().map(|l| l.unwrap()).collect()) 22 | } else { 23 | None 24 | } 25 | } 26 | 27 | fn main() -> io::Result<()> { 28 | let input = read_from_stdin(); 29 | let output = io::stderr(); 30 | let mut output = output.lock(); 31 | 32 | enable_raw_mode()?; 33 | crossterm::execute!(output, EnterAlternateScreen, EnableMouseCapture)?; 34 | let mut term = Terminal::new(CrosstermBackend::new(BufWriter::new(output)))?; 35 | 36 | let mut app = app::App::new(input); 37 | let output = app.run(&mut term)?; 38 | 39 | disable_raw_mode()?; 40 | crossterm::execute!( 41 | term.backend_mut(), 42 | LeaveAlternateScreen, 43 | DisableMouseCapture 44 | )?; 45 | term.show_cursor()?; 46 | 47 | if let Some(output) = output { 48 | println!("{}", output); 49 | } 50 | 51 | Ok(()) 52 | } 53 | -------------------------------------------------------------------------------- /src/regex_input.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | style::{Color, Style}, 3 | widgets::{Block, BorderType, Borders, Paragraph, Widget}, 4 | }; 5 | use regex::Regex; 6 | use tui_textarea::TextArea; 7 | 8 | pub struct RegexInput<'a> { 9 | pub textarea: TextArea<'a>, 10 | } 11 | 12 | impl RegexInput<'_> { 13 | pub fn new() -> Self { 14 | let mut textarea = TextArea::default(); 15 | textarea.set_placeholder_text("Enter a valid regex"); 16 | textarea.set_style(Style::default().fg(Color::LightGreen)); 17 | 18 | textarea.set_block( 19 | Block::default() 20 | .border_type(BorderType::Rounded) 21 | .borders(Borders::ALL) 22 | .title("Regex"), 23 | ); 24 | Self { textarea } 25 | } 26 | 27 | pub fn unfocused(&self) -> impl Widget + '_ { 28 | Paragraph::new(self.textarea.lines()[0].clone()).block( 29 | Block::new() 30 | .border_type(BorderType::Rounded) 31 | .border_style(Style::default().fg(Color::Gray)) 32 | .borders(Borders::ALL) 33 | .title("Regex"), 34 | ) 35 | } 36 | 37 | pub fn current_regex(&self) -> Option { 38 | Regex::new(&self.textarea.lines()[0]).ok() 39 | } 40 | 41 | pub fn current_regex_str(&self) -> String { 42 | self.textarea.lines()[0].clone() 43 | } 44 | 45 | pub fn validate(&mut self) { 46 | if let Err(err) = Regex::new(&self.textarea.lines()[0]) { 47 | self.textarea 48 | .set_style(Style::default().fg(Color::LightRed)); 49 | self.textarea.set_block( 50 | Block::default() 51 | .border_type(BorderType::Rounded) 52 | .borders(Borders::ALL) 53 | .border_style(Style::default().fg(Color::LightRed)) 54 | .title(format!("{}", err)), 55 | ); 56 | } else { 57 | self.textarea 58 | .set_style(Style::default().fg(Color::LightGreen)); 59 | self.textarea.set_block( 60 | Block::default() 61 | .border_type(BorderType::Rounded) 62 | .borders(Borders::ALL) 63 | .title("Regex"), 64 | ); 65 | } 66 | } 67 | } 68 | 69 | pub struct SubstitutionInput<'a> { 70 | pub textarea: TextArea<'a>, 71 | } 72 | 73 | impl SubstitutionInput<'_> { 74 | pub fn new() -> Self { 75 | let mut textarea = TextArea::default(); 76 | textarea.set_placeholder_text("Enter substitution string"); 77 | textarea.set_style(Style::default().fg(Color::LightGreen)); 78 | 79 | textarea.set_block( 80 | Block::default() 81 | .border_type(BorderType::Rounded) 82 | .borders(Borders::ALL) 83 | .title("Substitution"), 84 | ); 85 | Self { textarea } 86 | } 87 | 88 | pub fn unfocused(&self) -> impl Widget + '_ { 89 | Paragraph::new(self.textarea.lines()[0].clone()).block( 90 | Block::new() 91 | .border_type(BorderType::Rounded) 92 | .border_style(Style::default().fg(Color::Gray)) 93 | .borders(Borders::ALL) 94 | .title("Substitution"), 95 | ) 96 | } 97 | 98 | pub fn current_substitution(&self) -> String { 99 | self.textarea.lines()[0].clone() 100 | } 101 | } 102 | --------------------------------------------------------------------------------