mirror of
https://github.com/alexpasmantier/television.git
synced 2025-07-24 02:50:05 +00:00
feat(cable): add support for custom channels (#75)
This commit is contained in:
parent
fee4ed2671
commit
a5f5d20071
355
Cargo.lock
generated
355
Cargo.lock
generated
@ -46,9 +46,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.20"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "ansi-to-tui"
|
||||
version = "7.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67555e1f1ece39d737e28c8a017721287753af3f93225e4a445b29ccb0f5912c"
|
||||
dependencies = [
|
||||
"nom",
|
||||
"ratatui",
|
||||
"simdutf8",
|
||||
"smallvec",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ansi_colours"
|
||||
@ -110,9 +123,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.93"
|
||||
version = "1.0.94"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
|
||||
checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7"
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
@ -266,9 +279,9 @@ checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.8.0"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
|
||||
checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
|
||||
|
||||
[[package]]
|
||||
name = "bytesize"
|
||||
@ -313,25 +326,25 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cargo-platform"
|
||||
version = "0.1.8"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc"
|
||||
checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cargo_metadata"
|
||||
version = "0.18.1"
|
||||
version = "0.19.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037"
|
||||
checksum = "8769706aad5d996120af43197bf46ef6ad0fda35216b4505f926a365a232d924"
|
||||
dependencies = [
|
||||
"camino",
|
||||
"cargo-platform",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror 2.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -351,9 +364,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.1"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47"
|
||||
checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
@ -366,9 +379,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.21"
|
||||
version = "4.5.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f"
|
||||
checksum = "69371e34337c4c984bbe322360c2547210bf632eb2814bbe78a6e87a2935bd2b"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@ -376,9 +389,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.21"
|
||||
version = "4.5.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec"
|
||||
checksum = "6e24c1b4099818523236a8ca881d2b45db98dadfb4625cf6608c12069fcbbde1"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@ -856,12 +869,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.9"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
|
||||
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -879,6 +892,9 @@ name = "faster-hex"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2a2b11eda1d40935b26cf18f6833c526845ae8c41e58d09af6adeb6f0269183"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
@ -989,9 +1005,9 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
|
||||
|
||||
[[package]]
|
||||
name = "gix"
|
||||
version = "0.66.0"
|
||||
version = "0.68.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9048b8d1ae2104f045cb37e5c450fc49d5d8af22609386bfc739c11ba88995eb"
|
||||
checksum = "b04c66359b5e17f92395abc433861df0edf48f39f3f590818d1d7217327dd6a1"
|
||||
dependencies = [
|
||||
"gix-actor",
|
||||
"gix-commitgraph",
|
||||
@ -1025,20 +1041,20 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"signal-hook",
|
||||
"smallvec",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror 2.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-actor"
|
||||
version = "0.32.0"
|
||||
version = "0.33.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc19e312cd45c4a66cd003f909163dc2f8e1623e30a0c0c6df3776e89b308665"
|
||||
checksum = "32b24171f514cef7bb4dfb72a0b06dacf609b33ba8ad2489d4c4559a03b7afb3"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-date",
|
||||
"gix-utils",
|
||||
"itoa",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror 2.0.4",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
@ -1048,7 +1064,7 @@ version = "0.2.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d48b897b4bbc881aea994b4a5bbb340a04979d7be9089791304e04a9fbc66b53"
|
||||
dependencies = [
|
||||
"thiserror 2.0.3",
|
||||
"thiserror 2.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1057,28 +1073,28 @@ version = "0.4.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6ffbeb3a5c0b8b84c3fe4133a6f8c82fa962f4caefe8d0762eced025d3eb4f7"
|
||||
dependencies = [
|
||||
"thiserror 2.0.3",
|
||||
"thiserror 2.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-commitgraph"
|
||||
version = "0.24.3"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "133b06f67f565836ec0c473e2116a60fb74f80b6435e21d88013ac0e3c60fc78"
|
||||
checksum = "a8da6591a7868fb2b6dabddea6b09988b0b05e0213f938dbaa11a03dd7a48d85"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-chunk",
|
||||
"gix-features",
|
||||
"gix-hash",
|
||||
"memmap2",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror 2.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-config"
|
||||
version = "0.40.0"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78e797487e6ca3552491de1131b4f72202f282fb33f198b1c34406d765b42bb0"
|
||||
checksum = "6649b406ca1f99cb148959cf00468b231f07950f8ec438cc0903cda563606f19"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-config-value",
|
||||
@ -1090,7 +1106,7 @@ dependencies = [
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"smallvec",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror 2.0.4",
|
||||
"unicode-bom",
|
||||
"winnow",
|
||||
]
|
||||
@ -1105,7 +1121,7 @@ dependencies = [
|
||||
"bstr",
|
||||
"gix-path",
|
||||
"libc",
|
||||
"thiserror 2.0.3",
|
||||
"thiserror 2.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1117,26 +1133,26 @@ dependencies = [
|
||||
"bstr",
|
||||
"itoa",
|
||||
"jiff",
|
||||
"thiserror 2.0.3",
|
||||
"thiserror 2.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-diff"
|
||||
version = "0.46.0"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92c9afd80fff00f8b38b1c1928442feb4cd6d2232a6ed806b6b193151a3d336c"
|
||||
checksum = "a327be31a392144b60ab0b1c863362c32a1c8f7effdfa2141d5d5b6b916ef3bf"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-hash",
|
||||
"gix-object",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror 2.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-discover"
|
||||
version = "0.35.0"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0577366b9567376bc26e815fd74451ebd0e6218814e242f8e5b7072c58d956d2"
|
||||
checksum = "83bf6dfa4e266a4a9becb4d18fc801f92c3f7cc6c433dd86fdadbcf315ffb6ef"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"dunce",
|
||||
@ -1145,14 +1161,14 @@ dependencies = [
|
||||
"gix-path",
|
||||
"gix-ref",
|
||||
"gix-sec",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror 2.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-features"
|
||||
version = "0.38.2"
|
||||
version = "0.39.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac7045ac9fe5f9c727f38799d002a7ed3583cd777e3322a7c4b43e3cf437dc69"
|
||||
checksum = "7d85d673f2e022a340dba4713bed77ef2cf4cd737d2f3e0f159d45e0935fd81f"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"flate2",
|
||||
@ -1163,15 +1179,15 @@ dependencies = [
|
||||
"once_cell",
|
||||
"prodash",
|
||||
"sha1_smol",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror 2.0.4",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-fs"
|
||||
version = "0.11.3"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2bfe6249cfea6d0c0e0990d5226a4cb36f030444ba9e35e0639275db8f98575"
|
||||
checksum = "34740384d8d763975858fa2c176b68652a6fcc09f616e24e3ce967b0d370e4d8"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"gix-features",
|
||||
@ -1180,9 +1196,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gix-glob"
|
||||
version = "0.16.5"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74908b4bbc0a0a40852737e5d7889f676f081e340d5451a16e5b4c50d592f111"
|
||||
checksum = "aaf69a6bec0a3581567484bf99a4003afcaf6c469fd4214352517ea355cf3435"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"bstr",
|
||||
@ -1192,19 +1208,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gix-hash"
|
||||
version = "0.14.2"
|
||||
version = "0.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f93d7df7366121b5018f947a04d37f034717e113dcf9ccd85c34b58e57a74d5e"
|
||||
checksum = "0b5eccc17194ed0e67d49285e4853307e4147e95407f91c1c3e4a13ba9f4e4ce"
|
||||
dependencies = [
|
||||
"faster-hex",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror 2.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-hashtable"
|
||||
version = "0.5.2"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ddf80e16f3c19ac06ce415a38b8591993d3f73aede049cb561becb5b3a8e242"
|
||||
checksum = "0ef65b256631078ef733bc5530c4e6b1c2e7d5c2830b75d4e9034ab3997d18fe"
|
||||
dependencies = [
|
||||
"gix-hash",
|
||||
"hashbrown 0.14.5",
|
||||
@ -1213,9 +1229,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gix-index"
|
||||
version = "0.35.0"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cd4203244444017682176e65fd0180be9298e58ed90bd4a8489a357795ed22d"
|
||||
checksum = "270645fd20556b64c8ffa1540d921b281e6994413a0ca068596f97e9367a257a"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"bstr",
|
||||
@ -1236,64 +1252,66 @@ dependencies = [
|
||||
"memmap2",
|
||||
"rustix",
|
||||
"smallvec",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror 2.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-lock"
|
||||
version = "14.0.0"
|
||||
version = "15.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bc7fe297f1f4614774989c00ec8b1add59571dc9b024b4c00acb7dedd4e19d"
|
||||
checksum = "1cd3ab68a452db63d9f3ebdacb10f30dba1fa0d31ac64f4203d395ed1102d940"
|
||||
dependencies = [
|
||||
"gix-tempfile",
|
||||
"gix-utils",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror 2.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-object"
|
||||
version = "0.44.0"
|
||||
version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f5b801834f1de7640731820c2df6ba88d95480dc4ab166a5882f8ff12b88efa"
|
||||
checksum = "65d93e2bbfa83a307e47f45e45de7b6c04d7375a8bd5907b215f4bf45237d879"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-actor",
|
||||
"gix-date",
|
||||
"gix-features",
|
||||
"gix-hash",
|
||||
"gix-hashtable",
|
||||
"gix-utils",
|
||||
"gix-validate",
|
||||
"itoa",
|
||||
"smallvec",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror 2.0.4",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-odb"
|
||||
version = "0.63.0"
|
||||
version = "0.65.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3158068701c17df54f0ab2adda527f5a6aca38fd5fd80ceb7e3c0a2717ec747"
|
||||
checksum = "93bed6e1b577c25a6bb8e6ecbf4df525f29a671ddf5f2221821a56a8dbeec4e3"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"gix-date",
|
||||
"gix-features",
|
||||
"gix-fs",
|
||||
"gix-hash",
|
||||
"gix-hashtable",
|
||||
"gix-object",
|
||||
"gix-pack",
|
||||
"gix-path",
|
||||
"gix-quote",
|
||||
"parking_lot",
|
||||
"tempfile",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror 2.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-pack"
|
||||
version = "0.53.0"
|
||||
version = "0.55.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3223aa342eee21e1e0e403cad8ae9caf9edca55ef84c347738d10681676fd954"
|
||||
checksum = "9b91fec04d359544fecbb8e85117ec746fbaa9046ebafcefb58cb74f20dc76d4"
|
||||
dependencies = [
|
||||
"clru",
|
||||
"gix-chunk",
|
||||
@ -1304,7 +1322,7 @@ dependencies = [
|
||||
"gix-path",
|
||||
"memmap2",
|
||||
"smallvec",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror 2.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1317,7 +1335,7 @@ dependencies = [
|
||||
"gix-trace",
|
||||
"home",
|
||||
"once_cell",
|
||||
"thiserror 2.0.3",
|
||||
"thiserror 2.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1328,14 +1346,14 @@ checksum = "64a1e282216ec2ab2816cd57e6ed88f8009e634aec47562883c05ac8a7009a63"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-utils",
|
||||
"thiserror 2.0.3",
|
||||
"thiserror 2.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-ref"
|
||||
version = "0.47.0"
|
||||
version = "0.49.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae0d8406ebf9aaa91f55a57f053c5a1ad1a39f60fdf0303142b7be7ea44311e5"
|
||||
checksum = "1eae462723686272a58f49501015ef7c0d67c3e042c20049d8dd9c7eff92efde"
|
||||
dependencies = [
|
||||
"gix-actor",
|
||||
"gix-features",
|
||||
@ -1348,45 +1366,47 @@ dependencies = [
|
||||
"gix-utils",
|
||||
"gix-validate",
|
||||
"memmap2",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror 2.0.4",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-refspec"
|
||||
version = "0.25.0"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebb005f82341ba67615ffdd9f7742c87787544441c88090878393d0682869ca6"
|
||||
checksum = "00c056bb747868c7eb0aeb352c9f9181ab8ca3d0a2550f16470803500c6c413d"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-hash",
|
||||
"gix-revision",
|
||||
"gix-validate",
|
||||
"smallvec",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror 2.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-revision"
|
||||
version = "0.29.0"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba4621b219ac0cdb9256883030c3d56a6c64a6deaa829a92da73b9a576825e1e"
|
||||
checksum = "44488e0380847967bc3e3cacd8b22652e02ea1eb58afb60edd91847695cd2d8d"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"bstr",
|
||||
"gix-commitgraph",
|
||||
"gix-date",
|
||||
"gix-hash",
|
||||
"gix-hashtable",
|
||||
"gix-object",
|
||||
"gix-revwalk",
|
||||
"gix-trace",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror 2.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-revwalk"
|
||||
version = "0.15.0"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b41e72544b93084ee682ef3d5b31b1ba4d8fa27a017482900e5e044d5b1b3984"
|
||||
checksum = "510026fc32f456f8f067d8f37c34088b97a36b2229d88a6a5023ef179fcb109d"
|
||||
dependencies = [
|
||||
"gix-commitgraph",
|
||||
"gix-date",
|
||||
@ -1394,7 +1414,7 @@ dependencies = [
|
||||
"gix-hashtable",
|
||||
"gix-object",
|
||||
"smallvec",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror 2.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1411,9 +1431,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gix-tempfile"
|
||||
version = "14.0.2"
|
||||
version = "15.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "046b4927969fa816a150a0cda2e62c80016fe11fb3c3184e4dddf4e542f108aa"
|
||||
checksum = "2feb86ef094cc77a4a9a5afbfe5de626897351bbbd0de3cb9314baf3049adb82"
|
||||
dependencies = [
|
||||
"gix-fs",
|
||||
"libc",
|
||||
@ -1432,9 +1452,9 @@ checksum = "04bdde120c29f1fc23a24d3e115aeeea3d60d8e65bab92cc5f9d90d9302eb952"
|
||||
|
||||
[[package]]
|
||||
name = "gix-traverse"
|
||||
version = "0.41.0"
|
||||
version = "0.43.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "030da39af94e4df35472e9318228f36530989327906f38e27807df305fccb780"
|
||||
checksum = "3ff2ec9f779680f795363db1c563168b32b8d6728ec58564c628e85c92d29faf"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"gix-commitgraph",
|
||||
@ -1444,20 +1464,19 @@ dependencies = [
|
||||
"gix-object",
|
||||
"gix-revwalk",
|
||||
"smallvec",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror 2.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-url"
|
||||
version = "0.27.5"
|
||||
version = "0.28.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd280c5e84fb22e128ed2a053a0daeacb6379469be6a85e3d518a0636e160c89"
|
||||
checksum = "e09f97db3618fb8e473d7d97e77296b50aaee0ddcd6a867f07443e3e87391099"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-features",
|
||||
"gix-path",
|
||||
"home",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror 2.0.4",
|
||||
"url",
|
||||
]
|
||||
|
||||
@ -1478,7 +1497,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd520d09f9f585b34b32aba1d0b36ada89ab7fefb54a8ca3fe37fc482a750937"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"thiserror 2.0.3",
|
||||
"thiserror 2.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1506,9 +1525,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.1"
|
||||
version = "0.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3"
|
||||
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
@ -1530,12 +1549,6 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.4.0"
|
||||
@ -1736,12 +1749,12 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.6.0"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
|
||||
checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.15.1",
|
||||
"hashbrown 0.15.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1781,15 +1794,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.13"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "540654e97a3f4470a492cd30ff187bc95d89557a903a2bbf112e2fae98104ef2"
|
||||
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
|
||||
|
||||
[[package]]
|
||||
name = "jiff"
|
||||
version = "0.1.14"
|
||||
version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d9d414fc817d3e3d62b2598616733f76c4cc74fbac96069674739b881295c8"
|
||||
checksum = "db69f08d4fb10524cacdb074c10b296299d71274ddbc830a8ee65666867002e9"
|
||||
dependencies = [
|
||||
"jiff-tzdb-platform",
|
||||
"windows-sys 0.59.0",
|
||||
@ -1835,15 +1848,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.164"
|
||||
version = "0.2.167"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f"
|
||||
checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.8.5"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
|
||||
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.52.6",
|
||||
@ -1900,7 +1913,7 @@ version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.1",
|
||||
"hashbrown 0.15.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1962,11 +1975,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.0.2"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
|
||||
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
|
||||
dependencies = [
|
||||
"hermit-abi 0.3.9",
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
@ -2122,9 +2134,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "os_info"
|
||||
version = "3.8.2"
|
||||
version = "3.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae99c7fa6dd38c7cafe1ec085e804f8f555a2f8659b0dbe03f1f9963a9b51092"
|
||||
checksum = "e5ca711d8b83edbb00b44d504503cd247c9c0bd8b0fa2694f2a1a3d8165379ce"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
@ -2183,9 +2195,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pathdiff"
|
||||
version = "0.2.2"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d61c5ce1153ab5b689d0c074c4e7fc613e942dfb7dd9eea5ab202d2ad91fe361"
|
||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
@ -2271,7 +2283,7 @@ checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"hermit-abi 0.4.0",
|
||||
"hermit-abi",
|
||||
"pin-project-lite",
|
||||
"rustix",
|
||||
"tracing",
|
||||
@ -2305,9 +2317,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "prodash"
|
||||
version = "28.0.0"
|
||||
version = "29.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "744a264d26b88a6a7e37cbad97953fa233b94d585236310bcbc88474b4092d79"
|
||||
checksum = "a266d8d6020c61a437be704c5e618037588e1985c7dbb7bf8d265db84cffe325"
|
||||
dependencies = [
|
||||
"log",
|
||||
"parking_lot",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
@ -2659,6 +2675,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simdutf8"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.9"
|
||||
@ -2712,9 +2734,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.7"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
|
||||
checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
@ -2768,9 +2790,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.89"
|
||||
version = "2.0.90"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e"
|
||||
checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -2812,8 +2834,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "television"
|
||||
version = "0.5.3"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"ansi-to-tui",
|
||||
"better-panic",
|
||||
"clap",
|
||||
"color-eyre",
|
||||
@ -2832,7 +2855,6 @@ dependencies = [
|
||||
"television-fuzzy",
|
||||
"television-previewers",
|
||||
"television-utils",
|
||||
"termtree",
|
||||
"tokio",
|
||||
"toml",
|
||||
"tracing",
|
||||
@ -2843,7 +2865,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "television-channels"
|
||||
version = "0.0.7"
|
||||
version = "0.0.8"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"color-eyre",
|
||||
@ -2851,18 +2873,20 @@ dependencies = [
|
||||
"directories",
|
||||
"eyre",
|
||||
"ignore",
|
||||
"lazy_static",
|
||||
"serde",
|
||||
"strum",
|
||||
"television-derive",
|
||||
"television-fuzzy",
|
||||
"television-utils",
|
||||
"tokio",
|
||||
"toml",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "television-derive"
|
||||
version = "0.0.7"
|
||||
version = "0.0.8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -2871,7 +2895,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "television-fuzzy"
|
||||
version = "0.0.7"
|
||||
version = "0.0.8"
|
||||
dependencies = [
|
||||
"nucleo",
|
||||
"parking_lot",
|
||||
@ -2879,22 +2903,23 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "television-previewers"
|
||||
version = "0.0.7"
|
||||
version = "0.0.8"
|
||||
dependencies = [
|
||||
"color-eyre",
|
||||
"devicons",
|
||||
"lazy_static",
|
||||
"parking_lot",
|
||||
"regex",
|
||||
"syntect",
|
||||
"television-channels",
|
||||
"television-utils",
|
||||
"termtree",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "television-utils"
|
||||
version = "0.0.7"
|
||||
version = "0.0.8"
|
||||
dependencies = [
|
||||
"bat",
|
||||
"color-eyre",
|
||||
@ -2920,12 +2945,6 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termtree"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
@ -2937,11 +2956,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.3"
|
||||
version = "2.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa"
|
||||
checksum = "2f49a1853cf82743e3b7950f77e0f4d622ca36cf4317cba00c767838bac8d490"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.3",
|
||||
"thiserror-impl 2.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2957,9 +2976,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.3"
|
||||
version = "2.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568"
|
||||
checksum = "8381894bb3efe0c4acac3ded651301ceee58a15d47c2e34885ed1908ad667061"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -2978,9 +2997,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.36"
|
||||
version = "0.3.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
|
||||
checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
@ -3001,9 +3020,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.18"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
|
||||
checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
@ -3045,9 +3064,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.41.1"
|
||||
version = "1.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33"
|
||||
checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
@ -3108,9 +3127,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.40"
|
||||
version = "0.1.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
|
||||
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
@ -3119,9 +3138,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.27"
|
||||
version = "0.1.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
||||
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -3130,9 +3149,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
|
||||
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
@ -3140,9 +3159,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-error"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e"
|
||||
checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db"
|
||||
dependencies = [
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
@ -3161,9 +3180,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.18"
|
||||
version = "0.3.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
|
||||
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term 0.46.0",
|
||||
@ -3292,9 +3311,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
||||
|
||||
[[package]]
|
||||
name = "vergen"
|
||||
version = "9.0.1"
|
||||
version = "9.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "349ed9e45296a581f455bc18039878f409992999bc1d5da12a6800eb18c8752f"
|
||||
checksum = "31f25fc8f8f05df455c7941e87f093ad22522a9ff33d7a027774815acf6f0639"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_metadata",
|
||||
@ -3308,9 +3327,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vergen-gix"
|
||||
version = "1.0.2"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02ef5d49e57c96e025770171c1c7ee0e30cd6f712f21a1fe501a58be6d069192"
|
||||
checksum = "2c593af74f49155e618c8099309dd7937e4763990ac0752997d666d5cb32f81b"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"derive_builder",
|
||||
@ -3323,9 +3342,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vergen-lib"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "229eaddb0050920816cf051e619affaf18caa3dd512de8de5839ccbc8e53abb0"
|
||||
checksum = "c0c767e6751c09fc85cde58722cf2f1007e80e4c8d5a4321fc90d83dc54ca147"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"derive_builder",
|
||||
|
14
Cargo.toml
14
Cargo.toml
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "television"
|
||||
version = "0.5.3"
|
||||
version = "0.6.0"
|
||||
edition = "2021"
|
||||
description = "The revolution will be televised."
|
||||
license = "MIT"
|
||||
@ -54,11 +54,11 @@ name = "tv"
|
||||
|
||||
[dependencies]
|
||||
# workspace dependencies
|
||||
television-fuzzy = { path = "crates/television-fuzzy", version = "0.0.7" }
|
||||
television-derive = { path = "crates/television-derive", version = "0.0.7" }
|
||||
television-channels = { path = "crates/television-channels", version = "0.0.7" }
|
||||
television-previewers = { path = "crates/television-previewers", version = "0.0.7" }
|
||||
television-utils = { path = "crates/television-utils", version = "0.0.7" }
|
||||
television-fuzzy = { path = "crates/television-fuzzy", version = "0.0.8" }
|
||||
television-derive = { path = "crates/television-derive", version = "0.0.8" }
|
||||
television-channels = { path = "crates/television-channels", version = "0.0.8" }
|
||||
television-previewers = { path = "crates/television-previewers", version = "0.0.8" }
|
||||
television-utils = { path = "crates/television-utils", version = "0.0.8" }
|
||||
|
||||
# external dependencies
|
||||
better-panic = "0.3.0"
|
||||
@ -78,8 +78,8 @@ tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "serde"] }
|
||||
unicode-width = "0.2.0"
|
||||
human-panic = "2.0.2"
|
||||
termtree = "0.5.1"
|
||||
copypasta = "0.10.1"
|
||||
ansi-to-tui = "7.0.0"
|
||||
|
||||
|
||||
[build-dependencies]
|
||||
|
63
README.md
63
README.md
@ -115,7 +115,7 @@ Default keybindings are as follows:
|
||||
|
||||
| Key | Description |
|
||||
| :---: | ----------- |
|
||||
| <kbd>↑</kbd> / <kbd>↓</kbd> or <kbd>Ctrl</kbd> + <kbd>n</kbd> / <kbd>p</kbd> | Navigate through the list of entries |
|
||||
| <kbd>↑</kbd> / <kbd>↓</kbd> | Navigate through the list of entries |
|
||||
| <kbd>Ctrl</kbd> + <kbd>u</kbd> / <kbd>d</kbd> | Scroll the preview pane up / down |
|
||||
| <kbd>Enter</kbd> | Select the current entry |
|
||||
| <kbd>Ctrl</kbd> + <kbd>y</kbd> | Copy the selected entry to the clipboard |
|
||||
@ -123,10 +123,10 @@ Default keybindings are as follows:
|
||||
| <kbd>Ctrl</kbd> + <kbd>s</kbd> | Toggle send to channel mode |
|
||||
| <kbd>Esc</kbd> | Quit the application |
|
||||
|
||||
These keybindings can be customized in the configuration file (see [Customization](#customization)).
|
||||
These keybindings are all configurable (see [Customization](#customization)).
|
||||
|
||||
## Built-in Channels
|
||||
The following channels are currently available:
|
||||
The following built-in channels are currently available:
|
||||
- `Files`: search through files in a directory tree.
|
||||
- `Text`: search through textual content in a directory tree.
|
||||
- `GitRepos`: search through git repositories anywhere on the file system.
|
||||
@ -134,6 +134,51 @@ The following channels are currently available:
|
||||
- `Alias`: search through shell aliases and their values.
|
||||
- `Stdin`: search through lines of text from stdin.
|
||||
|
||||
## Cable channels
|
||||
Tired of broadcast television? Want to watch your favorite shows on demand? `television` has you covered with cable channels. Cable channels are channels that are not built-in to `television` but are instead provided by the community.
|
||||
You can find a list of available cable channels [on the wiki](https://github.com/alexpasmantier/television/wiki) and even contribute your own!
|
||||
|
||||
### Installing cable channels
|
||||
Installing cable channels is as simple as creating provider files in your configuration folder.
|
||||
|
||||
A provider file is a `*channels.toml` file that contains cable channel prototypes defined as follows:
|
||||
|
||||
**my-custom-channels.toml**
|
||||
```toml
|
||||
[[cable_channel]]
|
||||
name = "Git log"
|
||||
source_command = 'git log --oneline --date=short --pretty="format:%h %s %an %cd" "$@"'
|
||||
preview_command = 'git show -p --stat --pretty=fuller --color=always {0}'
|
||||
|
||||
[[cable_channel]]
|
||||
name = "My dotfiles"
|
||||
source_command = 'fd -t f . $HOME/.config'
|
||||
preview_command = 'bat -n --color=always {0}'
|
||||
```
|
||||
|
||||
This would add two new cable channels to `television` available using the remote control mode:
|
||||
|
||||

|
||||
|
||||
<details>
|
||||
|
||||
<summary>Deciding which part of the source command output to pass to the previewer:</summary>
|
||||
|
||||
By default, each line of the source command can be passed to the previewer using `{}`.
|
||||
|
||||
If you wish to pass only a part of the output to the previewer, you may do so by specifying the `preview_delimiter` to use as a separator and refering to the desired part using the corresponding index.
|
||||
|
||||
**Example:**
|
||||
```toml
|
||||
[[cable_channel]]
|
||||
name = "Disney channel"
|
||||
source_command = 'echo "one:two:three:four" && echo "five:six:seven:eight"'
|
||||
preview_command = 'echo {2}'
|
||||
preview_delimiter = ':'
|
||||
# which will pass "three" and "seven" to the preview command
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Design (high-level)
|
||||
#### Channels
|
||||
@ -213,10 +258,12 @@ Here is a list of terminal emulators that have currently been tested with `telev
|
||||
|
||||
|
||||
|
||||
## Customization
|
||||
## Configuration
|
||||
You may wish to customize the behavior of `television` by providing your own configuration file. The configuration file
|
||||
is a simple TOML file that allows you to customize the behavior of `television` in a number of ways.
|
||||
|
||||
Here are default locations where `television` expect the configuration files to be located for each platform:
|
||||
|
||||
|Platform|Value|
|
||||
|--------|:-----:|
|
||||
|Linux|`$HOME/.config/television/config.toml`|
|
||||
@ -228,11 +275,17 @@ television will expect the configuration file to be in `$XDG_CONFIG_HOME/televis
|
||||
|
||||
You may also override these default paths by setting the `TELEVISION_CONFIG` environment variable to the path of your desired configuration **folder**.
|
||||
|
||||
Example:
|
||||
<details>
|
||||
|
||||
<summary>
|
||||
Using a custom configuration file location:
|
||||
</summary>
|
||||
|
||||
```bash
|
||||
export TELEVISION_CONFIG=$HOME/.config/television
|
||||
touch $TELEVISION_CONFIG/config.toml
|
||||
```
|
||||
</details>
|
||||
|
||||
#### Default Configuration
|
||||
The default configuration file can be found in the repository's [./.config/config.toml](./.config/config.toml).
|
||||
|
BIN
assets/cable_channels.png
Normal file
BIN
assets/cable_channels.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 MiB |
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "television-channels"
|
||||
version = "0.0.7"
|
||||
version = "0.0.8"
|
||||
description.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
@ -13,9 +13,9 @@ edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
television-fuzzy = { path = "../television-fuzzy", version = "0.0.7" }
|
||||
television-utils = { path = "../television-utils", version = "0.0.7" }
|
||||
television-derive = { path = "../television-derive", version = "0.0.7" }
|
||||
television-fuzzy = { path = "../television-fuzzy", version = "0.0.8" }
|
||||
television-utils = { path = "../television-utils", version = "0.0.8" }
|
||||
television-derive = { path = "../television-derive", version = "0.0.8" }
|
||||
devicons = "0.6.11"
|
||||
tracing = "0.1.40"
|
||||
eyre = "0.6.12"
|
||||
@ -26,4 +26,6 @@ directories = "5.0.1"
|
||||
color-eyre = "0.6.3"
|
||||
serde = "1.0.214"
|
||||
strum = { version = "0.26.3", features = ["derive"] }
|
||||
lazy_static = "1.5.0"
|
||||
toml = "0.8.19"
|
||||
|
||||
|
37
crates/television-channels/src/cable.rs
Normal file
37
crates/television-channels/src/cable.rs
Normal file
@ -0,0 +1,37 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::{self, Display, Formatter},
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
pub struct CableChannelPrototype {
|
||||
pub name: String,
|
||||
pub source_command: String,
|
||||
pub preview_command: String,
|
||||
#[serde(default = "default_delimiter")]
|
||||
pub preview_delimiter: String,
|
||||
}
|
||||
|
||||
const DEFAULT_DELIMITER: &str = " ";
|
||||
|
||||
fn default_delimiter() -> String {
|
||||
DEFAULT_DELIMITER.to_string()
|
||||
}
|
||||
|
||||
impl Display for CableChannelPrototype {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, Default)]
|
||||
pub struct CableChannels(pub HashMap<String, CableChannelPrototype>);
|
||||
|
||||
impl Deref for CableChannels {
|
||||
type Target = HashMap<String, CableChannelPrototype>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
use crate::entry::Entry;
|
||||
use color_eyre::Result;
|
||||
use television_derive::{Broadcast, ToCliChannel, ToUnitChannel};
|
||||
|
||||
mod alias;
|
||||
mod cable;
|
||||
mod env;
|
||||
mod files;
|
||||
mod git_repos;
|
||||
@ -137,11 +139,28 @@ pub enum TelevisionChannel {
|
||||
#[exclude_from_unit]
|
||||
#[exclude_from_cli]
|
||||
RemoteControl(remote_control::RemoteControl),
|
||||
/// A custom channel.
|
||||
///
|
||||
/// This channel allows to search through custom data.
|
||||
#[exclude_from_unit]
|
||||
#[exclude_from_cli]
|
||||
Cable(cable::Channel),
|
||||
}
|
||||
|
||||
impl From<&Entry> for TelevisionChannel {
|
||||
fn from(entry: &Entry) -> Self {
|
||||
UnitChannel::from(entry.name.as_str()).into()
|
||||
UnitChannel::try_from(entry.name.as_str()).unwrap().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl TelevisionChannel {
|
||||
pub fn zap(&self, channel_name: &str) -> Result<TelevisionChannel> {
|
||||
match self {
|
||||
TelevisionChannel::RemoteControl(remote_control) => {
|
||||
remote_control.zap(channel_name)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
119
crates/television-channels/src/channels/cable.rs
Normal file
119
crates/television-channels/src/channels/cable.rs
Normal file
@ -0,0 +1,119 @@
|
||||
use crate::cable::CableChannelPrototype;
|
||||
use crate::channels::OnAir;
|
||||
use crate::entry::{Entry, PreviewCommand, PreviewType};
|
||||
use television_fuzzy::{
|
||||
matcher::{config::Config, injector::Injector},
|
||||
Matcher,
|
||||
};
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct Channel {
|
||||
name: String,
|
||||
matcher: Matcher<String>,
|
||||
entries_command: String,
|
||||
preview_command: PreviewCommand,
|
||||
}
|
||||
|
||||
impl Default for Channel {
|
||||
fn default() -> Self {
|
||||
Self::new(
|
||||
"Files",
|
||||
"find . -type f",
|
||||
PreviewCommand::new("bat -n --color=always {}", ":"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CableChannelPrototype> for Channel {
|
||||
fn from(prototype: CableChannelPrototype) -> Self {
|
||||
Self::new(
|
||||
&prototype.name,
|
||||
&prototype.source_command,
|
||||
PreviewCommand::new(
|
||||
&prototype.preview_command,
|
||||
&prototype.preview_delimiter,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
pub fn new(
|
||||
name: &str,
|
||||
entries_command: &str,
|
||||
preview_command: PreviewCommand,
|
||||
) -> Self {
|
||||
let matcher = Matcher::new(Config::default());
|
||||
let injector = matcher.injector();
|
||||
tokio::spawn(load_candidates(entries_command.to_string(), injector));
|
||||
Self {
|
||||
matcher,
|
||||
entries_command: entries_command.to_string(),
|
||||
preview_command,
|
||||
name: name.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_async)]
|
||||
async fn load_candidates(command: String, injector: Injector<String>) {
|
||||
let output = std::process::Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(command)
|
||||
.output()
|
||||
.expect("failed to execute process");
|
||||
|
||||
let branches = String::from_utf8(output.stdout).unwrap();
|
||||
|
||||
for line in branches.lines() {
|
||||
let () = injector.push(line.to_string(), |e, cols| {
|
||||
cols[0] = e.clone().into();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl OnAir for Channel {
|
||||
fn find(&mut self, pattern: &str) {
|
||||
self.matcher.find(pattern);
|
||||
}
|
||||
|
||||
fn results(&mut self, num_entries: u32, offset: u32) -> Vec<Entry> {
|
||||
self.matcher.tick();
|
||||
self.matcher
|
||||
.results(num_entries, offset)
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
let path = item.matched_string;
|
||||
Entry::new(
|
||||
path.clone(),
|
||||
PreviewType::Command(self.preview_command.clone()),
|
||||
)
|
||||
.with_name_match_ranges(item.match_indices)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_result(&self, index: u32) -> Option<Entry> {
|
||||
self.matcher.get_result(index).map(|item| {
|
||||
let path = item.matched_string;
|
||||
Entry::new(
|
||||
path.clone(),
|
||||
PreviewType::Command(self.preview_command.clone()),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn result_count(&self) -> u32 {
|
||||
self.matcher.matched_item_count
|
||||
}
|
||||
|
||||
fn total_count(&self) -> u32 {
|
||||
self.matcher.total_item_count
|
||||
}
|
||||
|
||||
fn running(&self) -> bool {
|
||||
self.matcher.status.running
|
||||
}
|
||||
|
||||
fn shutdown(&self) {}
|
||||
}
|
@ -1,12 +1,13 @@
|
||||
use devicons::FileIcon;
|
||||
use directories::BaseDirs;
|
||||
use ignore::overrides::OverrideBuilder;
|
||||
use lazy_static::lazy_static;
|
||||
use std::path::PathBuf;
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::channels::OnAir;
|
||||
use crate::entry::{Entry, PreviewType};
|
||||
use crate::entry::{Entry, PreviewCommand, PreviewType};
|
||||
use television_fuzzy::matcher::{config::Config, injector::Injector, Matcher};
|
||||
use television_utils::files::{walk_builder, DEFAULT_NUM_THREADS};
|
||||
|
||||
@ -38,6 +39,15 @@ impl Default for Channel {
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref PREVIEW_COMMAND: PreviewCommand = PreviewCommand {
|
||||
command: String::from(
|
||||
"cd {} && git log --pretty=medium --all --graph --color",
|
||||
),
|
||||
delimiter: ":".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
impl OnAir for Channel {
|
||||
fn find(&mut self, pattern: &str) {
|
||||
self.matcher.find(pattern);
|
||||
@ -50,9 +60,12 @@ impl OnAir for Channel {
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
let path = item.matched_string;
|
||||
Entry::new(path, PreviewType::Directory)
|
||||
.with_name_match_ranges(item.match_indices)
|
||||
.with_icon(self.icon)
|
||||
Entry::new(
|
||||
path.clone(),
|
||||
PreviewType::Command(PREVIEW_COMMAND.clone()),
|
||||
)
|
||||
.with_name_match_ranges(item.match_indices)
|
||||
.with_icon(self.icon)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@ -60,7 +73,11 @@ impl OnAir for Channel {
|
||||
fn get_result(&self, index: u32) -> Option<Entry> {
|
||||
self.matcher.get_result(index).map(|item| {
|
||||
let path = item.matched_string;
|
||||
Entry::new(path, PreviewType::Directory).with_icon(self.icon)
|
||||
Entry::new(
|
||||
path.clone(),
|
||||
PreviewType::Command(PREVIEW_COMMAND.clone()),
|
||||
)
|
||||
.with_icon(self.icon)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1,31 +1,91 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use crate::cable::{CableChannelPrototype, CableChannels};
|
||||
use crate::channels::{CliTvChannel, OnAir, TelevisionChannel, UnitChannel};
|
||||
use crate::entry::{Entry, PreviewType};
|
||||
use clap::ValueEnum;
|
||||
use color_eyre::Result;
|
||||
use devicons::FileIcon;
|
||||
use television_fuzzy::matcher::{config::Config, Matcher};
|
||||
|
||||
use super::cable;
|
||||
|
||||
pub struct RemoteControl {
|
||||
matcher: Matcher<String>,
|
||||
matcher: Matcher<RCButton>,
|
||||
cable_channels: Option<CableChannels>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum RCButton {
|
||||
Channel(UnitChannel),
|
||||
CableChannel(CableChannelPrototype),
|
||||
}
|
||||
|
||||
impl Display for RCButton {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
RCButton::Channel(channel) => write!(f, "{channel}"),
|
||||
RCButton::CableChannel(prototype) => write!(f, "{prototype}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const NUM_THREADS: usize = 1;
|
||||
|
||||
impl RemoteControl {
|
||||
pub fn new(channels: Vec<UnitChannel>) -> Self {
|
||||
pub fn new(
|
||||
builtin_channels: Vec<UnitChannel>,
|
||||
cable_channels: Option<CableChannels>,
|
||||
) -> Self {
|
||||
let matcher = Matcher::new(Config::default().n_threads(NUM_THREADS));
|
||||
let injector = matcher.injector();
|
||||
for channel in channels {
|
||||
let () = injector.push(channel.to_string(), |e, cols| {
|
||||
cols[0] = e.clone().into();
|
||||
let buttons =
|
||||
builtin_channels.into_iter().map(RCButton::Channel).chain(
|
||||
cable_channels
|
||||
.as_ref()
|
||||
.map(|channels| {
|
||||
channels.iter().map(|(_, prototype)| {
|
||||
RCButton::CableChannel(prototype.clone())
|
||||
})
|
||||
})
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
);
|
||||
for button in buttons {
|
||||
let () = injector.push(button.clone(), |e, cols| {
|
||||
cols[0] = e.to_string().clone().into();
|
||||
});
|
||||
}
|
||||
RemoteControl { matcher }
|
||||
RemoteControl {
|
||||
matcher,
|
||||
cable_channels,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_transitions_from(
|
||||
television_channel: &TelevisionChannel,
|
||||
) -> Self {
|
||||
Self::new(television_channel.available_transitions())
|
||||
Self::new(television_channel.available_transitions(), None)
|
||||
}
|
||||
|
||||
pub fn zap(&self, channel_name: &str) -> Result<TelevisionChannel> {
|
||||
if let Ok(channel) = UnitChannel::try_from(channel_name) {
|
||||
Ok(channel.into())
|
||||
} else {
|
||||
let maybe_prototype = self
|
||||
.cable_channels
|
||||
.as_ref()
|
||||
.and_then(|channels| channels.get(channel_name));
|
||||
match maybe_prototype {
|
||||
Some(prototype) => Ok(TelevisionChannel::Cable(
|
||||
cable::Channel::from(prototype.clone()),
|
||||
)),
|
||||
None => Err(color_eyre::eyre::eyre!(
|
||||
"No channel or cable channel prototype found for {}",
|
||||
channel_name
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,12 +94,20 @@ impl Default for RemoteControl {
|
||||
Self::new(
|
||||
CliTvChannel::value_variants()
|
||||
.iter()
|
||||
.map(|v| v.to_string().as_str().into())
|
||||
.flat_map(|v| UnitChannel::try_from(v.to_string().as_str()))
|
||||
.collect(),
|
||||
None,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_builtin_channels() -> Vec<UnitChannel> {
|
||||
CliTvChannel::value_variants()
|
||||
.iter()
|
||||
.flat_map(|v| UnitChannel::try_from(v.to_string().as_str()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
const TV_ICON: FileIcon = FileIcon {
|
||||
icon: '📺',
|
||||
color: "#000000",
|
||||
|
@ -1,10 +1,8 @@
|
||||
use std::{
|
||||
io::{stdin, BufRead},
|
||||
path::Path,
|
||||
thread::spawn,
|
||||
};
|
||||
|
||||
use devicons::FileIcon;
|
||||
use tracing::debug;
|
||||
|
||||
use super::OnAir;
|
||||
@ -13,32 +11,32 @@ use television_fuzzy::matcher::{config::Config, injector::Injector, Matcher};
|
||||
|
||||
pub struct Channel {
|
||||
matcher: Matcher<String>,
|
||||
icon: FileIcon,
|
||||
preview_type: PreviewType,
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(preview_type: Option<PreviewType>) -> Self {
|
||||
let matcher = Matcher::new(Config::default());
|
||||
let injector = matcher.injector();
|
||||
|
||||
spawn(move || stream_from_stdin(injector.clone()));
|
||||
spawn(move || stream_from_stdin(&injector));
|
||||
|
||||
Self {
|
||||
matcher,
|
||||
icon: FileIcon::from("nu"),
|
||||
preview_type: preview_type.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Channel {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
Self::new(None)
|
||||
}
|
||||
}
|
||||
|
||||
const TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
|
||||
fn stream_from_stdin(injector: Injector<String>) {
|
||||
fn stream_from_stdin(injector: &Injector<String>) {
|
||||
let mut stdin = stdin().lock();
|
||||
let mut buffer = String::new();
|
||||
|
||||
@ -78,34 +76,17 @@ impl OnAir for Channel {
|
||||
.results(num_entries, offset)
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
let path = Path::new(&item.matched_string);
|
||||
let icon = if path.try_exists().unwrap_or(false) {
|
||||
FileIcon::from(path)
|
||||
} else {
|
||||
self.icon
|
||||
};
|
||||
// NOTE: we're passing `PreviewType::Basic` here just as a placeholder
|
||||
// to avoid storing the preview command multiple times for each item.
|
||||
Entry::new(item.matched_string, PreviewType::Basic)
|
||||
.with_name_match_ranges(item.match_indices)
|
||||
.with_icon(icon)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_result(&self, index: u32) -> Option<Entry> {
|
||||
self.matcher.get_result(index).map(|item| {
|
||||
let path = Path::new(&item.matched_string);
|
||||
// if we recognize a file path, use a file icon
|
||||
// and set the preview type to "Files"
|
||||
if path.is_file() {
|
||||
Entry::new(item.matched_string.clone(), PreviewType::Files)
|
||||
.with_icon(FileIcon::from(path))
|
||||
} else if path.is_dir() {
|
||||
Entry::new(item.matched_string.clone(), PreviewType::Directory)
|
||||
.with_icon(FileIcon::from(path))
|
||||
} else {
|
||||
Entry::new(item.matched_string.clone(), PreviewType::Basic)
|
||||
.with_icon(self.icon)
|
||||
}
|
||||
Entry::new(item.matched_string.clone(), self.preview_type.clone())
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::path::PathBuf;
|
||||
use std::{fmt::Display, path::PathBuf};
|
||||
|
||||
use devicons::FileIcon;
|
||||
|
||||
@ -117,11 +117,32 @@ pub const ENTRY_PLACEHOLDER: Entry = Entry {
|
||||
preview_type: PreviewType::EnvVar,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct PreviewCommand {
|
||||
pub command: String,
|
||||
pub delimiter: String,
|
||||
}
|
||||
|
||||
impl PreviewCommand {
|
||||
pub fn new(command: &str, delimiter: &str) -> Self {
|
||||
Self {
|
||||
command: command.to_string(),
|
||||
delimiter: delimiter.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for PreviewCommand {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
|
||||
pub enum PreviewType {
|
||||
#[default]
|
||||
Basic,
|
||||
Directory,
|
||||
EnvVar,
|
||||
Files,
|
||||
Command(PreviewCommand),
|
||||
}
|
||||
|
@ -1,2 +1,3 @@
|
||||
pub mod cable;
|
||||
pub mod channels;
|
||||
pub mod entry;
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "television-derive"
|
||||
version = "0.0.7"
|
||||
version = "0.0.8"
|
||||
description.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
|
@ -332,20 +332,36 @@ fn impl_unit_channel(ast: &syn::DeriveInput) -> TokenStream {
|
||||
}
|
||||
};
|
||||
|
||||
// Generate From<&str> implementation
|
||||
let from_str_impl = quote! {
|
||||
impl From<&str> for UnitChannel {
|
||||
fn from(channel: &str) -> Self {
|
||||
// Generate TryFrom<&str> implementation
|
||||
let try_from_str_impl = quote! {
|
||||
impl std::convert::TryFrom<&str> for UnitChannel {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(channel: &str) -> Result<Self, Self::Error> {
|
||||
match channel {
|
||||
#(
|
||||
stringify!(#variant_names) => Self::#variant_names,
|
||||
stringify!(#variant_names) => Ok(Self::#variant_names),
|
||||
)*
|
||||
_ => panic!("Invalid unit channel name."),
|
||||
_ => Err(format!("Invalid unit channel name: {}", channel)),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Generate From<&str> implementation
|
||||
//let from_str_impl = quote! {
|
||||
// impl From<&str> for UnitChannel {
|
||||
// fn from(channel: &str) -> Self {
|
||||
// match channel {
|
||||
// #(
|
||||
// stringify!(#variant_names) => Self::#variant_names,
|
||||
// )*
|
||||
// _ => panic!("Invalid unit channel name."),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//};
|
||||
|
||||
// Generate Into<&str> implementation
|
||||
let into_str_impl = quote! {
|
||||
impl Into<&str> for UnitChannel {
|
||||
@ -363,7 +379,8 @@ fn impl_unit_channel(ast: &syn::DeriveInput) -> TokenStream {
|
||||
#unit_enum
|
||||
#into_impl
|
||||
#from_impl
|
||||
#from_str_impl
|
||||
#try_from_str_impl
|
||||
//#from_str_impl
|
||||
#into_str_impl
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "television-fuzzy"
|
||||
version = "0.0.7"
|
||||
version = "0.0.8"
|
||||
description.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "television-previewers"
|
||||
version = "0.0.7"
|
||||
version = "0.0.8"
|
||||
description.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
@ -14,12 +14,13 @@ rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
syntect = "5.2.0"
|
||||
television-channels = { path = "../television-channels", version = "0.0.7" }
|
||||
television-utils = { path = "../television-utils", version = "0.0.7" }
|
||||
television-channels = { path = "../television-channels", version = "0.0.8" }
|
||||
television-utils = { path = "../television-utils", version = "0.0.8" }
|
||||
tracing = "0.1.40"
|
||||
parking_lot = "0.12.3"
|
||||
tokio = "1.41.1"
|
||||
termtree = "0.5.1"
|
||||
devicons = "0.6.11"
|
||||
color-eyre = "0.6.3"
|
||||
regex = "1.11.1"
|
||||
lazy_static = "1.5.0"
|
||||
|
||||
|
@ -5,7 +5,7 @@ use television_channels::entry::{Entry, PreviewType};
|
||||
|
||||
pub mod basic;
|
||||
pub mod cache;
|
||||
pub mod directory;
|
||||
pub mod command;
|
||||
pub mod env;
|
||||
pub mod files;
|
||||
pub mod meta;
|
||||
@ -13,13 +13,12 @@ pub mod meta;
|
||||
// previewer types
|
||||
pub use basic::BasicPreviewer;
|
||||
pub use basic::BasicPreviewerConfig;
|
||||
pub use directory::DirectoryPreviewer;
|
||||
pub use directory::DirectoryPreviewerConfig;
|
||||
pub use command::CommandPreviewer;
|
||||
pub use command::CommandPreviewerConfig;
|
||||
pub use env::EnvVarPreviewer;
|
||||
pub use env::EnvVarPreviewerConfig;
|
||||
pub use files::FilePreviewer;
|
||||
pub use files::FilePreviewerConfig;
|
||||
//use ratatui_image::protocol::StatefulProtocol;
|
||||
use syntect::highlighting::Style;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@ -27,11 +26,11 @@ pub enum PreviewContent {
|
||||
Empty,
|
||||
FileTooLarge,
|
||||
SyntectHighlightedText(Vec<Vec<(Style, String)>>),
|
||||
//Image(Box<dyn StatefulProtocol>),
|
||||
Loading,
|
||||
NotSupported,
|
||||
PlainText(Vec<String>),
|
||||
PlainTextWrapped(String),
|
||||
AnsiText(String),
|
||||
}
|
||||
|
||||
pub const PREVIEW_NOT_SUPPORTED_MSG: &str =
|
||||
@ -48,6 +47,7 @@ pub struct Preview {
|
||||
pub title: String,
|
||||
pub content: PreviewContent,
|
||||
pub icon: Option<FileIcon>,
|
||||
pub stale: bool,
|
||||
}
|
||||
|
||||
impl Default for Preview {
|
||||
@ -56,6 +56,7 @@ impl Default for Preview {
|
||||
title: String::new(),
|
||||
content: PreviewContent::Empty,
|
||||
icon: None,
|
||||
stale: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -65,11 +66,20 @@ impl Preview {
|
||||
title: String,
|
||||
content: PreviewContent,
|
||||
icon: Option<FileIcon>,
|
||||
stale: bool,
|
||||
) -> Self {
|
||||
Preview {
|
||||
title,
|
||||
content,
|
||||
icon,
|
||||
stale,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stale(&self) -> Self {
|
||||
Preview {
|
||||
stale: true,
|
||||
..self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,6 +91,9 @@ impl Preview {
|
||||
PreviewContent::PlainText(lines) => {
|
||||
lines.len().try_into().unwrap_or(u16::MAX)
|
||||
}
|
||||
PreviewContent::AnsiText(text) => {
|
||||
text.lines().count().try_into().unwrap_or(u16::MAX)
|
||||
}
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
@ -89,17 +102,17 @@ impl Preview {
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Previewer {
|
||||
basic: BasicPreviewer,
|
||||
directory: DirectoryPreviewer,
|
||||
file: FilePreviewer,
|
||||
env_var: EnvVarPreviewer,
|
||||
command: CommandPreviewer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct PreviewerConfig {
|
||||
basic: BasicPreviewerConfig,
|
||||
directory: DirectoryPreviewerConfig,
|
||||
file: FilePreviewerConfig,
|
||||
env_var: EnvVarPreviewerConfig,
|
||||
command: CommandPreviewerConfig,
|
||||
}
|
||||
|
||||
impl PreviewerConfig {
|
||||
@ -108,11 +121,6 @@ impl PreviewerConfig {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn directory(mut self, config: DirectoryPreviewerConfig) -> Self {
|
||||
self.directory = config;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn file(mut self, config: FilePreviewerConfig) -> Self {
|
||||
self.file = config;
|
||||
self
|
||||
@ -129,24 +137,23 @@ impl Previewer {
|
||||
let config = config.unwrap_or_default();
|
||||
Previewer {
|
||||
basic: BasicPreviewer::new(Some(config.basic)),
|
||||
directory: DirectoryPreviewer::new(Some(config.directory)),
|
||||
file: FilePreviewer::new(Some(config.file)),
|
||||
env_var: EnvVarPreviewer::new(Some(config.env_var)),
|
||||
command: CommandPreviewer::new(Some(config.command)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn preview(&mut self, entry: &Entry) -> Arc<Preview> {
|
||||
match entry.preview_type {
|
||||
match &entry.preview_type {
|
||||
PreviewType::Basic => self.basic.preview(entry),
|
||||
PreviewType::Directory => self.directory.preview(entry),
|
||||
PreviewType::EnvVar => self.env_var.preview(entry),
|
||||
PreviewType::Files => self.file.preview(entry),
|
||||
PreviewType::Command(cmd) => self.command.preview(entry, cmd),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_config(&mut self, config: PreviewerConfig) {
|
||||
self.basic = BasicPreviewer::new(Some(config.basic));
|
||||
self.directory = DirectoryPreviewer::new(Some(config.directory));
|
||||
self.file = FilePreviewer::new(Some(config.file));
|
||||
self.env_var = EnvVarPreviewer::new(Some(config.env_var));
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ impl BasicPreviewer {
|
||||
title: entry.name.clone(),
|
||||
content: PreviewContent::PlainTextWrapped(entry.name.clone()),
|
||||
icon: entry.icon,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,111 +1,14 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet, VecDeque},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use tracing::debug;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use crate::previewers::Preview;
|
||||
|
||||
/// A ring buffer that also keeps track of the keys it contains to avoid duplicates.
|
||||
///
|
||||
/// This serves as a backend for the preview cache.
|
||||
/// Basic idea:
|
||||
/// - When a new key is pushed, if it's already in the buffer, do nothing.
|
||||
/// - If the buffer is full, remove the oldest key and push the new key.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use television_previewers::previewers::cache::RingSet;
|
||||
///
|
||||
/// let mut ring_set = RingSet::with_capacity(3);
|
||||
/// // push 3 values into the ringset
|
||||
/// assert_eq!(ring_set.push(1), None);
|
||||
/// assert_eq!(ring_set.push(2), None);
|
||||
/// assert_eq!(ring_set.push(3), None);
|
||||
///
|
||||
/// // check that the values are in the buffer
|
||||
/// assert!(ring_set.contains(&1));
|
||||
/// assert!(ring_set.contains(&2));
|
||||
/// assert!(ring_set.contains(&3));
|
||||
///
|
||||
/// // push an existing value (should do nothing)
|
||||
/// assert_eq!(ring_set.push(1), None);
|
||||
///
|
||||
/// // entries should still be there
|
||||
/// assert!(ring_set.contains(&1));
|
||||
/// assert!(ring_set.contains(&2));
|
||||
/// assert!(ring_set.contains(&3));
|
||||
///
|
||||
/// // push a new value, should remove the oldest value (1)
|
||||
/// assert_eq!(ring_set.push(4), Some(1));
|
||||
///
|
||||
/// // 1 is no longer there but 2 and 3 remain
|
||||
/// assert!(!ring_set.contains(&1));
|
||||
/// assert!(ring_set.contains(&2));
|
||||
/// assert!(ring_set.contains(&3));
|
||||
/// assert!(ring_set.contains(&4));
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub struct RingSet<T> {
|
||||
ring_buffer: VecDeque<T>,
|
||||
known_keys: HashSet<T>,
|
||||
capacity: usize,
|
||||
}
|
||||
|
||||
impl<T> RingSet<T>
|
||||
where
|
||||
T: Eq + std::hash::Hash + Clone + std::fmt::Debug,
|
||||
{
|
||||
/// Create a new `RingSet` with the given capacity.
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
RingSet {
|
||||
ring_buffer: VecDeque::with_capacity(capacity),
|
||||
known_keys: HashSet::with_capacity(capacity),
|
||||
capacity,
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a new item to the back of the buffer, removing the oldest item if the buffer is full.
|
||||
/// Returns the item that was removed, if any.
|
||||
/// If the item is already in the buffer, do nothing and return None.
|
||||
pub fn push(&mut self, item: T) -> Option<T> {
|
||||
// If the key is already in the buffer, do nothing
|
||||
if self.contains(&item) {
|
||||
debug!("Key already in ring buffer: {:?}", item);
|
||||
return None;
|
||||
}
|
||||
let mut popped_key = None;
|
||||
// If the buffer is full, remove the oldest key (e.g. pop from the front of the buffer)
|
||||
if self.ring_buffer.len() >= self.capacity {
|
||||
popped_key = self.pop();
|
||||
}
|
||||
// finally, push the new key to the back of the buffer
|
||||
self.ring_buffer.push_back(item.clone());
|
||||
self.known_keys.insert(item);
|
||||
popped_key
|
||||
}
|
||||
|
||||
fn pop(&mut self) -> Option<T> {
|
||||
if let Some(item) = self.ring_buffer.pop_front() {
|
||||
debug!("Removing key from ring buffer: {:?}", item);
|
||||
self.known_keys.remove(&item);
|
||||
Some(item)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains(&self, key: &T) -> bool {
|
||||
self.known_keys.contains(key)
|
||||
}
|
||||
}
|
||||
use television_utils::cache::RingSet;
|
||||
use tracing::debug;
|
||||
|
||||
/// Default size of the preview cache: 100 entries.
|
||||
///
|
||||
/// This does seem kind of arbitrary for now, will need to play around with it.
|
||||
/// At the moment, files over 4 MB are not previewed, so the cache size
|
||||
/// shouldn't exceed 400 MB.
|
||||
/// should never exceed 400 MB.
|
||||
const DEFAULT_PREVIEW_CACHE_SIZE: usize = 100;
|
||||
|
||||
/// A cache for previews.
|
||||
@ -132,9 +35,9 @@ impl PreviewCache {
|
||||
/// Insert a new preview into the cache.
|
||||
/// If the cache is full, the oldest entry will be removed.
|
||||
/// If the key is already in the cache, the preview will be updated.
|
||||
pub fn insert(&mut self, key: String, preview: Arc<Preview>) {
|
||||
pub fn insert(&mut self, key: String, preview: &Arc<Preview>) {
|
||||
debug!("Inserting preview into cache: {}", key);
|
||||
self.entries.insert(key.clone(), preview);
|
||||
self.entries.insert(key.clone(), Arc::clone(preview));
|
||||
if let Some(oldest_key) = self.ring_set.push(key) {
|
||||
debug!("Cache full, removing oldest entry: {}", oldest_key);
|
||||
self.entries.remove(&oldest_key);
|
||||
@ -151,7 +54,7 @@ impl PreviewCache {
|
||||
preview
|
||||
} else {
|
||||
let preview = Arc::new(f());
|
||||
self.insert(key, preview.clone());
|
||||
self.insert(key, &preview);
|
||||
preview
|
||||
}
|
||||
}
|
||||
@ -162,50 +65,3 @@ impl Default for PreviewCache {
|
||||
PreviewCache::new(DEFAULT_PREVIEW_CACHE_SIZE)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_ring_set() {
|
||||
let mut ring_set = RingSet::with_capacity(3);
|
||||
// push 3 values into the ringset
|
||||
assert_eq!(ring_set.push(1), None);
|
||||
assert_eq!(ring_set.push(2), None);
|
||||
assert_eq!(ring_set.push(3), None);
|
||||
|
||||
// check that the values are in the buffer
|
||||
assert!(ring_set.contains(&1));
|
||||
assert!(ring_set.contains(&2));
|
||||
assert!(ring_set.contains(&3));
|
||||
|
||||
// push an existing value (should do nothing)
|
||||
assert_eq!(ring_set.push(1), None);
|
||||
|
||||
// entries should still be there
|
||||
assert!(ring_set.contains(&1));
|
||||
assert!(ring_set.contains(&2));
|
||||
assert!(ring_set.contains(&3));
|
||||
|
||||
// push a new value, should remove the oldest value (1)
|
||||
assert_eq!(ring_set.push(4), Some(1));
|
||||
|
||||
// 1 is no longer there but 2 and 3 remain
|
||||
assert!(!ring_set.contains(&1));
|
||||
assert!(ring_set.contains(&2));
|
||||
assert!(ring_set.contains(&3));
|
||||
assert!(ring_set.contains(&4));
|
||||
|
||||
// push two new values, should remove 2 and 3
|
||||
assert_eq!(ring_set.push(5), Some(2));
|
||||
assert_eq!(ring_set.push(6), Some(3));
|
||||
|
||||
// 2 and 3 are no longer there but 4, 5 and 6 remain
|
||||
assert!(!ring_set.contains(&2));
|
||||
assert!(!ring_set.contains(&3));
|
||||
assert!(ring_set.contains(&4));
|
||||
assert!(ring_set.contains(&5));
|
||||
assert!(ring_set.contains(&6));
|
||||
}
|
||||
}
|
||||
|
252
crates/television-previewers/src/previewers/command.rs
Normal file
252
crates/television-previewers/src/previewers/command.rs
Normal file
@ -0,0 +1,252 @@
|
||||
/// git log --oneline --date=short --pretty="format:%C(auto)%h %s %Cblue%an %C(green)%cd" "$@" | ~/code/rust/television/target/release/tv --preview 'git show -p --stat --pretty=fuller --color=always {0}' --delimiter ' '
|
||||
use crate::previewers::cache::PreviewCache;
|
||||
use crate::previewers::{Preview, PreviewContent};
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use regex::Regex;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
use std::sync::Arc;
|
||||
use television_channels::entry::{Entry, PreviewCommand};
|
||||
use tracing::debug;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct CommandPreviewer {
|
||||
cache: Arc<Mutex<PreviewCache>>,
|
||||
config: CommandPreviewerConfig,
|
||||
concurrent_preview_tasks: Arc<AtomicU8>,
|
||||
last_previewed: Arc<Mutex<Arc<Preview>>>,
|
||||
in_flight_previews: Arc<Mutex<HashSet<String>>>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CommandPreviewerConfig {
|
||||
delimiter: String,
|
||||
}
|
||||
|
||||
const DEFAULT_DELIMITER: &str = ":";
|
||||
|
||||
impl Default for CommandPreviewerConfig {
|
||||
fn default() -> Self {
|
||||
CommandPreviewerConfig {
|
||||
delimiter: String::from(DEFAULT_DELIMITER),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandPreviewerConfig {
|
||||
pub fn new(delimiter: &str) -> Self {
|
||||
CommandPreviewerConfig {
|
||||
delimiter: String::from(delimiter),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_CONCURRENT_PREVIEW_TASKS: u8 = 3;
|
||||
|
||||
impl CommandPreviewer {
|
||||
pub fn new(config: Option<CommandPreviewerConfig>) -> Self {
|
||||
let config = config.unwrap_or_default();
|
||||
CommandPreviewer {
|
||||
cache: Arc::new(Mutex::new(PreviewCache::default())),
|
||||
config,
|
||||
concurrent_preview_tasks: Arc::new(AtomicU8::new(0)),
|
||||
last_previewed: Arc::new(Mutex::new(Arc::new(
|
||||
Preview::default().stale(),
|
||||
))),
|
||||
in_flight_previews: Arc::new(Mutex::new(HashSet::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn preview(
|
||||
&mut self,
|
||||
entry: &Entry,
|
||||
command: &PreviewCommand,
|
||||
) -> Arc<Preview> {
|
||||
// do we have a preview in cache for that entry?
|
||||
if let Some(preview) = self.cache.lock().get(&entry.name) {
|
||||
return preview.clone();
|
||||
}
|
||||
debug!("Preview cache miss for {:?}", entry.name);
|
||||
|
||||
// are we already computing a preview in the background for that entry?
|
||||
if self.in_flight_previews.lock().contains(&entry.name) {
|
||||
debug!("Preview already in flight for {:?}", entry.name);
|
||||
return self.last_previewed.lock().clone();
|
||||
}
|
||||
|
||||
if self.concurrent_preview_tasks.load(Ordering::Relaxed)
|
||||
< MAX_CONCURRENT_PREVIEW_TASKS
|
||||
{
|
||||
self.concurrent_preview_tasks
|
||||
.fetch_add(1, Ordering::Relaxed);
|
||||
let cache = self.cache.clone();
|
||||
let entry_c = entry.clone();
|
||||
let concurrent_tasks = self.concurrent_preview_tasks.clone();
|
||||
let command = command.clone();
|
||||
let last_previewed = self.last_previewed.clone();
|
||||
tokio::spawn(async move {
|
||||
try_preview(
|
||||
&command,
|
||||
&entry_c,
|
||||
&cache,
|
||||
&concurrent_tasks,
|
||||
&last_previewed,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
self.last_previewed.lock().clone()
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref COMMAND_PLACEHOLDER_REGEX: Regex =
|
||||
Regex::new(r"\{(\d+)\}").unwrap();
|
||||
}
|
||||
|
||||
/// Format the command with the entry name and provided placeholders
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use television_channels::entry::{PreviewCommand, PreviewType, Entry};
|
||||
/// use television_previewers::previewers::command::format_command;
|
||||
///
|
||||
/// let command = PreviewCommand {
|
||||
/// command: "something {} {2} {0}".to_string(),
|
||||
/// delimiter: ":".to_string(),
|
||||
/// };
|
||||
/// let entry = Entry::new("a:given:entry:to:preview".to_string(), PreviewType::Command(command.clone()));
|
||||
/// let formatted_command = format_command(&command, &entry);
|
||||
///
|
||||
/// assert_eq!(formatted_command, "something a:given:entry:to:preview entry a");
|
||||
/// ```
|
||||
pub fn format_command(command: &PreviewCommand, entry: &Entry) -> String {
|
||||
let parts = entry.name.split(&command.delimiter).collect::<Vec<&str>>();
|
||||
debug!("Parts: {:?}", parts);
|
||||
|
||||
let mut formatted_command = command.command.replace("{}", &entry.name);
|
||||
|
||||
formatted_command = COMMAND_PLACEHOLDER_REGEX
|
||||
.replace_all(&formatted_command, |caps: ®ex::Captures| {
|
||||
let index =
|
||||
caps.get(1).unwrap().as_str().parse::<usize>().unwrap();
|
||||
parts[index].to_string()
|
||||
})
|
||||
.to_string();
|
||||
|
||||
formatted_command
|
||||
}
|
||||
|
||||
pub fn try_preview(
|
||||
command: &PreviewCommand,
|
||||
entry: &Entry,
|
||||
cache: &Arc<Mutex<PreviewCache>>,
|
||||
concurrent_tasks: &Arc<AtomicU8>,
|
||||
last_previewed: &Arc<Mutex<Arc<Preview>>>,
|
||||
) {
|
||||
debug!("Computing preview for {:?}", entry.name);
|
||||
let command = format_command(command, entry);
|
||||
debug!("Formatted preview command: {:?}", command);
|
||||
|
||||
let output = std::process::Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(&command)
|
||||
.output()
|
||||
.expect("failed to execute process");
|
||||
|
||||
if output.status.success() {
|
||||
let content = String::from_utf8(output.stdout)
|
||||
.unwrap_or(String::from("Failed to read output\n"));
|
||||
let preview = Arc::new(Preview::new(
|
||||
entry.name.clone(),
|
||||
PreviewContent::AnsiText(content),
|
||||
None,
|
||||
false,
|
||||
));
|
||||
|
||||
cache.lock().insert(entry.name.clone(), &preview);
|
||||
let mut tp = last_previewed.lock();
|
||||
*tp = preview.stale().into();
|
||||
} else {
|
||||
let content = String::from_utf8(output.stderr)
|
||||
.unwrap_or(String::from("Failed to read error\n"));
|
||||
let preview = Arc::new(Preview::new(
|
||||
entry.name.clone(),
|
||||
PreviewContent::AnsiText(content),
|
||||
None,
|
||||
false,
|
||||
));
|
||||
cache.lock().insert(entry.name.clone(), &preview);
|
||||
}
|
||||
|
||||
concurrent_tasks.fetch_sub(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use television_channels::entry::{Entry, PreviewType};
|
||||
|
||||
#[test]
|
||||
fn test_format_command() {
|
||||
let command = PreviewCommand {
|
||||
command: "something {} {2} {0}".to_string(),
|
||||
delimiter: ":".to_string(),
|
||||
};
|
||||
let entry = Entry::new(
|
||||
"an:entry:to:preview".to_string(),
|
||||
PreviewType::Command(command.clone()),
|
||||
);
|
||||
let formatted_command = format_command(&command, &entry);
|
||||
|
||||
assert_eq!(formatted_command, "something an:entry:to:preview to an");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_command_no_placeholders() {
|
||||
let command = PreviewCommand {
|
||||
command: "something".to_string(),
|
||||
delimiter: ":".to_string(),
|
||||
};
|
||||
let entry = Entry::new(
|
||||
"an:entry:to:preview".to_string(),
|
||||
PreviewType::Command(command.clone()),
|
||||
);
|
||||
let formatted_command = format_command(&command, &entry);
|
||||
|
||||
assert_eq!(formatted_command, "something");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_command_with_global_placeholder_only() {
|
||||
let command = PreviewCommand {
|
||||
command: "something {}".to_string(),
|
||||
delimiter: ":".to_string(),
|
||||
};
|
||||
let entry = Entry::new(
|
||||
"an:entry:to:preview".to_string(),
|
||||
PreviewType::Command(command.clone()),
|
||||
);
|
||||
let formatted_command = format_command(&command, &entry);
|
||||
|
||||
assert_eq!(formatted_command, "something an:entry:to:preview");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_command_with_positional_placeholders_only() {
|
||||
let command = PreviewCommand {
|
||||
command: "something {0} -t {2}".to_string(),
|
||||
delimiter: ":".to_string(),
|
||||
};
|
||||
let entry = Entry::new(
|
||||
"an:entry:to:preview".to_string(),
|
||||
PreviewType::Command(command.clone()),
|
||||
);
|
||||
let formatted_command = format_command(&command, &entry);
|
||||
|
||||
assert_eq!(formatted_command, "something an -t to");
|
||||
}
|
||||
}
|
@ -1,117 +0,0 @@
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use devicons::FileIcon;
|
||||
use parking_lot::Mutex;
|
||||
use termtree::Tree;
|
||||
|
||||
use television_channels::entry::Entry;
|
||||
|
||||
use crate::previewers::cache::PreviewCache;
|
||||
use crate::previewers::{meta, Preview, PreviewContent};
|
||||
use television_utils::files::walk_builder;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DirectoryPreviewer {
|
||||
cache: Arc<Mutex<PreviewCache>>,
|
||||
_config: DirectoryPreviewerConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DirectoryPreviewerConfig {}
|
||||
|
||||
impl DirectoryPreviewer {
|
||||
pub fn new(config: Option<DirectoryPreviewerConfig>) -> Self {
|
||||
DirectoryPreviewer {
|
||||
cache: Arc::new(Mutex::new(PreviewCache::default())),
|
||||
_config: config.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn preview(&mut self, entry: &Entry) -> Arc<Preview> {
|
||||
if let Some(preview) = self.cache.lock().get(&entry.name) {
|
||||
return preview;
|
||||
}
|
||||
let preview = meta::loading(&entry.name);
|
||||
self.cache
|
||||
.lock()
|
||||
.insert(entry.name.clone(), preview.clone());
|
||||
let entry_c = entry.clone();
|
||||
let cache = self.cache.clone();
|
||||
tokio::spawn(async move {
|
||||
let preview = Arc::new(build_tree_preview(&entry_c));
|
||||
cache.lock().insert(entry_c.name.clone(), preview);
|
||||
});
|
||||
preview
|
||||
}
|
||||
}
|
||||
|
||||
fn build_tree_preview(entry: &Entry) -> Preview {
|
||||
let path = Path::new(&entry.name);
|
||||
let tree = tree(path, MAX_DEPTH, FIRST_LEVEL_MAX_ENTRIES, &mut 0);
|
||||
let tree_string = tree.to_string();
|
||||
Preview {
|
||||
title: entry.name.clone(),
|
||||
content: PreviewContent::PlainText(
|
||||
tree_string
|
||||
.lines()
|
||||
.map(std::borrow::ToOwned::to_owned)
|
||||
.collect(),
|
||||
),
|
||||
icon: entry.icon,
|
||||
}
|
||||
}
|
||||
|
||||
fn label<P: AsRef<Path>>(p: P, strip: &str) -> String {
|
||||
let icon = FileIcon::from(&p);
|
||||
let path = p.as_ref().strip_prefix(strip).unwrap_or(p.as_ref());
|
||||
format!("{} {}", icon, path.display())
|
||||
}
|
||||
|
||||
const MAX_DEPTH: u8 = 4;
|
||||
const FIRST_LEVEL_MAX_ENTRIES: u8 = 30;
|
||||
const NESTED_MAX_ENTRIES: u8 = 10;
|
||||
const MAX_ENTRIES: u8 = 200;
|
||||
|
||||
fn tree<P: AsRef<Path>>(
|
||||
p: P,
|
||||
max_depth: u8,
|
||||
nested_max_entries: u8,
|
||||
total_entry_count: &mut u8,
|
||||
) -> Tree<String> {
|
||||
let mut root = Tree::new(label(
|
||||
p.as_ref(),
|
||||
p.as_ref().parent().unwrap().to_str().unwrap(),
|
||||
));
|
||||
let w = walk_builder(p.as_ref(), 1, None, None)
|
||||
.max_depth(Some(1))
|
||||
.build();
|
||||
let mut level_entry_count: u8 = 0;
|
||||
|
||||
for path in w.skip(1).filter_map(Result::ok) {
|
||||
let m = path.metadata().unwrap();
|
||||
if m.is_dir() && max_depth > 1 {
|
||||
root.push(tree(
|
||||
path.path(),
|
||||
max_depth - 1,
|
||||
NESTED_MAX_ENTRIES,
|
||||
total_entry_count,
|
||||
));
|
||||
} else {
|
||||
root.push(Tree::new(label(
|
||||
path.path(),
|
||||
p.as_ref().to_str().unwrap(),
|
||||
)));
|
||||
}
|
||||
level_entry_count += 1;
|
||||
*total_entry_count += 1;
|
||||
if level_entry_count >= nested_max_entries
|
||||
|| *total_entry_count >= MAX_ENTRIES
|
||||
{
|
||||
root.push(Tree::new(String::from("...")));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
root
|
||||
}
|
@ -36,6 +36,7 @@ impl EnvVarPreviewer {
|
||||
PreviewContent::Empty
|
||||
},
|
||||
icon: entry.icon,
|
||||
..Default::default()
|
||||
});
|
||||
self.cache.insert(entry.clone(), preview.clone());
|
||||
preview
|
||||
|
@ -1,12 +1,13 @@
|
||||
use color_eyre::Result;
|
||||
//use image::{ImageReader, Rgb};
|
||||
//use ratatui_image::picker::Picker;
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::HashSet;
|
||||
use std::fs::File;
|
||||
use std::io::{BufRead, BufReader, Seek};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::sync::{
|
||||
atomic::{AtomicU8, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
use syntect::{
|
||||
highlighting::{Theme, ThemeSet},
|
||||
@ -17,11 +18,10 @@ use tracing::{debug, warn};
|
||||
use super::cache::PreviewCache;
|
||||
use crate::previewers::{meta, Preview, PreviewContent};
|
||||
use television_channels::entry;
|
||||
use television_utils::files::get_file_size;
|
||||
use television_utils::files::FileType;
|
||||
use television_utils::strings::preprocess_line;
|
||||
use television_utils::syntax::{
|
||||
self, load_highlighting_assets, HighlightingAssetsExt,
|
||||
use television_utils::{
|
||||
files::{get_file_size, FileType},
|
||||
strings::preprocess_line,
|
||||
syntax::{self, load_highlighting_assets, HighlightingAssetsExt},
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@ -31,7 +31,7 @@ pub struct FilePreviewer {
|
||||
pub syntax_theme: Arc<Theme>,
|
||||
concurrent_preview_tasks: Arc<AtomicU8>,
|
||||
last_previewed: Arc<Mutex<Arc<Preview>>>,
|
||||
//image_picker: Arc<Mutex<Picker>>,
|
||||
in_flight_previews: Arc<Mutex<HashSet<String>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
@ -49,7 +49,7 @@ impl FilePreviewerConfig {
|
||||
/// 4 MB
|
||||
const MAX_FILE_SIZE: u64 = 4 * 1024 * 1024;
|
||||
|
||||
const MAX_CONCURRENT_PREVIEW_TASKS: u8 = 2;
|
||||
const MAX_CONCURRENT_PREVIEW_TASKS: u8 = 3;
|
||||
|
||||
impl FilePreviewer {
|
||||
pub fn new(config: Option<FilePreviewerConfig>) -> Self {
|
||||
@ -63,17 +63,16 @@ impl FilePreviewer {
|
||||
},
|
||||
|c| hl_assets.get_theme_no_output(&c.theme).clone(),
|
||||
);
|
||||
//info!("getting image picker");
|
||||
//let image_picker = get_image_picker();
|
||||
//info!("got image picker");
|
||||
|
||||
FilePreviewer {
|
||||
cache: Arc::new(Mutex::new(PreviewCache::default())),
|
||||
syntax_set: Arc::new(syntax_set),
|
||||
syntax_theme: Arc::new(theme),
|
||||
concurrent_preview_tasks: Arc::new(AtomicU8::new(0)),
|
||||
last_previewed: Arc::new(Mutex::new(Arc::new(Preview::default()))),
|
||||
//image_picker: Arc::new(Mutex::new(image_picker)),
|
||||
last_previewed: Arc::new(Mutex::new(Arc::new(
|
||||
Preview::default().stale(),
|
||||
))),
|
||||
in_flight_previews: Arc::new(Mutex::new(HashSet::new())),
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,35 +81,40 @@ impl FilePreviewer {
|
||||
/// # Panics
|
||||
/// Panics if seeking to the start of the file fails.
|
||||
pub fn preview(&mut self, entry: &entry::Entry) -> Arc<Preview> {
|
||||
let path_buf = PathBuf::from(&entry.name);
|
||||
|
||||
// do we have a preview in cache for that entry?
|
||||
if let Some(preview) = self.cache.lock().get(&entry.name) {
|
||||
return preview;
|
||||
}
|
||||
debug!("No preview in cache for {:?}", entry.name);
|
||||
debug!("Preview cache miss for {:?}", entry.name);
|
||||
|
||||
// are we already computing a preview in the background for that entry?
|
||||
if self.in_flight_previews.lock().contains(&entry.name) {
|
||||
debug!("Preview already in flight for {:?}", entry.name);
|
||||
return self.last_previewed.lock().clone();
|
||||
}
|
||||
|
||||
if self.concurrent_preview_tasks.load(Ordering::Relaxed)
|
||||
< MAX_CONCURRENT_PREVIEW_TASKS
|
||||
{
|
||||
self.in_flight_previews.lock().insert(entry.name.clone());
|
||||
self.concurrent_preview_tasks
|
||||
.fetch_add(1, Ordering::Relaxed);
|
||||
let cache = self.cache.clone();
|
||||
let entry_c = entry.clone();
|
||||
let syntax_set = self.syntax_set.clone();
|
||||
let syntax_theme = self.syntax_theme.clone();
|
||||
let path = path_buf;
|
||||
let concurrent_tasks = self.concurrent_preview_tasks.clone();
|
||||
let last_previewed = self.last_previewed.clone();
|
||||
let in_flight_previews = self.in_flight_previews.clone();
|
||||
tokio::spawn(async move {
|
||||
try_preview(
|
||||
&entry_c,
|
||||
&path,
|
||||
&cache,
|
||||
&syntax_set,
|
||||
&syntax_theme,
|
||||
&concurrent_tasks,
|
||||
&last_previewed,
|
||||
&in_flight_previews,
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -118,49 +122,28 @@ impl FilePreviewer {
|
||||
self.last_previewed.lock().clone()
|
||||
}
|
||||
|
||||
//async fn compute_image_preview(&self, entry: &entry::Entry) {
|
||||
// let cache = self.cache.clone();
|
||||
// let picker = self.image_picker.clone();
|
||||
// let entry_c = entry.clone();
|
||||
// tokio::spawn(async move {
|
||||
// info!("Loading image: {:?}", entry_c.name);
|
||||
// if let Ok(dyn_image) =
|
||||
// ImageReader::open(entry_c.name.clone()).unwrap().decode()
|
||||
// {
|
||||
// let image = picker.lock().await.new_resize_protocol(dyn_image);
|
||||
// let preview = Arc::new(Preview::new(
|
||||
// entry_c.name.clone(),
|
||||
// PreviewContent::Image(image),
|
||||
// ));
|
||||
// cache
|
||||
// .lock()
|
||||
// .await
|
||||
// .insert(entry_c.name.clone(), preview.clone());
|
||||
// }
|
||||
// });
|
||||
//}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn cache_preview(&mut self, key: String, preview: Arc<Preview>) {
|
||||
self.cache.lock().insert(key, preview);
|
||||
self.cache.lock().insert(key, &preview);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_preview(
|
||||
entry: &entry::Entry,
|
||||
path: &PathBuf,
|
||||
cache: &Arc<Mutex<PreviewCache>>,
|
||||
syntax_set: &Arc<SyntaxSet>,
|
||||
syntax_theme: &Arc<Theme>,
|
||||
concurrent_tasks: &Arc<AtomicU8>,
|
||||
last_previewed: &Arc<Mutex<Arc<Preview>>>,
|
||||
in_flight_previews: &Arc<Mutex<HashSet<String>>>,
|
||||
) {
|
||||
debug!("Computing preview for {:?}", entry.name);
|
||||
let path = PathBuf::from(&entry.name);
|
||||
// check file size
|
||||
if get_file_size(path).map_or(false, |s| s > MAX_FILE_SIZE) {
|
||||
if get_file_size(&path).map_or(false, |s| s > MAX_FILE_SIZE) {
|
||||
debug!("File too large: {:?}", entry.name);
|
||||
let preview = meta::file_too_large(&entry.name);
|
||||
cache.lock().insert(entry.name.clone(), preview);
|
||||
cache.lock().insert(entry.name.clone(), &preview);
|
||||
}
|
||||
|
||||
if matches!(FileType::from(&path), FileType::Text) {
|
||||
@ -176,20 +159,21 @@ pub fn try_preview(
|
||||
syntax_set,
|
||||
syntax_theme,
|
||||
);
|
||||
cache.lock().insert(entry.name.clone(), preview.clone());
|
||||
cache.lock().insert(entry.name.clone(), &preview);
|
||||
in_flight_previews.lock().remove(&entry.name);
|
||||
let mut tp = last_previewed.lock();
|
||||
*tp = preview;
|
||||
*tp = preview.stale().into();
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Error opening file: {:?}", e);
|
||||
let p = meta::not_supported(&entry.name);
|
||||
cache.lock().insert(entry.name.clone(), p);
|
||||
cache.lock().insert(entry.name.clone(), &p);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug!("File isn't text-based: {:?}", entry.name);
|
||||
let preview = meta::not_supported(&entry.name);
|
||||
cache.lock().insert(entry.name.clone(), preview);
|
||||
cache.lock().insert(entry.name.clone(), &preview);
|
||||
}
|
||||
concurrent_tasks.fetch_sub(1, Ordering::Relaxed);
|
||||
}
|
||||
@ -224,6 +208,7 @@ fn compute_highlighted_text_preview(
|
||||
entry.name.clone(),
|
||||
PreviewContent::SyntectHighlightedText(highlighted_lines),
|
||||
entry.icon,
|
||||
false,
|
||||
))
|
||||
}
|
||||
Err(e) => {
|
||||
@ -233,17 +218,7 @@ fn compute_highlighted_text_preview(
|
||||
}
|
||||
}
|
||||
|
||||
//fn get_image_picker() -> Picker {
|
||||
// let mut picker = match Picker::from_termios() {
|
||||
// Ok(p) => p,
|
||||
// Err(_) => Picker::new((7, 14)),
|
||||
// };
|
||||
// picker.guess_protocol();
|
||||
// picker.background_color = Some(Rgb::<u8>([255, 0, 255]));
|
||||
// picker
|
||||
//}
|
||||
|
||||
/// This should be enough to most standard terminal sizes
|
||||
/// This should be enough for most terminal sizes
|
||||
const TEMP_PLAIN_TEXT_PREVIEW_HEIGHT: usize = 200;
|
||||
|
||||
#[allow(dead_code)]
|
||||
@ -268,5 +243,6 @@ fn plain_text_preview(title: &str, reader: BufReader<&File>) -> Arc<Preview> {
|
||||
title.to_string(),
|
||||
PreviewContent::PlainText(lines),
|
||||
None,
|
||||
false,
|
||||
))
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ pub fn not_supported(title: &str) -> Arc<Preview> {
|
||||
title.to_string(),
|
||||
PreviewContent::NotSupported,
|
||||
None,
|
||||
false,
|
||||
))
|
||||
}
|
||||
|
||||
@ -14,6 +15,7 @@ pub fn file_too_large(title: &str) -> Arc<Preview> {
|
||||
title.to_string(),
|
||||
PreviewContent::FileTooLarge,
|
||||
None,
|
||||
false,
|
||||
))
|
||||
}
|
||||
|
||||
@ -23,5 +25,6 @@ pub fn loading(title: &str) -> Arc<Preview> {
|
||||
title.to_string(),
|
||||
PreviewContent::Loading,
|
||||
None,
|
||||
false,
|
||||
))
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "television-utils"
|
||||
version = "0.0.7"
|
||||
version = "0.0.8"
|
||||
description.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
|
143
crates/television-utils/src/cache.rs
Normal file
143
crates/television-utils/src/cache.rs
Normal file
@ -0,0 +1,143 @@
|
||||
use std::collections::{HashSet, VecDeque};
|
||||
use tracing::debug;
|
||||
|
||||
/// A ring buffer that also keeps track of the keys it contains to avoid duplicates.
|
||||
///
|
||||
/// This serves as a backend for the preview cache.
|
||||
/// Basic idea:
|
||||
/// - When a new key is pushed, if it's already in the buffer, do nothing.
|
||||
/// - If the buffer is full, remove the oldest key and push the new key.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use television_utils::cache::RingSet;
|
||||
///
|
||||
/// let mut ring_set = RingSet::with_capacity(3);
|
||||
/// // push 3 values into the ringset
|
||||
/// assert_eq!(ring_set.push(1), None);
|
||||
/// assert_eq!(ring_set.push(2), None);
|
||||
/// assert_eq!(ring_set.push(3), None);
|
||||
///
|
||||
/// // check that the values are in the buffer
|
||||
/// assert!(ring_set.contains(&1));
|
||||
/// assert!(ring_set.contains(&2));
|
||||
/// assert!(ring_set.contains(&3));
|
||||
///
|
||||
/// // push an existing value (should do nothing)
|
||||
/// assert_eq!(ring_set.push(1), None);
|
||||
///
|
||||
/// // entries should still be there
|
||||
/// assert!(ring_set.contains(&1));
|
||||
/// assert!(ring_set.contains(&2));
|
||||
/// assert!(ring_set.contains(&3));
|
||||
///
|
||||
/// // push a new value, should remove the oldest value (1)
|
||||
/// assert_eq!(ring_set.push(4), Some(1));
|
||||
///
|
||||
/// // 1 is no longer there but 2 and 3 remain
|
||||
/// assert!(!ring_set.contains(&1));
|
||||
/// assert!(ring_set.contains(&2));
|
||||
/// assert!(ring_set.contains(&3));
|
||||
/// assert!(ring_set.contains(&4));
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub struct RingSet<T> {
|
||||
ring_buffer: VecDeque<T>,
|
||||
known_keys: HashSet<T>,
|
||||
capacity: usize,
|
||||
}
|
||||
|
||||
impl<T> RingSet<T>
|
||||
where
|
||||
T: Eq + std::hash::Hash + Clone + std::fmt::Debug,
|
||||
{
|
||||
/// Create a new `RingSet` with the given capacity.
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
RingSet {
|
||||
ring_buffer: VecDeque::with_capacity(capacity),
|
||||
known_keys: HashSet::with_capacity(capacity),
|
||||
capacity,
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a new item to the back of the buffer, removing the oldest item if the buffer is full.
|
||||
/// Returns the item that was removed, if any.
|
||||
/// If the item is already in the buffer, do nothing and return None.
|
||||
pub fn push(&mut self, item: T) -> Option<T> {
|
||||
// If the key is already in the buffer, do nothing
|
||||
if self.contains(&item) {
|
||||
debug!("Key already in ring buffer: {:?}", item);
|
||||
return None;
|
||||
}
|
||||
let mut popped_key = None;
|
||||
// If the buffer is full, remove the oldest key (e.g. pop from the front of the buffer)
|
||||
if self.ring_buffer.len() >= self.capacity {
|
||||
popped_key = self.pop();
|
||||
}
|
||||
// finally, push the new key to the back of the buffer
|
||||
self.ring_buffer.push_back(item.clone());
|
||||
self.known_keys.insert(item);
|
||||
popped_key
|
||||
}
|
||||
|
||||
fn pop(&mut self) -> Option<T> {
|
||||
if let Some(item) = self.ring_buffer.pop_front() {
|
||||
debug!("Removing key from ring buffer: {:?}", item);
|
||||
self.known_keys.remove(&item);
|
||||
Some(item)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains(&self, key: &T) -> bool {
|
||||
self.known_keys.contains(key)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_ring_set() {
|
||||
let mut ring_set = RingSet::with_capacity(3);
|
||||
// push 3 values into the ringset
|
||||
assert_eq!(ring_set.push(1), None);
|
||||
assert_eq!(ring_set.push(2), None);
|
||||
assert_eq!(ring_set.push(3), None);
|
||||
|
||||
// check that the values are in the buffer
|
||||
assert!(ring_set.contains(&1));
|
||||
assert!(ring_set.contains(&2));
|
||||
assert!(ring_set.contains(&3));
|
||||
|
||||
// push an existing value (should do nothing)
|
||||
assert_eq!(ring_set.push(1), None);
|
||||
|
||||
// entries should still be there
|
||||
assert!(ring_set.contains(&1));
|
||||
assert!(ring_set.contains(&2));
|
||||
assert!(ring_set.contains(&3));
|
||||
|
||||
// push a new value, should remove the oldest value (1)
|
||||
assert_eq!(ring_set.push(4), Some(1));
|
||||
|
||||
// 1 is no longer there but 2 and 3 remain
|
||||
assert!(!ring_set.contains(&1));
|
||||
assert!(ring_set.contains(&2));
|
||||
assert!(ring_set.contains(&3));
|
||||
assert!(ring_set.contains(&4));
|
||||
|
||||
// push two new values, should remove 2 and 3
|
||||
assert_eq!(ring_set.push(5), Some(2));
|
||||
assert_eq!(ring_set.push(6), Some(3));
|
||||
|
||||
// 2 and 3 are no longer there but 4, 5 and 6 remain
|
||||
assert!(!ring_set.contains(&2));
|
||||
assert!(!ring_set.contains(&3));
|
||||
assert!(ring_set.contains(&4));
|
||||
assert!(ring_set.contains(&5));
|
||||
assert!(ring_set.contains(&6));
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
pub mod cache;
|
||||
pub mod files;
|
||||
pub mod indices;
|
||||
pub mod stdin;
|
||||
|
@ -167,6 +167,31 @@ const NULL_CHARACTER: char = '\x00';
|
||||
const UNIT_SEPARATOR_CHARACTER: char = '\u{001F}';
|
||||
const APPLICATION_PROGRAM_COMMAND_CHARACTER: char = '\u{009F}';
|
||||
|
||||
pub struct ReplaceNonPrintableConfig {
|
||||
pub replace_tab: bool,
|
||||
pub tab_width: usize,
|
||||
pub replace_line_feed: bool,
|
||||
pub replace_control_characters: bool,
|
||||
}
|
||||
|
||||
impl ReplaceNonPrintableConfig {
|
||||
pub fn tab_width(&mut self, tab_width: usize) -> &mut Self {
|
||||
self.tab_width = tab_width;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ReplaceNonPrintableConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
replace_tab: true,
|
||||
tab_width: TAB_WIDTH,
|
||||
replace_line_feed: true,
|
||||
replace_control_characters: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::missing_panics_doc)]
|
||||
/// Replaces non-printable characters in the given byte slice with default printable characters.
|
||||
///
|
||||
@ -178,26 +203,26 @@ const APPLICATION_PROGRAM_COMMAND_CHARACTER: char = '\u{009F}';
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use television_utils::strings::replace_non_printable;
|
||||
/// use television_utils::strings::{replace_non_printable, ReplaceNonPrintableConfig};
|
||||
///
|
||||
/// let input = b"Hello, World!";
|
||||
/// let (output, offsets) = replace_non_printable(input, 2);
|
||||
/// let (output, offsets) = replace_non_printable(input, &ReplaceNonPrintableConfig::default());
|
||||
/// assert_eq!(output, "Hello, World!");
|
||||
/// assert_eq!(offsets, vec![0,0,0,0,0,0,0,0,0,0,0,0,0]);
|
||||
///
|
||||
/// let input = b"Hello,\tWorld!";
|
||||
/// let (output, offsets) = replace_non_printable(input, 4);
|
||||
/// let (output, offsets) = replace_non_printable(input, &ReplaceNonPrintableConfig::default().tab_width(4));
|
||||
/// assert_eq!(output, "Hello, World!");
|
||||
/// assert_eq!(offsets, vec![0,0,0,0,0,0,0,3,3,3,3,3,3]);
|
||||
///
|
||||
/// let input = b"Hello,\nWorld!";
|
||||
/// let (output, offsets) = replace_non_printable(input, 2);
|
||||
/// let (output, offsets) = replace_non_printable(input, &ReplaceNonPrintableConfig::default());
|
||||
/// assert_eq!(output, "Hello,World!");
|
||||
/// assert_eq!(offsets, vec![0,0,0,0,0,0,0,-1,-1,-1,-1,-1,-1]);
|
||||
/// ```
|
||||
pub fn replace_non_printable(
|
||||
input: &[u8],
|
||||
tab_width: usize,
|
||||
config: &ReplaceNonPrintableConfig,
|
||||
) -> (String, Vec<i16>) {
|
||||
let mut output = String::new();
|
||||
let mut offsets = Vec::new();
|
||||
@ -212,12 +237,13 @@ pub fn replace_non_printable(
|
||||
|
||||
match chr {
|
||||
// tab
|
||||
TAB_CHARACTER => {
|
||||
output.push_str(&" ".repeat(tab_width));
|
||||
cumulative_offset += i16::try_from(tab_width).unwrap() - 1;
|
||||
TAB_CHARACTER if config.replace_tab => {
|
||||
output.push_str(&" ".repeat(config.tab_width));
|
||||
cumulative_offset +=
|
||||
i16::try_from(config.tab_width).unwrap() - 1;
|
||||
}
|
||||
// line feed
|
||||
LINE_FEED_CHARACTER => {
|
||||
LINE_FEED_CHARACTER if config.replace_line_feed => {
|
||||
cumulative_offset -= 1;
|
||||
}
|
||||
|
||||
@ -226,7 +252,9 @@ pub fn replace_non_printable(
|
||||
// + BOM
|
||||
NULL_CHARACTER..=UNIT_SEPARATOR_CHARACTER
|
||||
| DELETE_CHARACTER..=APPLICATION_PROGRAM_COMMAND_CHARACTER
|
||||
| BOM_CHARACTER => {
|
||||
| BOM_CHARACTER
|
||||
if config.replace_control_characters =>
|
||||
{
|
||||
output.push(*NULL_SYMBOL);
|
||||
}
|
||||
// Unicode characters above 0x0700 seem unstable with ratatui
|
||||
@ -317,7 +345,7 @@ pub fn preprocess_line(line: &str) -> (String, Vec<i16>) {
|
||||
}
|
||||
}
|
||||
.as_bytes(),
|
||||
TAB_WIDTH,
|
||||
&ReplaceNonPrintableConfig::default(),
|
||||
)
|
||||
}
|
||||
|
||||
@ -515,7 +543,10 @@ mod tests {
|
||||
}
|
||||
|
||||
fn test_replace_non_printable(input: &str, expected: &str) {
|
||||
let (actual, _offset) = replace_non_printable(input.as_bytes(), 2);
|
||||
let (actual, _offset) = replace_non_printable(
|
||||
input.as_bytes(),
|
||||
&ReplaceNonPrintableConfig::default().tab_width(2),
|
||||
);
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
@ -562,7 +593,10 @@ mod tests {
|
||||
#[test]
|
||||
fn test_replace_non_printable_range_tab() {
|
||||
let input = b"Hello,\tWorld!";
|
||||
let (output, offsets) = replace_non_printable(input, 4);
|
||||
let (output, offsets) = replace_non_printable(
|
||||
input,
|
||||
&ReplaceNonPrintableConfig::default(),
|
||||
);
|
||||
assert_eq!(output, "Hello, World!");
|
||||
assert_eq!(offsets, vec![0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3]);
|
||||
}
|
||||
@ -570,7 +604,10 @@ mod tests {
|
||||
#[test]
|
||||
fn test_replace_non_printable_range_line_feed() {
|
||||
let input = b"Hello,\nWorld!";
|
||||
let (output, offsets) = replace_non_printable(input, 2);
|
||||
let (output, offsets) = replace_non_printable(
|
||||
input,
|
||||
&ReplaceNonPrintableConfig::default().tab_width(2),
|
||||
);
|
||||
assert_eq!(output, "Hello,World!");
|
||||
assert_eq!(offsets, vec![0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1]);
|
||||
}
|
||||
@ -578,12 +615,18 @@ mod tests {
|
||||
#[test]
|
||||
fn test_replace_non_printable_no_range_changes() {
|
||||
let input = b"Hello,\x00World!";
|
||||
let (output, offsets) = replace_non_printable(input, 2);
|
||||
let (output, offsets) = replace_non_printable(
|
||||
input,
|
||||
&ReplaceNonPrintableConfig::default().tab_width(2),
|
||||
);
|
||||
assert_eq!(output, "Hello,␀World!");
|
||||
assert_eq!(offsets, vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
|
||||
let input = b"Hello,\x7FWorld!";
|
||||
let (output, offsets) = replace_non_printable(input, 2);
|
||||
let (output, offsets) = replace_non_printable(
|
||||
input,
|
||||
&ReplaceNonPrintableConfig::default().tab_width(2),
|
||||
);
|
||||
assert_eq!(output, "Hello,␀World!");
|
||||
assert_eq!(offsets, vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
}
|
||||
|
68
crates/television/cable.rs
Normal file
68
crates/television/cable.rs
Normal file
@ -0,0 +1,68 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use color_eyre::Result;
|
||||
use television_channels::cable::{CableChannelPrototype, CableChannels};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::config::get_config_dir;
|
||||
|
||||
/// Just a proxy struct to deserialize prototypes
|
||||
#[derive(Debug, serde::Deserialize, Default)]
|
||||
struct ChannelPrototypes {
|
||||
#[serde(rename = "cable_channel")]
|
||||
prototypes: Vec<CableChannelPrototype>,
|
||||
}
|
||||
|
||||
const CABLE_FILE_NAME_SUFFIX: &str = "channels";
|
||||
const CABLE_FILE_FORMAT: &str = "toml";
|
||||
|
||||
/// Load the cable configuration from the config directory.
|
||||
///
|
||||
/// Cable is loaded by compiling all files that match the following
|
||||
/// pattern in the config directory: `*channels.toml`.
|
||||
///
|
||||
/// # Example:
|
||||
/// ```
|
||||
/// config_folder/
|
||||
/// ├── cable_channels.toml
|
||||
/// ├── my_channels.toml
|
||||
/// └── windows_channels.toml
|
||||
/// ```
|
||||
pub fn load_cable_channels() -> Result<CableChannels> {
|
||||
let config_dir = get_config_dir();
|
||||
|
||||
// list all files in the config directory
|
||||
let files = std::fs::read_dir(&config_dir)?;
|
||||
|
||||
// filter the files that match the pattern
|
||||
let file_paths = files
|
||||
.filter_map(|f| f.ok().map(|f| f.path()))
|
||||
.filter(|p| {
|
||||
p.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.map_or(false, |e| e == CABLE_FILE_FORMAT)
|
||||
})
|
||||
.filter(|p| {
|
||||
p.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.map_or(false, |s| s.ends_with(CABLE_FILE_NAME_SUFFIX))
|
||||
});
|
||||
|
||||
let all_prototypes = file_paths.fold(Vec::new(), |mut acc, p| {
|
||||
let r: ChannelPrototypes = toml::from_str(
|
||||
&std::fs::read_to_string(p)
|
||||
.expect("Unable to read configuration file"),
|
||||
)
|
||||
.unwrap_or_default();
|
||||
acc.extend(r.prototypes);
|
||||
acc
|
||||
});
|
||||
|
||||
debug!("Loaded cable channels: {:?}", all_prototypes);
|
||||
|
||||
let mut cable_channels = HashMap::new();
|
||||
for prototype in all_prototypes {
|
||||
cable_channels.insert(prototype.name.clone(), prototype);
|
||||
}
|
||||
Ok(CableChannels(cable_channels))
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
use clap::Parser;
|
||||
|
||||
use crate::config::{get_config_dir, get_data_dir};
|
||||
use television_channels::channels::CliTvChannel;
|
||||
use television_channels::{channels::CliTvChannel, entry::PreviewCommand};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version = version(), about)]
|
||||
@ -10,6 +10,15 @@ pub struct Cli {
|
||||
#[arg(value_enum, default_value = "files")]
|
||||
pub channel: CliTvChannel,
|
||||
|
||||
/// Use a custom preview command (currently only supported by the stdin channel)
|
||||
#[arg(short, long, value_name = "STRING")]
|
||||
pub preview: Option<String>,
|
||||
|
||||
/// The delimiter used to extract fields from the entry to provide to the preview command
|
||||
/// (defaults to ":")
|
||||
#[arg(long, value_name = "STRING", default_value = " ", value_parser = delimiter_parser)]
|
||||
pub delimiter: String,
|
||||
|
||||
/// Tick rate, i.e. number of ticks per second
|
||||
#[arg(short, long, value_name = "FLOAT", default_value_t = 50.0)]
|
||||
pub tick_rate: f64,
|
||||
@ -21,13 +30,14 @@ pub struct Cli {
|
||||
/// Passthrough keybindings (comma separated, e.g. "q,ctrl-w,ctrl-t") These keybindings will
|
||||
/// trigger selection of the current entry and be passed through to stdout along with the entry
|
||||
/// to be handled by the parent process.
|
||||
#[arg(short, long, value_name = "STRING")]
|
||||
#[arg(long, value_name = "STRING")]
|
||||
pub passthrough_keybindings: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PostProcessedCli {
|
||||
pub channel: CliTvChannel,
|
||||
pub preview_command: Option<PreviewCommand>,
|
||||
pub tick_rate: f64,
|
||||
pub frame_rate: f64,
|
||||
pub passthrough_keybindings: Vec<String>,
|
||||
@ -42,8 +52,14 @@ impl From<Cli> for PostProcessedCli {
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect();
|
||||
|
||||
let preview_command = cli.preview.map(|preview| PreviewCommand {
|
||||
command: preview,
|
||||
delimiter: cli.delimiter.clone(),
|
||||
});
|
||||
|
||||
Self {
|
||||
channel: cli.channel,
|
||||
preview_command,
|
||||
tick_rate: cli.tick_rate,
|
||||
frame_rate: cli.frame_rate,
|
||||
passthrough_keybindings,
|
||||
@ -97,9 +113,12 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::float_cmp)]
|
||||
fn test_from_cli() {
|
||||
let cli = Cli {
|
||||
channel: CliTvChannel::Files,
|
||||
preview: Some("bat -n --color=always {}".to_string()),
|
||||
delimiter: ":".to_string(),
|
||||
tick_rate: 50.0,
|
||||
frame_rate: 60.0,
|
||||
passthrough_keybindings: Some("q,ctrl-w,ctrl-t".to_string()),
|
||||
@ -108,6 +127,13 @@ mod tests {
|
||||
let post_processed_cli: PostProcessedCli = cli.into();
|
||||
|
||||
assert_eq!(post_processed_cli.channel, CliTvChannel::Files);
|
||||
assert_eq!(
|
||||
post_processed_cli.preview_command,
|
||||
Some(PreviewCommand {
|
||||
command: "bat -n --color=always {}".to_string(),
|
||||
delimiter: ":".to_string()
|
||||
})
|
||||
);
|
||||
assert_eq!(post_processed_cli.tick_rate, 50.0);
|
||||
assert_eq!(post_processed_cli.frame_rate, 60.0);
|
||||
assert_eq!(
|
||||
@ -116,3 +142,11 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn delimiter_parser(s: &str) -> Result<String, String> {
|
||||
Ok(match s {
|
||||
"" => ":".to_string(),
|
||||
"\\t" => "\t".to_string(),
|
||||
_ => s.to_string(),
|
||||
})
|
||||
}
|
||||
|
@ -8,8 +8,6 @@ use television_previewers::previewers::PreviewerConfig;
|
||||
pub struct PreviewersConfig {
|
||||
#[serde(default)]
|
||||
pub basic: BasicPreviewerConfig,
|
||||
#[serde(default)]
|
||||
pub directory: DirectoryPreviewerConfig,
|
||||
pub file: FilePreviewerConfig,
|
||||
#[serde(default)]
|
||||
pub env_var: EnvVarPreviewerConfig,
|
||||
@ -26,7 +24,6 @@ impl From<PreviewersConfig> for ValueKind {
|
||||
fn from(val: PreviewersConfig) -> Self {
|
||||
let mut m = HashMap::new();
|
||||
m.insert(String::from("basic"), val.basic.into());
|
||||
m.insert(String::from("directory"), val.directory.into());
|
||||
m.insert(String::from("file"), val.file.into());
|
||||
m.insert(String::from("env_var"), val.env_var.into());
|
||||
ValueKind::Table(m)
|
||||
@ -42,15 +39,6 @@ impl From<BasicPreviewerConfig> for ValueKind {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
pub struct DirectoryPreviewerConfig {}
|
||||
|
||||
impl From<DirectoryPreviewerConfig> for ValueKind {
|
||||
fn from(_val: DirectoryPreviewerConfig) -> Self {
|
||||
ValueKind::Table(HashMap::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
pub struct FilePreviewerConfig {
|
||||
//pub max_file_size: u64,
|
||||
|
@ -5,6 +5,7 @@ use clap::Parser;
|
||||
use cli::PostProcessedCli;
|
||||
use color_eyre::Result;
|
||||
use television_channels::channels::TelevisionChannel;
|
||||
use television_channels::entry::PreviewType;
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::app::App;
|
||||
@ -14,6 +15,7 @@ use television_utils::stdin::is_readable_stdin;
|
||||
|
||||
pub mod action;
|
||||
pub mod app;
|
||||
pub mod cable;
|
||||
pub mod cli;
|
||||
pub mod config;
|
||||
pub mod errors;
|
||||
@ -38,7 +40,9 @@ async fn main() -> Result<()> {
|
||||
{
|
||||
if is_readable_stdin() {
|
||||
debug!("Using stdin channel");
|
||||
TelevisionChannel::Stdin(StdinChannel::default())
|
||||
TelevisionChannel::Stdin(StdinChannel::new(
|
||||
args.preview_command.map(PreviewType::Command),
|
||||
))
|
||||
} else {
|
||||
debug!("Using {:?} channel", args.channel);
|
||||
args.channel.to_channel()
|
||||
|
@ -1,17 +1,21 @@
|
||||
use crate::app::Keymap;
|
||||
use crate::picker::Picker;
|
||||
use crate::ui::input::actions::InputActionHandler;
|
||||
use crate::ui::layout::{Dimensions, InputPosition, Layout};
|
||||
use crate::ui::spinner::Spinner;
|
||||
use crate::ui::spinner::SpinnerState;
|
||||
use crate::{action::Action, config::Config};
|
||||
use crate::ui::{
|
||||
cache::RenderedPreviewCache,
|
||||
input::actions::InputActionHandler,
|
||||
layout::{Dimensions, InputPosition, Layout},
|
||||
spinner::Spinner,
|
||||
};
|
||||
use crate::{action::Action, config::Config, ui::spinner::SpinnerState};
|
||||
use crate::{app::Keymap, cable::load_cable_channels};
|
||||
use color_eyre::Result;
|
||||
use copypasta::{ClipboardContext, ClipboardProvider};
|
||||
use ratatui::{layout::Rect, style::Color, widgets::Paragraph, Frame};
|
||||
use ratatui::{layout::Rect, style::Color, Frame};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use television_channels::channels::{
|
||||
remote_control::RemoteControl, OnAir, TelevisionChannel, UnitChannel,
|
||||
remote_control::{load_builtin_channels, RemoteControl},
|
||||
OnAir, TelevisionChannel, UnitChannel,
|
||||
};
|
||||
use television_channels::entry::{Entry, ENTRY_PLACEHOLDER};
|
||||
use television_previewers::previewers::Previewer;
|
||||
@ -40,14 +44,8 @@ pub struct Television {
|
||||
pub preview_scroll: Option<u16>,
|
||||
pub preview_pane_height: u16,
|
||||
current_preview_total_lines: u16,
|
||||
/// A cache for meta paragraphs (i.e. previews like "Not Supported", etc.).
|
||||
///
|
||||
/// The key is a tuple of the preview name and the dimensions of the
|
||||
/// preview pane. This is a little extra security to ensure meta previews
|
||||
/// are rendered correctly even when resizing the terminal while still
|
||||
/// benefiting from a cache mechanism.
|
||||
pub meta_paragraph_cache: HashMap<(String, u16, u16), Paragraph<'static>>,
|
||||
pub icon_color_cache: HashMap<String, Color>,
|
||||
pub rendered_preview_cache: Arc<Mutex<RenderedPreviewCache<'static>>>,
|
||||
pub(crate) spinner: Spinner,
|
||||
pub(crate) spinner_state: SpinnerState,
|
||||
}
|
||||
@ -61,6 +59,8 @@ impl Television {
|
||||
};
|
||||
let previewer = Previewer::new(Some(config.previewers.clone().into()));
|
||||
let keymap = Keymap::from(&config.keybindings);
|
||||
let builtin_channels = load_builtin_channels();
|
||||
let cable_channels = load_cable_channels().unwrap_or_default();
|
||||
|
||||
channel.find(EMPTY_STRING);
|
||||
let spinner = Spinner::default();
|
||||
@ -70,7 +70,7 @@ impl Television {
|
||||
keymap,
|
||||
channel,
|
||||
remote_control: TelevisionChannel::RemoteControl(
|
||||
RemoteControl::default(),
|
||||
RemoteControl::new(builtin_channels, Some(cable_channels)),
|
||||
),
|
||||
mode: Mode::Channel,
|
||||
current_pattern: EMPTY_STRING.to_string(),
|
||||
@ -81,8 +81,10 @@ impl Television {
|
||||
preview_scroll: None,
|
||||
preview_pane_height: 0,
|
||||
current_preview_total_lines: 0,
|
||||
meta_paragraph_cache: HashMap::new(),
|
||||
icon_color_cache: HashMap::new(),
|
||||
rendered_preview_cache: Arc::new(Mutex::new(
|
||||
RenderedPreviewCache::default(),
|
||||
)),
|
||||
spinner,
|
||||
spinner_state: SpinnerState::from(&spinner),
|
||||
}
|
||||
@ -289,9 +291,6 @@ impl Television {
|
||||
Action::ScrollPreviewHalfPageUp => self.scroll_preview_up(20),
|
||||
Action::ToggleRemoteControl => match self.mode {
|
||||
Mode::Channel => {
|
||||
self.remote_control = TelevisionChannel::RemoteControl(
|
||||
RemoteControl::default(),
|
||||
);
|
||||
self.mode = Mode::RemoteControl;
|
||||
}
|
||||
Mode::RemoteControl => {
|
||||
@ -312,8 +311,9 @@ impl Television {
|
||||
.unwrap()
|
||||
.send(Action::SelectAndExit)?,
|
||||
Mode::RemoteControl => {
|
||||
// FIXME: this is kind of shitty
|
||||
let new_channel = TelevisionChannel::from(&entry);
|
||||
let new_channel = self
|
||||
.remote_control
|
||||
.zap(entry.name.as_str())?;
|
||||
// this resets the RC picker
|
||||
self.reset_picker_selection();
|
||||
self.reset_picker_input();
|
||||
@ -322,9 +322,9 @@ impl Television {
|
||||
self.change_channel(new_channel);
|
||||
}
|
||||
Mode::SendToChannel => {
|
||||
let new_channel = self
|
||||
.channel
|
||||
.transition_to(entry.name.as_str().into());
|
||||
let new_channel = self.channel.transition_to(
|
||||
entry.name.as_str().try_into().unwrap(),
|
||||
);
|
||||
self.reset_picker_selection();
|
||||
self.reset_picker_input();
|
||||
self.remote_control.find(EMPTY_STRING);
|
||||
@ -406,9 +406,7 @@ impl Television {
|
||||
self.draw_preview_content_block(
|
||||
f,
|
||||
layout.preview_window,
|
||||
selected_entry
|
||||
.line_number
|
||||
.map(|l| u16::try_from(l).unwrap_or(0)),
|
||||
&selected_entry,
|
||||
&preview,
|
||||
);
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
use ratatui::style::Color;
|
||||
|
||||
pub mod cache;
|
||||
pub(crate) mod help;
|
||||
pub mod input;
|
||||
pub mod keymap;
|
||||
|
38
crates/television/ui/cache.rs
Normal file
38
crates/television/ui/cache.rs
Normal file
@ -0,0 +1,38 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use ratatui::widgets::Paragraph;
|
||||
use television_utils::cache::RingSet;
|
||||
|
||||
const DEFAULT_RENDERED_PREVIEW_CACHE_SIZE: usize = 25;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RenderedPreviewCache<'a> {
|
||||
previews: HashMap<String, Arc<Paragraph<'a>>>,
|
||||
ring_set: RingSet<String>,
|
||||
}
|
||||
|
||||
impl<'a> RenderedPreviewCache<'a> {
|
||||
pub fn new(capacity: usize) -> Self {
|
||||
RenderedPreviewCache {
|
||||
previews: HashMap::new(),
|
||||
ring_set: RingSet::with_capacity(capacity),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, key: &str) -> Option<Arc<Paragraph<'a>>> {
|
||||
self.previews.get(key).cloned()
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, key: String, preview: &Arc<Paragraph<'a>>) {
|
||||
self.previews.insert(key.clone(), preview.clone());
|
||||
if let Some(oldest_key) = self.ring_set.push(key) {
|
||||
self.previews.remove(&oldest_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RenderedPreviewCache<'_> {
|
||||
fn default() -> Self {
|
||||
RenderedPreviewCache::new(DEFAULT_RENDERED_PREVIEW_CACHE_SIZE)
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
use crate::television::Television;
|
||||
use crate::ui::BORDER_COLOR;
|
||||
use ansi_to_tui::IntoText;
|
||||
use color_eyre::eyre::Result;
|
||||
use ratatui::layout::{Alignment, Rect};
|
||||
use ratatui::prelude::{Color, Line, Modifier, Span, Style, Stylize, Text};
|
||||
@ -9,10 +10,14 @@ use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use syntect::highlighting::Color as SyntectColor;
|
||||
use television_channels::channels::OnAir;
|
||||
use television_channels::entry::Entry;
|
||||
use television_previewers::previewers::{
|
||||
Preview, PreviewContent, FILE_TOO_LARGE_MSG, PREVIEW_NOT_SUPPORTED_MSG,
|
||||
};
|
||||
use television_utils::strings::{shrink_with_ellipsis, EMPTY_STRING};
|
||||
use television_utils::strings::{
|
||||
replace_non_printable, shrink_with_ellipsis, ReplaceNonPrintableConfig,
|
||||
EMPTY_STRING,
|
||||
};
|
||||
|
||||
// preview
|
||||
pub const DEFAULT_PREVIEW_TITLE_FG: Color = Color::Blue;
|
||||
@ -42,7 +47,11 @@ impl Television {
|
||||
}
|
||||
preview_title_spans.push(Span::styled(
|
||||
shrink_with_ellipsis(
|
||||
&preview.title,
|
||||
&replace_non_printable(
|
||||
preview.title.as_bytes(),
|
||||
&ReplaceNonPrintableConfig::default(),
|
||||
)
|
||||
.0,
|
||||
rect.width.saturating_sub(4) as usize,
|
||||
),
|
||||
Style::default().fg(DEFAULT_PREVIEW_TITLE_FG).bold(),
|
||||
@ -64,7 +73,7 @@ impl Television {
|
||||
&mut self,
|
||||
f: &mut Frame,
|
||||
rect: Rect,
|
||||
target_line: Option<u16>,
|
||||
entry: &Entry,
|
||||
preview: &Arc<Preview>,
|
||||
) {
|
||||
let preview_outer_block = Block::default()
|
||||
@ -85,127 +94,213 @@ impl Television {
|
||||
let inner = preview_outer_block.inner(rect);
|
||||
f.render_widget(preview_outer_block, rect);
|
||||
|
||||
//if let PreviewContent::Image(img) = &preview.content {
|
||||
// let image_component = StatefulImage::new(None);
|
||||
// frame.render_stateful_widget(
|
||||
// image_component,
|
||||
// inner,
|
||||
// &mut img.clone(),
|
||||
// );
|
||||
//} else {
|
||||
let preview_block = self.build_preview_paragraph(
|
||||
let target_line =
|
||||
entry.line_number.map(|l| u16::try_from(l).unwrap_or(0));
|
||||
let cache_key = compute_cache_key(entry);
|
||||
|
||||
self.maybe_init_preview_scroll(target_line, inner.height);
|
||||
|
||||
// Check if the rendered preview content is already in the cache
|
||||
if let Some(preview_paragraph) =
|
||||
self.rendered_preview_cache.lock().unwrap().get(&cache_key)
|
||||
{
|
||||
let p = preview_paragraph.as_ref().clone();
|
||||
f.render_widget(
|
||||
p.scroll((self.preview_scroll.unwrap_or(0), 0)),
|
||||
inner,
|
||||
);
|
||||
return;
|
||||
}
|
||||
// If not, render the preview content and cache it if not empty
|
||||
let rp = Self::build_preview_paragraph(
|
||||
preview_inner_block,
|
||||
inner,
|
||||
preview,
|
||||
preview.content.clone(),
|
||||
target_line,
|
||||
self.preview_scroll,
|
||||
);
|
||||
if !preview.stale {
|
||||
self.rendered_preview_cache
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(cache_key, &Arc::new(rp.clone()));
|
||||
}
|
||||
f.render_widget(
|
||||
Arc::new(rp)
|
||||
.as_ref()
|
||||
.clone()
|
||||
.scroll((self.preview_scroll.unwrap_or(0), 0)),
|
||||
inner,
|
||||
);
|
||||
f.render_widget(preview_block, inner);
|
||||
//}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
const FILL_CHAR_SLANTED: char = '╱';
|
||||
const FILL_CHAR_EMPTY: char = ' ';
|
||||
|
||||
pub fn build_preview_paragraph<'b>(
|
||||
&'b mut self,
|
||||
preview_block: Block<'b>,
|
||||
// FIXME: I broke the previewer (srolling is not working as intended)
|
||||
// and it looks like the previewer displays the wrong previews
|
||||
pub fn build_preview_paragraph(
|
||||
preview_block: Block,
|
||||
inner: Rect,
|
||||
preview: &Arc<Preview>,
|
||||
preview_content: PreviewContent,
|
||||
target_line: Option<u16>,
|
||||
) -> Paragraph<'b> {
|
||||
self.maybe_init_preview_scroll(target_line, inner.height);
|
||||
match &preview.content {
|
||||
preview_scroll: Option<u16>,
|
||||
) -> Paragraph {
|
||||
match preview_content {
|
||||
PreviewContent::AnsiText(text) => Self::build_ansi_text_paragraph(
|
||||
text,
|
||||
preview_block,
|
||||
preview_scroll,
|
||||
),
|
||||
PreviewContent::PlainText(content) => {
|
||||
let mut lines = Vec::new();
|
||||
for (i, line) in content.iter().enumerate() {
|
||||
lines.push(Line::from(vec![
|
||||
build_line_number_span(i + 1).style(Style::default().fg(
|
||||
if matches!(
|
||||
target_line,
|
||||
Some(l) if l == u16::try_from(i).unwrap_or(0) + 1
|
||||
)
|
||||
{
|
||||
DEFAULT_PREVIEW_GUTTER_SELECTED_FG
|
||||
} else {
|
||||
DEFAULT_PREVIEW_GUTTER_FG
|
||||
},
|
||||
)),
|
||||
Span::styled(" │ ",
|
||||
Style::default().fg(DEFAULT_PREVIEW_GUTTER_FG).dim()),
|
||||
Span::styled(
|
||||
line.to_string(),
|
||||
Style::default().fg(DEFAULT_PREVIEW_CONTENT_FG).bg(
|
||||
if matches!(target_line, Some(l) if l == u16::try_from(i).unwrap() + 1) {
|
||||
DEFAULT_SELECTED_PREVIEW_BG
|
||||
} else {
|
||||
Color::Reset
|
||||
},
|
||||
),
|
||||
),
|
||||
]));
|
||||
}
|
||||
let text = Text::from(lines);
|
||||
Paragraph::new(text)
|
||||
.block(preview_block)
|
||||
.scroll((self.preview_scroll.unwrap_or(0), 0))
|
||||
Self::build_plain_text_paragraph(
|
||||
content,
|
||||
preview_block,
|
||||
target_line,
|
||||
preview_scroll,
|
||||
)
|
||||
}
|
||||
PreviewContent::PlainTextWrapped(content) => {
|
||||
let mut lines = Vec::new();
|
||||
for line in content.lines() {
|
||||
lines.push(Line::styled(
|
||||
line.to_string(),
|
||||
Style::default().fg(DEFAULT_PREVIEW_CONTENT_FG),
|
||||
));
|
||||
}
|
||||
let text = Text::from(lines);
|
||||
Paragraph::new(text)
|
||||
.block(preview_block)
|
||||
.wrap(Wrap { trim: true })
|
||||
Self::build_plain_text_wrapped_paragraph(
|
||||
content,
|
||||
preview_block,
|
||||
)
|
||||
}
|
||||
PreviewContent::SyntectHighlightedText(highlighted_lines) => {
|
||||
compute_paragraph_from_highlighted_lines(
|
||||
Self::build_syntect_highlighted_paragraph(
|
||||
highlighted_lines,
|
||||
target_line.map(|l| l as usize),
|
||||
self.preview_scroll.unwrap_or(0),
|
||||
self.preview_pane_height,
|
||||
preview_block,
|
||||
target_line,
|
||||
preview_scroll,
|
||||
)
|
||||
.block(preview_block)
|
||||
.alignment(Alignment::Left)
|
||||
.scroll((self.preview_scroll.unwrap_or(0), 0))
|
||||
}
|
||||
// meta
|
||||
PreviewContent::Loading => self
|
||||
.build_meta_preview_paragraph(
|
||||
inner,
|
||||
"Loading...",
|
||||
Self::FILL_CHAR_EMPTY,
|
||||
)
|
||||
.block(preview_block)
|
||||
.alignment(Alignment::Left)
|
||||
.style(Style::default().add_modifier(Modifier::ITALIC)),
|
||||
PreviewContent::NotSupported => self
|
||||
.build_meta_preview_paragraph(
|
||||
PreviewContent::Loading => Self::build_meta_preview_paragraph(
|
||||
inner,
|
||||
"Loading...",
|
||||
Self::FILL_CHAR_EMPTY,
|
||||
)
|
||||
.block(preview_block)
|
||||
.alignment(Alignment::Left)
|
||||
.style(Style::default().add_modifier(Modifier::ITALIC)),
|
||||
PreviewContent::NotSupported => {
|
||||
Self::build_meta_preview_paragraph(
|
||||
inner,
|
||||
PREVIEW_NOT_SUPPORTED_MSG,
|
||||
Self::FILL_CHAR_EMPTY,
|
||||
)
|
||||
.block(preview_block)
|
||||
.alignment(Alignment::Left)
|
||||
.style(Style::default().add_modifier(Modifier::ITALIC)),
|
||||
PreviewContent::FileTooLarge => self
|
||||
.build_meta_preview_paragraph(
|
||||
.style(Style::default().add_modifier(Modifier::ITALIC))
|
||||
}
|
||||
PreviewContent::FileTooLarge => {
|
||||
Self::build_meta_preview_paragraph(
|
||||
inner,
|
||||
FILE_TOO_LARGE_MSG,
|
||||
Self::FILL_CHAR_EMPTY,
|
||||
)
|
||||
.block(preview_block)
|
||||
.alignment(Alignment::Left)
|
||||
.style(Style::default().add_modifier(Modifier::ITALIC)),
|
||||
.style(Style::default().add_modifier(Modifier::ITALIC))
|
||||
}
|
||||
PreviewContent::Empty => Paragraph::new(Text::raw(EMPTY_STRING)),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_ansi_text_paragraph(
|
||||
text: String,
|
||||
preview_block: Block,
|
||||
preview_scroll: Option<u16>,
|
||||
) -> Paragraph {
|
||||
let text = replace_non_printable(
|
||||
text.as_bytes(),
|
||||
&ReplaceNonPrintableConfig {
|
||||
replace_line_feed: false,
|
||||
replace_control_characters: false,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.0
|
||||
.into_text()
|
||||
.unwrap();
|
||||
Paragraph::new(text)
|
||||
.block(preview_block)
|
||||
.scroll((preview_scroll.unwrap_or(0), 0))
|
||||
}
|
||||
|
||||
fn build_plain_text_paragraph(
|
||||
text: Vec<String>,
|
||||
preview_block: Block,
|
||||
target_line: Option<u16>,
|
||||
preview_scroll: Option<u16>,
|
||||
) -> Paragraph {
|
||||
let mut lines = Vec::new();
|
||||
for (i, line) in text.iter().enumerate() {
|
||||
lines.push(Line::from(vec![
|
||||
build_line_number_span(i + 1).style(Style::default().fg(
|
||||
if matches!(
|
||||
target_line,
|
||||
Some(l) if l == u16::try_from(i).unwrap_or(0) + 1
|
||||
)
|
||||
{
|
||||
DEFAULT_PREVIEW_GUTTER_SELECTED_FG
|
||||
} else {
|
||||
DEFAULT_PREVIEW_GUTTER_FG
|
||||
},
|
||||
)),
|
||||
Span::styled(" │ ",
|
||||
Style::default().fg(DEFAULT_PREVIEW_GUTTER_FG).dim()),
|
||||
Span::styled(
|
||||
line.to_string(),
|
||||
Style::default().fg(DEFAULT_PREVIEW_CONTENT_FG).bg(
|
||||
if matches!(target_line, Some(l) if l == u16::try_from(i).unwrap() + 1) {
|
||||
DEFAULT_SELECTED_PREVIEW_BG
|
||||
} else {
|
||||
Color::Reset
|
||||
},
|
||||
),
|
||||
),
|
||||
]));
|
||||
}
|
||||
let text = Text::from(lines);
|
||||
Paragraph::new(text)
|
||||
.block(preview_block)
|
||||
.scroll((preview_scroll.unwrap_or(0), 0))
|
||||
}
|
||||
|
||||
fn build_plain_text_wrapped_paragraph(
|
||||
text: String,
|
||||
preview_block: Block,
|
||||
) -> Paragraph {
|
||||
let mut lines = Vec::new();
|
||||
for line in text.lines() {
|
||||
lines.push(Line::styled(
|
||||
line.to_string(),
|
||||
Style::default().fg(DEFAULT_PREVIEW_CONTENT_FG),
|
||||
));
|
||||
}
|
||||
let text = Text::from(lines);
|
||||
Paragraph::new(text)
|
||||
.block(preview_block)
|
||||
.wrap(Wrap { trim: true })
|
||||
}
|
||||
|
||||
fn build_syntect_highlighted_paragraph(
|
||||
highlighted_lines: Vec<Vec<(syntect::highlighting::Style, String)>>,
|
||||
preview_block: Block,
|
||||
target_line: Option<u16>,
|
||||
preview_scroll: Option<u16>,
|
||||
) -> Paragraph {
|
||||
compute_paragraph_from_highlighted_lines(
|
||||
&highlighted_lines,
|
||||
target_line.map(|l| l as usize),
|
||||
)
|
||||
.block(preview_block)
|
||||
.alignment(Alignment::Left)
|
||||
.scroll((preview_scroll.unwrap_or(0), 0))
|
||||
}
|
||||
|
||||
pub fn maybe_init_preview_scroll(
|
||||
&mut self,
|
||||
target_line: Option<u16>,
|
||||
@ -218,18 +313,10 @@ impl Television {
|
||||
}
|
||||
|
||||
pub fn build_meta_preview_paragraph<'a>(
|
||||
&mut self,
|
||||
inner: Rect,
|
||||
message: &str,
|
||||
fill_char: char,
|
||||
) -> Paragraph<'a> {
|
||||
if let Some(paragraph) = self.meta_paragraph_cache.get(&(
|
||||
message.to_string(),
|
||||
inner.width,
|
||||
inner.height,
|
||||
)) {
|
||||
return paragraph.clone();
|
||||
}
|
||||
let message_len = message.len();
|
||||
if message_len + 8 > inner.width as usize {
|
||||
return Paragraph::new(Text::from(EMPTY_STRING));
|
||||
@ -279,12 +366,7 @@ impl Television {
|
||||
}
|
||||
|
||||
// Create a paragraph with the generated content
|
||||
let p = Paragraph::new(Text::from(lines));
|
||||
self.meta_paragraph_cache.insert(
|
||||
(message.to_string(), inner.width, inner.height),
|
||||
p.clone(),
|
||||
);
|
||||
p
|
||||
Paragraph::new(Text::from(lines))
|
||||
}
|
||||
}
|
||||
|
||||
@ -295,18 +377,11 @@ fn build_line_number_span<'a>(line_number: usize) -> Span<'a> {
|
||||
fn compute_paragraph_from_highlighted_lines(
|
||||
highlighted_lines: &[Vec<(syntect::highlighting::Style, String)>],
|
||||
line_specifier: Option<usize>,
|
||||
scroll: u16,
|
||||
preview_pane_height: u16,
|
||||
) -> Paragraph<'static> {
|
||||
let preview_lines: Vec<Line> = highlighted_lines
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, l)| {
|
||||
if i < scroll as usize
|
||||
|| i >= (scroll + preview_pane_height) as usize
|
||||
{
|
||||
return Line::from(Span::raw(EMPTY_STRING));
|
||||
}
|
||||
let line_number =
|
||||
build_line_number_span(i + 1).style(Style::default().fg(
|
||||
if line_specifier.is_some()
|
||||
@ -372,3 +447,11 @@ fn convert_syn_color_to_ratatui_color(
|
||||
) -> Color {
|
||||
Color::Rgb(color.r, color.g, color.b)
|
||||
}
|
||||
|
||||
fn compute_cache_key(entry: &Entry) -> String {
|
||||
let mut cache_key = entry.name.clone();
|
||||
if let Some(line_number) = entry.line_number {
|
||||
cache_key.push_str(&line_number.to_string());
|
||||
}
|
||||
cache_key
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user