diff -Nru tailspin-1.3.0+dfsg/Cargo.lock tailspin-2.0.0+dfsg/Cargo.lock --- tailspin-1.3.0+dfsg/Cargo.lock 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/Cargo.lock 2023-11-05 06:58:05.000000000 +0000 @@ -28,16 +28,15 @@ [[package]] name = "anstream" -version = "0.3.2" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", - "is-terminal", "utf8parse", ] @@ -67,9 +66,9 @@ [[package]] name = "anstyle-wincon" -version = "1.0.1" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" dependencies = [ "anstyle", "windows-sys 0.48.0", @@ -77,9 +76,9 @@ [[package]] name = "async-trait" -version = "0.1.72" +version = "0.1.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" +checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", @@ -115,9 +114,9 @@ [[package]] name = "bitflags" -version = "2.3.3" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" [[package]] name = "bytes" @@ -139,20 +138,19 @@ [[package]] name = "clap" -version = "4.3.19" +version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd304a20bff958a57f04c4e96a2e7594cc4490a0e809cbd48bb6437edaa452d" +checksum = "ac495e00dcec98c83465d5ad66c5c4fabd652fd6686e7c6269b117e729a6f17b" dependencies = [ "clap_builder", "clap_derive", - "once_cell", ] [[package]] name = "clap_builder" -version = "4.3.19" +version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c6a3f08f1fe5662a35cfe393aec09c4df95f60ee93b7556505260f75eee9e1" +checksum = "c77ed9a32a62e6ca27175d00d29d05ca32e396ea1eb5fb01d8256b669cec7663" dependencies = [ "anstream", "anstyle", @@ -162,18 +160,18 @@ [[package]] name = "clap_complete" -version = "4.3.2" +version = "4.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc443334c81a804575546c5a8a79b4913b50e28d69232903604cada1de817ce" +checksum = "bffe91f06a11b4b9420f62103854e90867812cd5d01557f853c5ee8e791b12ae" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.3.12" +version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" dependencies = [ "heck", "proc-macro2", @@ -183,9 +181,36 @@ [[package]] name = "clap_lex" -version = "0.5.0" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + +[[package]] +name = "color-eyre" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a667583cca8c4f8436db8de46ea8233c42a7d9ae424a82d338f2e4675229204" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +checksum = "1ba75b3d9449ecdccb27ecbc479fdc0b87fa2dd43d2f8298f9bf0e59aacc8dce" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] [[package]] name = "colorchoice" @@ -225,9 +250,9 @@ [[package]] name = "ctrlc" -version = "3.4.0" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a011bbe2c35ce9c1f143b7af6f94f29a167beb4cd1d29e6740ce836f723120e" +checksum = "82e95fbd621905b854affdc67943b043a0fbb6ed7385fd5a25650d19a8a6cfdf" dependencies = [ "nix", "windows-sys 0.48.0", @@ -282,6 +307,16 @@ ] [[package]] +name = "eyre" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] name = "fastrand" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -369,6 +404,12 @@ checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" [[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] name = "indexmap" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -437,9 +478,9 @@ [[package]] name = "libc" -version = "0.2.146" +version = "0.2.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" +checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" [[package]] name = "linemux" @@ -455,9 +496,9 @@ [[package]] name = "linux-raw-sys" -version = "0.4.3" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" +checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" [[package]] name = "lock_api" @@ -477,9 +518,9 @@ [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" [[package]] name = "miniz_oxide" @@ -504,14 +545,13 @@ [[package]] name = "nix" -version = "0.26.2" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.1", "cfg-if", "libc", - "static_assertions", ] [[package]] @@ -563,6 +603,12 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + +[[package]] name = "parking_lot" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -587,9 +633,9 @@ [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" [[package]] name = "pin-utils" @@ -670,6 +716,15 @@ ] [[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] name = "redox_users" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -682,9 +737,9 @@ [[package]] name = "regex" -version = "1.9.1" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick", "memchr", @@ -694,9 +749,9 @@ [[package]] name = "regex-automata" -version = "0.3.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d3daa6976cffb758ec878f108ba0e062a45b2d6ca3a2cca965338855476caf" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" dependencies = [ "aho-corasick", "memchr", @@ -705,9 +760,9 @@ [[package]] name = "regex-syntax" -version = "0.7.4" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "rustc-demangle" @@ -717,11 +772,11 @@ [[package]] name = "rustix" -version = "0.38.1" +version = "0.38.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc6396159432b5c8490d4e301d8c705f61860b8b6c863bf79942ce5401968f3" +checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" dependencies = [ - "bitflags 2.3.3", + "bitflags 2.4.1", "errno", "libc", "linux-raw-sys", @@ -745,18 +800,18 @@ [[package]] name = "serde" -version = "1.0.179" +version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a5bf42b8d227d4abf38a1ddb08602e229108a517cd4e5bb28f9c7eaafdce5c0" +checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.179" +version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "741e124f5485c7e60c03b043f79f320bff3527f4bbf12cf3831750dc46a0ec2c" +checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" dependencies = [ "proc-macro2", "quote", @@ -765,14 +820,23 @@ [[package]] name = "serde_spanned" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" dependencies = [ "serde", ] [[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] name = "shellexpand" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -807,21 +871,15 @@ [[package]] name = "socket2" -version = "0.4.9" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" dependencies = [ "libc", - "winapi", + "windows-sys 0.48.0", ] [[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -829,9 +887,9 @@ [[package]] name = "syn" -version = "2.0.25" +version = "2.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e3fc8c0c74267e2df136e5e5fb656a464158aa57624053375eb9c8c6e25ae2" +checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" dependencies = [ "proc-macro2", "quote", @@ -840,11 +898,12 @@ [[package]] name = "tailspin" -version = "1.3.0" +version = "2.0.0" dependencies = [ "async-trait", "clap", "clap_complete", + "color-eyre", "colored", "ctrlc", "lazy_static", @@ -855,19 +914,30 @@ "serde", "shellexpand", "tempfile", + "terminal_size", "tokio", "toml", ] [[package]] name = "tempfile" -version = "3.7.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" dependencies = [ "cfg-if", "fastrand", - "redox_syscall 0.3.5", + "redox_syscall 0.4.1", + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "terminal_size" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" +dependencies = [ "rustix", "windows-sys 0.48.0", ] @@ -893,12 +963,21 @@ ] [[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] name = "tokio" -version = "1.29.1" +version = "1.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" dependencies = [ - "autocfg", "backtrace", "bytes", "libc", @@ -925,9 +1004,9 @@ [[package]] name = "toml" -version = "0.7.6" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542" +checksum = "8ff9e3abce27ee2c9a37f9ad37238c1bdd4e789c84ba37df76aa4d528f5072cc" dependencies = [ "serde", "serde_spanned", @@ -937,18 +1016,18 @@ [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.19.12" +version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c500344a19072298cd05a7224b3c0c629348b78692bf48466c5238656e315a78" +checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" dependencies = [ "indexmap", "serde", @@ -958,6 +1037,48 @@ ] [[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", +] + +[[package]] name = "unicode-ident" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -970,6 +1091,12 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] name = "walkdir" version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1150,9 +1277,9 @@ [[package]] name = "winnow" -version = "0.4.7" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448" +checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" dependencies = [ "memchr", ] diff -Nru tailspin-1.3.0+dfsg/Cargo.toml tailspin-2.0.0+dfsg/Cargo.toml --- tailspin-1.3.0+dfsg/Cargo.toml 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/Cargo.toml 2023-11-05 06:58:05.000000000 +0000 @@ -1,6 +1,6 @@ [package] name = "tailspin" -version = "1.3.0" +version = "2.0.0" edition = "2021" authors = ["Ben Sadeh"] description = "A log file highlighter" @@ -11,21 +11,23 @@ [[bin]] path = "src/main.rs" -name = "spin" +name = "tspin" [dependencies] -async-trait = "0.1.72" -clap = { version = "4.3.19", features = ["derive"] } -clap_complete = "4.3.2" +async-trait = "0.1.74" +clap = { version = "4.4.7", features = ["derive"] } +clap_complete = "4.4.4" +color-eyre = "0.6.2" colored = "2" -ctrlc = "3.4.0" +ctrlc = "3.4.1" lazy_static = "1.4.0" linemux = "0.3" once_cell = "1.18.0" rand = "0.8.5" -regex = "1.9.1" +regex = "1.10.2" serde = { version = "1.0", features = ["derive"] } shellexpand = "3.1.0" -tempfile = "3.7.0" -tokio = { version = "1.29.1", features = ["full"] } -toml = "0.7.6" +tempfile = "3.8.1" +terminal_size = "0.3.0" +tokio = { version = "1.33.0", features = ["full"] } +toml = "0.8.6" diff -Nru tailspin-1.3.0+dfsg/CHANGELOG.md tailspin-2.0.0+dfsg/CHANGELOG.md --- tailspin-1.3.0+dfsg/CHANGELOG.md 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/CHANGELOG.md 2023-11-05 06:58:05.000000000 +0000 @@ -1,6 +1,48 @@ # Changelog +## 1.7.1 -## 1.3.0 + + + +## 2.0.0 + +- Changed the binary name from `spin` to `tspin` + +This is a symbolic release to settle on a new binary name with fewer conflicts. Both `tailspin` and `spin` already exist +as binaries in different systems and distributions. `tspin` is a short and unique name that is unlikely to conflict with +other binaries. + +## 1.6.1 + +- Fixed a bug where the `--print` flag would occasionally cause a panic + +## 1.6.0 + +- Added new highlight group under Keywords highlighter: HTTP methods +- Added option for adding a border to keywords highlighter +- Disable highlights with `disable` for all highlight groups except Keywords +- Simplified the configuration file format +- Date and time can be configured to be hidden + +## 1.5.1 + +- Update man pages + +## 1.5.0 - 16.09.23 + +- Errors are now printed to `stderr` instead of `stdout` +- Date highlighter now supports different highlights for date and time segment +- Added Key Value highlighter +- Added unix process highlighter + +## 1.4.0 - 12.08.23 + +- Added `-t`/`--tail` flag to start reading from the end of a file. +- Fixed a bug where opening a folder would include hidden files +- Improved initial output when watching folders +- Improved output when trying to open a file or folder which doesn't exist + +## 1.3.0 - 31.07.23 - Added support for tailing folders - Changed behavior: `tailspin` will now print to `stdout` by default if used in a pipe. For diff -Nru tailspin-1.3.0+dfsg/completions/spin.bash tailspin-2.0.0+dfsg/completions/spin.bash --- tailspin-1.3.0+dfsg/completions/spin.bash 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/completions/spin.bash 1970-01-01 00:00:00.000000000 +0000 @@ -1,58 +0,0 @@ -_spin() { - local i cur prev opts cmd - COMPREPLY=() - cur="${COMP_WORDS[COMP_CWORD]}" - prev="${COMP_WORDS[COMP_CWORD-1]}" - cmd="" - opts="" - - for i in ${COMP_WORDS[@]} - do - case "${cmd},${i}" in - ",$1") - cmd="spin" - ;; - *) - ;; - esac - done - - case "${cmd}" in - spin) - opts="-f -p -c -l -h -V --follow --print --config-path --follow-command --create-default-config --show-default-config --z-generate-shell-completions --help --version [FILE]" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - --config-path) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - -c) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --follow-command) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - -l) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --z-generate-shell-completions) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - esac -} - -complete -F _spin -o bashdefault -o default spin diff -Nru tailspin-1.3.0+dfsg/completions/spin.fish tailspin-2.0.0+dfsg/completions/spin.fish --- tailspin-1.3.0+dfsg/completions/spin.fish 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/completions/spin.fish 1970-01-01 00:00:00.000000000 +0000 @@ -1,9 +0,0 @@ -complete -c spin -s c -l config-path -d 'Path to a custom configuration file' -r -complete -c spin -s l -l follow-command -d 'Continuously listens to the stdout of the provided command and prevents interrupt events (Ctrl + C) from reaching the command' -r -complete -c spin -l z-generate-shell-completions -d 'Print completions to stdout' -r -complete -c spin -s f -l follow -d 'Follow (tail) the contents of the file' -complete -c spin -s p -l print -d 'Print the output to stdout' -complete -c spin -l create-default-config -d 'Generate a new configuration file' -complete -c spin -l show-default-config -d 'Print the default configuration' -complete -c spin -s h -l help -d 'Print help' -complete -c spin -s V -l version -d 'Print version' diff -Nru tailspin-1.3.0+dfsg/completions/spin.zsh tailspin-2.0.0+dfsg/completions/spin.zsh --- tailspin-1.3.0+dfsg/completions/spin.zsh 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/completions/spin.zsh 1970-01-01 00:00:00.000000000 +0000 @@ -1,47 +0,0 @@ -#compdef spin - -autoload -U is-at-least - -_spin() { - typeset -A opt_args - typeset -a _arguments_options - local ret=1 - - if is-at-least 5.2; then - _arguments_options=(-s -S -C) - else - _arguments_options=(-s -C) - fi - - local context curcontext="$curcontext" state line - _arguments "${_arguments_options[@]}" \ -'-c+[Path to a custom configuration file]:CONFIG_PATH: ' \ -'--config-path=[Path to a custom configuration file]:CONFIG_PATH: ' \ -'(-f --follow)-l+[Continuously listens to the stdout of the provided command and prevents interrupt events (Ctrl + C) from reaching the command]:LISTEN_COMMAND: ' \ -'(-f --follow)--follow-command=[Continuously listens to the stdout of the provided command and prevents interrupt events (Ctrl + C) from reaching the command]:LISTEN_COMMAND: ' \ -'--z-generate-shell-completions=[Print completions to stdout]:GENERATE_SHELL_COMPLETIONS: ' \ -'-f[Follow (tail) the contents of the file]' \ -'--follow[Follow (tail) the contents of the file]' \ -'(-f --follow)-p[Print the output to stdout]' \ -'(-f --follow)--print[Print the output to stdout]' \ -'--create-default-config[Generate a new configuration file]' \ -'(--create-default-config)--show-default-config[Print the default configuration]' \ -'-h[Print help]' \ -'--help[Print help]' \ -'-V[Print version]' \ -'--version[Print version]' \ -'::FILE -- Path to file or folder:' \ -&& ret=0 -} - -(( $+functions[_spin_commands] )) || -_spin_commands() { - local commands; commands=() - _describe -t commands 'spin commands' commands "$@" -} - -if [ "$funcstack[1]" = "_spin" ]; then - _spin "$@" -else - compdef _spin spin -fi diff -Nru tailspin-1.3.0+dfsg/completions/tspin.bash tailspin-2.0.0+dfsg/completions/tspin.bash --- tailspin-1.3.0+dfsg/completions/tspin.bash 1970-01-01 00:00:00.000000000 +0000 +++ tailspin-2.0.0+dfsg/completions/tspin.bash 2023-11-05 06:58:05.000000000 +0000 @@ -0,0 +1,58 @@ +_tspin() { + local i cur prev opts cmd + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + cmd="" + opts="" + + for i in ${COMP_WORDS[@]} + do + case "${cmd},${i}" in + ",$1") + cmd="tspin" + ;; + *) + ;; + esac + done + + case "${cmd}" in + tspin) + opts="-f -t -p -c -l -h -V --follow --tail --print --config-path --follow-command --z-generate-shell-completions --help --version [FILE]" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + --config-path) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -c) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --follow-command) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -l) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --z-generate-shell-completions) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + esac +} + +complete -F _tspin -o nosort -o bashdefault -o default tspin diff -Nru tailspin-1.3.0+dfsg/completions/tspin.fish tailspin-2.0.0+dfsg/completions/tspin.fish --- tailspin-1.3.0+dfsg/completions/tspin.fish 1970-01-01 00:00:00.000000000 +0000 +++ tailspin-2.0.0+dfsg/completions/tspin.fish 2023-11-05 06:58:05.000000000 +0000 @@ -0,0 +1,8 @@ +complete -c tspin -s c -l config-path -d 'Path to a custom configuration file' -r +complete -c tspin -s l -l follow-command -d 'Continuously listens to the stdout of the provided command and prevents interrupt events (Ctrl + C) from reaching the command' -r +complete -c tspin -l z-generate-shell-completions -d 'Print completions to stdout' -r +complete -c tspin -s f -l follow -d 'Follow (tail) the contents of the file' +complete -c tspin -s t -l tail -d 'Start at the end of the file' +complete -c tspin -s p -l print -d 'Print the output to stdout' +complete -c tspin -s h -l help -d 'Print help' +complete -c tspin -s V -l version -d 'Print version' diff -Nru tailspin-1.3.0+dfsg/completions/tspin.zsh tailspin-2.0.0+dfsg/completions/tspin.zsh --- tailspin-1.3.0+dfsg/completions/tspin.zsh 1970-01-01 00:00:00.000000000 +0000 +++ tailspin-2.0.0+dfsg/completions/tspin.zsh 2023-11-05 06:58:05.000000000 +0000 @@ -0,0 +1,47 @@ +#compdef tspin + +autoload -U is-at-least + +_tspin() { + typeset -A opt_args + typeset -a _arguments_options + local ret=1 + + if is-at-least 5.2; then + _arguments_options=(-s -S -C) + else + _arguments_options=(-s -C) + fi + + local context curcontext="$curcontext" state line + _arguments "${_arguments_options[@]}" \ +'-c+[Path to a custom configuration file]:CONFIG_PATH: ' \ +'--config-path=[Path to a custom configuration file]:CONFIG_PATH: ' \ +'(-f --follow)-l+[Continuously listens to the stdout of the provided command and prevents interrupt events (Ctrl + C) from reaching the command]:LISTEN_COMMAND: ' \ +'(-f --follow)--follow-command=[Continuously listens to the stdout of the provided command and prevents interrupt events (Ctrl + C) from reaching the command]:LISTEN_COMMAND: ' \ +'--z-generate-shell-completions=[Print completions to stdout]:GENERATE_SHELL_COMPLETIONS: ' \ +'-f[Follow (tail) the contents of the file]' \ +'--follow[Follow (tail) the contents of the file]' \ +'-t[Start at the end of the file]' \ +'--tail[Start at the end of the file]' \ +'(-f --follow)-p[Print the output to stdout]' \ +'(-f --follow)--print[Print the output to stdout]' \ +'-h[Print help]' \ +'--help[Print help]' \ +'-V[Print version]' \ +'--version[Print version]' \ +'::FILE -- Path to file or folder:' \ +&& ret=0 +} + +(( $+functions[_tspin_commands] )) || +_tspin_commands() { + local commands; commands=() + _describe -t commands 'tspin commands' commands "$@" +} + +if [ "$funcstack[1]" = "_tspin" ]; then + _tspin "$@" +else + compdef _tspin tspin +fi diff -Nru tailspin-1.3.0+dfsg/data/config.toml tailspin-2.0.0+dfsg/data/config.toml --- tailspin-1.3.0+dfsg/data/config.toml 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/data/config.toml 1970-01-01 00:00:00.000000000 +0000 @@ -1,54 +0,0 @@ -[groups.date] -style = { fg = "magenta" } - -[groups.number] -style = { fg = "cyan" } - -[groups.quotes] -style = { fg = "yellow" } -token = '"' - -[groups.uuid] -segment = { fg = "blue", italic = true } -separator = { fg = "red" } - -[groups.ip] -segment = { fg = "blue", italic = true } -separator = { fg = "red" } - -[groups.path] -segment = { fg = "green", italic = true } -separator = { fg = "yellow" } - -[groups.url] -http = { faint = true } -https = { bold = true } -host = { fg = "blue", faint = true } -path = { fg = "blue" } -query_params_key = { fg = "magenta" } -query_params_value = { fg = "cyan" } -symbols = { fg = "red" } - -[[groups.keywords]] -words = ['ERROR'] -style = { fg = "red" } - -[[groups.keywords]] -words = ['WARN'] -style = { fg = "yellow" } - -[[groups.keywords]] -words = ['INFO'] -style = { fg = "white" } - -[[groups.keywords]] -words = ['DEBUG'] -style = { fg = "green" } - -[[groups.keywords]] -words = ['TRACE'] -style = { faint = true } - -[[groups.keywords]] -words = ['null', 'true', 'false'] -style = { fg = "red", italic = true } \ No newline at end of file diff -Nru tailspin-1.3.0+dfsg/debian/changelog tailspin-2.0.0+dfsg/debian/changelog --- tailspin-1.3.0+dfsg/debian/changelog 2023-08-13 08:26:58.000000000 +0000 +++ tailspin-2.0.0+dfsg/debian/changelog 2023-11-05 08:09:40.000000000 +0000 @@ -1,3 +1,15 @@ +tailspin (2.0.0+dfsg-1) unstable; urgency=medium + + * drop patch 2001, obsoleted by Debian package changes; + update and unfuzz patches; + add build-dependencies + on packages for crates color-eyre terminal_size; + bump build-dependency for crate toml; + stop builddepend on package for crate futures-util + * update long description, based on upstream documentation + + -- Jonas Smedegaard Sun, 05 Nov 2023 09:09:40 +0100 + tailspin (1.3.0+dfsg-1) unstable; urgency=medium * update patches; diff -Nru tailspin-1.3.0+dfsg/debian/control tailspin-2.0.0+dfsg/debian/control --- tailspin-1.3.0+dfsg/debian/control 2023-08-12 23:59:32.000000000 +0000 +++ tailspin-2.0.0+dfsg/debian/control 2023-11-05 08:09:40.000000000 +0000 @@ -14,9 +14,9 @@ librust-clap-4+derive-dev, librust-clap-complete-4+default-dev, librust-linemux-0.3+default-dev, + librust-color-eyre-0.6+default-dev, librust-colored-2+default-dev, librust-ctrlc-3+default-dev, - librust-futures-util-dev, librust-lazy-static-1+default-dev, librust-once-cell-1+default-dev, librust-rand-0.8+default-dev, @@ -25,9 +25,10 @@ librust-serde-1+derive-dev, librust-shellexpand-3+default-dev, librust-tempfile-3+default-dev, + librust-terminal-size-0.2+default-dev, librust-tokio-1+default-dev, librust-tokio-1+full-dev, - librust-toml-0.5+default-dev, + librust-toml-0.7+default-dev, libstring-shellquote-perl, Standards-Version: 4.6.2 Homepage: https://github.com/bensadeh/tailspin @@ -49,10 +50,8 @@ ${cargo:X-Cargo-Built-Using}, Description: log file highlighter tailspin is a command line tool - for viewing (and `tail`-ing) log files. + for viewing and tailing log files. It highlights important keywords - to make navigating log files easier. - . - tailspin is fast and easy to customize. - It uses less under the hood - to provide scrollback, search and filtering. + to make navigating log files easier, + using less under the hood + for scrollback, search and filtering. diff -Nru tailspin-1.3.0+dfsg/debian/copyright_hints tailspin-2.0.0+dfsg/debian/copyright_hints --- tailspin-1.3.0+dfsg/debian/copyright_hints 2023-08-13 00:02:23.000000000 +0000 +++ tailspin-2.0.0+dfsg/debian/copyright_hints 2023-11-05 08:09:40.000000000 +0000 @@ -8,10 +8,9 @@ Cargo.lock Cargo.toml README.md - completions/spin.bash - completions/spin.fish - completions/spin.zsh - data/config.toml + completions/tspin.bash + completions/tspin.fish + completions/tspin.zsh debian/TODO debian/bash-completion debian/clean @@ -23,7 +22,6 @@ debian/manpages debian/patches/1001_deps.patch debian/patches/1002_rename.patch - debian/patches/2001_IsTerminal.patch debian/patches/README debian/patches/series debian/rules @@ -35,42 +33,43 @@ example-logs/example3 example-logs/example4 example-logs/example5 + rustfmt.toml src/cli/mod.rs src/color.rs - src/controller/config.rs - src/controller/mod.rs - src/file_utils/mod.rs + src/config/mod.rs src/highlight_processor.rs src/highlight_utils.rs src/highlighters/date.rs src/highlighters/ip.rs + src/highlighters/key_value.rs src/highlighters/keyword.rs src/highlighters/mod.rs src/highlighters/number.rs src/highlighters/path.rs + src/highlighters/process.rs src/highlighters/quotes.rs + src/highlighters/time.rs src/highlighters/url.rs src/highlighters/uuid.rs - src/io_stream/linemux_reader.rs - src/io_stream/mod.rs - src/io_stream/temp_file_writer.rs - src/io_stream/template_io.rs - src/io_stream/traits.rs + src/io/controller/mod.rs + src/io/mod.rs + src/io/presenter/empty.rs + src/io/presenter/less.rs + src/io/presenter/mod.rs + src/io/reader/command.rs + src/io/reader/linemux.rs + src/io/reader/mod.rs + src/io/reader/stdin.rs + src/io/writer/mod.rs + src/io/writer/stdout.rs + src/io/writer/temp_file.rs src/line_info.rs src/main.rs - src/presenter/empty.rs - src/presenter/less.rs - src/presenter/mod.rs - src/reader/command.rs - src/reader/linemux.rs - src/reader/mod.rs - src/reader/stdin.rs + src/regex/mod.rs + src/theme/defaults.rs src/theme/mod.rs src/theme_io/mod.rs src/types.rs - src/writer/mod.rs - src/writer/stdout.rs - src/writer/temp_file.rs util/generate_all.sh util/generate_man_pages.sh util/generate_shell_completions.sh @@ -78,8 +77,8 @@ License: UNKNOWN FIXME -Files: man/spin.1 - util/spin.adoc +Files: man/tspin.1 + util/tspin.adoc Copyright: NONE License: Expat FIXME diff -Nru tailspin-1.3.0+dfsg/debian/patches/1001_deps.patch tailspin-2.0.0+dfsg/debian/patches/1001_deps.patch --- tailspin-1.3.0+dfsg/debian/patches/1001_deps.patch 2023-08-12 23:46:40.000000000 +0000 +++ tailspin-2.0.0+dfsg/debian/patches/1001_deps.patch 2023-11-05 08:09:40.000000000 +0000 @@ -5,26 +5,31 @@ This patch header follows DEP-3: http://dep.debian.net/deps/dep3/ --- a/Cargo.toml +++ b/Cargo.toml -@@ -15,17 +15,17 @@ +@@ -14,12 +14,12 @@ + name = "tspin" [dependencies] - async-trait = "0.1.72" --clap = { version = "4.3.19", features = ["derive"] } --clap_complete = "4.3.2" -+clap = { version = "4.1.13", features = ["derive"] } -+clap_complete = "4.3.1" +-async-trait = "0.1.74" +-clap = { version = "4.4.7", features = ["derive"] } +-clap_complete = "4.4.4" ++async-trait = "0.1.72" ++clap = { version = "4.4.6", features = ["derive"] } ++clap_complete = "4.4.3" + color-eyre = "0.6.2" colored = "2" - ctrlc = "3.4.0" +-ctrlc = "3.4.1" ++ctrlc = "3.4.0" lazy_static = "1.4.0" linemux = "0.3" once_cell = "1.18.0" - rand = "0.8.5" --regex = "1.9.1" -+regex = "1.7.1" +@@ -27,7 +27,7 @@ + regex = "1.10.2" serde = { version = "1.0", features = ["derive"] } shellexpand = "3.1.0" --tempfile = "3.7.0" -+tempfile = "3.6.0" - tokio = { version = "1.29.1", features = ["full"] } --toml = "0.7.6" -+toml = ">= 0.5, < 0.8" +-tempfile = "3.8.1" +-terminal_size = "0.3.0" ++tempfile = "3.8.0" ++terminal_size = ">= 0.2.6, < 0.4" + tokio = { version = "1.33.0", features = ["full"] } +-toml = "0.8.6" ++toml = "0.7.6" diff -Nru tailspin-1.3.0+dfsg/debian/patches/1002_rename.patch tailspin-2.0.0+dfsg/debian/patches/1002_rename.patch --- tailspin-1.3.0+dfsg/debian/patches/1002_rename.patch 2023-08-05 23:31:41.000000000 +0000 +++ tailspin-2.0.0+dfsg/debian/patches/1002_rename.patch 2023-11-05 08:09:40.000000000 +0000 @@ -6,14 +6,12 @@ This patch header follows DEP-3: http://dep.debian.net/deps/dep3/ --- a/Cargo.toml +++ b/Cargo.toml -@@ -9,10 +9,6 @@ - license = "MIT" - rust-version = "1.70" +@@ -11,7 +11,7 @@ + + [[bin]] + path = "src/main.rs" +-name = "tspin" ++name = "tailspin" --[[bin]] --path = "src/main.rs" --name = "spin" -- [dependencies] async-trait = "0.1.72" - clap = { version = "4.1.13", features = ["derive"] } diff -Nru tailspin-1.3.0+dfsg/debian/patches/2001_IsTerminal.patch tailspin-2.0.0+dfsg/debian/patches/2001_IsTerminal.patch --- tailspin-1.3.0+dfsg/debian/patches/2001_IsTerminal.patch 2023-08-12 23:55:40.000000000 +0000 +++ tailspin-2.0.0+dfsg/debian/patches/2001_IsTerminal.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,44 +0,0 @@ -Description: replace std::io::IsTerminal with crate atty - std::io::IsTerminal is unstable until rustc v0.70, - which is not yet in Debian. -Author: Jonas Smedegaard -Bug: https://github.com/bensadeh/tailspin/issues/21 -Last-Update: 2023-07-23 ---- -This patch header follows DEP-3: http://dep.debian.net/deps/dep3/ ---- a/src/controller/config.rs -+++ b/src/controller/config.rs -@@ -4,9 +4,10 @@ - Config, Error, FolderInfo, Input, Output, PathAndLineCount, GENERAL_ERROR, - MISUSE_SHELL_BUILTIN, OK, - }; -+use atty::Stream; - use colored::*; - use std::fs; --use std::io::{stdin, IsTerminal}; -+use std::io::stdin; - use std::path::Path; - - enum InputType { -@@ -21,7 +22,7 @@ - } - - pub fn create_config(args: Cli) -> Result { -- let has_data_from_stdin = !stdin().is_terminal(); -+ let has_data_from_stdin = !atty::is(Stream::Stdin); - - validate_input( - has_data_from_stdin, ---- a/Cargo.toml -+++ b/Cargo.toml -@@ -7,9 +7,9 @@ - repository = "https://github.com/bensadeh/tailspin" - keywords = ["log", "syntax-highlighting", "tail", "less"] - license = "MIT" --rust-version = "1.70" - - [dependencies] -+atty = "0.2" - async-trait = "0.1.72" - clap = { version = "4.1.13", features = ["derive"] } - clap_complete = "4.3.1" diff -Nru tailspin-1.3.0+dfsg/debian/patches/series tailspin-2.0.0+dfsg/debian/patches/series --- tailspin-1.3.0+dfsg/debian/patches/series 2023-08-05 23:32:06.000000000 +0000 +++ tailspin-2.0.0+dfsg/debian/patches/series 2023-11-05 08:09:40.000000000 +0000 @@ -1,3 +1,2 @@ 1001_deps.patch 1002_rename.patch -2001_IsTerminal.patch diff -Nru tailspin-1.3.0+dfsg/example-logs/example1 tailspin-2.0.0+dfsg/example-logs/example1 --- tailspin-1.3.0+dfsg/example-logs/example1 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/example-logs/example1 2023-11-05 06:58:05.000000000 +0000 @@ -8,10 +8,10 @@ Sun Aug 14 10:51:22.602 DEBUG sharingd (542) is not entitled for com.apple.wifi.join_history, will not allow request # Date and Time -2022-08-29 08:11:36 Task completed. +07:46:34 Data backup created +10:51:19.251 Event logged +2022-08-29 08:11:36 Task completed 2022-09-09 11:48:34,534 Last login attempt failed -2022-09-22T07:46:34.171800155Z Server encountered an error -2022-09-22T07:46:34.172389889Z Data backup created 2022-09-22T07:46:34.171800155Z Event logged for debugging purposes # Numbers @@ -28,6 +28,13 @@ https://www.openai.com/docs/api?apikey=abc123 https://api.example.org/v1/users/123/edit?param=true&flag=false +# HTTP Methods +User with IP 192.168.1.105 initiated a GET request for /about/us +A POST request by 192.168.1.106 to /api/create-account failed +Received a PUT operation on /api/v1/users/update-profile +During routine checks at 14:13:40, a PATCH request was detected +Unauthorized DELETE attempt on /api/v2/data/secure by user + # UUID User logged in, user ID: 5f7d1bce-81ab-4a87-af78-9a37f26c58b1 Invalid request: 9f5f25be-6b08-4aeb-a10a-023b86581b1f @@ -63,8 +70,19 @@ Purchase for item "Red Bicycle" was successful. Thank you! Configuration file "PrimaryConfig" loaded. Starting operations. Processing new order for customer "JohnDoe". Stand by. -Fetch operation for record "ID_A56789" failed. Retrying. -Connection established with "Core Server". Commencing data xfer. -Data sync issue detected with "InventoryDB". Initiate restore. -File "DataLog" failed to update. Possible permission issue. + +# Key-value pairs +ts=08:11:36 caller=module_service.go:59 module=table-manager +ts=08:11:36 caller=module_service.go:59 module=ingester-querier +ts=08:11:36 caller=lifecycler.go:530 msg="file path is empty" +ts=2022-09-22 caller=ring.go:275 msg="ring doesn't exist" +ts=2022-09-21 caller=client.go:247 msg="value is nil" + +# Processes +kernel[0]: Page fault at address 0xabcdef +mysqld[789]: Connected from 127.0.0.1 +docker[654]: Container abcdefgh started successfully +sshd[222]: Accepted publickey for root from 192.168.1.20 +cron[101]: Running scheduled job ID 23456 + diff -Nru tailspin-1.3.0+dfsg/man/spin.1 tailspin-2.0.0+dfsg/man/spin.1 --- tailspin-1.3.0+dfsg/man/spin.1 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/man/spin.1 1970-01-01 00:00:00.000000000 +0000 @@ -1,82 +0,0 @@ -'\" t -.\" Title: spin -.\" Author: [see the "AUTHOR(S)" section] -.\" Generator: Asciidoctor 2.0.20 -.\" Date: 2023-07-31 -.\" Manual: tailspin -.\" Source: tailspin 1.3.0 -.\" Language: English -.\" -.TH "SPIN" "1" "2023-07-31" "tailspin 1.3.0" "tailspin" -.ie \n(.g .ds Aq \(aq -.el .ds Aq ' -.ss \n[.ss] 0 -.nh -.ad l -.de URL -\fI\\$2\fP <\\$1>\\$3 -.. -.als MTO URL -.if \n[.g] \{\ -. mso www.tmac -. am URL -. ad l -. . -. am MTO -. ad l -. . -. LINKSTYLE blue R < > -.\} -.SH "NAME" -spin \- A log file highlighter -.SH "SYNOPSIS" -.sp -\fBspin\fP [\fIOPTION\fP]... [\fIFILE\fP|\fIFOLDER\fP]... -.SH "DESCRIPTION" -.sp -tailspin is a command line tool that highlights log files. -.SH "OPTIONS" -.sp -\fB\-f, \-\-follow\fP -.RS 4 -Follow (tail) the contents of the file. -Always true if opening a folder or using the \fI\-\-follow\-command\fP flag. -.RE -.sp -\fB\-p, \-\-print\fP -.RS 4 -Print the output to stdout instead of viewing the contents in the pager \fIless\fP. -Always true if stdin has data. -.RE -.sp -\fB\-c, \-\-config\-path\fP \fICONFIG_PATH\fP -.RS 4 -Specify the path to a custom configuration file. -.RE -.sp -\fB\-l, \-\-follow\-command\fP \fICOMMAND\fP -.RS 4 -Continuously listens to stdout of the provided command. -The command traps the interrupt signal to allow for cancelling and resuming follow mode while inside \fIless\fP. -.RE -.sp -\fB\-\-create\-default\-config\fP -.RS 4 -Generate a new configuration file. -Uses XDG_CONFIG_HOME as the location if set. -Otherwise, uses ~/.config/tailspin/config.toml as the default path. -Will not overwrite current config file if one is already present. -.RE -.sp -\fB\-\-show\-default\-config\fP -.RS 4 -Print the default configuration. -.RE -.SH "SEE ALSO" -.sp -\fBless\fP(1), \fBtail\fP(1) -.SH "ABOUT" -.sp -Ben Sadeh (github.com/bensadeh/tailspin) -.sp -Released under the MIT License \ No newline at end of file diff -Nru tailspin-1.3.0+dfsg/man/tspin.1 tailspin-2.0.0+dfsg/man/tspin.1 --- tailspin-1.3.0+dfsg/man/tspin.1 1970-01-01 00:00:00.000000000 +0000 +++ tailspin-2.0.0+dfsg/man/tspin.1 2023-11-05 06:58:05.000000000 +0000 @@ -0,0 +1,76 @@ +'\" t +.\" Title: tspin +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.20 +.\" Date: 2023-11-05 +.\" Manual: tailspin +.\" Source: tailspin 2.0.0 +.\" Language: English +.\" +.TH "TSPIN" "1" "2023-11-05" "tailspin 2.0.0" "tailspin" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +tspin \- A log file highlighter +.SH "SYNOPSIS" +.sp +\fBspin\fP [\fIOPTION\fP]... [\fIFILE\fP|\fIFOLDER\fP]... +.SH "DESCRIPTION" +.sp +tailspin is a command line tool that highlights log files. +.SH "OPTIONS" +.sp +\fB\-f, \-\-follow\fP +.RS 4 +Follow (tail) the contents of the file. +Always true if opening a folder or using the \fI\-\-follow\-command\fP flag. +.RE +.sp +\fB\-t, \-\-tail\fP +.RS 4 +Start at the end of the file. +Always true if opening a folder. +.RE +.sp +\fB\-p, \-\-print\fP +.RS 4 +Print the output to stdout instead of viewing the contents in the pager \fIless\fP. +Always true if using stdin. +.RE +.sp +\fB\-c, \-\-config\-path\fP \fICONFIG_PATH\fP +.RS 4 +Specify the path to a custom configuration file. +Defaults to \fIXDG_CONFIG_HOME/tailspin/config.toml\fP or \fI~/.config/tailspin/config.toml\fP if not set. +.RE +.sp +\fB\-l, \-\-follow\-command\fP \fICOMMAND\fP +.RS 4 +Continuously listen to stdout of the provided command. +The command traps the interrupt signal to allow for cancelling and resuming follow mode while inside \fIless\fP. +.RE +.SH "SEE ALSO" +.sp +\fBless\fP(1), \fBtail\fP(1) +.SH "ABOUT" +.sp +Ben Sadeh (github.com/bensadeh/tailspin) +.sp +Released under the MIT License \ No newline at end of file diff -Nru tailspin-1.3.0+dfsg/README.md tailspin-2.0.0+dfsg/README.md --- tailspin-1.3.0+dfsg/README.md 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/README.md 2023-11-05 06:58:05.000000000 +0000 @@ -2,7 +2,7 @@

-# +#

A log file highlighter @@ -16,9 +16,10 @@ - 🪵 View (or `tail`) any log file of any format - 🍰 No setup or config required -- 🌈 Highlight numbers, dates, IP-addresses, UUIDs, URLs and more +- 🌈 Highlights numbers, dates, IP-addresses, UUIDs, URLs and more - ⚙️ All highlight groups are customizable -- 🔍 Uses `less` under the hood to provide **scrollback**, **search** and **filtering** +- 🧬 Easy to integrate with other commands +- 🔍 Uses `less` under the hood for scrollback, search and filtering # @@ -27,7 +28,9 @@ * [Overview](#overview) * [Installing](#installing) * [Highlight Groups](#highlight-groups) +* [Watching folders](#watching-folders) * [Customizing Highlight Groups](#customizing-highlight-groups) +* [Working with `stdin` and `stdout`](#working-with-stdin-and-stdout) * [Using the pager `less`](#using-the-pager-less) * [Settings](#settings) @@ -44,31 +47,34 @@ ## Installing -### Cargo +### Package Managers + +The binary name for `tailspin` is `tspin`. ```console -# Install +# Homebrew +brew install tailspin + +# Cargo cargo install tailspin -# View log file -spin [file] -``` +# AUR +paru -S tailspin -> **Note** -> When installing via cargo, make sure that `$HOME/.cargo/bin` is in your `PATH` environment variable +# Nix +nix-shell -p tailspin -### Debian +# NetBSD +pkgin install tailspin +``` -```console -# Install -apt install tailspin +### From Source -# View log file -tailspin [file] +```console +cargo install --path . ``` -> **Note** -> Because of a name collision with another `apt` package, the binary name on Debian is `tailspin` +Binary will be placed in `~/.cargo/bin`, make sure you add the folder to your `PATH` environment variable. ## Highlight Groups @@ -78,78 +84,261 @@

+
+Config + +```toml +[date] +style = { fg = "magenta" } +# To shorten the date, uncomment the line below +# shorten = { to = "␣", style = { fg = "magenta" } } + +[time] +time = { fg = "blue" } +zone = { fg = "red" } +# To shorten the time, uncomment the line below +# shorten = { to = "␣", style = { fg = "blue" } } +``` + +
+ ### Keywords

+
+Config + +```toml +[[keywords]] +words = ['null', 'true', 'false'] +style = { fg = "red", italic = true } + +[[keywords]] +words = ['GET'] +style = { fg = "black", bg = "green" } +border = true + +# You can add as many keywords as you'd like +``` + +
+ ### URLs

+
+Config + +```toml +[url] +http = { faint = true } +https = { bold = true } +host = { fg = "blue", faint = true } +path = { fg = "blue" } +query_params_key = { fg = "magenta" } +query_params_value = { fg = "cyan" } +symbols = { fg = "red" } +``` + +
+ ### Numbers

+
+Config + +```toml +[number] +style = { fg = "cyan" } +``` + +
+ ### IP Addresses

+
+Config + +```toml +[ip] +segment = { fg = "blue", italic = true } +separator = { fg = "red" } +``` + +
+ ### Quotes

+
+Config + +```toml +[quotes] +style = { fg = "yellow" } +token = '"' +``` + +
+ ### Unix file paths

+
+Config + +```toml +[path] +segment = { fg = "green", italic = true } +separator = { fg = "yellow" } +``` + +
+ +### HTTP methods + +

+ +

+ +
+Config +See Keywords +
+ ### UUIDs

+
+Config + +```toml +[uuid] +segment = { fg = "blue", italic = true } +separator = { fg = "red" } +``` + +
+ +### Key-value pairs + +

+ +

+ +
+Config + +```toml +[key_value] +key = { faint = true } +separator = { fg = "white" } +``` + +
+ +### Unix processes + +

+ +

+ +
+Config + +```toml +[process] +name = { fg = "green" } +separator = { fg = "red" } +id = { fg = "yellow" } +``` + +
+ +## Watching folders + +`tailspin` can listen for newline entries in a given folder. Watching folders is useful for monitoring log files that +are rotated. + +

+ +

+ +When watching folders, `tailspin` will start in follow mode (abort with Ctrl + C) and will only print +newline entries which arrive after the initial start. + ## Customizing Highlight Groups ### Overview -`tailspin` uses a single `config.toml` file to configure all highlight groups. When customizing highlights, it is -advised to start with the `--create-default-config ` flag to place a `config.toml` with default options -in `~/.config/tailspin`. - -To disable a highlight group, either comment it out or delete it. +Create `config.toml` in `~/.config/tailspin` to customize highlight groups. -Highlights have the following shape: +Styles have the following shape: ```toml style = { fg = "color", bg = "color", italic = false, bold = false, underline = false } ``` +### Disabling Highlight Groups + +To disable a highlight group, set the `disabled` field to true: + +```toml +[date] +disabled = true +``` + ### Adding Keywords To add custom keywords, either include them in the list of keywords or add new entries: ```toml -[[groups.keywords]] +[[keywords]] words = ['MyCustomKeyword'] style = { fg = "green" } -[[groups.keywords]] +[[keywords]] words = ['null', 'true', 'false'] style = { fg = "red", italic = true } ``` +## Working with `stdin` and `stdout` + +By default, `tailspin` will open a file in the pager `less`. However, if you pipe something into `tailspin`, it will +print the highlighted output directly to `stdout`. This is similar to running `tspin [file] --print`. + +To let `tailspin` highlight the logs of different commands, you can pipe the output of those commands into `tailspin` +like so: + +```console +journalctl -f | tspin +cat /var/log/syslog | tspin +kubectl logs -f pod_name | tspin +``` + ## Using the pager `less` ### Overview @@ -174,7 +363,7 @@ To stop following the file, interrupt with Ctrl + C. This will stop the tailing, but keep the file open, allowing you to review the existing content. -To resume following the file from within `less`, press Shift + f. +To resume following the file from within `less`, press Shift + F. ### Search @@ -195,12 +384,11 @@ ## Settings ```console --f, --follow Follow (tail) the contents of the file +-f, --follow Follow the contents of the file +-t, --tail Start at the end of the file -p, --print Print the output to stdout -c, --config-path PATH Path to a custom configuration file --t, --tail-command 'CMD' Tails the output of the provided command - --create-default-config Generate a new configuration file - --show-default-config Print the default configuration +-t, --follow-command 'CMD' Follows the output of the provided command ``` diff -Nru tailspin-1.3.0+dfsg/rustfmt.toml tailspin-2.0.0+dfsg/rustfmt.toml --- tailspin-1.3.0+dfsg/rustfmt.toml 1970-01-01 00:00:00.000000000 +0000 +++ tailspin-2.0.0+dfsg/rustfmt.toml 2023-11-05 06:58:05.000000000 +0000 @@ -0,0 +1 @@ +max_width = 120 \ No newline at end of file diff -Nru tailspin-1.3.0+dfsg/src/cli/mod.rs tailspin-2.0.0+dfsg/src/cli/mod.rs --- tailspin-1.3.0+dfsg/src/cli/mod.rs 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/cli/mod.rs 2023-11-05 06:58:05.000000000 +0000 @@ -1,19 +1,24 @@ use clap::{Command, CommandFactory, Parser}; use clap_complete::{generate, Generator, Shell}; use std::io; +use std::process::exit; #[derive(Parser)] -#[command(name = "spin")] +#[command(name = "tspin")] #[command(author, version, about)] pub struct Cli { /// Path to file or folder #[clap(name = "FILE")] - pub file_path: Option, + pub file_or_folder_path: Option, /// Follow (tail) the contents of the file #[clap(short = 'f', long = "follow")] pub follow: bool, + /// Start at the end of the file + #[clap(short = 't', long = "tail")] + pub tail: bool, + /// Print the output to stdout #[clap(short = 'p', long = "print", conflicts_with = "follow")] pub to_stdout: bool, @@ -26,21 +31,28 @@ #[clap(short = 'l', long = "follow-command", conflicts_with = "follow")] pub listen_command: Option, - /// Generate a new configuration file - #[clap(long = "create-default-config")] - pub create_default_config: bool, - - /// Print the default configuration - #[clap(long = "show-default-config", conflicts_with = "create_default_config")] - pub show_default_config: bool, - /// Print completions to stdout #[clap(long = "z-generate-shell-completions", hide = true)] pub generate_shell_completions: Option, } -pub fn get_args() -> Cli { - Cli::parse() +pub fn get_args_or_exit_early() -> Cli { + let args = Cli::parse(); + + if should_exit_early(&args) { + exit(0); + } + + args +} + +fn should_exit_early(args: &Cli) -> bool { + if args.generate_shell_completions.is_some() { + print_completions_to_stdout(); + return true; + } + + false } pub fn print_completions_to_stdout() { diff -Nru tailspin-1.3.0+dfsg/src/color.rs tailspin-2.0.0+dfsg/src/color.rs --- tailspin-1.3.0+dfsg/src/color.rs 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/color.rs 2023-11-05 06:58:05.000000000 +0000 @@ -15,6 +15,7 @@ White, Magenta, Cyan, + Black, #[default] None, } @@ -25,6 +26,7 @@ Green, Blue, Yellow, + Magenta, White, #[default] None, @@ -46,14 +48,16 @@ Fg::White => Some("37"), Fg::Magenta => Some("35"), Fg::Cyan => Some("36"), + Fg::Black => Some("30"), Fg::None => None, }; let bg_code = match style.bg { Bg::Red => Some("41"), Bg::Green => Some("42"), - Bg::Blue => Some("44"), Bg::Yellow => Some("43"), + Bg::Blue => Some("44"), + Bg::Magenta => Some("45"), Bg::White => Some("47"), Bg::None => None, }; @@ -67,11 +71,7 @@ bg_code, ]; - let joined_codes = codes - .iter() - .filter_map(|&code| code) - .collect::>() - .join(";"); + let joined_codes = codes.iter().filter_map(|&code| code).collect::>().join(";"); format!("\x1b[{}m", joined_codes) } @@ -88,6 +88,7 @@ "magenta" => Ok(Fg::Magenta), "cyan" => Ok(Fg::Cyan), "white" => Ok(Fg::White), + "black" => Ok(Fg::Black), _ => Ok(Fg::None), } } @@ -103,6 +104,7 @@ Fg::Magenta => write!(f, "\x1b[35m"), Fg::Cyan => write!(f, "\x1b[36m"), Fg::White => write!(f, "\x1b[37m"), + Fg::Black => write!(f, "\x1b[30m"), Fg::None => write!(f, "\x1b[0m"), } } @@ -127,16 +129,8 @@ } fn visit_str(self, v: &str) -> Result { - let fg = v.parse().map_err(|_| E::custom("Parse error"))?; - - match fg { - Fg::Red | Fg::Green | Fg::Blue | Fg::Yellow | Fg::Magenta | Fg::Cyan | Fg::White => { - Ok(fg) - } - _ => { - colored_panic("Invalid foreground color: ", v); - } - } + v.parse() + .map_err(|_| E::custom(format!("Invalid foreground color: {}", v))) } } @@ -150,6 +144,7 @@ "blue" => Ok(Bg::Blue), "yellow" => Ok(Bg::Yellow), "white" => Ok(Bg::White), + "magenta" => Ok(Bg::Magenta), _ => Ok(Bg::None), } } @@ -174,27 +169,7 @@ } fn visit_str(self, v: &str) -> Result { - let bg = v.parse().map_err(|_| { - colored_panic("Parse error", &format!("Invalid background color: {}", v)) - })?; - - match bg { - Bg::Red | Bg::Green | Bg::Blue | Bg::Yellow | Bg::White => Ok(bg), - _ => { - colored_panic("Invalid background color: ", v); - } - } + v.parse() + .map_err(|_| E::custom(format!("Invalid background color: {}", v))) } } - -fn colored_panic(message: &str, invalid_value: &str) -> ! { - let color_yellow: &str = "\x1b[33m"; - let color_reset: &str = "\x1b[0m"; - - let colored_message = format!( - "{}{}{}{}", - message, color_yellow, invalid_value, color_reset, - ); - eprintln!("{}", colored_message); - std::process::exit(1); -} diff -Nru tailspin-1.3.0+dfsg/src/config/mod.rs tailspin-2.0.0+dfsg/src/config/mod.rs --- tailspin-1.3.0+dfsg/src/config/mod.rs 1970-01-01 00:00:00.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/config/mod.rs 2023-11-05 06:58:05.000000000 +0000 @@ -0,0 +1,210 @@ +use crate::cli::Cli; +use crate::types::{ + Config, Error, FolderInfo, Input, Output, PathAndLineCount, GENERAL_ERROR, MISUSE_SHELL_BUILTIN, OK, +}; +use color_eyre::owo_colors::OwoColorize; +use std::fs; +use std::fs::{DirEntry, File}; +use std::io::{stdin, BufRead, IsTerminal}; +use std::path::Path; +use std::process::exit; + +enum InputType { + Stdin, + Command(String), + FileOrFolder(String), +} + +enum PathType { + File, + Folder, +} + +pub fn create_config_or_exit_early(args: Cli) -> Config { + match create_config(args) { + Ok(c) => c, + Err(e) => { + match e.exit_code { + OK => println!("{}", e.message), + _ => eprintln!("{}", e.message), + } + exit(e.exit_code); + } + } +} + +fn create_config(args: Cli) -> Result { + let has_data_from_stdin = !stdin().is_terminal(); + + validate_input( + has_data_from_stdin, + args.file_or_folder_path.is_some(), + args.listen_command.is_some(), + )?; + + let input_type = determine_input_type(&args, has_data_from_stdin)?; + let input = get_input(input_type)?; + let output = get_output(has_data_from_stdin, args.to_stdout); + let follow = should_follow(args.follow, args.listen_command.is_some(), &input); + + let config = Config { + input, + output, + follow, + tail: args.tail, + }; + + Ok(config) +} + +fn validate_input( + has_data_from_stdin: bool, + has_file_or_folder_input: bool, + has_follow_command_input: bool, +) -> Result<(), Error> { + if !has_data_from_stdin && !has_file_or_folder_input && !has_follow_command_input { + return Err(Error { + exit_code: OK, + message: format!("Missing filename ({} for help)", "tspin --help".magenta()), + }); + } + + if has_file_or_folder_input && has_follow_command_input { + return Err(Error { + exit_code: MISUSE_SHELL_BUILTIN, + message: format!("Cannot read from both file and {}", "--listen-command".magenta()), + }); + } + + Ok(()) +} + +fn determine_input_type(args: &Cli, has_data_from_stdin: bool) -> Result { + if has_data_from_stdin { + return Ok(InputType::Stdin); + } + + if let Some(command) = &args.listen_command { + return Ok(InputType::Command(command.clone())); + } + + if let Some(path) = &args.file_or_folder_path { + return Ok(InputType::FileOrFolder(path.clone())); + } + + Err(Error { + exit_code: GENERAL_ERROR, + message: "Could not determine input type".to_string(), + }) +} + +fn get_input(input_type: InputType) -> Result { + match input_type { + InputType::Stdin => Ok(Input::Stdin), + InputType::Command(cmd) => Ok(Input::Command(cmd)), + InputType::FileOrFolder(path) => determine_input(path), + } +} + +fn get_output(has_data_from_stdin: bool, is_print_flag: bool) -> Output { + if has_data_from_stdin || is_print_flag { + return Output::Stdout; + } + + Output::TempFile +} + +fn determine_input(path: String) -> Result { + match check_path_type(&path)? { + PathType::File => { + let line_count = count_lines(&path); + Ok(Input::File(PathAndLineCount { path, line_count })) + } + PathType::Folder => { + let mut paths = list_files_in_directory(Path::new(&path))?; + paths.sort(); + + Ok(Input::Folder(FolderInfo { + folder_name: path, + file_paths: paths, + })) + } + } +} + +fn check_path_type>(path: P) -> Result { + let metadata = fs::metadata(path.as_ref()).map_err(|_| Error { + exit_code: GENERAL_ERROR, + message: format!("{}: No such file or directory", path.as_ref().display().red()), + })?; + + if metadata.is_file() { + Ok(PathType::File) + } else if metadata.is_dir() { + Ok(PathType::Folder) + } else { + Err(Error { + exit_code: GENERAL_ERROR, + message: "Path is neither a file nor a directory".into(), + }) + } +} + +fn should_follow(follow: bool, has_follow_command: bool, input: &Input) -> bool { + if has_follow_command { + return true; + } + + if matches!(input, Input::Folder(_)) { + return true; + } + + follow +} + +fn list_files_in_directory(path: &Path) -> Result, Error> { + if !path.is_dir() { + return Err(Error { + exit_code: GENERAL_ERROR, + message: "Path is not a directory".into(), + }); + } + + fs::read_dir(path) + .map_err(|_| Error { + exit_code: GENERAL_ERROR, + message: "Unable to read directory".into(), + })? + .filter_map(Result::ok) + .filter(is_normal_file) + .map(entry_to_string) + .collect() +} + +fn is_normal_file(entry: &DirEntry) -> bool { + entry.path().is_file() + && entry + .path() + .file_name() + .and_then(|name| name.to_str()) + .map(|name| !name.starts_with('.')) + .unwrap_or(false) +} + +fn entry_to_string(entry: DirEntry) -> Result { + entry + .path() + .to_str() + .ok_or(Error { + exit_code: GENERAL_ERROR, + message: "Non-UTF8 filename".into(), + }) + .map(|s| s.to_string()) +} + +fn count_lines>(file_path: P) -> usize { + let file = File::open(file_path).expect("Could not open file"); + let reader = std::io::BufReader::new(file); + + reader.lines().count() +} diff -Nru tailspin-1.3.0+dfsg/src/controller/config.rs tailspin-2.0.0+dfsg/src/controller/config.rs --- tailspin-1.3.0+dfsg/src/controller/config.rs 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/controller/config.rs 1970-01-01 00:00:00.000000000 +0000 @@ -1,163 +0,0 @@ -use crate::cli::Cli; -use crate::file_utils::{count_lines, list_files_in_directory}; -use crate::types::{ - Config, Error, FolderInfo, Input, Output, PathAndLineCount, GENERAL_ERROR, - MISUSE_SHELL_BUILTIN, OK, -}; -use colored::*; -use std::fs; -use std::io::{stdin, IsTerminal}; -use std::path::Path; - -enum InputType { - Stdin, - Command(String), - FileOrFolder(String), -} - -enum PathType { - File, - Folder, -} - -pub fn create_config(args: Cli) -> Result { - let has_data_from_stdin = !stdin().is_terminal(); - - validate_input( - has_data_from_stdin, - args.file_path.is_some(), - args.listen_command.is_some(), - )?; - - let input_type = determine_input_type(&args, has_data_from_stdin)?; - let input = get_input(input_type)?; - let output = get_output(has_data_from_stdin, args.to_stdout); - let follow = should_follow(args.follow, args.listen_command.is_some(), &input); - - let config = Config { - input, - output, - follow, - }; - - Ok(config) -} - -fn validate_input( - has_data_from_stdin: bool, - has_file_or_folder_input: bool, - has_follow_command_input: bool, -) -> Result<(), Error> { - if !has_data_from_stdin && !has_file_or_folder_input && !has_follow_command_input { - return Err(Error { - exit_code: OK, - message: format!( - "Missing filename (\"{}\" for help)", - "spin --help".magenta() - ), - }); - } - - if has_data_from_stdin && has_file_or_folder_input { - return Err(Error { - exit_code: MISUSE_SHELL_BUILTIN, - message: format!( - "Cannot read from both stdin and `{}`", - "--listen-command".magenta() - ), - }); - } - - if has_file_or_folder_input && has_follow_command_input { - return Err(Error { - exit_code: MISUSE_SHELL_BUILTIN, - message: format!( - "Cannot read from both file and `{}`", - "--listen-command".magenta() - ), - }); - } - - Ok(()) -} - -fn determine_input_type(args: &Cli, has_data_from_stdin: bool) -> Result { - if has_data_from_stdin { - return Ok(InputType::Stdin); - } - - if let Some(command) = &args.listen_command { - return Ok(InputType::Command(command.clone())); - } - - if let Some(path) = &args.file_path { - return Ok(InputType::FileOrFolder(path.clone())); - } - - Err(Error { - exit_code: GENERAL_ERROR, - message: "Could not determine input type".to_string(), - }) -} - -fn get_input(input_type: InputType) -> Result { - match input_type { - InputType::Stdin => Ok(Input::Stdin), - InputType::Command(cmd) => Ok(Input::Command(cmd)), - InputType::FileOrFolder(path) => determine_input(path), - } -} - -fn get_output(has_data_from_stdin: bool, is_print_flag: bool) -> Output { - if has_data_from_stdin || is_print_flag { - return Output::Stdout; - } - - Output::TempFile -} - -fn determine_input(path: String) -> Result { - match check_path_type(&path)? { - PathType::File => { - let line_count = count_lines(&path); - Ok(Input::File(PathAndLineCount { path, line_count })) - } - PathType::Folder => { - let paths = list_files_in_directory(Path::new(&path))?; - Ok(Input::Folder(FolderInfo { - folder_name: path, - file_paths: paths, - })) - } - } -} - -fn check_path_type>(path: P) -> Result { - let metadata = fs::metadata(path.as_ref()).map_err(|_| Error { - exit_code: GENERAL_ERROR, - message: "Failed to access path metadata".into(), - })?; - - if metadata.is_file() { - Ok(PathType::File) - } else if metadata.is_dir() { - Ok(PathType::Folder) - } else { - Err(Error { - exit_code: GENERAL_ERROR, - message: "Path is neither a file nor a directory".into(), - }) - } -} - -fn should_follow(follow: bool, has_follow_command: bool, input: &Input) -> bool { - if has_follow_command { - return true; - } - - if matches!(input, Input::Folder(_)) { - return true; - } - - follow -} diff -Nru tailspin-1.3.0+dfsg/src/controller/mod.rs tailspin-2.0.0+dfsg/src/controller/mod.rs --- tailspin-1.3.0+dfsg/src/controller/mod.rs 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/controller/mod.rs 1970-01-01 00:00:00.000000000 +0000 @@ -1,97 +0,0 @@ -pub mod config; - -use crate::presenter::less::Less; -use crate::presenter::Present; -use crate::reader::linemux::Linemux; -use crate::reader::AsyncLineReader; -use crate::writer::temp_file::TempFile; -use crate::writer::AsyncLineWriter; -use async_trait::async_trait; -use tokio::io; - -use crate::presenter::empty::NoPresenter; -use crate::reader::command::CommandReader; -use crate::reader::stdin::StdinReader; -use crate::types::{Config, Input, Output}; -use crate::writer::stdout::StdoutWriter; -use tokio::sync::oneshot::Sender; - -pub struct Io { - reader: Box, - writer: Box, -} - -pub struct Presenter { - presenter: Box, -} - -pub async fn get_io_and_presenter( - config: Config, - reached_eof_tx: Option>, -) -> (Io, Presenter) { - let reader = get_reader(config.input, config.follow, reached_eof_tx).await; - let (writer, presenter) = get_writer(config.output, config.follow).await; - - (Io { reader, writer }, Presenter { presenter }) -} - -async fn get_reader( - input: Input, - follow: bool, - reached_eof_tx: Option>, -) -> Box { - match input { - Input::File(file_info) => { - Linemux::get_reader_single(file_info.path, file_info.line_count, follow, reached_eof_tx) - .await - } - Input::Folder(info) => { - Linemux::get_reader_multiple(info.folder_name, info.file_paths, reached_eof_tx).await - } - Input::Stdin => StdinReader::get_reader(reached_eof_tx), - Input::Command(cmd) => CommandReader::get_reader(cmd, reached_eof_tx).await, - } -} - -async fn get_writer( - output: Output, - follow: bool, -) -> (Box, Box) { - match output { - Output::TempFile => { - let result = TempFile::get_writer_result().await; - let writer = result.writer; - let temp_file_path = result.temp_file_path; - - let presenter = Less::get_presenter(temp_file_path, follow); - - (writer, presenter) - } - Output::Stdout => { - let writer = StdoutWriter::new(); - let presenter = NoPresenter::get_presenter(); - - (writer, presenter) - } - } -} - -#[async_trait] -impl AsyncLineReader for Io { - async fn next_line(&mut self) -> io::Result> { - self.reader.next_line().await - } -} - -#[async_trait] -impl AsyncLineWriter for Io { - async fn write_line(&mut self, line: &str) -> io::Result<()> { - self.writer.write_line(line).await - } -} - -impl Present for Presenter { - fn present(&self) { - self.presenter.present() - } -} diff -Nru tailspin-1.3.0+dfsg/src/file_utils/mod.rs tailspin-2.0.0+dfsg/src/file_utils/mod.rs --- tailspin-1.3.0+dfsg/src/file_utils/mod.rs 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/file_utils/mod.rs 1970-01-01 00:00:00.000000000 +0000 @@ -1,43 +0,0 @@ -use crate::types::{Error, GENERAL_ERROR}; -use std::fs; -use std::fs::File; -use std::io::BufRead; -use std::path::Path; - -pub fn list_files_in_directory(path: &Path) -> Result, Error> { - let mut files = Vec::new(); - - if path.is_dir() { - for entry_result in fs::read_dir(path).map_err(|_| Error { - exit_code: GENERAL_ERROR, - message: "Unable to read directory".into(), - })? { - let entry = entry_result.map_err(|_| Error { - exit_code: GENERAL_ERROR, - message: "Unable to read directory entry".into(), - })?; - let entry_path = entry.path(); - - if entry_path.is_file() { - files.push( - entry_path - .to_str() - .ok_or(Error { - exit_code: GENERAL_ERROR, - message: "Non-UTF8 filename".into(), - })? - .to_string(), - ); - } - } - } - - Ok(files) -} - -pub fn count_lines>(file_path: P) -> usize { - let file = File::open(file_path).expect("Could not open file"); - let reader = std::io::BufReader::new(file); - - reader.lines().count() -} diff -Nru tailspin-1.3.0+dfsg/src/highlighters/date.rs tailspin-2.0.0+dfsg/src/highlighters/date.rs --- tailspin-1.3.0+dfsg/src/highlighters/date.rs 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/highlighters/date.rs 2023-11-05 06:58:05.000000000 +0000 @@ -1,128 +1,43 @@ -use crate::color; use crate::color::to_ansi; -use crate::highlighters::HighlightFn; +use crate::highlight_utils; use crate::line_info::LineInfo; -use crate::theme::Style; -use lazy_static::lazy_static; -use regex::Regex; - -pub fn highlight(style: &Style) -> HighlightFn { - let color = to_ansi(style); - - Box::new(move |input: &str, line_info: &LineInfo| -> String { - highlight_dates(&color, input, line_info, &DATE_REGEX) - }) +use crate::regex::DATE_REGEX; +use crate::theme::{Shorten, Style}; +use crate::types::Highlight; + +pub struct DateHighlighter { + style: String, + shorten: Option, } -lazy_static! { - static ref DATE_REGEX: Regex = { - Regex::new( - r"(?x) # Enable comments and whitespace insensitivity - \b # Word boundary, ensures we are at the start of a date/time string - ( # Begin capturing group for the entire date/time string - \d{4}-\d{2}-\d{2} # Matches date in the format: yyyy-mm-dd - (?: # Begin non-capturing group for the time and timezone - (?:\s|T) # Matches either a whitespace or T (separator between date and time) - \d{2}:\d{2}:\d{2} # Matches time in the format: hh:mm:ss - ([.,]\d+)? # Optionally matches fractional seconds - (Z|[+-]\d{2})? # Optionally matches Z or timezone offset in the format: +hh or -hh - )? # End non-capturing group for the time and timezone - | # Alternation, matches either the pattern above or below - \d{2}:\d{2}:\d{2} # Matches time in the format: hh:mm:ss - ([.,]\d+)? # Optionally matches fractional seconds - ) # End capturing group for the entire date/time string - \b # Word boundary, ensures we are at the end of a date/time string - ").expect("Invalid regex pattern") - }; +impl DateHighlighter { + pub fn new(style: &Style, shorten: Option) -> Self { + Self { + style: to_ansi(style), + shorten, + } + } } -fn highlight_dates(color: &str, input: &str, line_info: &LineInfo, date_regex: &Regex) -> String { - // if line does not have at least two dashes or two colons, it is not a date - if line_info.dashes < 2 && line_info.colons < 2 { - return input.to_string(); - } +impl Highlight for DateHighlighter { + fn should_short_circuit(&self, line_info: &LineInfo) -> bool { + if line_info.dashes < 2 { + return true; + } - let highlighted = date_regex.replace_all(input, |caps: ®ex::Captures<'_>| { - format!("{}{}{}", color, &caps[0], color::RESET) - }); + false + } - highlighted.into_owned() -} + fn apply(&self, input: &str) -> String { + if let Some(shorten) = &self.shorten { + return highlight_utils::replace_with_awareness( + to_ansi(&shorten.clone().style).as_str(), + input, + &shorten.to, + &DATE_REGEX, + ); + } -#[cfg(test)] -mod tests { - use super::*; - use crate::color::{Bg, Fg}; - - #[test] - fn test_highlight_dates() { - let red = to_ansi(&Style { - fg: Fg::Red, - bg: Bg::None, - italic: false, - bold: false, - underline: false, - faint: false, - }); - - let line_info = &LineInfo { - dashes: 2, - dots: 0, - slashes: 0, - double_quotes: 0, - colons: 2, - }; - - let input1 = "The time is 10:51:19.251."; - let expected_output1 = format!("The time is {}10:51:19.251{}.", red, color::RESET); - let input2 = "The time is 08:23:55.927."; - let expected_output2 = format!("The time is {}08:23:55.927{}.", red, color::RESET); - let input3 = "The date is 2022-08-29 08:11:36."; - let expected_output3 = format!("The date is {}2022-08-29 08:11:36{}.", red, color::RESET); - let input4 = "The date is 2022-09-22T07:46:34.171800155Z."; - let expected_output4 = format!( - "The date is {}2022-09-22T07:46:34.171800155Z{}.", - red, - color::RESET - ); - let input5 = "The time is 08:11:36."; - let expected_output5 = format!("The time is {}08:11:36{}.", red, color::RESET); - let input6 = "The time is 11:48:34,534."; - let expected_output6 = format!("The time is {}11:48:34,534{}.", red, color::RESET); - let input7 = "The date and time are 2022-09-09 11:48:34,534."; - let expected_output7 = format!( - "The date and time are {}2022-09-09 11:48:34,534{}.", - red, - color::RESET - ); - - assert_eq!( - highlight_dates(&red, input1, line_info, &DATE_REGEX), - expected_output1 - ); - assert_eq!( - highlight_dates(&red, input2, line_info, &DATE_REGEX), - expected_output2 - ); - assert_eq!( - highlight_dates(&red, input3, line_info, &DATE_REGEX), - expected_output3 - ); - assert_eq!( - highlight_dates(&red, input4, line_info, &DATE_REGEX), - expected_output4 - ); - assert_eq!( - highlight_dates(&red, input5, line_info, &DATE_REGEX), - expected_output5 - ); - assert_eq!( - highlight_dates(&red, input6, line_info, &DATE_REGEX), - expected_output6 - ); - assert_eq!( - highlight_dates(&red, input7, line_info, &DATE_REGEX), - expected_output7 - ); + highlight_utils::highlight_with_awareness_replace_all(&self.style, input, &DATE_REGEX) } } diff -Nru tailspin-1.3.0+dfsg/src/highlighters/ip.rs tailspin-2.0.0+dfsg/src/highlighters/ip.rs --- tailspin-1.3.0+dfsg/src/highlighters/ip.rs 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/highlighters/ip.rs 2023-11-05 06:58:05.000000000 +0000 @@ -1,45 +1,43 @@ use crate::color; use crate::color::to_ansi; use crate::highlight_utils::highlight_with_awareness; -use crate::highlighters::HighlightFn; use crate::line_info::LineInfo; +use crate::regex::IP_ADDRESS_REGEX; use crate::theme::Style; -use lazy_static::lazy_static; +use crate::types::Highlight; use regex::{Captures, Regex}; -pub fn highlight(segment: &Style, separator: &Style) -> HighlightFn { - let segment_color = to_ansi(segment); - let separator_color = to_ansi(separator); - - Box::new(move |input: &str, line_info: &LineInfo| -> String { - highlight_ip_addresses( - &segment_color, - &separator_color, - input, - line_info, - &IP_ADDRESS_REGEX, - ) - }) +pub struct IpHighlighter { + segment_color: String, + separator_color: String, } -lazy_static! { - static ref IP_ADDRESS_REGEX: Regex = { - Regex::new(r"(\b\d{1,3})(\.)(\d{1,3})(\.)(\d{1,3})(\.)(\d{1,3}\b)") - .expect("Invalid IP address regex pattern") - }; +impl IpHighlighter { + pub fn new(segment: &Style, separator: &Style) -> Self { + let segment_color = to_ansi(segment); + let separator_color = to_ansi(separator); + IpHighlighter { + segment_color, + separator_color, + } + } } -fn highlight_ip_addresses( - segment_color: &str, - separator_color: &str, - input: &str, - line_info: &LineInfo, - ip_address_regex: &Regex, -) -> String { - if line_info.dots < 3 { - return input.to_string(); +impl Highlight for IpHighlighter { + fn should_short_circuit(&self, line_info: &LineInfo) -> bool { + if line_info.dots < 3 { + return true; + } + + false } + fn apply(&self, input: &str) -> String { + highlight_ip_addresses(&self.segment_color, &self.separator_color, input, &IP_ADDRESS_REGEX) + } +} + +fn highlight_ip_addresses(segment_color: &str, separator_color: &str, input: &str, ip_address_regex: &Regex) -> String { let highlight_groups = [ (segment_color, 1), (separator_color, 2), @@ -50,10 +48,6 @@ (segment_color, 7), ]; - if line_info.dots < 3 { - return input.to_string(); - } - highlight_with_awareness(input, ip_address_regex, |caps: &Captures<'_>| { let mut output = String::new(); for &(color, group) in &highlight_groups { @@ -69,25 +63,11 @@ #[test] fn test_highlight_ip_addresses() { - let line_info = &LineInfo { - dashes: 0, - dots: 3, - slashes: 0, - double_quotes: 0, - colons: 0, - }; - let ip_address = "192.168.0.1"; let segment_color = "\x1b[31m"; // ANSI color code for red let separator_color = "\x1b[32m"; // ANSI color code for green - let highlighted = highlight_ip_addresses( - segment_color, - separator_color, - ip_address, - line_info, - &IP_ADDRESS_REGEX, - ); + let highlighted = highlight_ip_addresses(segment_color, separator_color, ip_address, &IP_ADDRESS_REGEX); let expected = format!( "{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}", @@ -118,25 +98,11 @@ #[test] fn test_highlight_ip_addresses_no_ip() { - let line_info = &LineInfo { - dashes: 0, - dots: 3, - slashes: 0, - double_quotes: 0, - colons: 0, - }; - let text = "this is a test string with no IP address"; let segment_color = "\x1b[31m"; let separator_color = "\x1b[32m"; - let highlighted = highlight_ip_addresses( - segment_color, - separator_color, - text, - line_info, - &IP_ADDRESS_REGEX, - ); + let highlighted = highlight_ip_addresses(segment_color, separator_color, text, &IP_ADDRESS_REGEX); // The input string does not contain an IP address, so it should be returned as-is assert_eq!(highlighted, text); diff -Nru tailspin-1.3.0+dfsg/src/highlighters/key_value.rs tailspin-2.0.0+dfsg/src/highlighters/key_value.rs --- tailspin-1.3.0+dfsg/src/highlighters/key_value.rs 1970-01-01 00:00:00.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/highlighters/key_value.rs 2023-11-05 06:58:05.000000000 +0000 @@ -0,0 +1,51 @@ +use crate::color::to_ansi; +use crate::line_info::LineInfo; +use crate::regex::KEY_VALUE_REGEX; +use crate::theme::Style; +use crate::types::Highlight; + +pub struct KeyValueHighlighter { + key_color: String, + equals_sign_color: String, +} + +impl KeyValueHighlighter { + pub fn new(key_style: &Style, equals_sign_style: &Style) -> Self { + Self { + key_color: to_ansi(key_style), + equals_sign_color: to_ansi(equals_sign_style), + } + } +} + +impl Highlight for KeyValueHighlighter { + fn should_short_circuit(&self, line_info: &LineInfo) -> bool { + if line_info.equals < 1 { + return true; + } + + false + } + + fn apply(&self, input: &str) -> String { + highlight_key_values(&self.key_color, &self.equals_sign_color, input) + } +} + +fn highlight_key_values(key_color: &str, equals_sign_color: &str, input: &str) -> String { + KEY_VALUE_REGEX + .replace_all(input, |captures: ®ex::Captures| { + let space_or_start = captures.name("space_or_start").map(|s| s.as_str()).unwrap_or_default(); + let key = captures + .name("key") + .map(|k| format!("{}{}\x1B[0m", key_color, k.as_str())) + .unwrap_or_default(); + let equals_sign = captures + .name("equals") + .map(|e| format!("{}{}\x1B[0m", equals_sign_color, e.as_str())) + .unwrap_or_default(); + + format!("{}{}{}", space_or_start, key, equals_sign) + }) + .to_string() +} diff -Nru tailspin-1.3.0+dfsg/src/highlighters/keyword.rs tailspin-2.0.0+dfsg/src/highlighters/keyword.rs --- tailspin-1.3.0+dfsg/src/highlighters/keyword.rs 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/highlighters/keyword.rs 2023-11-05 06:58:05.000000000 +0000 @@ -1,45 +1,52 @@ use crate::color::to_ansi; -use crate::highlight_utils; -use crate::highlighters::HighlightFn; use crate::line_info::LineInfo; use crate::theme::Style; -use once_cell::sync::OnceCell; +use crate::types::Highlight; +use crate::{color, highlight_utils}; use regex::Regex; -use std::collections::HashMap; -static KEYWORDS: OnceCell> = OnceCell::new(); +pub struct KeywordHighlighter { + keyword_regex: Regex, + color: String, + border: bool, +} -pub fn init_keywords(keywords: Vec) { - let mut map = HashMap::new(); - for keyword in keywords { - let escaped = regex::escape(&keyword); - let regex = Regex::new(&format!(r"\b{}\b", escaped)).expect("Invalid regex pattern"); - map.insert(keyword, regex); +impl KeywordHighlighter { + pub fn new(keywords: &[String], style: &Style, border: bool) -> Self { + let keyword_pattern = keywords + .iter() + .map(|word| regex::escape(word)) + .collect::>() + .join("|"); + + let keyword_regex = Regex::new(&format!(r"\b({})\b", keyword_pattern)).expect("Invalid regex pattern"); + + Self { + keyword_regex, + color: to_ansi(style), + border, + } } - KEYWORDS - .set(map) - .expect("KEYWORDS should not have been initialized before"); } -pub fn highlight(keyword: String, style: &Style) -> HighlightFn { - let color = to_ansi(style); - - Box::new(move |input: &str, line_info: &LineInfo| -> String { - let keywords = KEYWORDS - .get() - .expect("KEYWORDS should have been initialized"); - let keyword_regex = keywords.get(&keyword).expect("Keyword regex not found"); +impl Highlight for KeywordHighlighter { + fn should_short_circuit(&self, _line_info: &LineInfo) -> bool { + false + } - highlight_keywords(&keyword, &color, input, line_info, keyword_regex) - }) + fn apply(&self, input: &str) -> String { + highlight_keywords(&self.color, input, &self.keyword_regex, self.border) + } } -fn highlight_keywords( - _keyword: &str, - color: &str, - input: &str, - _line_info: &LineInfo, - keyword_regex: &Regex, -) -> String { - highlight_utils::highlight_with_awareness_replace_all(color, input, keyword_regex) +fn highlight_keywords(color: &str, input: &str, keyword_regex: &Regex, border: bool) -> String { + if border { + keyword_regex + .replace_all(input, |cap: ®ex::Captures| { + format!("{} {} {}", color, &cap[0], color::RESET) + }) + .to_string() + } else { + highlight_utils::highlight_with_awareness_replace_all(color, input, keyword_regex) + } } diff -Nru tailspin-1.3.0+dfsg/src/highlighters/mod.rs tailspin-2.0.0+dfsg/src/highlighters/mod.rs --- tailspin-1.3.0+dfsg/src/highlighters/mod.rs 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/highlighters/mod.rs 2023-11-05 06:58:05.000000000 +0000 @@ -1,110 +1,141 @@ mod date; mod ip; +mod key_value; mod keyword; mod number; mod path; +mod process; mod quotes; +mod time; mod url; mod uuid; -use crate::line_info::LineInfo; -use crate::theme::Keyword; -use crate::theme::Style; -use crate::theme::Theme; - -type HighlightFn = Box String + Send>; -type HighlightFnVec = Vec; +use crate::highlighters::date::DateHighlighter; +use crate::highlighters::ip::IpHighlighter; +use crate::highlighters::key_value::KeyValueHighlighter; +use crate::highlighters::keyword::KeywordHighlighter; +use crate::highlighters::number::NumberHighlighter; +use crate::highlighters::path::PathHighlighter; +use crate::highlighters::process::ProcessHighlighter; +use crate::highlighters::quotes::QuoteHighlighter; +use crate::highlighters::time::TimeHighlighter; +use crate::highlighters::url::UrlHighlighter; +use crate::highlighters::uuid::UuidHighlighter; +use crate::theme::defaults::get_default_keywords; +use crate::theme::{Keyword, Theme}; +use crate::types::Highlight; pub struct Highlighters { - pub before: HighlightFnVec, - pub main: HighlightFnVec, - pub after: HighlightFnVec, -} - -struct FlattenKeyword { - pub keyword: String, - pub style: Style, + pub before: Vec>, + pub main: Vec>, + pub after: Vec>, } impl Highlighters { - pub fn new(config: Theme) -> Highlighters { - let mut before_fns: HighlightFnVec = Vec::new(); - let mut main_fns: HighlightFnVec = Vec::new(); - let mut after_fns: HighlightFnVec = Vec::new(); - - // Dates - if let Some(dates) = &config.groups.date { - before_fns.push(date::highlight(&dates.style)); + pub fn new(theme: &Theme) -> Highlighters { + Highlighters { + before: Self::set_before_fns(theme), + main: Self::set_main_fns(theme), + after: Self::set_after_fns(theme), } + } + + fn set_before_fns(theme: &Theme) -> Vec> { + let mut before_fns: Vec> = Vec::new(); - // URLs - if let Some(url) = &config.groups.url { - before_fns.push(url::highlight(url)); + if !theme.date.disabled { + before_fns.push(Box::new(DateHighlighter::new( + &theme.date.style, + theme.date.shorten.clone(), + ))); } - // Paths - if let Some(path) = &config.groups.path { - before_fns.push(path::highlight(&path.segment, &path.separator)); + if !theme.time.disabled { + before_fns.push(Box::new(TimeHighlighter::new( + &theme.time.time, + &theme.time.zone, + theme.time.shorten.clone(), + ))); } - // IPs - if let Some(ip) = &config.groups.ip { - before_fns.push(ip::highlight(&ip.segment, &ip.separator)); + if !theme.url.disabled { + before_fns.push(Box::new(UrlHighlighter::new(&theme.url))); } - // UUIDs - if let Some(uuid) = &config.groups.uuid { - before_fns.push(uuid::highlight(&uuid.segment, &uuid.separator)); + if !theme.path.disabled { + before_fns.push(Box::new(PathHighlighter::new( + &theme.path.segment, + &theme.path.separator, + ))); } - // Numbers - if let Some(numbers) = &config.groups.number { - main_fns.push(number::highlight(&numbers.style)); + if !theme.ip.disabled { + before_fns.push(Box::new(IpHighlighter::new(&theme.ip.segment, &theme.ip.separator))); } - // Keywords - let flattened_keywords = Self::flatten(&config); - let keyword_strings: Vec = flattened_keywords - .iter() - .map(|kw| kw.keyword.clone()) - .collect(); + if !theme.key_value.disabled { + before_fns.push(Box::new(KeyValueHighlighter::new( + &theme.key_value.key, + &theme.key_value.separator, + ))); + } - keyword::init_keywords(keyword_strings); + if !theme.uuid.disabled { + before_fns.push(Box::new(UuidHighlighter::new( + &theme.uuid.segment, + &theme.uuid.separator, + ))); + } - for keyword in flattened_keywords { - main_fns.push(keyword::highlight(keyword.keyword, &keyword.style)); + if !theme.process.disabled { + before_fns.push(Box::new(ProcessHighlighter::new( + &theme.process.name, + &theme.process.separator, + &theme.process.id, + ))); } - // Quotes - if let Some(quotes_group) = &config.groups.quotes { - after_fns.push(quotes::highlight("es_group.style, quotes_group.token)); + before_fns + } + + fn set_main_fns(theme: &Theme) -> Vec> { + let mut main_fns: Vec> = Vec::new(); + let keywords = Self::get_keywords(&theme.keywords, true); + + if !theme.number.disabled { + main_fns.push(Box::new(NumberHighlighter::new(&theme.number.style))); } - Highlighters { - before: before_fns, - main: main_fns, - after: after_fns, + for keyword in keywords { + main_fns.push(Box::new(KeywordHighlighter::new( + &keyword.words, + &keyword.style, + keyword.border, + ))); } + + main_fns } - fn flatten(config: &Theme) -> Vec { - let keywords_or_empty = config.groups.keywords.clone().unwrap_or_default(); + fn set_after_fns(theme: &Theme) -> Vec> { + let mut after_fns: Vec> = Vec::new(); - Self::flatten_keywords(keywords_or_empty) - } + if !theme.quotes.disabled { + after_fns.push(Box::new(QuoteHighlighter::new(&theme.quotes.style, theme.quotes.token))); + } - fn flatten_keywords(keywords: Vec) -> Vec { - let mut flatten_keywords = Vec::new(); + after_fns + } - for keyword in keywords { - for string in keyword.words { - flatten_keywords.push(FlattenKeyword { - keyword: string, - style: keyword.style.clone(), - }); + fn get_keywords(custom_keywords: &Option>, disable_default_keywords: bool) -> Vec { + if disable_default_keywords { + let default_keywords = get_default_keywords(); + match custom_keywords { + Some(ck) => [default_keywords, ck.clone()].concat(), + None => default_keywords, } + } else { + custom_keywords.clone().unwrap_or_default() } - - flatten_keywords } } diff -Nru tailspin-1.3.0+dfsg/src/highlighters/number.rs tailspin-2.0.0+dfsg/src/highlighters/number.rs --- tailspin-1.3.0+dfsg/src/highlighters/number.rs 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/highlighters/number.rs 2023-11-05 06:58:05.000000000 +0000 @@ -1,40 +1,30 @@ use crate::color::to_ansi; use crate::highlight_utils; -use crate::highlighters::HighlightFn; use crate::line_info::LineInfo; +use crate::regex::NUMBER_REGEX; use crate::theme::Style; -use lazy_static::lazy_static; -use regex::Regex; +use crate::types::Highlight; -pub fn highlight(style: &Style) -> HighlightFn { - let color = to_ansi(style); +pub struct NumberHighlighter { + color: String, +} - Box::new(move |input: &str, line_info: &LineInfo| -> String { - highlight_numbers(&color, input, line_info, &NUMBER_REGEX) - }) +impl NumberHighlighter { + pub fn new(style: &Style) -> Self { + Self { color: to_ansi(style) } + } } -lazy_static! { - static ref NUMBER_REGEX: Regex = { - Regex::new( - r"(?x) # Enable comments and whitespace insensitivity - \b # Word boundary, ensures we are at the start of a number - \d+ # Matches one or more digits - (\. # Start a group to match a decimal part - \d+ # Matches one or more digits after the dot - )? # The decimal part is optional - \b # Word boundary, ensures we are at the end of a number - ", - ) - .expect("Invalid regex pattern") - }; +impl Highlight for NumberHighlighter { + fn should_short_circuit(&self, _line_info: &LineInfo) -> bool { + false + } + + fn apply(&self, input: &str) -> String { + highlight_numbers(&self.color, input) + } } -fn highlight_numbers( - color: &str, - input: &str, - _line_info: &LineInfo, - number_regex: &Regex, -) -> String { - highlight_utils::highlight_with_awareness_replace_all(color, input, number_regex) +fn highlight_numbers(color: &str, input: &str) -> String { + highlight_utils::highlight_with_awareness_replace_all(color, input, &NUMBER_REGEX) } diff -Nru tailspin-1.3.0+dfsg/src/highlighters/path.rs tailspin-2.0.0+dfsg/src/highlighters/path.rs --- tailspin-1.3.0+dfsg/src/highlighters/path.rs 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/highlighters/path.rs 2023-11-05 06:58:05.000000000 +0000 @@ -1,103 +1,91 @@ use crate::color; use crate::color::to_ansi; use crate::highlight_utils::highlight_with_awareness; -use crate::highlighters::HighlightFn; use crate::line_info::LineInfo; +use crate::regex::PATH_REGEX; use crate::theme::Style; -use lazy_static::lazy_static; -use regex::{Captures, Regex}; +use crate::types::Highlight; +use regex::Captures; -pub fn highlight(segment: &Style, separator: &Style) -> HighlightFn { - let segment_color = to_ansi(segment); - let separator_color = to_ansi(separator); - - Box::new(move |input: &str, line_info: &LineInfo| -> String { - highlight_paths( - &segment_color, - &separator_color, - input, - line_info, - &PATH_REGEX, - ) - }) +pub struct PathHighlighter { + segment_color: String, + separator_color: String, } -lazy_static! { - static ref PATH_REGEX: Regex = { - Regex::new( - r"(?x) # Enable extended mode for readability - (?P # Capture the path segment - [~/.][\w./-]* # Match zero or more word characters, dots, slashes, or hyphens - /[\w.-]* # Match a path segment separated by a slash - )" - ).expect("Invalid regex pattern") - }; +impl Highlight for PathHighlighter { + fn should_short_circuit(&self, line_info: &LineInfo) -> bool { + if line_info.slashes == 0 { + return true; + } + + false + } + + fn apply(&self, input: &str) -> String { + self.highlight_paths(input) + } } -fn highlight_paths( - segment_color: &str, - separator_color: &str, - input: &str, - line_info: &LineInfo, - path_regex: &Regex, -) -> String { - if line_info.slashes == 0 { - return input.to_string(); - } - - highlight_with_awareness(input, path_regex, |caps: &Captures<'_>| { - let mut output = String::new(); - let path = &caps[0]; - let chars: Vec<_> = path.chars().collect(); - - // Check if path starts with a valid character and not a double slash - if !(chars[0] == '/' - || chars[0] == '~' - || (chars[0] == '.' && chars.len() > 1 && chars[1] == '/')) - || (chars[0] == '/' && chars.len() > 1 && chars[1] == '/') - { - return path.to_string(); +impl PathHighlighter { + pub fn new(segment: &Style, separator: &Style) -> Self { + Self { + segment_color: to_ansi(segment), + separator_color: to_ansi(separator), } + } - for i in 0..chars.len() { - if chars[i] == '/' { - output.push_str(&format!("{}{}{}", separator_color, chars[i], color::RESET)); - } else { - output.push_str(&format!("{}{}{}", segment_color, chars[i], color::RESET)); + fn highlight_paths(&self, input: &str) -> String { + highlight_with_awareness(input, &PATH_REGEX, |caps: &Captures<'_>| { + let mut output = String::new(); + let path = &caps[0]; + let chars: Vec<_> = path.chars().collect(); + + // Check if path starts with a valid character and not a double slash + if !(chars[0] == '/' || chars[0] == '~' || (chars[0] == '.' && chars.len() > 1 && chars[1] == '/')) + || (chars[0] == '/' && chars.len() > 1 && chars[1] == '/') + { + return path.to_string(); } - } - output - }) + + for &char in &chars { + match char { + '/' => output.push_str(&format!("{}{}{}", &self.separator_color, char, color::RESET)), + _ => output.push_str(&format!("{}{}{}", &self.segment_color, char, color::RESET)), + } + } + + output + }) + } } #[cfg(test)] mod tests { use super::*; + use crate::color::Fg; #[test] fn test_highlight_paths() { - let line_info = &LineInfo { - dashes: 0, - dots: 0, - slashes: 1, - double_quotes: 0, - colons: 0, - }; - let path = "~/Documents/../user/."; - let segment_color = "\x1b[31m"; // ANSI color code for red - let separator_color = "\x1b[32m"; // ANSI color code for green + let segment_style = Style { + fg: Fg::Red, + ..Default::default() + }; + let separator_style = Style { + fg: Fg::Green, + ..Default::default() + }; - let highlighted = - highlight_paths(segment_color, separator_color, path, line_info, &PATH_REGEX); + let highlighter = PathHighlighter::new(&segment_style, &separator_style); + let highlighted = highlighter.apply(path); let expected = path .chars() .map(|ch| { if ch == '/' { - format!("{}{}{}", separator_color, ch, color::RESET) + format!("{}{}{}", to_ansi(&separator_style), ch, color::RESET) } else { - format!("{}{}{}", segment_color, ch, color::RESET) + format!("{}{}{}", to_ansi(&segment_style), ch, color::RESET) } }) .collect::(); @@ -106,22 +94,19 @@ #[test] fn test_highlight_paths_no_path() { - let line_info = &LineInfo { - dashes: 0, - dots: 0, - slashes: 0, - double_quotes: 0, - colons: 0, - }; - let text = "this is a test string with no path"; - let segment_color = "\x1b[31m"; // ANSI color code for red - let separator_color = "\x1b[32m"; // ANSI color code for green + let segment_style = Style { + fg: Fg::Red, + ..Default::default() + }; + let separator_style = Style { + fg: Fg::Green, + ..Default::default() + }; - let highlighted = - highlight_paths(segment_color, separator_color, text, line_info, &PATH_REGEX); + let highlighter = PathHighlighter::new(&segment_style, &separator_style); + let highlighted = highlighter.apply(text); - // The input string does not contain a path, so it should be returned as-is assert_eq!(highlighted, text); } } diff -Nru tailspin-1.3.0+dfsg/src/highlighters/process.rs tailspin-2.0.0+dfsg/src/highlighters/process.rs --- tailspin-1.3.0+dfsg/src/highlighters/process.rs 1970-01-01 00:00:00.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/highlighters/process.rs 2023-11-05 06:58:05.000000000 +0000 @@ -0,0 +1,60 @@ +use crate::color::{to_ansi, RESET}; +use crate::line_info::LineInfo; +use crate::regex::PROCESS_REGEX; +use crate::theme::Style; +use crate::types::Highlight; + +pub struct ProcessHighlighter { + process_name_color: String, + bracket_color: String, + process_num_color: String, +} + +impl ProcessHighlighter { + pub fn new(process_name_style: &Style, bracket_style: &Style, process_num_style: &Style) -> Self { + Self { + process_name_color: to_ansi(process_name_style), + bracket_color: to_ansi(bracket_style), + process_num_color: to_ansi(process_num_style), + } + } +} + +impl Highlight for ProcessHighlighter { + fn should_short_circuit(&self, line_info: &LineInfo) -> bool { + if line_info.left_bracket < 1 || line_info.right_bracket < 1 { + return true; + } + + false + } + + fn apply(&self, input: &str) -> String { + highlight_processes( + &self.process_name_color, + &self.bracket_color, + &self.process_num_color, + input, + ) + } +} + +fn highlight_processes(process_name_color: &str, bracket_color: &str, process_num_color: &str, input: &str) -> String { + PROCESS_REGEX + .replace_all(input, |captures: ®ex::Captures| { + let process_name = captures + .name("process_name") + .map(|p| format!("{}{}", process_name_color, p.as_str())) + .unwrap_or_default(); + let process_num = captures + .name("process_num") + .map(|n| format!("{}{}", process_num_color, n.as_str())) + .unwrap_or_default(); + + format!( + "{}{}{}[{}{}{}]{}", + process_name, RESET, bracket_color, process_num, RESET, bracket_color, RESET + ) + }) + .to_string() +} diff -Nru tailspin-1.3.0+dfsg/src/highlighters/quotes.rs tailspin-2.0.0+dfsg/src/highlighters/quotes.rs --- tailspin-1.3.0+dfsg/src/highlighters/quotes.rs 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/highlighters/quotes.rs 2023-11-05 06:58:05.000000000 +0000 @@ -1,16 +1,36 @@ use crate::color; use crate::color::to_ansi; use crate::highlighters::quotes::State::{InsideQuote, OutsideQuote}; -use crate::highlighters::HighlightFn; use crate::line_info::LineInfo; use crate::theme::Style; +use crate::types::Highlight; -pub fn highlight(style: &Style, quotes_token: char) -> HighlightFn { - let color = to_ansi(style); +pub struct QuoteHighlighter { + color: String, + quotes_token: char, +} + +impl QuoteHighlighter { + pub fn new(style: &Style, quotes_token: char) -> Self { + Self { + color: to_ansi(style), + quotes_token, + } + } +} + +impl Highlight for QuoteHighlighter { + fn should_short_circuit(&self, line_info: &LineInfo) -> bool { + if line_info.double_quotes == 0 || line_info.double_quotes % 2 != 0 { + return true; + } - Box::new(move |input: &str, line_info: &LineInfo| -> String { - highlight_inside_quotes(&color, input, quotes_token, line_info) - }) + false + } + + fn apply(&self, input: &str) -> String { + self.highlight_inside_quotes(input) + } } enum State { @@ -21,140 +41,98 @@ OutsideQuote, } -fn highlight_inside_quotes( - color: &str, - input: &str, - quotes_token: char, - line_info: &LineInfo, -) -> String { - if line_info.double_quotes == 0 || line_info.double_quotes % 2 != 0 { - return input.to_string(); - } - - let mut state = OutsideQuote; - let mut output = String::new(); - - for ch in input.chars() { - match &mut state { - InsideQuote { - color_inside_quote: color, - ref mut potential_reset_code, - } => { - if ch == quotes_token { - output.push(ch); - output.push_str(color::RESET); - state = OutsideQuote; - continue; +impl QuoteHighlighter { + fn highlight_inside_quotes(&self, input: &str) -> String { + let mut state = OutsideQuote; + let mut output = String::new(); + + for ch in input.chars() { + match &mut state { + InsideQuote { + color_inside_quote: color, + ref mut potential_reset_code, + } => { + if ch == self.quotes_token { + output.push(ch); + output.push_str(color::RESET); + state = OutsideQuote; + continue; + } + + potential_reset_code.push(ch); + if potential_reset_code.as_str() == color::RESET { + output.push_str(potential_reset_code); + output.push_str(color); + potential_reset_code.clear(); + } else if !color::RESET.starts_with(potential_reset_code.as_str()) { + output.push_str(potential_reset_code); + potential_reset_code.clear(); + } } + OutsideQuote => { + if ch == self.quotes_token { + output.push_str(&self.color); + output.push(ch); + state = InsideQuote { + color_inside_quote: self.color.clone(), + potential_reset_code: String::new(), + }; + continue; + } - potential_reset_code.push(ch); - if potential_reset_code.as_str() == color::RESET { - output.push_str(potential_reset_code); - output.push_str(color); - potential_reset_code.clear(); - } else if !color::RESET.starts_with(potential_reset_code.as_str()) { - output.push_str(potential_reset_code); - potential_reset_code.clear(); - } - } - OutsideQuote => { - if ch == quotes_token { - output.push_str(color); output.push(ch); - state = InsideQuote { - color_inside_quote: color.to_string(), - potential_reset_code: String::new(), - }; - continue; } + }; + } - output.push(ch); - } - }; + output } - - output } #[cfg(test)] mod tests { use super::*; - use crate::color::{Bg, Fg}; + use crate::color::Fg; #[test] fn highlight_quotes_with_ansi() { let style = Style { fg: Fg::Yellow, - bg: Bg::None, - italic: false, - bold: false, - underline: false, - faint: false, - }; - - let line_info = &LineInfo { - dashes: 0, - dots: 0, - slashes: 0, - double_quotes: 2, - colons: 0, + ..Default::default() }; - let highlighter = highlight(&style, '"'); - let result = highlighter( - "outside \"hello \x1b[34;42;3m42\x1b[0m world\" outside", - line_info, - ); - let expected = - "outside \x1b[33m\"hello \x1b[34;42;3m42\x1b[0m\x1b[33m world\"\x1b[0m outside"; + let highlighter = QuoteHighlighter::new(&style, '"'); + let result = highlighter.apply("outside \"hello \x1b[34;42;3m42\x1b[0m world\" outside"); + let expected = "outside \x1b[33m\"hello \x1b[34;42;3m42\x1b[0m\x1b[33m world\"\x1b[0m outside"; assert_eq!(result, expected); } #[test] fn highlight_quotes_without_ansi() { - let line_info = &LineInfo { - dashes: 0, - dots: 0, - slashes: 0, - double_quotes: 2, - colons: 0, + let style = Style { + fg: Fg::Red, + ..Default::default() }; - let color = "[color]"; + + let highlighter = QuoteHighlighter::new(&style, '"'); let input = "outside \"hello \x1b[34;42;3m42\x1b[0m world\" outside"; - let result = highlight_inside_quotes(color, input, '"', line_info); - let expected = - "outside [color]\"hello \x1b[34;42;3m42\x1b[0m[color] world\"\x1b[0m outside"; + let result = highlighter.apply(input); + let expected = "outside \x1b[31m\"hello \x1b[34;42;3m42\x1b[0m\x1b[31m world\"\x1b[0m outside"; assert_eq!(result, expected); } #[test] fn do_nothing_on_uneven_number_of_quotes() { - let style = Style { - fg: Fg::Red, - bg: Bg::None, - italic: false, - bold: false, - underline: false, - faint: false, - }; - - let line_info = &LineInfo { - dashes: 0, - dots: 0, - slashes: 0, + let line_info = LineInfo { double_quotes: 1, - colons: 0, + ..Default::default() }; - let highlighter = highlight(&style, '"'); - let result = highlighter( - "outside \" \"hello \x1b[34;42;3m42\x1b[0m world\" outside", - line_info, - ); - let expected = "outside \" \"hello \x1b[34;42;3m42\x1b[0m world\" outside"; + let highlighter = QuoteHighlighter::new(&Style::default(), '"'); + let should_short_circuit_actual = highlighter.should_short_circuit(&line_info); - assert_eq!(result, expected); + assert!(should_short_circuit_actual); } } diff -Nru tailspin-1.3.0+dfsg/src/highlighters/time.rs tailspin-2.0.0+dfsg/src/highlighters/time.rs --- tailspin-1.3.0+dfsg/src/highlighters/time.rs 1970-01-01 00:00:00.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/highlighters/time.rs 2023-11-05 06:58:05.000000000 +0000 @@ -0,0 +1,77 @@ +use crate::color::to_ansi; +use crate::line_info::LineInfo; +use crate::regex::TIME_REGEX; +use crate::theme::{Shorten, Style}; +use crate::types::Highlight; +use crate::{color, highlight_utils}; + +pub struct TimeHighlighter { + time: String, + zone: String, + shorten: Option, +} + +impl TimeHighlighter { + pub fn new(time: &Style, zone: &Style, shorten: Option) -> Self { + Self { + time: to_ansi(time), + zone: to_ansi(zone), + shorten, + } + } + + fn highlight_time(&self, input: &str) -> String { + let highlighted = TIME_REGEX.replace_all(input, |caps: ®ex::Captures<'_>| { + let t_part = if let Some(m) = caps.name("T") { + format!("{}{}{}", self.zone, m.as_str(), color::RESET) + } else { + String::new() + }; + + let time_part = if let Some(m) = caps.name("time") { + format!("{}{}{}", self.time, m.as_str(), color::RESET) + } else { + String::new() + }; + + let frac_part = if let Some(m) = caps.name("frac") { + format!("{}{}{}", self.time, m.as_str(), color::RESET) + } else { + String::new() + }; + + let zone_part = if let Some(m) = caps.name("tz") { + format!("{}{}{}", self.zone, m.as_str(), color::RESET) + } else { + String::new() + }; + + format!("{}{}{}{}", t_part, time_part, frac_part, zone_part) + }); + + highlighted.into_owned() + } +} + +impl Highlight for TimeHighlighter { + fn should_short_circuit(&self, line_info: &LineInfo) -> bool { + if line_info.colons < 2 { + return true; + } + + false + } + + fn apply(&self, input: &str) -> String { + if let Some(shorten) = &self.shorten { + return highlight_utils::replace_with_awareness( + to_ansi(&shorten.clone().style).as_str(), + input, + &shorten.to, + &TIME_REGEX, + ); + } + + self.highlight_time(input) + } +} diff -Nru tailspin-1.3.0+dfsg/src/highlighters/url.rs tailspin-2.0.0+dfsg/src/highlighters/url.rs --- tailspin-1.3.0+dfsg/src/highlighters/url.rs 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/highlighters/url.rs 2023-11-05 06:58:05.000000000 +0000 @@ -1,73 +1,74 @@ use crate::color; use crate::color::to_ansi; -use crate::highlighters::HighlightFn; use crate::line_info::LineInfo; +use crate::regex::{QUERY_PARAMS_REGEX, URL_REGEX}; use crate::theme::Url; -use lazy_static::lazy_static; +use crate::types::Highlight; use regex::Regex; -pub fn highlight(url_group: &Url) -> HighlightFn { - let http_color = to_ansi(&url_group.http); - let https_color = to_ansi(&url_group.https); - let host_color = to_ansi(&url_group.host); - let path_color = to_ansi(&url_group.path); - let query_params_key_color = to_ansi(&url_group.query_params_key); - let query_params_value_color = to_ansi(&url_group.query_params_value); - let symbols_color = to_ansi(&url_group.symbols); - - Box::new(move |input: &str, line_info: &LineInfo| -> String { - highlight_urls( - &http_color, - &https_color, - &host_color, - &path_color, - &query_params_key_color, - &query_params_value_color, - &symbols_color, - input, - line_info, - &URL_REGEX, - &QUERY_PARAMS_REGEX, - ) - }) -} - -lazy_static! { - static ref URL_REGEX: Regex = { - Regex::new( - r"(?Phttp|https)(:)(//)(?P[^:/\n\s]+)(?P[/a-zA-Z0-9\-_.]*)?(?P\?[^#\n ]*)?") - .expect("Invalid regex pattern") - }; - static ref QUERY_PARAMS_REGEX: Regex = { - Regex::new(r"(?P[?&])(?P[^=]*)(?P=)(?P[^&]*)") - .expect("Invalid query params regex pattern") - }; +pub struct UrlHighlighter { + url_components: UrlComponents, + url_regex: Regex, + query_params_regex: Regex, +} + +struct UrlComponents { + http_color: String, + https_color: String, + host_color: String, + path_color: String, + query_params_key_color: String, + query_params_value_color: String, + symbols_color: String, +} + +impl UrlHighlighter { + pub fn new(url_group: &Url) -> Self { + let url_components = UrlComponents { + http_color: to_ansi(&url_group.http), + https_color: to_ansi(&url_group.https), + host_color: to_ansi(&url_group.host), + path_color: to_ansi(&url_group.path), + query_params_key_color: to_ansi(&url_group.query_params_key), + query_params_value_color: to_ansi(&url_group.query_params_value), + symbols_color: to_ansi(&url_group.symbols), + }; + + Self { + url_components, + url_regex: URL_REGEX.clone(), + query_params_regex: QUERY_PARAMS_REGEX.clone(), + } + } +} + +impl Highlight for UrlHighlighter { + fn should_short_circuit(&self, line_info: &LineInfo) -> bool { + if line_info.slashes < 1 || line_info.colons == 0 { + return true; + } + + false + } + + fn apply(&self, input: &str) -> String { + highlight_urls(&self.url_components, input, &self.url_regex, &self.query_params_regex) + } } fn highlight_urls( - http_color: &str, - https_color: &str, - host_color: &str, - path_color: &str, - query_params_key_color: &str, - query_params_value_color: &str, - symbols_color: &str, + url_components: &UrlComponents, input: &str, - line_info: &LineInfo, url_regex: &Regex, query_params_regex: &Regex, ) -> String { - if line_info.slashes < 1 || line_info.colons == 0 { - return input.to_string(); - } - let highlighted = url_regex.replace_all(input, |caps: ®ex::Captures<'_>| { let mut output = String::new(); if let Some(protocol) = caps.name("protocol") { let color = match protocol.as_str() { - "http" => http_color, - "https" => https_color, + "http" => &url_components.http_color, + "https" => &url_components.https_color, _ => color::RESET, }; output.push_str(&format!( @@ -80,34 +81,42 @@ } if let Some(host) = caps.name("host") { - output.push_str(&format!("{}{}{}", host_color, host.as_str(), color::RESET)); + output.push_str(&format!( + "{}{}{}", + &url_components.host_color, + host.as_str(), + color::RESET + )); } if let Some(path) = caps.name("path") { - output.push_str(&format!("{}{}{}", path_color, path.as_str(), color::RESET)); + output.push_str(&format!( + "{}{}{}", + &url_components.path_color, + path.as_str(), + color::RESET + )); } if let Some(query) = caps.name("query") { - let query_highlighted = query_params_regex.replace_all( - query.as_str(), - |query_caps: ®ex::Captures<'_>| { + let query_highlighted = + query_params_regex.replace_all(query.as_str(), |query_caps: ®ex::Captures<'_>| { let delimiter = query_caps.name("delimiter").map_or("", |m| m.as_str()); let key = query_caps.name("key").map_or("", |m| m.as_str()); let equal = query_caps.name("equal").map_or("", |m| m.as_str()); let value = query_caps.name("value").map_or("", |m| m.as_str()); format!( "{}{}{}{}{}{}{}{}", - symbols_color, + &url_components.symbols_color, delimiter, - query_params_key_color, + &url_components.query_params_key_color, key, - symbols_color, + &url_components.symbols_color, equal, - query_params_value_color, + &url_components.query_params_value_color, value ) - }, - ); + }); output.push_str(&format!("{}{}", query_highlighted, color::RESET)); } @@ -122,123 +131,83 @@ #[cfg(test)] mod tests { use super::*; + use crate::color::Fg; + use crate::theme::{Style, Url}; #[test] fn test_highlight_urls() { - let line_info = &LineInfo { - dashes: 0, - dots: 0, - slashes: 2, - double_quotes: 0, - colons: 1, - }; - let http_color = "\x1b[31m"; // Red color - let https_color = "\x1b[32m"; // Green color - let host_color = "\x1b[33m"; // Yellow color - let path_color = "\x1b[34m"; // Blue color - let query_params_key_color = "\x1b[35m"; // Magenta color - let query_params_value_color = "\x1b[36m"; // Cyan color - let symbols_color = "\x1b[37m"; // White color - let input = "Visit http://www.example.com/path?param1=value1¶m2=value2"; + let url_group = get_default_group(); + + let highlighter = UrlHighlighter::new(&url_group); + let input = "Visit https://www.example.com/path?param1=value1¶m2=value2"; let expected_output = - "Visit \u{1b}[31mhttp:\u{1b}[0m//\u{1b}[0m\u{1b}[33mwww.example.com\u{1b}\ - [0m\u{1b}[34m/path\u{1b}[0m\u{1b}[37m?\u{1b}[35mparam1\u{1b}[37m=\u{1b}[36mvalue1\u{1b}\ - [37m&\u{1b}[35mparam2\u{1b}[37m=\u{1b}[36mvalue2\u{1b}[0m\u{1b}[0m"; - - assert_eq!( - highlight_urls( - http_color, - https_color, - host_color, - path_color, - query_params_key_color, - query_params_value_color, - symbols_color, - input, - line_info, - &URL_REGEX, - &QUERY_PARAMS_REGEX, - ), - expected_output - ); + "Visit \u{1b}[31mhttps:\u{1b}[0m//\u{1b}[0m\u{1b}[33mwww.example.com\u{1b}[0m\u{1b}[34m/path\u{1b}[0m\u{1b}[37m?\u{1b}[35mparam1\u{1b}[37m=\u{1b}[36mvalue1\u{1b}[37m&\u{1b}[35mparam2\u{1b}[37m=\u{1b}[36mvalue2\u{1b}[0m\u{1b}[0m"; + + assert_eq!(highlighter.apply(input), expected_output); } #[test] fn test_short_circuit_on_few_slashes() { - let line_info = &LineInfo { - dashes: 0, - dots: 0, + let line_info = LineInfo { slashes: 1, - double_quotes: 0, - colons: 0, + ..Default::default() }; - let http_color = "\x1b[31m"; // Red color - let https_color = "\x1b[32m"; // Green color - let host_color = "\x1b[33m"; // Yellow color - let path_color = "\x1b[34m"; // Blue color - let query_params_key_color = "\x1b[35m"; // Magenta color - let query_params_value_color = "\x1b[36m"; // Cyan color - let symbols_color = "\x1b[37m"; // White color - let input = "Visit http://www.example.com/path?param1=value1¶m2=value2"; - - let expected_output = "Visit http://www.example.com/path?param1=value1¶m2=value2"; - - assert_eq!( - highlight_urls( - http_color, - https_color, - host_color, - path_color, - query_params_key_color, - query_params_value_color, - symbols_color, - input, - line_info, - &URL_REGEX, - &QUERY_PARAMS_REGEX, - ), - expected_output - ); + let url_group = get_default_group(); + + let highlighter = UrlHighlighter::new(&url_group); + let should_short_circuit_actual = highlighter.should_short_circuit(&line_info); + + assert!(should_short_circuit_actual); } #[test] fn test_short_circuit_on_no_colons() { - let line_info = &LineInfo { - dashes: 0, - dots: 0, + let line_info = LineInfo { slashes: 2, - double_quotes: 0, - colons: 0, + ..Default::default() }; - let http_color = "\x1b[31m"; // Red color - let https_color = "\x1b[32m"; // Green color - let host_color = "\x1b[33m"; // Yellow color - let path_color = "\x1b[34m"; // Blue color - let query_params_key_color = "\x1b[35m"; // Magenta color - let query_params_value_color = "\x1b[36m"; // Cyan color - let symbols_color = "\x1b[37m"; // White color - let input = "Visit http://www.example.com/path?param1=value1¶m2=value2"; - - let expected_output = "Visit http://www.example.com/path?param1=value1¶m2=value2"; - - assert_eq!( - highlight_urls( - http_color, - https_color, - host_color, - path_color, - query_params_key_color, - query_params_value_color, - symbols_color, - input, - line_info, - &URL_REGEX, - &QUERY_PARAMS_REGEX, - ), - expected_output - ); + let url_group = get_default_group(); + let highlighter = UrlHighlighter::new(&url_group); + + let should_short_circuit_actual = highlighter.should_short_circuit(&line_info); + + assert!(should_short_circuit_actual); + } + + fn get_default_group() -> Url { + Url { + http: Style { + fg: Fg::Red, + ..Default::default() + }, + https: Style { + fg: Fg::Red, + ..Default::default() + }, + host: Style { + fg: Fg::Yellow, + ..Default::default() + }, + path: Style { + fg: Fg::Blue, + ..Default::default() + }, + query_params_key: Style { + fg: Fg::Magenta, + ..Default::default() + }, + query_params_value: Style { + fg: Fg::Cyan, + ..Default::default() + }, + symbols: Style { + fg: Fg::White, + ..Default::default() + }, + disabled: false, + } } } diff -Nru tailspin-1.3.0+dfsg/src/highlighters/uuid.rs tailspin-2.0.0+dfsg/src/highlighters/uuid.rs --- tailspin-1.3.0+dfsg/src/highlighters/uuid.rs 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/highlighters/uuid.rs 2023-11-05 06:58:05.000000000 +0000 @@ -1,57 +1,41 @@ use crate::color; use crate::color::to_ansi; use crate::highlight_utils::highlight_with_awareness; -use crate::highlighters::HighlightFn; use crate::line_info::LineInfo; +use crate::regex::UUID_REGEX; use crate::theme::Style; -use lazy_static::lazy_static; +use crate::types::Highlight; use regex::{Captures, Regex}; -pub fn highlight(segment: &Style, separator: &Style) -> HighlightFn { - let segment_color = to_ansi(segment); - let separator_color = to_ansi(separator); - - Box::new(move |input: &str, line_info: &LineInfo| -> String { - highlight_uuids( - &segment_color, - &separator_color, - input, - line_info, - &UUID_REGEX, - ) - }) +pub struct UuidHighlighter { + segment_color: String, + separator_color: String, } -lazy_static! { - static ref UUID_REGEX: Regex = { - Regex::new( - r"(?x) - (\b[0-9a-fA-F]{8}\b) # Match first segment of UUID - (-) # Match separator - (\b[0-9a-fA-F]{4}\b) # Match second segment of UUID - (-) # Match separator - (\b[0-9a-fA-F]{4}\b) # Match third segment of UUID - (-) # Match separator - (\b[0-9a-fA-F]{4}\b) # Match fourth segment of UUID - (-) # Match separator - (\b[0-9a-fA-F]{12}\b) # Match last segment of UUID - ", - ) - .expect("Invalid UUID regex pattern") - }; +impl UuidHighlighter { + pub fn new(segment: &Style, separator: &Style) -> Self { + Self { + segment_color: to_ansi(segment), + separator_color: to_ansi(separator), + } + } } -fn highlight_uuids( - segment_color: &str, - separator_color: &str, - input: &str, - line_info: &LineInfo, - uuid_regex: &Regex, -) -> String { - if line_info.dashes < 4 { - return input.to_string(); +impl Highlight for UuidHighlighter { + fn should_short_circuit(&self, line_info: &LineInfo) -> bool { + if line_info.dashes < 4 { + return true; + } + + false } + fn apply(&self, input: &str) -> String { + highlight_uuids(&self.segment_color, &self.separator_color, input, &UUID_REGEX) + } +} + +fn highlight_uuids(segment_color: &str, separator_color: &str, input: &str, uuid_regex: &Regex) -> String { highlight_with_awareness(input, uuid_regex, |caps: &Captures<'_>| { let mut output = String::new(); for i in 1..caps.len() { @@ -68,24 +52,26 @@ #[cfg(test)] mod tests { use super::*; + use crate::color::Fg; + use crate::theme::Style; #[test] fn test_highlight_uuids() { - let line_info = &LineInfo { - dashes: 4, - dots: 0, - slashes: 0, - double_quotes: 0, - colons: 0, + let uuid = "550e8400-e29b-41d4-a716-446655440000"; + let segment = Style { + fg: Fg::Red, + ..Default::default() + }; + let separator = Style { + fg: Fg::Green, + ..Default::default() }; - let uuid = "550e8400-e29b-41d4-a716-446655440000"; + let highlighter = UuidHighlighter::new(&segment, &separator); + let highlighted = highlighter.apply(uuid); + let segment_color = "\x1b[31m"; // ANSI color code for red let separator_color = "\x1b[32m"; // ANSI color code for green - - let highlighted = - highlight_uuids(segment_color, separator_color, uuid, line_info, &UUID_REGEX); - let expected = format!( "{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}", segment_color, @@ -121,22 +107,19 @@ #[test] fn test_highlight_uuids_no_uuid() { - let line_info = &LineInfo { - dashes: 4, - dots: 0, - slashes: 0, - double_quotes: 0, - colons: 0, - }; - let text = "this is a test string with no uuid"; - let segment_color = "\x1b[31m"; - let separator_color = "\x1b[32m"; + let segment = Style { + fg: Fg::Red, + ..Default::default() + }; + let separator = Style { + fg: Fg::Green, + ..Default::default() + }; - let highlighted = - highlight_uuids(segment_color, separator_color, text, line_info, &UUID_REGEX); + let highlighter = UuidHighlighter::new(&segment, &separator); + let highlighted = highlighter.apply(text); - // The input string does not contain a UUID, so it should be returned as-is assert_eq!(highlighted, text); } } diff -Nru tailspin-1.3.0+dfsg/src/highlight_processor.rs tailspin-2.0.0+dfsg/src/highlight_processor.rs --- tailspin-1.3.0+dfsg/src/highlight_processor.rs 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/highlight_processor.rs 2023-11-05 06:58:05.000000000 +0000 @@ -1,5 +1,6 @@ use crate::highlighters::Highlighters; use crate::line_info::LineInfo; +use crate::types::Highlight; pub struct HighlightProcessor { highlighters: Highlighters, @@ -11,21 +12,28 @@ } pub fn apply(&self, text: &str) -> String { - let mut result = String::from(text); let line_info = LineInfo::process(text); - for highlight in &self.highlighters.before { - result = highlight(&result, &line_info); - } - - for highlight in &self.highlighters.main { - result = highlight(&result, &line_info); - } - - for highlight in &self.highlighters.after { - result = highlight(&result, &line_info); - } + let stages = [ + &self.highlighters.before, + &self.highlighters.main, + &self.highlighters.after, + ]; + + stages.iter().fold(String::from(text), |result, highlighters| { + self.apply_highlighters(&result, &line_info, highlighters) + }) + } - result + fn apply_highlighters( + &self, + text: &str, + line_info: &LineInfo, + highlighters: &[Box], + ) -> String { + highlighters + .iter() + .filter(|highlighter| !highlighter.should_short_circuit(line_info)) + .fold(String::from(text), |result, highlighter| highlighter.apply(&result)) } } diff -Nru tailspin-1.3.0+dfsg/src/highlight_utils.rs tailspin-2.0.0+dfsg/src/highlight_utils.rs --- tailspin-1.3.0+dfsg/src/highlight_utils.rs 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/highlight_utils.rs 2023-11-05 06:58:05.000000000 +0000 @@ -3,17 +3,33 @@ use regex::{Captures, Regex}; lazy_static! { - static ref ESCAPE_CODE_REGEX: Regex = - Regex::new(r"\x1b\[[0-9;]*m").expect("Invalid regex pattern"); + static ref ESCAPE_CODE_REGEX: Regex = Regex::new(r"\x1b\[[0-9;]*m").expect("Invalid regex pattern"); } const MAX_ALLOCATION_SIZE: usize = 1024 * 1024; // 1 MiB -pub(crate) fn highlight_with_awareness_replace_all( - color: &str, - input: &str, - regex: &Regex, -) -> String { +pub(crate) fn replace_with_awareness(color: &str, input: &str, replace_with: &str, regex: &Regex) -> String { + let chunks = split_into_chunks(input); + + let mut output = calculate_and_allocate_capacity(input); + for chunk in chunks { + match chunk { + Chunk::Normal(text) => { + let highlighted = regex.replace_all(text, |_caps: &Captures<'_>| { + format!("{}{}{}", color, replace_with, color::RESET) + }); + output.push_str(&highlighted); + } + Chunk::Highlighted(text) => { + output.push_str(text); + } + } + } + + output +} + +pub(crate) fn highlight_with_awareness_replace_all(color: &str, input: &str, regex: &Regex) -> String { let chunks = split_into_chunks(input); let mut output = calculate_and_allocate_capacity(input); diff -Nru tailspin-1.3.0+dfsg/src/io/controller/mod.rs tailspin-2.0.0+dfsg/src/io/controller/mod.rs --- tailspin-1.3.0+dfsg/src/io/controller/mod.rs 1970-01-01 00:00:00.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/io/controller/mod.rs 2023-11-05 06:58:05.000000000 +0000 @@ -0,0 +1,87 @@ +use async_trait::async_trait; +use tokio::io; + +use crate::io::presenter::empty::NoPresenter; +use crate::io::presenter::less::Less; +use crate::io::presenter::Present; +use crate::io::reader::command::CommandReader; +use crate::io::reader::linemux::Linemux; +use crate::io::reader::stdin::StdinReader; +use crate::io::reader::AsyncLineReader; +use crate::io::writer::stdout::StdoutWriter; +use crate::io::writer::temp_file::TempFile; +use crate::io::writer::AsyncLineWriter; +use crate::types::{Config, Input, Output}; +use tokio::sync::oneshot::Sender; + +pub struct Io { + reader: Box, + writer: Box, +} + +pub struct Presenter { + presenter: Box, +} + +pub async fn get_io_and_presenter(config: Config, reached_eof_tx: Option>) -> (Io, Presenter) { + let reader = get_reader(config.input, config.follow, config.tail, reached_eof_tx).await; + let (writer, presenter) = get_writer(config.output, config.follow).await; + + (Io { reader, writer }, Presenter { presenter }) +} + +async fn get_reader( + input: Input, + follow: bool, + tail: bool, + reached_eof_tx: Option>, +) -> Box { + match input { + Input::File(file_info) => { + Linemux::get_reader_single(file_info.path, file_info.line_count, follow, tail, reached_eof_tx).await + } + Input::Folder(info) => Linemux::get_reader_multiple(info.folder_name, info.file_paths, reached_eof_tx).await, + Input::Stdin => StdinReader::get_reader(reached_eof_tx), + Input::Command(cmd) => CommandReader::get_reader(cmd, reached_eof_tx).await, + } +} + +async fn get_writer(output: Output, follow: bool) -> (Box, Box) { + match output { + Output::TempFile => { + let result = TempFile::get_writer_result().await; + let writer = result.writer; + let temp_file_path = result.temp_file_path; + + let presenter = Less::get_presenter(temp_file_path, follow); + + (writer, presenter) + } + Output::Stdout => { + let writer = StdoutWriter::init(); + let presenter = NoPresenter::get_presenter(); + + (writer, presenter) + } + } +} + +#[async_trait] +impl AsyncLineReader for Io { + async fn next_line(&mut self) -> io::Result> { + self.reader.next_line().await + } +} + +#[async_trait] +impl AsyncLineWriter for Io { + async fn write_line(&mut self, line: &str) -> io::Result<()> { + self.writer.write_line(line).await + } +} + +impl Present for Presenter { + fn present(&self) { + self.presenter.present() + } +} diff -Nru tailspin-1.3.0+dfsg/src/io/mod.rs tailspin-2.0.0+dfsg/src/io/mod.rs --- tailspin-1.3.0+dfsg/src/io/mod.rs 1970-01-01 00:00:00.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/io/mod.rs 2023-11-05 06:58:05.000000000 +0000 @@ -0,0 +1,4 @@ +pub mod controller; +pub mod presenter; +pub mod reader; +pub mod writer; diff -Nru tailspin-1.3.0+dfsg/src/io/presenter/empty.rs tailspin-2.0.0+dfsg/src/io/presenter/empty.rs --- tailspin-1.3.0+dfsg/src/io/presenter/empty.rs 1970-01-01 00:00:00.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/io/presenter/empty.rs 2023-11-05 06:58:05.000000000 +0000 @@ -0,0 +1,15 @@ +use crate::io::presenter::Present; + +pub struct NoPresenter {} + +impl NoPresenter { + pub fn get_presenter() -> Box { + Box::new(Self {}) + } +} + +impl Present for NoPresenter { + fn present(&self) { + // no-op + } +} diff -Nru tailspin-1.3.0+dfsg/src/io/presenter/less.rs tailspin-2.0.0+dfsg/src/io/presenter/less.rs --- tailspin-1.3.0+dfsg/src/io/presenter/less.rs 1970-01-01 00:00:00.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/io/presenter/less.rs 2023-11-05 06:58:05.000000000 +0000 @@ -0,0 +1,57 @@ +use crate::io::presenter::Present; +use std::process::Command; + +pub struct Less { + file_path: String, + follow: bool, +} + +impl Less { + pub fn get_presenter(file_path: String, follow: bool) -> Box { + Box::new(Self { file_path, follow }) + } +} + +impl Present for Less { + fn present(&self) { + pass_ctrl_c_events_to_child_process(); + + let args = get_args(self.follow); + let status = Command::new("less") + .env("LESSSECURE", "1") + .args(args.as_slice()) + .arg(self.file_path.clone()) + .status(); + + match status { + Ok(status) => { + if !status.success() { + eprintln!("Failed to open file with less"); + } + } + Err(err) => { + eprintln!("Failed to run less: {}", err); + } + } + } +} + +fn pass_ctrl_c_events_to_child_process() { + // Without this handling, pressing Ctrl + C causes the program to exit + // immediately instead of passing the signal down to the child process (less) + ctrlc::set_handler(|| {}).expect("Error setting Ctrl-C handler"); +} + +fn get_args(follow: bool) -> Vec { + let mut args = vec![ + "--ignore-case".to_string(), + "--RAW-CONTROL-CHARS".to_string(), + "--".to_string(), // End of option arguments + ]; + + if follow { + args.insert(0, "+F".to_string()); + } + + args +} diff -Nru tailspin-1.3.0+dfsg/src/io/presenter/mod.rs tailspin-2.0.0+dfsg/src/io/presenter/mod.rs --- tailspin-1.3.0+dfsg/src/io/presenter/mod.rs 1970-01-01 00:00:00.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/io/presenter/mod.rs 2023-11-05 06:58:05.000000000 +0000 @@ -0,0 +1,6 @@ +pub mod empty; +pub mod less; + +pub trait Present: Send { + fn present(&self); +} diff -Nru tailspin-1.3.0+dfsg/src/io/reader/command.rs tailspin-2.0.0+dfsg/src/io/reader/command.rs --- tailspin-1.3.0+dfsg/src/io/reader/command.rs 1970-01-01 00:00:00.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/io/reader/command.rs 2023-11-05 06:58:05.000000000 +0000 @@ -0,0 +1,75 @@ +use crate::io::reader::AsyncLineReader; +use async_trait::async_trait; +use std::process::Stdio; +use tokio::io; +use tokio::io::{AsyncBufReadExt, BufReader}; +use tokio::process::Command as AsyncCommand; +use tokio::sync::oneshot::Sender; + +pub struct CommandReader { + reader: BufReader, +} + +impl CommandReader { + pub async fn get_reader( + command: String, + mut reached_eof_tx: Option>, + ) -> Box { + if let Some(reached_eof) = reached_eof_tx.take() { + reached_eof + .send(()) + .expect("Failed sending EOF signal to oneshot channel"); + }; + + let trap_command = format!("trap '' INT; {}", command); + + let child = AsyncCommand::new("sh") + .arg("-c") + .arg(trap_command) + .stdout(Stdio::piped()) + .spawn() + .expect("Could not spawn process"); + + let stdout = child.stdout.expect("Could not spawn child process"); + + let reader = BufReader::new(stdout); + + Box::new(CommandReader { reader }) + } + + async fn read_bytes_until_newline(&mut self) -> io::Result> { + let mut buffer = Vec::new(); + + self.reader.read_until(b'\n', &mut buffer).await?; + + Ok(buffer) + } + + fn strip_newline_character(buffer: Vec) -> Vec { + let mut buf = buffer; + + if let Some(last_byte) = buf.last() { + if *last_byte == b'\n' { + buf.pop(); + } + } + + buf + } +} + +#[async_trait] +impl AsyncLineReader for CommandReader { + async fn next_line(&mut self) -> io::Result> { + let buffer = self.read_bytes_until_newline().await?; + + if buffer.is_empty() { + return Ok(None); + } + + let buffer = Self::strip_newline_character(buffer); + let line = String::from_utf8_lossy(&buffer).into_owned(); + + Ok(Some(line)) + } +} diff -Nru tailspin-1.3.0+dfsg/src/io/reader/linemux.rs tailspin-2.0.0+dfsg/src/io/reader/linemux.rs --- tailspin-1.3.0+dfsg/src/io/reader/linemux.rs 1970-01-01 00:00:00.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/io/reader/linemux.rs 2023-11-05 06:58:05.000000000 +0000 @@ -0,0 +1,150 @@ +use crate::io::reader::AsyncLineReader; +use async_trait::async_trait; +use color_eyre::owo_colors::OwoColorize; +use linemux::MuxedLines; +use std::io; +use terminal_size::{terminal_size, Height, Width}; +use tokio::sync::oneshot::Sender; + +pub struct Linemux { + custom_message: Option, + number_of_lines: Option, + current_line: usize, + reached_eof_tx: Option>, + lines: MuxedLines, +} + +impl Linemux { + pub async fn get_reader_single( + file_path: String, + number_of_lines: usize, + follow: bool, + tail: bool, + mut reached_eof_tx: Option>, + ) -> Box { + let mut lines = MuxedLines::new().expect("Could not instantiate linemux"); + + if tail { + if let Some(reached_eof) = reached_eof_tx.take() { + reached_eof + .send(()) + .expect("Failed sending EOF signal to oneshot channel"); + } + + lines.add_file(&file_path).await.expect("Could not add file to linemux"); + } else { + lines + .add_file_from_start(&file_path) + .await + .expect("Could not add file to linemux"); + } + + let number_of_lines = if follow { Some(1) } else { Some(number_of_lines) }; + + Box::new(Self { + custom_message: None, + number_of_lines, + current_line: 0, + reached_eof_tx, + lines, + }) + } + + pub async fn get_reader_multiple( + folder_name: String, + file_paths: Vec, + mut reached_eof_tx: Option>, + ) -> Box { + use std::path::Path; + + if let Some(reached_eof) = reached_eof_tx.take() { + reached_eof + .send(()) + .expect("Failed sending EOF signal to oneshot channel"); + } + + let mut lines = MuxedLines::new().expect("Could not instantiate linemux"); + + let file_list = file_paths + .iter() + .enumerate() + .map(|(index, path)| { + let file_name = Path::new(path) + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(path); + + if index == file_paths.len() - 1 { + format!(" └─ {}", file_name.bold()) + } else { + format!(" ├─ {}", file_name.bold()) + } + }) + .collect::>() + .join("\n"); + + let separator = get_separator(); + let dimmed_separator = separator.dimmed(); + let custom_message = format!( + "Watching {} \n{}\n{}\n", + folder_name.green(), + file_list, + dimmed_separator + ); + + for file_path in file_paths { + lines.add_file(&file_path).await.expect("Could not add file to linemux"); + } + + Box::new(Self { + custom_message: Some(custom_message), + number_of_lines: None, + current_line: 0, + reached_eof_tx, + lines, + }) + } + + fn send_eof_signal(&mut self) { + if let Some(reached_eof) = self.reached_eof_tx.take() { + reached_eof + .send(()) + .expect("Failed sending EOF signal to oneshot channel"); + } + } +} + +fn get_separator() -> String { + let size = terminal_size(); + if let Some((Width(w), Height(_h))) = size { + "▁".repeat(w as usize) + } else { + "".to_string() + } +} + +#[async_trait] +impl AsyncLineReader for Linemux { + async fn next_line(&mut self) -> io::Result> { + self.current_line += 1; + + if let Some(custom_message) = self.custom_message.take() { + return Ok(Some(custom_message)); + } + + let line = match self.lines.next_line().await { + Ok(Some(line)) => line, + _ => return Ok(None), + }; + + let next_line = line.line().to_owned(); + + if let Some(number_of_lines) = self.number_of_lines { + if self.current_line >= number_of_lines { + self.send_eof_signal(); + } + } + + Ok(Some(next_line)) + } +} diff -Nru tailspin-1.3.0+dfsg/src/io/reader/mod.rs tailspin-2.0.0+dfsg/src/io/reader/mod.rs --- tailspin-1.3.0+dfsg/src/io/reader/mod.rs 1970-01-01 00:00:00.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/io/reader/mod.rs 2023-11-05 06:58:05.000000000 +0000 @@ -0,0 +1,11 @@ +pub mod command; +pub mod linemux; +pub mod stdin; + +use async_trait::async_trait; +use tokio::io; + +#[async_trait] +pub trait AsyncLineReader { + async fn next_line(&mut self) -> io::Result>; +} diff -Nru tailspin-1.3.0+dfsg/src/io/reader/stdin.rs tailspin-2.0.0+dfsg/src/io/reader/stdin.rs --- tailspin-1.3.0+dfsg/src/io/reader/stdin.rs 1970-01-01 00:00:00.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/io/reader/stdin.rs 2023-11-05 06:58:05.000000000 +0000 @@ -0,0 +1,64 @@ +use crate::io::reader::AsyncLineReader; +use async_trait::async_trait; +use tokio::io; +use tokio::io::{AsyncBufReadExt, BufReader, Stdin}; +use tokio::sync::oneshot::Sender; + +pub struct StdinReader { + reader: BufReader, + reached_eof_tx: Option>, +} + +impl StdinReader { + pub fn get_reader(reached_eof_tx: Option>) -> Box { + Box::new(StdinReader { + reader: BufReader::new(tokio::io::stdin()), + reached_eof_tx, + }) + } + + async fn read_bytes_until_newline(&mut self) -> io::Result> { + let mut buffer = Vec::new(); + + self.reader.read_until(b'\n', &mut buffer).await?; + + Ok(buffer) + } + + fn strip_newline_character(buffer: Vec) -> Vec { + let mut buf = buffer; + + if let Some(last_byte) = buf.last() { + if *last_byte == b'\n' { + buf.pop(); + } + } + + buf + } + + fn send_eof_signal(&mut self) { + if let Some(reached_eof) = self.reached_eof_tx.take() { + reached_eof + .send(()) + .expect("Failed sending EOF signal to oneshot channel"); + } + } +} + +#[async_trait] +impl AsyncLineReader for StdinReader { + async fn next_line(&mut self) -> io::Result> { + let buffer = self.read_bytes_until_newline().await?; + + if buffer.is_empty() { + self.send_eof_signal(); + return Ok(None); + } + + let buffer = Self::strip_newline_character(buffer); + let line = String::from_utf8_lossy(&buffer).into_owned(); + + Ok(Some(line)) + } +} diff -Nru tailspin-1.3.0+dfsg/src/io/writer/mod.rs tailspin-2.0.0+dfsg/src/io/writer/mod.rs --- tailspin-1.3.0+dfsg/src/io/writer/mod.rs 1970-01-01 00:00:00.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/io/writer/mod.rs 2023-11-05 06:58:05.000000000 +0000 @@ -0,0 +1,10 @@ +pub mod stdout; +pub mod temp_file; + +use async_trait::async_trait; +use tokio::io; + +#[async_trait] +pub trait AsyncLineWriter { + async fn write_line(&mut self, line: &str) -> io::Result<()>; +} diff -Nru tailspin-1.3.0+dfsg/src/io/writer/stdout.rs tailspin-2.0.0+dfsg/src/io/writer/stdout.rs --- tailspin-1.3.0+dfsg/src/io/writer/stdout.rs 1970-01-01 00:00:00.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/io/writer/stdout.rs 2023-11-05 06:58:05.000000000 +0000 @@ -0,0 +1,20 @@ +use crate::io::writer::AsyncLineWriter; +use async_trait::async_trait; +use tokio::io; + +pub struct StdoutWriter {} + +impl StdoutWriter { + pub fn init() -> Box { + Box::new(StdoutWriter {}) + } +} + +#[async_trait] +impl AsyncLineWriter for StdoutWriter { + async fn write_line(&mut self, line: &str) -> io::Result<()> { + println!("{}", line); + + Ok(()) + } +} diff -Nru tailspin-1.3.0+dfsg/src/io/writer/temp_file.rs tailspin-2.0.0+dfsg/src/io/writer/temp_file.rs --- tailspin-1.3.0+dfsg/src/io/writer/temp_file.rs 1970-01-01 00:00:00.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/io/writer/temp_file.rs 2023-11-05 06:58:05.000000000 +0000 @@ -0,0 +1,61 @@ +use crate::io::writer::AsyncLineWriter; +use async_trait::async_trait; +use rand::random; +use std::path::PathBuf; +use tempfile::TempDir; +use tokio::fs::File; +use tokio::io; +use tokio::io::{AsyncWriteExt, BufWriter}; + +pub struct TempFile { + _temp_dir: TempDir, + temp_file_writer: BufWriter, +} + +pub struct TempFileWriterResult { + pub writer: Box, + pub temp_file_path: String, +} + +impl TempFile { + pub async fn get_writer_result() -> TempFileWriterResult { + let (temp_dir, temp_file_path, temp_file_writer) = create_temp_file().await; + + let temp_file_path_string = temp_file_path + .to_str() + .expect("Could not get path to temp file") + .to_owned(); + + TempFileWriterResult { + writer: Box::new(TempFile { + _temp_dir: temp_dir, + temp_file_writer, + }), + temp_file_path: temp_file_path_string, + } + } +} + +#[async_trait] +impl AsyncLineWriter for TempFile { + async fn write_line(&mut self, line: &str) -> io::Result<()> { + let line_with_newline = format!("{}\n", line); + self.temp_file_writer.write_all(line_with_newline.as_bytes()).await?; + self.temp_file_writer.flush().await?; + + Ok(()) + } +} + +async fn create_temp_file() -> (TempDir, PathBuf, BufWriter) { + let unique_id: u32 = random(); + let filename = format!("tailspin.temp.{}", unique_id); + + let temp_dir = tempfile::tempdir().unwrap(); + + let temp_file_path = temp_dir.path().join(filename); + let output_file = File::create(&temp_file_path).await.unwrap(); + let output_writer = BufWriter::new(output_file); + + (temp_dir, temp_file_path, output_writer) +} diff -Nru tailspin-1.3.0+dfsg/src/io_stream/linemux_reader.rs tailspin-2.0.0+dfsg/src/io_stream/linemux_reader.rs --- tailspin-1.3.0+dfsg/src/io_stream/linemux_reader.rs 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/io_stream/linemux_reader.rs 1970-01-01 00:00:00.000000000 +0000 @@ -1,69 +0,0 @@ -// use crate::io_stream::traits::AsyncLineReader; -// use async_trait::async_trait; -// use linemux::MuxedLines; -// use std::fs::File; -// use std::io; -// use std::io::{BufRead, BufReader}; -// use std::path::Path; -// use tokio::sync::oneshot::Sender; -// -// pub struct LinemuxReader { -// file_path: String, -// number_of_lines: usize, -// current_line: usize, -// reached_eof_tx: Option>, -// lines: MuxedLines, -// } -// -// impl LinemuxReader { -// pub async fn new( -// file_path: String, -// number_of_lines: usize, -// reached_eof_tx: Option>, -// ) -> io::Result { -// let mut lines = MuxedLines::new()?; -// lines.add_file_from_start(&file_path).await?; -// -// Ok(Self { -// file_path, -// number_of_lines, -// current_line: 1, -// reached_eof_tx, -// lines, -// }) -// } -// } -// -// async fn count_lines(file_path: &str) -> io::Result { -// let file_path = file_path.to_owned(); -// -// let line_count = tokio::task::spawn_blocking(move || { -// let path = Path::new(&file_path); -// let file = File::open(&path).expect("Could not open file"); -// let reader = BufReader::new(file); -// reader.lines().count() -// }) -// .await -// .unwrap(); -// -// Ok(line_count) -// } -// -// #[async_trait] -// impl AsyncLineReader for LinemuxReader { -// async fn next_line(&mut self) -> io::Result> { -// if let Ok(Some(line)) = self.lines.next_line().await { -// if self.current_line == self.number_of_lines { -// if let Some(reached_eof) = self.reached_eof_tx.take() { -// reached_eof -// .send(()) -// .expect("Failed sending EOF signal to oneshot channel"); -// } -// } -// self.current_line += 1; -// return Ok(Some(line.line().to_owned())); -// } -// -// Ok(None) -// } -// } diff -Nru tailspin-1.3.0+dfsg/src/io_stream/mod.rs tailspin-2.0.0+dfsg/src/io_stream/mod.rs --- tailspin-1.3.0+dfsg/src/io_stream/mod.rs 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/io_stream/mod.rs 1970-01-01 00:00:00.000000000 +0000 @@ -1,88 +0,0 @@ -// mod linemux_reader; -// mod temp_file_writer; -// mod template_io; -// mod traits; -// -// use crate::io_stream::traits::{AsyncLineReader, AsyncLineWriter}; -// -// use async_trait::async_trait; -// use linemux::MuxedLines; -// use tokio::io::AsyncWriteExt; -// use tokio::sync::oneshot::Sender; -// use tokio::{fs, io}; -// -// pub use template_io::TemplateIOStream; -// pub use traits::LineIOStream; - -// pub struct TailFileIoStream { -// reader: MuxedLinesWrapper, -// writer: W, -// line_count: usize, -// reached_eof_tx: Option>, -// current_line: usize, -// } -// -// #[async_trait] -// impl LineIOStream for TailFileIoStream { -// async fn next_line(&mut self) -> io::Result> { -// let line = self.reader.next_line().await?; -// -// if self.current_line == self.line_count { -// if let Some(reached_eof) = self.reached_eof_tx.take() { -// reached_eof -// .send(()) -// .expect("Failed sending EOF signal to oneshot channel"); -// } -// } -// self.current_line += 1; -// -// Ok(line) -// } -// -// async fn write_line(&mut self, line: &str) -> io::Result<()> { -// self.writer.write_line(line).await -// } -// } -// -// impl TailFileIoStream { -// pub async fn new( -// file_path: &str, -// writer: W, -// line_count: usize, -// reached_eof_tx: Option>, -// ) -> io::Result { -// let mut lines = MuxedLines::new()?; -// dbg!(file_path.clone()); -// lines.add_file_from_start(file_path).await?; -// let reader = MuxedLinesWrapper(lines); -// -// Ok(Self { -// reader, -// writer, -// line_count, -// reached_eof_tx, -// current_line: 1, -// }) -// } -// } -// -// pub struct MuxedLinesWrapper(MuxedLines); -// -// #[async_trait] -// impl AsyncLineReader for MuxedLinesWrapper { -// async fn next_line(&mut self) -> io::Result> { -// self.0 -// .next_line() -// .await -// .map(|opt| opt.map(|line| line.line().to_string())) -// } -// } -// -// #[async_trait] -// impl AsyncLineWriter for io::BufWriter { -// async fn write_line(&mut self, line: &str) -> io::Result<()> { -// self.write_all(line.as_bytes()).await?; -// self.write_all(b"\n").await?; -// self.flush().await -// } -// } diff -Nru tailspin-1.3.0+dfsg/src/io_stream/temp_file_writer.rs tailspin-2.0.0+dfsg/src/io_stream/temp_file_writer.rs --- tailspin-1.3.0+dfsg/src/io_stream/temp_file_writer.rs 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/io_stream/temp_file_writer.rs 1970-01-01 00:00:00.000000000 +0000 @@ -1,48 +0,0 @@ -// use crate::io_stream::traits::AsyncLineWriter; -// use async_trait::async_trait; -// use rand::random; -// use std::path::PathBuf; -// use tempfile::TempDir; -// use tokio::fs::File; -// use tokio::io; -// use tokio::io::{AsyncWriteExt, BufWriter}; -// -// pub struct TempFileWriter { -// temp_dir: TempDir, -// output_path: PathBuf, -// output_writer: BufWriter, -// } -// -// impl TempFileWriter { -// pub async fn new() -> Self { -// let (temp_dir, output_path, output_writer) = create_temp_file().await; -// Self { -// temp_dir, -// output_path, -// output_writer, -// } -// } -// } -// -// #[async_trait] -// impl AsyncLineWriter for TempFileWriter { -// async fn write_line(&mut self, line: &str) -> io::Result<()> { -// self.output_writer.write_all(line.as_bytes()).await?; -// self.output_writer.flush().await?; -// -// Ok(()) -// } -// } -// -// async fn create_temp_file() -> (TempDir, PathBuf, BufWriter) { -// let unique_id: u32 = random(); -// let filename = format!("tailspin.temp.{}", unique_id); -// -// let temp_dir = tempfile::tempdir().unwrap(); -// -// let output_path = temp_dir.path().join(filename); -// let output_file = File::create(&output_path).await.unwrap(); -// let output_writer = BufWriter::new(output_file); -// -// (temp_dir, output_path, output_writer) -// } diff -Nru tailspin-1.3.0+dfsg/src/io_stream/template_io.rs tailspin-2.0.0+dfsg/src/io_stream/template_io.rs --- tailspin-1.3.0+dfsg/src/io_stream/template_io.rs 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/io_stream/template_io.rs 1970-01-01 00:00:00.000000000 +0000 @@ -1,66 +0,0 @@ -use crate::io_stream::linemux_reader::LinemuxReader; -use crate::io_stream::temp_file_writer::TempFileWriter; -use crate::io_stream::traits::{AsyncLineReader, AsyncLineWriter}; -use crate::io_stream::LineIOStream; -use async_trait::async_trait; -use tokio::io; -use tokio::sync::oneshot::Sender; - -pub struct TemplateIOStream { - reader: Box, - writer: Box, -} - -pub struct Foo { - pub bar: i32, -} - -impl Foo { - async fn new() -> Self { - Foo { bar: 42 } - } - - pub fn bar(&self) -> i32 { - self.bar - } -} - -impl TemplateIOStream { - pub(crate) async fn new( - file_path: String, - number_of_lines: usize, - reached_eof_tx: Option>, - ) -> Self { - let reader = LinemuxReader::new(file_path, number_of_lines, reached_eof_tx) - .await - .unwrap(); - let writer = TempFileWriter::new().await; - - Self { - reader: Box::new(reader), - writer: Box::new(writer), - } - } -} - -pub async fn create_stream_and_foo( - file_path: String, - number_of_lines: usize, - reached_eof_tx: Option>, -) -> (TemplateIOStream, Foo) { - let stream = TemplateIOStream::new(file_path, number_of_lines, reached_eof_tx).await; - let foo = Foo::new().await; - - (stream, foo) -} - -#[async_trait] -impl LineIOStream for TemplateIOStream { - async fn next_line(&mut self) -> io::Result> { - self.reader.next_line().await - } - - async fn write_line(&mut self, line: &str) -> io::Result<()> { - self.writer.write_line(line).await - } -} diff -Nru tailspin-1.3.0+dfsg/src/io_stream/traits.rs tailspin-2.0.0+dfsg/src/io_stream/traits.rs --- tailspin-1.3.0+dfsg/src/io_stream/traits.rs 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/io_stream/traits.rs 1970-01-01 00:00:00.000000000 +0000 @@ -1,18 +0,0 @@ -// use async_trait::async_trait; -// use tokio::io; -// -// #[async_trait] -// pub trait LineIOStream: Send { -// async fn next_line(&mut self) -> io::Result>; -// async fn write_line(&mut self, line: &str) -> io::Result<()>; -// } -// -// #[async_trait] -// pub trait AsyncLineReader { -// async fn next_line(&mut self) -> io::Result>; -// } -// -// #[async_trait] -// pub trait AsyncLineWriter { -// async fn write_line(&mut self, line: &str) -> io::Result<()>; -// } diff -Nru tailspin-1.3.0+dfsg/src/line_info.rs tailspin-2.0.0+dfsg/src/line_info.rs --- tailspin-1.3.0+dfsg/src/line_info.rs 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/line_info.rs 2023-11-05 06:58:05.000000000 +0000 @@ -1,26 +1,36 @@ +#[derive(Default)] pub struct LineInfo { - pub slashes: usize, - pub dots: usize, + pub colons: usize, pub dashes: usize, + pub dots: usize, pub double_quotes: usize, - pub colons: usize, + pub equals: usize, + pub slashes: usize, + pub left_bracket: usize, + pub right_bracket: usize, } impl LineInfo { pub fn process(line: &str) -> LineInfo { - let mut slashes = 0; - let mut dots = 0; + let mut colons = 0; let mut dashes = 0; + let mut dots = 0; let mut double_quotes = 0; - let mut colons = 0; + let mut equals = 0; + let mut slashes = 0; + let mut left_bracket = 0; + let mut right_bracket = 0; for c in line.chars() { match c { - '/' => slashes += 1, - '.' => dots += 1, + ':' => colons += 1, '-' => dashes += 1, + '.' => dots += 1, '"' => double_quotes += 1, - ':' => colons += 1, + '=' => equals += 1, + '/' => slashes += 1, + '[' => left_bracket += 1, + ']' => right_bracket += 1, _ => {} } } @@ -30,7 +40,10 @@ dots, dashes, double_quotes, + equals, colons, + left_bracket, + right_bracket, } } } diff -Nru tailspin-1.3.0+dfsg/src/main.rs tailspin-2.0.0+dfsg/src/main.rs --- tailspin-1.3.0+dfsg/src/main.rs 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/main.rs 2023-11-05 06:58:05.000000000 +0000 @@ -1,56 +1,46 @@ mod cli; mod color; -mod controller; -mod file_utils; +mod config; mod highlight_processor; mod highlight_utils; mod highlighters; -mod io_stream; +mod io; mod line_info; -mod presenter; -mod reader; +mod regex; mod theme; mod theme_io; mod types; -mod writer; -use crate::cli::Cli; -use crate::controller::config::create_config; -use crate::controller::get_io_and_presenter; use crate::highlight_processor::HighlightProcessor; -use crate::presenter::Present; -use crate::reader::AsyncLineReader; -use crate::writer::AsyncLineWriter; - -use std::process::exit; +use crate::io::controller::get_io_and_presenter; +use crate::io::presenter::Present; +use crate::io::reader::AsyncLineReader; +use crate::io::writer::AsyncLineWriter; +use crate::theme::Theme; +use crate::types::Config; +use color_eyre::eyre::Result; use tokio::sync::oneshot; #[tokio::main] -async fn main() { - let args = cli::get_args(); +async fn main() -> Result<()> { + color_eyre::install()?; - if should_exit_early(&args) { - exit(0); - } + let args = cli::get_args_or_exit_early(); + let theme = theme_io::load_theme(args.config_path.clone()); + let config = config::create_config_or_exit_early(args); - let config_path = args.config_path.clone(); - let theme = theme_io::load_theme(config_path); + run(theme, config).await; - let highlighter = highlighters::Highlighters::new(theme); - let highlight_processor = HighlightProcessor::new(highlighter); + Ok(()) +} +pub async fn run(theme: Theme, config: Config) { let (reached_eof_tx, reached_eof_rx) = oneshot::channel::<()>(); - - let config = match create_config(args) { - Ok(c) => c, - Err(e) => { - println!("{}", e.message); - exit(e.exit_code); - } - }; - let (io, presenter) = get_io_and_presenter(config, Some(reached_eof_tx)).await; + let highlighter = highlighters::Highlighters::new(&theme); + let highlight_processor = HighlightProcessor::new(highlighter); + tokio::spawn(process_lines(io, highlight_processor)); reached_eof_rx @@ -60,26 +50,6 @@ presenter.present(); } -fn should_exit_early(args: &Cli) -> bool { - if args.generate_shell_completions.is_some() { - cli::print_completions_to_stdout(); - return true; - } - - if args.create_default_config { - theme_io::create_default_config(); - return true; - } - - if args.show_default_config { - let default_config = theme_io::default_theme(); - println!("{}", default_config); - return true; - } - - false -} - async fn process_lines( mut io: T, highlight_processor: HighlightProcessor, diff -Nru tailspin-1.3.0+dfsg/src/presenter/empty.rs tailspin-2.0.0+dfsg/src/presenter/empty.rs --- tailspin-1.3.0+dfsg/src/presenter/empty.rs 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/presenter/empty.rs 1970-01-01 00:00:00.000000000 +0000 @@ -1,15 +0,0 @@ -use crate::presenter::Present; - -pub struct NoPresenter {} - -impl NoPresenter { - pub fn get_presenter() -> Box { - Box::new(Self {}) - } -} - -impl Present for NoPresenter { - fn present(&self) { - // no-op - } -} diff -Nru tailspin-1.3.0+dfsg/src/presenter/less.rs tailspin-2.0.0+dfsg/src/presenter/less.rs --- tailspin-1.3.0+dfsg/src/presenter/less.rs 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/presenter/less.rs 1970-01-01 00:00:00.000000000 +0000 @@ -1,57 +0,0 @@ -use crate::presenter::Present; -use std::process::Command; - -pub struct Less { - file_path: String, - follow: bool, -} - -impl Less { - pub fn get_presenter(file_path: String, follow: bool) -> Box { - Box::new(Self { file_path, follow }) - } -} - -impl Present for Less { - fn present(&self) { - pass_ctrl_c_events_to_child_process(); - - let args = get_args(self.follow); - let status = Command::new("less") - .env("LESSSECURE", "1") - .args(args.as_slice()) - .arg(self.file_path.clone()) - .status(); - - match status { - Ok(status) => { - if !status.success() { - eprintln!("Failed to open file with less"); - } - } - Err(err) => { - eprintln!("Failed to run less: {}", err); - } - } - } -} - -fn pass_ctrl_c_events_to_child_process() { - // Without this handling, pressing Ctrl + C causes the program to exit - // immediately instead of passing the signal down to the child process (less) - ctrlc::set_handler(|| {}).expect("Error setting Ctrl-C handler"); -} - -fn get_args(follow: bool) -> Vec { - let mut args = vec![ - "--ignore-case".to_string(), - "--RAW-CONTROL-CHARS".to_string(), - "--".to_string(), // End of option arguments - ]; - - if follow { - args.insert(0, "+F".to_string()); - } - - args -} diff -Nru tailspin-1.3.0+dfsg/src/presenter/mod.rs tailspin-2.0.0+dfsg/src/presenter/mod.rs --- tailspin-1.3.0+dfsg/src/presenter/mod.rs 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/presenter/mod.rs 1970-01-01 00:00:00.000000000 +0000 @@ -1,6 +0,0 @@ -pub mod empty; -pub mod less; - -pub trait Present: Send { - fn present(&self); -} diff -Nru tailspin-1.3.0+dfsg/src/reader/command.rs tailspin-2.0.0+dfsg/src/reader/command.rs --- tailspin-1.3.0+dfsg/src/reader/command.rs 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/reader/command.rs 1970-01-01 00:00:00.000000000 +0000 @@ -1,75 +0,0 @@ -use crate::reader::AsyncLineReader; -use async_trait::async_trait; -use std::process::Stdio; -use tokio::io; -use tokio::io::{AsyncBufReadExt, BufReader}; -use tokio::process::Command as AsyncCommand; -use tokio::sync::oneshot::Sender; - -pub struct CommandReader { - reader: BufReader, -} - -impl CommandReader { - pub async fn get_reader( - command: String, - mut reached_eof_tx: Option>, - ) -> Box { - if let Some(reached_eof) = reached_eof_tx.take() { - reached_eof - .send(()) - .expect("Failed sending EOF signal to oneshot channel"); - }; - - let trap_command = format!("trap '' INT; {}", command); - - let child = AsyncCommand::new("sh") - .arg("-c") - .arg(trap_command) - .stdout(Stdio::piped()) - .spawn() - .expect("Could not spawn process"); - - let stdout = child.stdout.expect("Could not spawn child process"); - - let reader = BufReader::new(stdout); - - Box::new(CommandReader { reader }) - } - - async fn read_bytes_until_newline(&mut self) -> io::Result> { - let mut buffer = Vec::new(); - - self.reader.read_until(b'\n', &mut buffer).await?; - - Ok(buffer) - } - - fn strip_newline_character(buffer: Vec) -> Vec { - let mut buf = buffer; - - if let Some(last_byte) = buf.last() { - if *last_byte == b'\n' { - buf.pop(); - } - } - - buf - } -} - -#[async_trait] -impl AsyncLineReader for CommandReader { - async fn next_line(&mut self) -> io::Result> { - let buffer = self.read_bytes_until_newline().await?; - - if buffer.is_empty() { - return Ok(None); - } - - let buffer = Self::strip_newline_character(buffer); - let line = String::from_utf8_lossy(&buffer).into_owned(); - - Ok(Some(line)) - } -} diff -Nru tailspin-1.3.0+dfsg/src/reader/linemux.rs tailspin-2.0.0+dfsg/src/reader/linemux.rs --- tailspin-1.3.0+dfsg/src/reader/linemux.rs 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/reader/linemux.rs 1970-01-01 00:00:00.000000000 +0000 @@ -1,110 +0,0 @@ -use crate::reader::AsyncLineReader; -use async_trait::async_trait; -use colored::Colorize; -use linemux::MuxedLines; -use std::io; -use tokio::sync::oneshot::Sender; - -pub struct Linemux { - custom_message: Option, - number_of_lines: Option, - current_line: usize, - reached_eof_tx: Option>, - lines: MuxedLines, -} - -impl Linemux { - pub async fn get_reader_single( - file_path: String, - number_of_lines: usize, - follow: bool, - reached_eof_tx: Option>, - ) -> Box { - let mut lines = MuxedLines::new().expect("Could not instantiate linemux"); - - lines - .add_file_from_start(&file_path) - .await - .expect("Could not add file to linemux"); - - let number_of_lines = if follow { - Some(1) - } else { - Some(number_of_lines) - }; - - Box::new(Self { - custom_message: None, - number_of_lines, - current_line: 1, - reached_eof_tx, - lines, - }) - } - - pub async fn get_reader_multiple( - folder_name: String, - file_paths: Vec, - mut reached_eof_tx: Option>, - ) -> Box { - if let Some(reached_eof) = reached_eof_tx.take() { - reached_eof - .send(()) - .expect("Failed sending EOF signal to oneshot channel"); - } - - let mut lines = MuxedLines::new().expect("Could not instantiate linemux"); - - let custom_message = format!( - "Tailing {} files in {}", - file_paths.len().to_string().cyan(), - folder_name.green(), - ); - - for file_path in file_paths { - lines - .add_file(&file_path) - .await - .expect("Could not add file to linemux"); - } - - Box::new(Self { - custom_message: Some(custom_message), - number_of_lines: None, - current_line: 1, - reached_eof_tx, - lines, - }) - } - - fn send_eof_signal(&mut self) { - if let Some(reached_eof) = self.reached_eof_tx.take() { - reached_eof - .send(()) - .expect("Failed sending EOF signal to oneshot channel"); - } - } -} - -#[async_trait] -impl AsyncLineReader for Linemux { - async fn next_line(&mut self) -> io::Result> { - if let Some(custom_message) = self.custom_message.take() { - return Ok(Some(custom_message)); - } - - let line = match self.lines.next_line().await { - Ok(Some(line)) => line, - _ => return Ok(None), - }; - - if let Some(number_of_lines) = self.number_of_lines { - if self.current_line == number_of_lines { - self.send_eof_signal(); - } - } - - self.current_line += 1; - Ok(Some(line.line().to_owned())) - } -} diff -Nru tailspin-1.3.0+dfsg/src/reader/mod.rs tailspin-2.0.0+dfsg/src/reader/mod.rs --- tailspin-1.3.0+dfsg/src/reader/mod.rs 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/reader/mod.rs 1970-01-01 00:00:00.000000000 +0000 @@ -1,11 +0,0 @@ -pub mod command; -pub mod linemux; -pub mod stdin; - -use async_trait::async_trait; -use tokio::io; - -#[async_trait] -pub trait AsyncLineReader { - async fn next_line(&mut self) -> io::Result>; -} diff -Nru tailspin-1.3.0+dfsg/src/reader/stdin.rs tailspin-2.0.0+dfsg/src/reader/stdin.rs --- tailspin-1.3.0+dfsg/src/reader/stdin.rs 2023-07-31 15:36:49.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/reader/stdin.rs 1970-01-01 00:00:00.000000000 +0000 @@ -1,64 +0,0 @@ -use crate::reader::AsyncLineReader; -use async_trait::async_trait; -use tokio::io; -use tokio::io::{AsyncBufReadExt, BufReader, Stdin}; -use tokio::sync::oneshot::Sender; - -pub struct StdinReader { - reader: BufReader, - reached_eof_tx: Option>, -} - -impl StdinReader { - pub fn get_reader(reached_eof_tx: Option>) -> Box { - Box::new(StdinReader { - reader: BufReader::new(tokio::io::stdin()), - reached_eof_tx, - }) - } - - async fn read_bytes_until_newline(&mut self) -> io::Result> { - let mut buffer = Vec::new(); - - self.reader.read_until(b'\n', &mut buffer).await?; - - Ok(buffer) - } - - fn strip_newline_character(buffer: Vec) -> Vec { - let mut buf = buffer; - - if let Some(last_byte) = buf.last() { - if *last_byte == b'\n' { - buf.pop(); - } - } - - buf - } - - fn send_eof_signal(&mut self) { - if let Some(reached_eof) = self.reached_eof_tx.take() { - reached_eof - .send(()) - .expect("Failed sending EOF signal to oneshot channel"); - } - } -} - -#[async_trait] -impl AsyncLineReader for StdinReader { - async fn next_line(&mut self) -> io::Result> { - let buffer = self.read_bytes_until_newline().await?; - - if buffer.is_empty() { - self.send_eof_signal(); - return Ok(None); - } - - let buffer = Self::strip_newline_character(buffer); - let line = String::from_utf8_lossy(&buffer).into_owned(); - - Ok(Some(line)) - } -} diff -Nru tailspin-1.3.0+dfsg/src/regex/mod.rs tailspin-2.0.0+dfsg/src/regex/mod.rs --- tailspin-1.3.0+dfsg/src/regex/mod.rs 1970-01-01 00:00:00.000000000 +0000 +++ tailspin-2.0.0+dfsg/src/regex/mod.rs 2023-11-05 06:58:05.000000000 +0000 @@ -0,0 +1,82 @@ +use lazy_static::lazy_static; +use regex::Regex; + +lazy_static! { + pub static ref DATE_REGEX: Regex = { + Regex::new( + r"(?x) + \d{4}-\d{2}-\d{2} + ", + ) + .expect("Invalid regex pattern") + }; + pub static ref TIME_REGEX: Regex = { + Regex::new( + r"(?x) + (?: + (?P[T\s])? # Capture separator (either a space or T) + (?P