mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-21 10:50:07 +00:00
refactor(cable)!: cable format redesign
This commit is contained in:
parent
2e99fba9c0
commit
fe931519a9
475
Cargo.lock
generated
475
Cargo.lock
generated
@ -32,6 +32,21 @@ version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anes"
|
||||
version = "0.1.6"
|
||||
@ -153,6 +168,15 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.17.0"
|
||||
@ -186,12 +210,36 @@ dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ciborium"
|
||||
version = "0.2.2"
|
||||
@ -221,9 +269,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.36"
|
||||
version = "4.5.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04"
|
||||
checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@ -231,9 +279,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.36"
|
||||
version = "4.5.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5"
|
||||
checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@ -311,6 +359,30 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "criterion"
|
||||
version = "0.5.1"
|
||||
@ -407,6 +479,16 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.11"
|
||||
@ -451,6 +533,16 @@ dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "directories"
|
||||
version = "6.0.0"
|
||||
@ -523,6 +615,16 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
@ -596,6 +698,16 @@ dependencies = [
|
||||
"pin-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.15"
|
||||
@ -658,6 +770,23 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||
|
||||
[[package]]
|
||||
name = "human-panic"
|
||||
version = "2.0.2"
|
||||
@ -674,6 +803,30 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.63"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
@ -1007,6 +1160,57 @@ version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||
|
||||
[[package]]
|
||||
name = "pest"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"thiserror 2.0.12",
|
||||
"ucd-trie",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_derive"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_generator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_generator"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_meta",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_meta"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"pest",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.16"
|
||||
@ -1183,6 +1387,20 @@ version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom 0.2.15",
|
||||
"libc",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "roff"
|
||||
version = "0.2.2"
|
||||
@ -1227,6 +1445,50 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
|
||||
dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.20"
|
||||
@ -1295,6 +1557,17 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
@ -1304,6 +1577,12 @@ dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.17"
|
||||
@ -1362,6 +1641,32 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "string_pipeline"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0af3613597e31606b54dd5d62be86b8f50922b40d2b7d3d145146caf5c154c05"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"clap_mangen",
|
||||
"once_cell",
|
||||
"pest",
|
||||
"pest_derive",
|
||||
"regex",
|
||||
"serde_json",
|
||||
"strip-ansi-escapes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strip-ansi-escapes"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025"
|
||||
dependencies = [
|
||||
"vte",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
@ -1390,6 +1695,12 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.100"
|
||||
@ -1423,13 +1734,17 @@ dependencies = [
|
||||
"ratatui",
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"signal-hook",
|
||||
"string_pipeline",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"toml",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"unicode-width 0.2.0",
|
||||
"ureq",
|
||||
"walkdir",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
@ -1630,6 +1945,18 @@ dependencies = [
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
|
||||
|
||||
[[package]]
|
||||
name = "ucd-trie"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
@ -1665,6 +1992,48 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7a3e9af6113ecd57b8c63d3cd76a385b2e3881365f1f489e54f49801d0c83ea"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"flate2",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"rustls",
|
||||
"rustls-pemfile",
|
||||
"rustls-pki-types",
|
||||
"ureq-proto",
|
||||
"utf-8",
|
||||
"webpki-roots 0.26.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ureq-proto"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fadf18427d33828c311234884b7ba2afb57143e6e7e69fda7ee883b624661e36"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf-8"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
@ -1686,6 +2055,21 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "vte"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
@ -1779,6 +2163,24 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
|
||||
dependencies = [
|
||||
"webpki-roots 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
@ -1810,6 +2212,65 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
@ -1909,3 +2370,9 @@ checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
||||
|
@ -53,6 +53,10 @@ lazy-regex = { version = "3.4.1", features = [
|
||||
"lite",
|
||||
], default-features = false }
|
||||
ansi-to-tui = "7.0.0"
|
||||
walkdir = "2.5.0"
|
||||
string_pipeline = "0.11.1"
|
||||
ureq = "3.0.11"
|
||||
serde_json = "1.0.140"
|
||||
|
||||
|
||||
# target specific dependencies
|
||||
|
@ -8,12 +8,11 @@ use ratatui::layout::Rect;
|
||||
use ratatui::prelude::{Line, Style};
|
||||
use ratatui::style::Color;
|
||||
use ratatui::widgets::{Block, BorderType, Borders, ListDirection, Padding};
|
||||
use television::channels::prototypes::ChannelPrototype;
|
||||
use television::{
|
||||
action::Action,
|
||||
channels::{
|
||||
entry::{Entry, into_ranges},
|
||||
prototypes::Cable,
|
||||
},
|
||||
cable::Cable,
|
||||
channels::entry::{Entry, into_ranges},
|
||||
config::{Config, ConfigEnv},
|
||||
screen::{colors::ResultsColorscheme, results::build_results_list},
|
||||
television::Television,
|
||||
@ -27,10 +26,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
// I don't know how exactly right now just having it here instead
|
||||
let entries = [
|
||||
Entry {
|
||||
name: "typeshed/LICENSE".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/LICENSE".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{f016}',
|
||||
color: "#7e8e91",
|
||||
@ -38,10 +36,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/README.md".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/README.md".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{f48a}',
|
||||
color: "#dddddd",
|
||||
@ -49,10 +46,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/stdlib/re.pyi".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/stdlib/re.pyi".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{e606}',
|
||||
color: "#ffbc03",
|
||||
@ -60,10 +56,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/stdlib/io.pyi".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/stdlib/io.pyi".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{e606}',
|
||||
color: "#ffbc03",
|
||||
@ -71,10 +66,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/stdlib/gc.pyi".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/stdlib/gc.pyi".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{e606}',
|
||||
color: "#ffbc03",
|
||||
@ -82,10 +76,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/stdlib/uu.pyi".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/stdlib/uu.pyi".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{e606}',
|
||||
color: "#ffbc03",
|
||||
@ -93,10 +86,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/stdlib/nt.pyi".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/stdlib/nt.pyi".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{e606}',
|
||||
color: "#ffbc03",
|
||||
@ -104,10 +96,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/stdlib/dis.pyi".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/stdlib/dis.pyi".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{e606}',
|
||||
color: "#ffbc03",
|
||||
@ -115,10 +106,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/stdlib/imp.pyi".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/stdlib/imp.pyi".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{e606}',
|
||||
color: "#ffbc03",
|
||||
@ -126,10 +116,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/stdlib/bdb.pyi".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/stdlib/bdb.pyi".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{e606}',
|
||||
color: "#ffbc03",
|
||||
@ -137,10 +126,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/stdlib/abc.pyi".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/stdlib/abc.pyi".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{e606}',
|
||||
color: "#ffbc03",
|
||||
@ -148,10 +136,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/stdlib/cgi.pyi".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/stdlib/cgi.pyi".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{e606}',
|
||||
color: "#ffbc03",
|
||||
@ -159,10 +146,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/stdlib/bz2.pyi".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/stdlib/bz2.pyi".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{e606}',
|
||||
color: "#ffbc03",
|
||||
@ -170,10 +156,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/stdlib/grp.pyi".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/stdlib/grp.pyi".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{e606}',
|
||||
color: "#ffbc03",
|
||||
@ -181,10 +166,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/stdlib/ast.pyi".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/stdlib/ast.pyi".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{e606}',
|
||||
color: "#ffbc03",
|
||||
@ -192,10 +176,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/stdlib/csv.pyi".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/stdlib/csv.pyi".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{e606}',
|
||||
color: "#ffbc03",
|
||||
@ -203,10 +186,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/stdlib/pdb.pyi".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/stdlib/pdb.pyi".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{e606}',
|
||||
color: "#ffbc03",
|
||||
@ -214,10 +196,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/stdlib/pwd.pyi".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/stdlib/pwd.pyi".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{e606}',
|
||||
color: "#ffbc03",
|
||||
@ -225,10 +206,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/stdlib/ssl.pyi".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/stdlib/ssl.pyi".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{e606}',
|
||||
color: "#ffbc03",
|
||||
@ -236,10 +216,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/stdlib/tty.pyi".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/stdlib/tty.pyi".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{e606}',
|
||||
color: "#ffbc03",
|
||||
@ -247,10 +226,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/stdlib/nis.pyi".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/stdlib/nis.pyi".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{e606}',
|
||||
color: "#ffbc03",
|
||||
@ -258,10 +236,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/stdlib/pty.pyi".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/stdlib/pty.pyi".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{e606}',
|
||||
color: "#ffbc03",
|
||||
@ -269,10 +246,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/stdlib/cmd.pyi".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/stdlib/cmd.pyi".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{e606}',
|
||||
color: "#ffbc03",
|
||||
@ -280,10 +256,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/tests/utils.py".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/tests/utils.py".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{e606}',
|
||||
color: "#ffbc03",
|
||||
@ -291,10 +266,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/pyproject.toml".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/pyproject.toml".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{e6b2}',
|
||||
color: "#9c4221",
|
||||
@ -302,10 +276,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/MAINTAINERS.md".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/MAINTAINERS.md".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{f48a}',
|
||||
color: "#dddddd",
|
||||
@ -313,10 +286,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/stdlib/enum.pyi".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/stdlib/enum.pyi".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{e606}',
|
||||
color: "#ffbc03",
|
||||
@ -324,10 +296,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/stdlib/hmac.pyi".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/stdlib/hmac.pyi".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{e606}',
|
||||
color: "#ffbc03",
|
||||
@ -335,10 +306,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/stdlib/uuid.pyi".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/stdlib/uuid.pyi".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{e606}',
|
||||
color: "#ffbc03",
|
||||
@ -346,10 +316,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/stdlib/glob.pyi".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/stdlib/glob.pyi".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{e606}',
|
||||
color: "#ffbc03",
|
||||
@ -357,10 +326,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/stdlib/_ast.pyi".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/stdlib/_ast.pyi".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{e606}',
|
||||
color: "#ffbc03",
|
||||
@ -368,10 +336,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/stdlib/_csv.pyi".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/stdlib/_csv.pyi".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{e606}',
|
||||
color: "#ffbc03",
|
||||
@ -379,10 +346,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/stdlib/code.pyi".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/stdlib/code.pyi".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{e606}',
|
||||
color: "#ffbc03",
|
||||
@ -390,10 +356,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/stdlib/spwd.pyi".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/stdlib/spwd.pyi".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{e606}',
|
||||
color: "#ffbc03",
|
||||
@ -401,10 +366,9 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/stdlib/_msi.pyi".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/stdlib/_msi.pyi".to_string(),
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{e606}',
|
||||
color: "#ffbc03",
|
||||
@ -412,8 +376,8 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
line_number: None,
|
||||
},
|
||||
Entry {
|
||||
name: "typeshed/stdlib/time.pyi".to_string(),
|
||||
value: None,
|
||||
display: None,
|
||||
raw: "typeshed/stdlib/time.pyi".to_string(),
|
||||
|
||||
icon: Some(FileIcon {
|
||||
icon: '\u{e606}',
|
||||
@ -421,7 +385,6 @@ pub fn draw_results_list(c: &mut Criterion) {
|
||||
}),
|
||||
line_number: None,
|
||||
name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
|
||||
value_match_ranges: None,
|
||||
},
|
||||
];
|
||||
|
||||
@ -464,7 +427,9 @@ pub fn draw(c: &mut Criterion) {
|
||||
|
||||
let rt = Runtime::new().unwrap();
|
||||
|
||||
let cable = Cable::default();
|
||||
let cable = Cable::from_prototypes(vec![ChannelPrototype::new(
|
||||
"files", "fd -t f",
|
||||
)]);
|
||||
|
||||
c.bench_function("draw", |b| {
|
||||
b.to_async(&rt).iter_batched(
|
||||
@ -478,13 +443,13 @@ pub fn draw(c: &mut Criterion) {
|
||||
// Wait for the channel to finish loading
|
||||
let mut tv = Television::new(
|
||||
tx,
|
||||
&channel_prototype,
|
||||
channel_prototype,
|
||||
config,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
Cable::default(),
|
||||
cable.clone(),
|
||||
);
|
||||
tv.find("television");
|
||||
for _ in 0..5 {
|
||||
|
40
cable/unix/files.toml
Normal file
40
cable/unix/files.toml
Normal file
@ -0,0 +1,40 @@
|
||||
[metadata]
|
||||
name = "files"
|
||||
description = "A channel to select files and directories"
|
||||
requirements = ["fd", "bat"]
|
||||
|
||||
[source]
|
||||
command = "fd -t f"
|
||||
interactive = false
|
||||
# env
|
||||
display = "{}" # show the full path
|
||||
output = "{}" # output the full path
|
||||
|
||||
[preview]
|
||||
command = "bat -n --color=always {}"
|
||||
env = { "BAT_THEME" = "ansi" }
|
||||
|
||||
[ui]
|
||||
layout = "landscape"
|
||||
ui_scale = 100
|
||||
show_help_bar = false
|
||||
show_preview_panel = true
|
||||
input_bar_position = "bottom"
|
||||
|
||||
[keybindings]
|
||||
quit = ["esc", "ctrl-c"]
|
||||
select_next_entry = ["down", "ctrl-n", "ctrl-j"]
|
||||
select_prev_entry = ["up", "ctrl-p", "ctrl-k"]
|
||||
confirm_selection = "enter"
|
||||
|
||||
# [actions.'open in $EDITOR']
|
||||
# command = "$EDITOR {}"
|
||||
# mode = "become" # "become" / "spawn" / "transform"
|
||||
#
|
||||
# [actions.'remove']
|
||||
# command = "rm {}"
|
||||
# mode = "spawn"
|
||||
#
|
||||
# [actions.'rename']
|
||||
# command = "read -p \"New name: \" new_name && mv {} $new_name"
|
||||
# mode = "spawn"
|
37
cable/unix/text.toml
Normal file
37
cable/unix/text.toml
Normal file
@ -0,0 +1,37 @@
|
||||
[metadata]
|
||||
name = "text"
|
||||
description = "A channel to find and select text from files"
|
||||
requirements = ["rg", "bat"]
|
||||
|
||||
[source]
|
||||
command = "rg . --no-heading --line-number"
|
||||
|
||||
[preview]
|
||||
command = "bat -n --color=always {split:\\::0}"
|
||||
env = { "BAT_THEME" = "ansi" }
|
||||
offset = "{split:\\::1}"
|
||||
|
||||
[ui]
|
||||
layout = "landscape"
|
||||
ui_scale = 100
|
||||
show_help_bar = false
|
||||
show_preview_panel = true
|
||||
input_bar_position = "bottom"
|
||||
|
||||
[keybindings]
|
||||
quit = ["esc", "ctrl-c"]
|
||||
select_next_entry = ["down", "ctrl-n", "ctrl-j"]
|
||||
select_prev_entry = ["up", "ctrl-p", "ctrl-k"]
|
||||
confirm_selection = "enter"
|
||||
|
||||
# [actions.'open in $EDITOR']
|
||||
# command = "$EDITOR {}"
|
||||
# mode = "become" # "become" / "spawn" / "transform"
|
||||
#
|
||||
# [actions.'remove']
|
||||
# command = "rm {}"
|
||||
# mode = "spawn"
|
||||
#
|
||||
# [actions.'rename']
|
||||
# command = "read -p \"New name: \" new_name && mv {} $new_name"
|
||||
# mode = "spawn"
|
@ -6,10 +6,8 @@ use tracing::{debug, trace};
|
||||
|
||||
use crate::{
|
||||
action::Action,
|
||||
channels::{
|
||||
entry::Entry,
|
||||
prototypes::{Cable, ChannelPrototype},
|
||||
},
|
||||
cable::Cable,
|
||||
channels::{entry::Entry, prototypes::ChannelPrototype},
|
||||
config::{Config, default_tick_rate},
|
||||
event::{Event, EventLoop, Key},
|
||||
keymap::Keymap,
|
||||
@ -137,11 +135,11 @@ const ACTION_BUF_SIZE: usize = 8;
|
||||
|
||||
impl App {
|
||||
pub fn new(
|
||||
channel_prototype: &ChannelPrototype,
|
||||
channel_prototype: ChannelPrototype,
|
||||
config: Config,
|
||||
input: Option<String>,
|
||||
options: AppOptions,
|
||||
cable_channels: &Cable,
|
||||
cable_channels: Cable,
|
||||
) -> Self {
|
||||
let (action_tx, action_rx) = mpsc::unbounded_channel();
|
||||
let (render_tx, render_rx) = mpsc::unbounded_channel();
|
||||
@ -159,7 +157,7 @@ impl App {
|
||||
options.no_remote,
|
||||
options.no_help,
|
||||
options.exact,
|
||||
cable_channels.clone(),
|
||||
cable_channels,
|
||||
);
|
||||
|
||||
Self {
|
||||
|
@ -1,15 +1,53 @@
|
||||
use std::path::PathBuf;
|
||||
use std::{
|
||||
ops::Deref,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use tracing::{debug, error};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use crate::{
|
||||
channels::prototypes::{Cable, ChannelPrototype},
|
||||
channels::prototypes::ChannelPrototype, cli::unknown_channel_exit,
|
||||
config::get_config_dir,
|
||||
};
|
||||
|
||||
/// A neat `HashMap` of channel prototypes indexed by their name.
|
||||
///
|
||||
/// This is used to store cable channel prototypes throughout the application
|
||||
/// in a way that facilitates answering questions like "what's the prototype
|
||||
/// for `files`?" or "does this channel exist?".
|
||||
#[derive(Debug, serde::Deserialize, Clone)]
|
||||
pub struct Cable(pub FxHashMap<String, ChannelPrototype>);
|
||||
|
||||
impl Deref for Cable {
|
||||
type Target = FxHashMap<String, ChannelPrototype>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Cable {
|
||||
pub fn get_channel(&self, name: &str) -> ChannelPrototype {
|
||||
self.get(name)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| unknown_channel_exit(name))
|
||||
}
|
||||
|
||||
pub fn has_channel(&self, name: &str) -> bool {
|
||||
self.contains_key(name)
|
||||
}
|
||||
|
||||
pub fn from_prototypes(prototypes: Vec<ChannelPrototype>) -> Self {
|
||||
let mut map = FxHashMap::default();
|
||||
for prototype in prototypes {
|
||||
map.insert(prototype.metadata.name.clone(), prototype);
|
||||
}
|
||||
Cable(map)
|
||||
}
|
||||
}
|
||||
|
||||
/// Just a proxy struct to deserialize prototypes
|
||||
#[derive(Debug, serde::Deserialize, Default)]
|
||||
pub struct CableSpec {
|
||||
@ -17,106 +55,99 @@ pub struct CableSpec {
|
||||
pub prototypes: Vec<ChannelPrototype>,
|
||||
}
|
||||
|
||||
const CABLE_FILE_NAME_SUFFIX: &str = "channels";
|
||||
const CABLE_FILE_FORMAT: &str = "toml";
|
||||
pub const CHANNEL_FILE_FORMAT: &str = "toml";
|
||||
/// ```ignore
|
||||
/// config_folder/
|
||||
/// ├── config.toml
|
||||
/// └── cable/
|
||||
/// ├── channel_1.toml
|
||||
/// ├── channel_2.toml
|
||||
/// └── ...
|
||||
/// ```
|
||||
pub const CABLE_DIR_NAME: &str = "cable";
|
||||
|
||||
#[cfg(unix)]
|
||||
const DEFAULT_CABLE_CHANNELS: &str =
|
||||
include_str!("../cable/unix-channels.toml");
|
||||
fn get_cable_files<P>(cable_dir: P) -> Vec<PathBuf>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
WalkDir::new(cable_dir)
|
||||
.into_iter()
|
||||
.map(|e| e.unwrap().path().to_owned())
|
||||
.filter(|p| {
|
||||
p.is_file()
|
||||
&& p.extension().is_some()
|
||||
&& p.extension().unwrap() == CHANNEL_FILE_FORMAT
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
const DEFAULT_CABLE_CHANNELS: &str =
|
||||
include_str!("../cable/windows-channels.toml");
|
||||
fn load_prototypes<I>(cable_files: I) -> Vec<ChannelPrototype>
|
||||
where
|
||||
I: IntoIterator<Item = PathBuf>,
|
||||
{
|
||||
cable_files
|
||||
.into_iter()
|
||||
.filter_map(|p| match std::fs::read_to_string(&p) {
|
||||
Ok(content) => {
|
||||
match toml::from_str::<ChannelPrototype>(&content) {
|
||||
Ok(prototype) => {
|
||||
debug!(
|
||||
"Loaded cable channel prototype from {:?}: {}",
|
||||
p, prototype.metadata.name
|
||||
);
|
||||
Some(prototype)
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Failed to parse cable channel file {:?}: {}",
|
||||
p, e
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to read cable channel file {:?}: {}", p, e);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Load the cable configuration from the config directory.
|
||||
/// Load cable channels from the config directory.
|
||||
///
|
||||
/// Cable is loaded by compiling all files that match the following
|
||||
/// pattern in the config directory: `*channels.toml`.
|
||||
/// Cable is loaded by compiling all files located in the `cable/` subdirectory
|
||||
/// of the user's configuration directory (+ defaults)
|
||||
///
|
||||
/// # Example:
|
||||
/// ```ignore
|
||||
/// config_folder/
|
||||
/// ├── cable_channels.toml
|
||||
/// ├── my_channels.toml
|
||||
/// └── windows_channels.toml
|
||||
/// ├── config.toml
|
||||
/// └── cable/
|
||||
/// ├── channel_1.toml
|
||||
/// ├── channel_2.toml
|
||||
/// └── ...
|
||||
/// ```
|
||||
pub fn load_cable() -> Result<Cable> {
|
||||
let config_dir = get_config_dir();
|
||||
pub fn load_cable() -> Option<Cable> {
|
||||
let cable_dir = get_config_dir().join(CABLE_DIR_NAME);
|
||||
debug!("Cable directory: {}", cable_dir.to_string_lossy());
|
||||
let cable_files = get_cable_files(&cable_dir);
|
||||
debug!("Found cable channel files: {:?}", cable_files);
|
||||
|
||||
// list all files in the config directory
|
||||
let files = std::fs::read_dir(&config_dir)?;
|
||||
if cable_files.is_empty() {
|
||||
println!(
|
||||
"It seems you don't have any cable channels configured yet.
|
||||
Run `tv update-channels` to get the latest default cable channels and/or add your own in `{}`.
|
||||
|
||||
// filter the files that match the pattern
|
||||
let file_paths: Vec<PathBuf> = files
|
||||
.filter_map(|f| f.ok().map(|f| f.path()))
|
||||
.filter(|p| is_cable_file_format(p) && p.is_file())
|
||||
.collect();
|
||||
|
||||
debug!("Found cable channel files: {:?}", file_paths);
|
||||
if file_paths.is_empty() {
|
||||
debug!("No user defined cable channels found");
|
||||
More info: https://github.com/alexpasmantier/television/blob/main/README.md",
|
||||
cable_dir.to_string_lossy()
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
let default_prototypes =
|
||||
toml::from_str::<CableSpec>(DEFAULT_CABLE_CHANNELS)
|
||||
.expect("Failed to parse default cable channels");
|
||||
let prototypes = load_prototypes(cable_files);
|
||||
|
||||
let prototypes = file_paths.iter().fold(
|
||||
Vec::<ChannelPrototype>::new(),
|
||||
|mut acc, p| {
|
||||
match toml::from_str::<CableSpec>(
|
||||
&std::fs::read_to_string(p)
|
||||
.expect("Unable to read configuration file"),
|
||||
) {
|
||||
Ok(pts) => acc.extend(pts.prototypes),
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Failed to parse cable channel file {:?}: {}",
|
||||
p, e
|
||||
);
|
||||
}
|
||||
}
|
||||
acc
|
||||
},
|
||||
);
|
||||
debug!("Loaded {} cable channels", prototypes.len());
|
||||
|
||||
debug!("Loaded {} custom cable channels", prototypes.len());
|
||||
if prototypes.is_empty() {
|
||||
debug!("No custom cable channels found");
|
||||
}
|
||||
|
||||
let mut cable_channels = FxHashMap::default();
|
||||
// custom prototypes take precedence over default ones
|
||||
for prototype in default_prototypes
|
||||
.prototypes
|
||||
.into_iter()
|
||||
.chain(prototypes.into_iter())
|
||||
{
|
||||
cable_channels.insert(prototype.name.clone(), prototype);
|
||||
}
|
||||
Ok(Cable(cable_channels))
|
||||
}
|
||||
|
||||
fn is_cable_file_format<P>(p: P) -> bool
|
||||
where
|
||||
P: AsRef<std::path::Path>,
|
||||
{
|
||||
let p = p.as_ref();
|
||||
p.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.map_or_else(|| false, |s| s.ends_with(CABLE_FILE_NAME_SUFFIX))
|
||||
&& p.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.map_or_else(|| false, |e| e.to_lowercase() == CABLE_FILE_FORMAT)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_is_cable_file() {
|
||||
let path = std::path::Path::new("cable_channels.toml");
|
||||
assert!(is_cable_file_format(path));
|
||||
}
|
||||
Some(Cable::from_prototypes(prototypes))
|
||||
}
|
||||
|
@ -5,47 +5,28 @@ use std::process::Stdio;
|
||||
use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::channels::{
|
||||
entry::Entry, preview::PreviewCommand, prototypes::ChannelPrototype,
|
||||
};
|
||||
use crate::channels::prototypes::SourceSpec;
|
||||
use crate::channels::{entry::Entry, prototypes::ChannelPrototype};
|
||||
use crate::matcher::Matcher;
|
||||
use crate::matcher::{config::Config, injector::Injector};
|
||||
use crate::utils::command::shell_command;
|
||||
|
||||
use crate::utils::strings::format_string;
|
||||
|
||||
pub struct Channel {
|
||||
pub name: String,
|
||||
pub prototype: ChannelPrototype,
|
||||
matcher: Matcher<String>,
|
||||
pub preview_command: Option<PreviewCommand>,
|
||||
selected_entries: FxHashSet<Entry>,
|
||||
crawl_handle: tokio::task::JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl Default for Channel {
|
||||
fn default() -> Self {
|
||||
Self::new(&ChannelPrototype::new(
|
||||
"files",
|
||||
"find . -type f",
|
||||
false,
|
||||
Some(PreviewCommand::new("cat {}", ":", None)),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
pub fn new(prototype: &ChannelPrototype) -> Self {
|
||||
pub fn new(prototype: ChannelPrototype) -> Self {
|
||||
let matcher = Matcher::new(Config::default());
|
||||
let injector = matcher.injector();
|
||||
let crawl_handle = tokio::spawn(load_candidates(
|
||||
prototype.source_command.to_string(),
|
||||
prototype.interactive,
|
||||
injector,
|
||||
));
|
||||
let crawl_handle =
|
||||
tokio::spawn(load_candidates(prototype.source.clone(), injector));
|
||||
Self {
|
||||
prototype,
|
||||
matcher,
|
||||
preview_command: prototype.preview_command.clone(),
|
||||
name: prototype.name.to_string(),
|
||||
selected_entries: HashSet::with_hasher(FxBuildHasher),
|
||||
crawl_handle,
|
||||
}
|
||||
@ -61,27 +42,25 @@ impl Channel {
|
||||
.results(num_entries, offset)
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
let path = item.matched_string;
|
||||
Entry::new(path).with_name_match_indices(&item.match_indices)
|
||||
Entry::new(item.inner)
|
||||
.with_display(item.matched_string)
|
||||
.with_match_indices(&item.match_indices)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_result(&self, index: u32) -> Option<Entry> {
|
||||
self.matcher.get_result(index).map(|item| {
|
||||
let name = item.matched_string;
|
||||
if let Some(cmd) = &self.preview_command {
|
||||
if let Some(offset_expr) = &cmd.offset_expr {
|
||||
let offset_string =
|
||||
format_string(offset_expr, &name, &cmd.delimiter);
|
||||
let offset_str = {
|
||||
offset_string
|
||||
.strip_prefix('\'')
|
||||
.and_then(|s| s.strip_suffix('\''))
|
||||
.unwrap_or(&offset_string)
|
||||
};
|
||||
let mut entry = Entry::new(item.inner.clone())
|
||||
.with_display(item.matched_string)
|
||||
.with_match_indices(&item.match_indices);
|
||||
if let Some(p) = &self.prototype.preview {
|
||||
if let Some(offset_expr) = &p.offset {
|
||||
let offset_str = offset_expr
|
||||
.format(&item.inner)
|
||||
.unwrap_or_else(|_| panic!("Failed to format offset expression '{}' with name '{}'", offset_expr.template_string(), item.inner));
|
||||
|
||||
return Entry::new(name).with_line_number(
|
||||
entry = entry.with_line_number(
|
||||
offset_str.parse::<usize>().unwrap_or_else(|_| {
|
||||
panic!(
|
||||
"Failed to parse line number from {}",
|
||||
@ -91,7 +70,7 @@ impl Channel {
|
||||
);
|
||||
}
|
||||
}
|
||||
Entry::new(name)
|
||||
entry
|
||||
})
|
||||
}
|
||||
|
||||
@ -122,23 +101,22 @@ impl Channel {
|
||||
pub fn shutdown(&self) {}
|
||||
|
||||
pub fn supports_preview(&self) -> bool {
|
||||
self.preview_command.is_some()
|
||||
self.prototype.preview.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_async)]
|
||||
async fn load_candidates(
|
||||
command: String,
|
||||
interactive: bool,
|
||||
injector: Injector<String>,
|
||||
) {
|
||||
debug!("Loading candidates from command: {:?}", command);
|
||||
let mut child = shell_command(interactive)
|
||||
.arg(command)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.expect("failed to execute process");
|
||||
async fn load_candidates(source: SourceSpec, injector: Injector<String>) {
|
||||
debug!("Loading candidates from command: {:?}", source.command);
|
||||
let mut child = shell_command(
|
||||
source.command.inner.template_string(),
|
||||
source.command.interactive,
|
||||
&source.command.env,
|
||||
)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.expect("failed to execute process");
|
||||
|
||||
if let Some(out) = child.stdout.take() {
|
||||
let reader = BufReader::new(out);
|
||||
@ -150,13 +128,25 @@ async fn load_candidates(
|
||||
if !l.trim().is_empty() {
|
||||
let () = injector.push(l, |e, cols| {
|
||||
// PERF: maybe we can avoid cloning here by using &Utf32Str
|
||||
cols[0] = e.clone().into();
|
||||
if let Some(display) = &source.display {
|
||||
let formatted = display.format(e).unwrap_or_else(|_| {
|
||||
panic!(
|
||||
"Failed to format display expression '{}' with entry '{}'",
|
||||
display.template_string(),
|
||||
e
|
||||
);
|
||||
});
|
||||
cols[0] = formatted.into();
|
||||
} else {
|
||||
cols[0] = e.clone().into();
|
||||
}
|
||||
});
|
||||
produced_output = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if the command didn't produce any output, check stderr and display that instead
|
||||
if !produced_output {
|
||||
let reader = BufReader::new(child.stderr.take().unwrap());
|
||||
for line in reader.lines() {
|
@ -1,20 +1,15 @@
|
||||
use std::{
|
||||
fmt::Write,
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
use devicons::FileIcon;
|
||||
|
||||
#[derive(Clone, Debug, Eq)]
|
||||
pub struct Entry {
|
||||
/// The name of the entry.
|
||||
pub name: String,
|
||||
/// An optional value associated with the entry.
|
||||
pub value: Option<String>,
|
||||
/// The raw entry (as captured from the source)
|
||||
pub raw: String,
|
||||
/// The actual entry string that will be displayed in the UI.
|
||||
pub display: Option<String>,
|
||||
/// The optional ranges for matching characters in the name.
|
||||
pub name_match_ranges: Option<Vec<(u32, u32)>>,
|
||||
/// The optional ranges for matching characters in the value.
|
||||
pub value_match_ranges: Option<Vec<(u32, u32)>>,
|
||||
/// The optional icon associated with the entry.
|
||||
pub icon: Option<FileIcon>,
|
||||
/// The optional line number associated with the entry.
|
||||
@ -23,7 +18,7 @@ pub struct Entry {
|
||||
|
||||
impl Hash for Entry {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.name.hash(state);
|
||||
self.raw.hash(state);
|
||||
if let Some(line_number) = self.line_number {
|
||||
line_number.hash(state);
|
||||
}
|
||||
@ -32,7 +27,7 @@ impl Hash for Entry {
|
||||
|
||||
impl PartialEq<Entry> for &Entry {
|
||||
fn eq(&self, other: &Entry) -> bool {
|
||||
self.name == other.name
|
||||
self.raw == other.raw
|
||||
&& (self.line_number.is_none() && other.line_number.is_none()
|
||||
|| self.line_number == other.line_number)
|
||||
}
|
||||
@ -40,7 +35,7 @@ impl PartialEq<Entry> for &Entry {
|
||||
|
||||
impl PartialEq<Entry> for Entry {
|
||||
fn eq(&self, other: &Entry) -> bool {
|
||||
self.name == other.name
|
||||
self.raw == other.raw
|
||||
&& (self.line_number.is_none() && other.line_number.is_none()
|
||||
|| self.line_number == other.line_number)
|
||||
}
|
||||
@ -82,9 +77,8 @@ impl Entry {
|
||||
/// use devicons::FileIcon;
|
||||
///
|
||||
/// let entry = Entry::new("name".to_string())
|
||||
/// .with_value("value".to_string())
|
||||
/// .with_name_match_indices(&vec![0])
|
||||
/// .with_value_match_indices(&vec![0])
|
||||
/// .with_display("Display Name".to_string())
|
||||
/// .with_match_indices(&vec![0])
|
||||
/// .with_icon(FileIcon::default())
|
||||
/// .with_line_number(0);
|
||||
/// ```
|
||||
@ -95,32 +89,26 @@ impl Entry {
|
||||
/// # Returns
|
||||
/// A new entry with the given name and preview type.
|
||||
/// The other fields are set to `None` by default.
|
||||
pub fn new(name: String) -> Self {
|
||||
pub fn new(raw: String) -> Self {
|
||||
Self {
|
||||
name,
|
||||
value: None,
|
||||
raw,
|
||||
display: None,
|
||||
name_match_ranges: None,
|
||||
value_match_ranges: None,
|
||||
icon: None,
|
||||
line_number: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_value(mut self, value: String) -> Self {
|
||||
self.value = Some(value);
|
||||
pub fn with_display(mut self, display: String) -> Self {
|
||||
self.display = Some(display);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_name_match_indices(mut self, indices: &[u32]) -> Self {
|
||||
pub fn with_match_indices(mut self, indices: &[u32]) -> Self {
|
||||
self.name_match_ranges = Some(into_ranges(indices));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_value_match_indices(mut self, indices: &[u32]) -> Self {
|
||||
self.value_match_ranges = Some(into_ranges(indices));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_icon(mut self, icon: FileIcon) -> Self {
|
||||
self.icon = Some(icon);
|
||||
self
|
||||
@ -131,12 +119,12 @@ impl Entry {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn display(&self) -> &str {
|
||||
self.display.as_deref().unwrap_or(&self.raw)
|
||||
}
|
||||
|
||||
pub fn stdout_repr(&self) -> String {
|
||||
let mut repr = self.name.clone();
|
||||
if let Some(line_number) = self.line_number {
|
||||
write!(repr, ":{}", line_number).unwrap();
|
||||
}
|
||||
repr
|
||||
self.raw.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@ -171,26 +159,12 @@ mod tests {
|
||||
#[test]
|
||||
fn test_leaves_name_intact() {
|
||||
let entry = Entry {
|
||||
name: "test name with spaces".to_string(),
|
||||
value: None,
|
||||
raw: "test name with spaces".to_string(),
|
||||
display: None,
|
||||
name_match_ranges: None,
|
||||
value_match_ranges: None,
|
||||
icon: None,
|
||||
line_number: None,
|
||||
};
|
||||
assert_eq!(entry.stdout_repr(), "test name with spaces");
|
||||
}
|
||||
#[test]
|
||||
fn test_uses_line_number_information() {
|
||||
let a: usize = 10;
|
||||
let entry = Entry {
|
||||
name: "test_file_name.rs".to_string(),
|
||||
value: None,
|
||||
name_match_ranges: None,
|
||||
value_match_ranges: None,
|
||||
icon: None,
|
||||
line_number: Some(a),
|
||||
};
|
||||
assert_eq!(entry.stdout_repr(), "test_file_name.rs:10");
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
pub mod cable;
|
||||
pub mod channel;
|
||||
pub mod entry;
|
||||
pub mod preview;
|
||||
pub mod prototypes;
|
||||
pub mod remote_control;
|
||||
|
@ -1,125 +0,0 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{channels::entry::Entry, utils::strings::format_string};
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default, Deserialize)]
|
||||
pub struct PreviewCommand {
|
||||
pub command: String,
|
||||
#[serde(default = "default_delimiter")]
|
||||
pub delimiter: String,
|
||||
#[serde(rename = "offset")]
|
||||
pub offset_expr: Option<String>,
|
||||
}
|
||||
|
||||
pub const DEFAULT_DELIMITER: &str = " ";
|
||||
|
||||
/// The default delimiter to use for the preview command to use to split
|
||||
/// entries into multiple referenceable parts.
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn default_delimiter() -> String {
|
||||
DEFAULT_DELIMITER.to_string()
|
||||
}
|
||||
|
||||
impl PreviewCommand {
|
||||
pub fn new(
|
||||
command: &str,
|
||||
delimiter: &str,
|
||||
offset_expr: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
command: command.to_string(),
|
||||
delimiter: delimiter.to_string(),
|
||||
offset_expr,
|
||||
}
|
||||
}
|
||||
|
||||
/// Format the command with the entry name and provided placeholders.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use television::channels::{preview::PreviewCommand, entry::Entry};
|
||||
///
|
||||
/// let command = PreviewCommand {
|
||||
/// command: "something {} {2} {0}".to_string(),
|
||||
/// delimiter: ":".to_string(),
|
||||
/// offset_expr: None,
|
||||
/// };
|
||||
/// let entry = Entry::new("a:given:entry:to:preview".to_string());
|
||||
///
|
||||
/// let formatted_command = command.format_with(&entry);
|
||||
///
|
||||
/// assert_eq!(formatted_command, "something 'a:given:entry:to:preview' 'entry' 'a'");
|
||||
/// ```
|
||||
pub fn format_with(&self, entry: &Entry) -> String {
|
||||
format_string(&self.command, &entry.name, &self.delimiter)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for PreviewCommand {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{self:?}")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::channels::entry::Entry;
|
||||
|
||||
#[test]
|
||||
fn test_format_command() {
|
||||
let command = PreviewCommand {
|
||||
command: "something {} {2} {0}".to_string(),
|
||||
delimiter: ":".to_string(),
|
||||
offset_expr: None,
|
||||
};
|
||||
let entry = Entry::new("an:entry:to:preview".to_string());
|
||||
let formatted_command = command.format_with(&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(),
|
||||
offset_expr: None,
|
||||
};
|
||||
let entry = Entry::new("an:entry:to:preview".to_string());
|
||||
let formatted_command = command.format_with(&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(),
|
||||
offset_expr: None,
|
||||
};
|
||||
let entry = Entry::new("an:entry:to:preview".to_string());
|
||||
let formatted_command = command.format_with(&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(),
|
||||
offset_expr: None,
|
||||
};
|
||||
let entry = Entry::new("an:entry:to:preview".to_string());
|
||||
let formatted_command = command.format_with(&entry);
|
||||
|
||||
assert_eq!(formatted_command, "something 'an' -t 'to'");
|
||||
}
|
||||
}
|
@ -1,149 +1,400 @@
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::{
|
||||
fmt::{self, Display, Formatter},
|
||||
ops::Deref,
|
||||
};
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
|
||||
use crate::{
|
||||
cable::CableSpec, channels::preview::PreviewCommand,
|
||||
cli::unknown_channel_exit,
|
||||
config::KeyBindings,
|
||||
screen::layout::{InputPosition, Orientation},
|
||||
};
|
||||
use rustc_hash::FxHashMap;
|
||||
use string_pipeline::MultiTemplate;
|
||||
|
||||
/// A prototype for cable channels.
|
||||
///
|
||||
/// This can be seen as a cable channel specification, which is used to
|
||||
/// create a cable channel.
|
||||
///
|
||||
/// The prototype contains the following fields:
|
||||
/// - `name`: The name of the channel. This will be used to identify the
|
||||
/// channel throughout the application and in UI menus.
|
||||
/// - `source_command`: The command to run to get the source for the channel.
|
||||
/// This is a shell command that will be run in the background.
|
||||
/// - `interactive`: Whether the source command should be run in an interactive
|
||||
/// shell. This is useful for commands that need the user's environment e.g.
|
||||
/// `alias`.
|
||||
/// - `preview_command`: The command to run on each entry to get the preview
|
||||
/// for the channel. If this is not `None`, the channel will display a preview
|
||||
/// pane with the output of this command.
|
||||
/// - `preview_delimiter`: The delimiter to use to split an entry into
|
||||
/// multiple parts that can then be referenced in the preview command (e.g.
|
||||
/// `{1} + {2}`).
|
||||
/// - `preview_offset`: a litteral expression that will be interpreted later on
|
||||
/// in order to determine the vertical offset at which the preview should be
|
||||
/// displayed.
|
||||
///
|
||||
/// # Example
|
||||
/// The default files channel might look something like this:
|
||||
/// ```toml
|
||||
/// [[cable_channel]]
|
||||
/// name = "files"
|
||||
/// source_command = "fd -t f"
|
||||
/// preview_command = "cat {}"
|
||||
/// ```
|
||||
#[derive(Clone, Debug, serde::Deserialize, PartialEq)]
|
||||
pub struct ChannelPrototype {
|
||||
pub name: String,
|
||||
pub source_command: String,
|
||||
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
||||
pub struct CommandSpec {
|
||||
#[serde(
|
||||
rename = "command",
|
||||
deserialize_with = "deserialize_template",
|
||||
serialize_with = "serialize_template"
|
||||
)]
|
||||
pub inner: MultiTemplate,
|
||||
#[serde(default)]
|
||||
pub interactive: bool,
|
||||
#[serde(rename = "preview")]
|
||||
pub preview_command: Option<PreviewCommand>,
|
||||
#[serde(default)]
|
||||
pub env: FxHashMap<String, String>,
|
||||
}
|
||||
|
||||
const STDIN_CHANNEL_NAME: &str = "stdin";
|
||||
const STDIN_SOURCE_COMMAND: &str = "cat";
|
||||
impl Display for CommandSpec {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.inner.template_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl ChannelPrototype {
|
||||
impl CommandSpec {
|
||||
pub fn new(
|
||||
name: &str,
|
||||
source_command: &str,
|
||||
inner: MultiTemplate,
|
||||
interactive: bool,
|
||||
preview_command: Option<PreviewCommand>,
|
||||
env: FxHashMap<String, String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
source_command: source_command.to_string(),
|
||||
inner,
|
||||
interactive,
|
||||
preview_command,
|
||||
env,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stdin(preview: Option<PreviewCommand>) -> Self {
|
||||
fn serialize_template<S>(
|
||||
command: &MultiTemplate,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let raw = command.template_string();
|
||||
serializer.serialize_str(raw)
|
||||
}
|
||||
|
||||
#[allow(clippy::ref_option)]
|
||||
fn serialize_maybe_template<S>(
|
||||
command: &Option<MultiTemplate>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match command {
|
||||
Some(template) => serialize_template(template, serializer),
|
||||
None => serializer.serialize_none(),
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_template<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<MultiTemplate, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let raw: String = serde::Deserialize::deserialize(deserializer)?;
|
||||
MultiTemplate::parse(&raw).map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
fn deserialize_maybe_template<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<MultiTemplate>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let raw: Option<String> = serde::Deserialize::deserialize(deserializer)?;
|
||||
match raw {
|
||||
Some(template) => MultiTemplate::parse(&template)
|
||||
.map(Some)
|
||||
.map_err(serde::de::Error::custom),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
||||
pub struct ChannelPrototype {
|
||||
pub metadata: Metadata,
|
||||
#[serde(rename = "source")]
|
||||
pub source: SourceSpec,
|
||||
#[serde(default, rename = "preview")]
|
||||
pub preview: Option<PreviewSpec>,
|
||||
#[serde(default, rename = "ui")]
|
||||
pub ui: Option<UiSpec>,
|
||||
#[serde(default)]
|
||||
pub keybindings: Option<KeyBindings>,
|
||||
// actions: Vec<Action>,
|
||||
}
|
||||
|
||||
impl ChannelPrototype {
|
||||
// FIXME: is this really needed? maybe tests should use a toml spec directly
|
||||
pub fn new(name: &str, source_command: &str) -> Self {
|
||||
Self {
|
||||
name: STDIN_CHANNEL_NAME.to_string(),
|
||||
source_command: STDIN_SOURCE_COMMAND.to_string(),
|
||||
interactive: false,
|
||||
preview_command: preview,
|
||||
metadata: Metadata {
|
||||
name: name.to_string(),
|
||||
description: Some(String::new()),
|
||||
requirements: vec![],
|
||||
},
|
||||
source: SourceSpec {
|
||||
command: CommandSpec {
|
||||
inner: MultiTemplate::parse(source_command).expect(
|
||||
"Failed to parse source command MultiTemplate",
|
||||
),
|
||||
interactive: false,
|
||||
env: FxHashMap::default(),
|
||||
},
|
||||
display: None,
|
||||
output: None,
|
||||
},
|
||||
preview: None,
|
||||
ui: None,
|
||||
keybindings: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_preview(self, preview_command: Option<PreviewCommand>) -> Self {
|
||||
Self::new(
|
||||
&self.name,
|
||||
&self.source_command,
|
||||
self.interactive,
|
||||
preview_command,
|
||||
)
|
||||
pub fn stdin(preview: Option<PreviewSpec>) -> Self {
|
||||
Self {
|
||||
metadata: Metadata {
|
||||
name: "stdin".to_string(),
|
||||
description: Some(
|
||||
"A channel that reads from stdin".to_string(),
|
||||
),
|
||||
requirements: vec![],
|
||||
},
|
||||
source: SourceSpec {
|
||||
command: CommandSpec {
|
||||
inner: MultiTemplate::parse("cat").unwrap(),
|
||||
interactive: false,
|
||||
env: FxHashMap::default(),
|
||||
},
|
||||
display: None,
|
||||
output: None,
|
||||
},
|
||||
preview,
|
||||
ui: None,
|
||||
keybindings: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_preview(mut self, preview: Option<PreviewSpec>) -> Self {
|
||||
self.preview = preview;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ChannelPrototype {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.metadata.name)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Metadata {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
requirements: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
||||
pub struct SourceSpec {
|
||||
#[serde(flatten)]
|
||||
pub command: CommandSpec,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "deserialize_maybe_template",
|
||||
serialize_with = "serialize_maybe_template"
|
||||
)]
|
||||
pub display: Option<MultiTemplate>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "deserialize_maybe_template",
|
||||
serialize_with = "serialize_maybe_template"
|
||||
)]
|
||||
pub output: Option<MultiTemplate>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
||||
pub struct PreviewSpec {
|
||||
#[serde(flatten)]
|
||||
pub command: CommandSpec,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "deserialize_maybe_template",
|
||||
serialize_with = "serialize_maybe_template"
|
||||
)]
|
||||
pub offset: Option<MultiTemplate>,
|
||||
}
|
||||
|
||||
impl PreviewSpec {
|
||||
pub fn new(command: CommandSpec, offset: Option<MultiTemplate>) -> Self {
|
||||
Self { command, offset }
|
||||
}
|
||||
|
||||
pub fn from_str_command(command: &str) -> Self {
|
||||
Self {
|
||||
command: CommandSpec {
|
||||
inner: MultiTemplate::parse(command)
|
||||
.expect("Failed to parse preview command"),
|
||||
interactive: false,
|
||||
env: FxHashMap::default(),
|
||||
},
|
||||
offset: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
||||
pub struct UiSpec {
|
||||
#[serde(default)]
|
||||
pub ui_scale: Option<u16>,
|
||||
#[serde(default)]
|
||||
pub show_help_bar: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub show_preview_panel: Option<bool>,
|
||||
// `layout` is clearer for the user but collides with the overall `Layout` type.
|
||||
#[serde(rename = "layout", default)]
|
||||
pub orientation: Option<Orientation>,
|
||||
#[serde(default)]
|
||||
pub input_bar_position: Option<InputPosition>,
|
||||
}
|
||||
|
||||
pub const DEFAULT_PROTOTYPE_NAME: &str = "files";
|
||||
|
||||
impl Display for ChannelPrototype {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
/// A neat `HashMap` of channel prototypes indexed by their name.
|
||||
///
|
||||
/// This is used to store cable channel prototypes throughout the application
|
||||
/// in a way that facilitates answering questions like "what's the prototype
|
||||
/// for `files`?" or "does this channel exist?".
|
||||
#[derive(Debug, serde::Deserialize, Clone)]
|
||||
pub struct Cable(pub FxHashMap<String, ChannelPrototype>);
|
||||
|
||||
impl Deref for Cable {
|
||||
type Target = FxHashMap<String, ChannelPrototype>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Cable {
|
||||
pub fn get_channel(&self, name: &str) -> ChannelPrototype {
|
||||
self.get(name)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| unknown_channel_exit(name))
|
||||
}
|
||||
|
||||
pub fn has_channel(&self, name: &str) -> bool {
|
||||
self.contains_key(name)
|
||||
}
|
||||
}
|
||||
|
||||
/// A default cable channels specification that is compiled into the
|
||||
/// application.
|
||||
#[cfg(unix)]
|
||||
const DEFAULT_CABLE_CHANNELS_FILE: &str =
|
||||
include_str!("../../cable/unix-channels.toml");
|
||||
/// A default cable channels specification that is compiled into the
|
||||
/// application.
|
||||
#[cfg(not(unix))]
|
||||
const DEFAULT_CABLE_CHANNELS_FILE: &str =
|
||||
include_str!("../../cable/windows-channels.toml");
|
||||
|
||||
impl Default for Cable {
|
||||
/// Fallback to the default cable channels specification (the template file
|
||||
/// included in the repo).
|
||||
fn default() -> Self {
|
||||
let s = toml::from_str::<CableSpec>(DEFAULT_CABLE_CHANNELS_FILE)
|
||||
.expect("Unable to parse default cable channels");
|
||||
let mut prototypes = FxHashMap::default();
|
||||
for prototype in s.prototypes {
|
||||
prototypes.insert(prototype.name.clone(), prototype);
|
||||
}
|
||||
Cable(prototypes)
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{action::Action, config::Binding, event::Key};
|
||||
|
||||
use super::*;
|
||||
use toml::from_str;
|
||||
|
||||
#[test]
|
||||
fn test_channel_prototype_deserialization() {
|
||||
let toml_data = r#"
|
||||
[metadata]
|
||||
name = "files"
|
||||
description = "A channel to select files and directories"
|
||||
requirements = ["fd", "bat"]
|
||||
|
||||
[source]
|
||||
command = "fd -t f"
|
||||
interactive = false
|
||||
env = {}
|
||||
display = "{split:/:-1}" # only show the last path segment ('/a/b/c' -> 'c')
|
||||
ansi = false
|
||||
output = "{}" # output the full path
|
||||
|
||||
[preview]
|
||||
command = "bat -n --color=always {}"
|
||||
env = { "BAT_THEME" = "ansi" }
|
||||
interactive = false
|
||||
|
||||
[ui]
|
||||
layout = "landscape"
|
||||
ui_scale = 100
|
||||
show_help_bar = false
|
||||
show_preview_panel = true
|
||||
input_bar_position = "bottom"
|
||||
|
||||
[keybindings]
|
||||
quit = ["esc", "ctrl-c"]
|
||||
select_next_entry = ["down", "ctrl-n", "ctrl-j"]
|
||||
select_prev_entry = ["up", "ctrl-p", "ctrl-k"]
|
||||
confirm_selection = "enter"
|
||||
"#;
|
||||
|
||||
let prototype: ChannelPrototype = from_str(toml_data).unwrap();
|
||||
|
||||
assert_eq!(prototype.metadata.name, "files");
|
||||
assert_eq!(
|
||||
prototype.metadata.description,
|
||||
Some("A channel to select files and directories".to_string())
|
||||
);
|
||||
assert_eq!(format!("{}", prototype.source.command.inner), "fd -t f");
|
||||
assert!(!prototype.source.command.interactive);
|
||||
assert_eq!(
|
||||
prototype.source.display.unwrap().template_string(),
|
||||
"{split:/:-1}"
|
||||
);
|
||||
assert_eq!(prototype.source.output.unwrap().template_string(), "{}");
|
||||
assert_eq!(
|
||||
format!("{}", prototype.preview.unwrap().command.inner),
|
||||
"bat -n --color=always {}"
|
||||
);
|
||||
let ui = prototype.ui.unwrap();
|
||||
assert_eq!(ui.orientation, Some(Orientation::Landscape));
|
||||
assert_eq!(ui.ui_scale, Some(100));
|
||||
assert!(!(ui.show_help_bar.unwrap()));
|
||||
assert!(ui.show_preview_panel.unwrap());
|
||||
assert_eq!(ui.input_bar_position, Some(InputPosition::Bottom));
|
||||
let keybindings = prototype.keybindings.unwrap();
|
||||
assert_eq!(
|
||||
keybindings.0.get(&Action::Quit),
|
||||
Some(&Binding::MultipleKeys(vec![Key::Esc, Key::Ctrl('c')]))
|
||||
);
|
||||
assert_eq!(
|
||||
keybindings.0.get(&Action::SelectNextEntry),
|
||||
Some(&Binding::MultipleKeys(vec![
|
||||
Key::Down,
|
||||
Key::Ctrl('n'),
|
||||
Key::Ctrl('j')
|
||||
]))
|
||||
);
|
||||
assert_eq!(
|
||||
keybindings.0.get(&Action::SelectPrevEntry),
|
||||
Some(&Binding::MultipleKeys(vec![
|
||||
Key::Up,
|
||||
Key::Ctrl('p'),
|
||||
Key::Ctrl('k')
|
||||
]))
|
||||
);
|
||||
assert_eq!(
|
||||
keybindings.0.get(&Action::ConfirmSelection),
|
||||
Some(&Binding::SingleKey(Key::Enter))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_channel_prototype_deserialization_bare_minimum() {
|
||||
let toml_data = r#"
|
||||
[metadata]
|
||||
name = "files"
|
||||
description = "A channel to select files and directories"
|
||||
requirements = ["fd"]
|
||||
|
||||
[source]
|
||||
command = "fd -t f"
|
||||
"#;
|
||||
|
||||
let prototype: ChannelPrototype = from_str(toml_data).unwrap();
|
||||
|
||||
assert_eq!(prototype.metadata.name, "files");
|
||||
assert_eq!(
|
||||
prototype.metadata.description,
|
||||
Some("A channel to select files and directories".to_string())
|
||||
);
|
||||
assert_eq!(format!("{}", prototype.source.command.inner), "fd -t f");
|
||||
assert!(!prototype.source.command.interactive);
|
||||
assert!(prototype.source.command.env.is_empty());
|
||||
assert!(prototype.source.display.is_none());
|
||||
assert!(prototype.source.output.is_none());
|
||||
assert!(prototype.preview.is_none());
|
||||
assert!(prototype.ui.is_none());
|
||||
assert!(prototype.keybindings.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_channel_prototype_deserialization_partial_ui_options() {
|
||||
let toml_data = r#"
|
||||
[metadata]
|
||||
name = "files"
|
||||
description = "A channel to select files and directories"
|
||||
requirements = ["fd"]
|
||||
|
||||
[source]
|
||||
command = "fd -t f"
|
||||
|
||||
[ui]
|
||||
layout = "landscape"
|
||||
ui_scale = 40
|
||||
"#;
|
||||
|
||||
let prototype: ChannelPrototype = from_str(toml_data).unwrap();
|
||||
|
||||
assert_eq!(prototype.metadata.name, "files");
|
||||
assert_eq!(
|
||||
prototype.metadata.description,
|
||||
Some("A channel to select files and directories".to_string())
|
||||
);
|
||||
assert_eq!(format!("{}", prototype.source.command.inner), "fd -t f");
|
||||
assert!(!prototype.source.command.interactive);
|
||||
assert!(prototype.source.command.env.is_empty());
|
||||
assert!(prototype.source.display.is_none());
|
||||
assert!(prototype.source.output.is_none());
|
||||
|
||||
let ui = prototype.ui.unwrap();
|
||||
assert_eq!(ui.orientation, Some(Orientation::Landscape));
|
||||
assert_eq!(ui.ui_scale, Some(40));
|
||||
assert!(ui.show_help_bar.is_none());
|
||||
assert!(ui.show_preview_panel.is_none());
|
||||
assert!(ui.input_bar_position.is_none());
|
||||
}
|
||||
}
|
||||
|
@ -1,25 +1,22 @@
|
||||
use crate::{
|
||||
channels::{
|
||||
entry::Entry,
|
||||
prototypes::{Cable, ChannelPrototype},
|
||||
},
|
||||
cable::Cable,
|
||||
channels::{entry::Entry, prototypes::ChannelPrototype},
|
||||
matcher::{Matcher, config::Config},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use devicons::FileIcon;
|
||||
|
||||
pub struct RemoteControl {
|
||||
matcher: Matcher<String>,
|
||||
cable_channels: Option<Cable>,
|
||||
cable_channels: Cable,
|
||||
}
|
||||
|
||||
const NUM_THREADS: usize = 1;
|
||||
|
||||
impl RemoteControl {
|
||||
pub fn new(cable_channels: Option<Cable>) -> Self {
|
||||
pub fn new(cable_channels: Cable) -> Self {
|
||||
let matcher = Matcher::new(Config::default().n_threads(NUM_THREADS));
|
||||
let injector = matcher.injector();
|
||||
for c in cable_channels.as_ref().unwrap_or(&Cable::default()).keys() {
|
||||
for c in cable_channels.keys() {
|
||||
let () = injector.push(c.clone(), |e, cols| {
|
||||
cols[0] = e.to_string().into();
|
||||
});
|
||||
@ -30,24 +27,8 @@ impl RemoteControl {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn zap(&self, channel_name: &str) -> Result<ChannelPrototype> {
|
||||
match self
|
||||
.cable_channels
|
||||
.as_ref()
|
||||
.and_then(|channels| channels.get(channel_name).cloned())
|
||||
{
|
||||
Some(prototype) => Ok(prototype),
|
||||
None => Err(anyhow::anyhow!(
|
||||
"No channel or cable channel prototype found for {}",
|
||||
channel_name
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RemoteControl {
|
||||
fn default() -> Self {
|
||||
Self::new(None)
|
||||
pub fn zap(&self, channel_name: &str) -> ChannelPrototype {
|
||||
self.cable_channels.get_channel(channel_name)
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,7 +55,7 @@ impl RemoteControl {
|
||||
.map(|item| {
|
||||
let path = item.matched_string;
|
||||
Entry::new(path)
|
||||
.with_name_match_indices(&item.match_indices)
|
||||
.with_match_indices(&item.match_indices)
|
||||
.with_icon(CABLE_ICON)
|
||||
})
|
||||
.collect()
|
||||
|
@ -37,13 +37,6 @@ pub struct Cli {
|
||||
#[arg(long, default_value = "false", verbatim_doc_comment)]
|
||||
pub no_preview: bool,
|
||||
|
||||
/// The delimiter used to extract fields from the entry to provide to the
|
||||
/// preview command.
|
||||
///
|
||||
/// See the `preview` option for more information.
|
||||
#[arg(long, value_name = "STRING", default_value = " ", value_parser = delimiter_parser, verbatim_doc_comment)]
|
||||
pub delimiter: String,
|
||||
|
||||
/// The application's tick rate.
|
||||
///
|
||||
/// The tick rate is the number of times the application will update per
|
||||
@ -168,6 +161,9 @@ pub enum Command {
|
||||
#[arg(value_enum)]
|
||||
shell: Shell,
|
||||
},
|
||||
/// Downloads the latest collection of default channel prototypes from github
|
||||
/// and saves them to the local configuration directory.
|
||||
UpdateChannels,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, ValueEnum)]
|
||||
@ -178,12 +174,3 @@ pub enum Shell {
|
||||
PowerShell,
|
||||
Cmd,
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn delimiter_parser(s: &str) -> Result<String, String> {
|
||||
Ok(match s {
|
||||
"" => " ".to_string(),
|
||||
"\\t" => "\t".to_string(),
|
||||
_ => s.to_string(),
|
||||
})
|
||||
}
|
||||
|
@ -1,15 +1,13 @@
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::path::Path;
|
||||
use string_pipeline::MultiTemplate;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::{
|
||||
cable,
|
||||
channels::{
|
||||
preview::PreviewCommand,
|
||||
prototypes::{Cable, ChannelPrototype},
|
||||
},
|
||||
cable::{self, Cable},
|
||||
channels::prototypes::{ChannelPrototype, CommandSpec, PreviewSpec},
|
||||
cli::args::{Cli, Command},
|
||||
config::{KeyBindings, get_config_dir, get_data_dir},
|
||||
};
|
||||
@ -20,7 +18,7 @@ pub mod args;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PostProcessedCli {
|
||||
pub channel: Option<String>,
|
||||
pub preview_command: Option<PreviewCommand>,
|
||||
pub preview_spec: Option<PreviewSpec>,
|
||||
pub no_preview: bool,
|
||||
pub tick_rate: Option<f64>,
|
||||
pub frame_rate: Option<f64>,
|
||||
@ -41,7 +39,7 @@ impl Default for PostProcessedCli {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
channel: None,
|
||||
preview_command: None,
|
||||
preview_spec: None,
|
||||
no_preview: false,
|
||||
tick_rate: None,
|
||||
frame_rate: None,
|
||||
@ -60,35 +58,48 @@ impl Default for PostProcessedCli {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn post_process(cli: Cli, cable: &Cable) -> PostProcessedCli {
|
||||
pub fn post_process(cli: Cli) -> PostProcessedCli {
|
||||
// Parse literal keybindings passed through the CLI
|
||||
let keybindings = cli.keybindings.as_ref().map(|kb| {
|
||||
parse_keybindings_literal(kb, CLI_KEYBINDINGS_DELIMITER)
|
||||
.unwrap_or_else(|e| cli_parsing_error_exit(&e.to_string()))
|
||||
});
|
||||
|
||||
// Parse the preview command if provided
|
||||
let preview_command = cli.preview.as_ref().map(|preview| PreviewCommand {
|
||||
command: preview.clone(),
|
||||
delimiter: cli.delimiter.clone(),
|
||||
offset_expr: cli.preview_offset.clone(),
|
||||
// Parse the preview spec if provided
|
||||
let preview_spec = cli.preview.as_ref().map(|preview| {
|
||||
let command_spec = CommandSpec::new(
|
||||
MultiTemplate::parse(preview).unwrap_or_else(|e| {
|
||||
cli_parsing_error_exit(&format!(
|
||||
"Error parsing preview command: {e}"
|
||||
))
|
||||
}),
|
||||
false,
|
||||
FxHashMap::default(),
|
||||
);
|
||||
PreviewSpec::new(
|
||||
command_spec,
|
||||
cli.preview_offset.map(|offset_str| {
|
||||
MultiTemplate::parse(&offset_str).unwrap_or_else(|e| {
|
||||
cli_parsing_error_exit(&format!(
|
||||
"Error parsing preview offset: {e}"
|
||||
))
|
||||
})
|
||||
}),
|
||||
)
|
||||
});
|
||||
|
||||
// Determine channel and working_directory
|
||||
let (channel, working_directory) = match &cli.channel {
|
||||
Some(c) if !cable.has_channel(c) => {
|
||||
if cli.working_directory.is_none() && Path::new(c).exists() {
|
||||
(None, Some(c.clone()))
|
||||
} else {
|
||||
unknown_channel_exit(c);
|
||||
}
|
||||
Some(c) if Path::new(c).exists() => {
|
||||
// If the channel is a path, use it as the working directory
|
||||
(None, Some(c.clone()))
|
||||
}
|
||||
_ => (cli.channel.clone(), cli.working_directory.clone()),
|
||||
};
|
||||
|
||||
PostProcessedCli {
|
||||
channel,
|
||||
preview_command,
|
||||
preview_spec,
|
||||
no_preview: cli.no_preview,
|
||||
tick_rate: cli.tick_rate,
|
||||
frame_rate: cli.frame_rate,
|
||||
@ -138,7 +149,8 @@ fn parse_keybindings_literal(
|
||||
}
|
||||
|
||||
pub fn list_channels() {
|
||||
for c in cable::load_cable().unwrap_or_default().keys() {
|
||||
let channels = cable::load_cable().expect("Failed to load cable channels");
|
||||
for c in channels.keys() {
|
||||
println!("\t{c}");
|
||||
}
|
||||
}
|
||||
@ -257,21 +269,20 @@ mod tests {
|
||||
let cli = Cli {
|
||||
channel: Some("files".to_string()),
|
||||
preview: Some("bat -n --color=always {}".to_string()),
|
||||
delimiter: ":".to_string(),
|
||||
working_directory: Some("/home/user".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let cable = cable::load_cable().unwrap_or_default();
|
||||
let post_processed_cli = post_process(cli, &cable);
|
||||
let post_processed_cli = post_process(cli);
|
||||
|
||||
assert_eq!(
|
||||
post_processed_cli.preview_command,
|
||||
Some(PreviewCommand {
|
||||
command: "bat -n --color=always {}".to_string(),
|
||||
delimiter: ":".to_string(),
|
||||
offset_expr: None,
|
||||
})
|
||||
post_processed_cli
|
||||
.preview_spec
|
||||
.unwrap()
|
||||
.command
|
||||
.inner
|
||||
.template_string(),
|
||||
"bat -n --color=always {}".to_string(),
|
||||
);
|
||||
assert_eq!(post_processed_cli.tick_rate, None);
|
||||
assert_eq!(post_processed_cli.frame_rate, None);
|
||||
@ -286,12 +297,10 @@ mod tests {
|
||||
fn test_from_cli_no_args() {
|
||||
let cli = Cli {
|
||||
channel: Some(".".to_string()),
|
||||
delimiter: ":".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let cable = cable::load_cable().unwrap_or_default();
|
||||
let post_processed_cli = post_process(cli, &cable);
|
||||
let post_processed_cli = post_process(cli);
|
||||
|
||||
assert_eq!(
|
||||
post_processed_cli.working_directory,
|
||||
@ -305,7 +314,6 @@ mod tests {
|
||||
let cli = Cli {
|
||||
channel: Some("files".to_string()),
|
||||
preview: Some(":env_var:".to_string()),
|
||||
delimiter: ":".to_string(),
|
||||
keybindings: Some(
|
||||
"quit=\"esc\";select_next_entry=[\"down\",\"ctrl-j\"]"
|
||||
.to_string(),
|
||||
@ -313,8 +321,7 @@ mod tests {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let cable = cable::load_cable().unwrap_or_default();
|
||||
let post_processed_cli = post_process(cli, &cable);
|
||||
let post_processed_cli = post_process(cli);
|
||||
|
||||
let mut expected = KeyBindings::default();
|
||||
expected.insert(Action::Quit, Binding::SingleKey(Key::Esc));
|
||||
@ -336,7 +343,11 @@ mod tests {
|
||||
(
|
||||
command_mapping,
|
||||
"env",
|
||||
cable::load_cable().unwrap_or_default(),
|
||||
Cable::from_prototypes(vec![
|
||||
ChannelPrototype::new("files", "fd -t f"),
|
||||
ChannelPrototype::new("env", "env"),
|
||||
ChannelPrototype::new("git", "git status"),
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
@ -354,7 +365,7 @@ mod tests {
|
||||
&channels,
|
||||
);
|
||||
|
||||
assert_eq!(channel.name, "files");
|
||||
assert_eq!(channel.metadata.name, "files");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -371,7 +382,7 @@ mod tests {
|
||||
&channels,
|
||||
);
|
||||
|
||||
assert_eq!(channel.name, fallback);
|
||||
assert_eq!(channel.metadata.name, fallback);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -388,6 +399,6 @@ mod tests {
|
||||
&channels,
|
||||
);
|
||||
|
||||
assert_eq!(channel.name, fallback);
|
||||
assert_eq!(channel.metadata.name, fallback);
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,10 @@ pub use themes::Theme;
|
||||
use tracing::{debug, warn};
|
||||
pub use ui::UiConfig;
|
||||
|
||||
use crate::channels::prototypes::DEFAULT_PROTOTYPE_NAME;
|
||||
use crate::{
|
||||
cable::CABLE_DIR_NAME,
|
||||
channels::prototypes::{DEFAULT_PROTOTYPE_NAME, UiSpec},
|
||||
};
|
||||
|
||||
mod keybindings;
|
||||
pub mod shell_integration;
|
||||
@ -84,9 +87,12 @@ impl ConfigEnv {
|
||||
pub fn init() -> Result<Self> {
|
||||
let data_dir = get_data_dir();
|
||||
let config_dir = get_config_dir();
|
||||
let cable_dir = config_dir.join(CABLE_DIR_NAME);
|
||||
|
||||
std::fs::create_dir_all(&config_dir)
|
||||
.context("Failed creating configuration directory")?;
|
||||
std::fs::create_dir_all(&cable_dir)
|
||||
.context("Failed creating cable directory")?;
|
||||
std::fs::create_dir_all(&data_dir)
|
||||
.context("Failed creating data directory")?;
|
||||
|
||||
@ -128,8 +134,7 @@ impl Config {
|
||||
Self::load_user_config(&config_env.config_dir)?;
|
||||
|
||||
// merge the user configuration with the default configuration
|
||||
let final_cfg =
|
||||
Self::merge_user_with_default(default_config, user_cfg);
|
||||
let final_cfg = Self::merge_with_default(default_config, user_cfg);
|
||||
|
||||
debug!(
|
||||
"Configuration: \n{}",
|
||||
@ -162,23 +167,20 @@ impl Config {
|
||||
Ok(user_cfg)
|
||||
}
|
||||
|
||||
fn merge_user_with_default(
|
||||
mut default: Config,
|
||||
mut user: Config,
|
||||
) -> Config {
|
||||
fn merge_with_default(mut default: Config, mut new: Config) -> Config {
|
||||
// use default fallback channel as a fallback if user hasn't specified one
|
||||
if user.shell_integration.fallback_channel.is_empty() {
|
||||
user.shell_integration
|
||||
if new.shell_integration.fallback_channel.is_empty() {
|
||||
new.shell_integration
|
||||
.fallback_channel
|
||||
.clone_from(&default.shell_integration.fallback_channel);
|
||||
}
|
||||
|
||||
// merge shell integration triggers with commands
|
||||
default.shell_integration.merge_triggers();
|
||||
user.shell_integration.merge_triggers();
|
||||
new.shell_integration.merge_triggers();
|
||||
// merge shell integration commands with default commands
|
||||
if user.shell_integration.commands.is_empty() {
|
||||
user.shell_integration
|
||||
if new.shell_integration.commands.is_empty() {
|
||||
new.shell_integration
|
||||
.commands
|
||||
.clone_from(&default.shell_integration.commands);
|
||||
}
|
||||
@ -186,19 +188,41 @@ impl Config {
|
||||
// merge shell integration keybindings with default keybindings
|
||||
let mut merged_keybindings =
|
||||
default.shell_integration.keybindings.clone();
|
||||
merged_keybindings.extend(user.shell_integration.keybindings.clone());
|
||||
user.shell_integration.keybindings = merged_keybindings;
|
||||
merged_keybindings.extend(new.shell_integration.keybindings.clone());
|
||||
new.shell_integration.keybindings = merged_keybindings;
|
||||
|
||||
// merge keybindings with default keybindings
|
||||
let keybindings =
|
||||
merge_keybindings(default.keybindings.clone(), &user.keybindings);
|
||||
user.keybindings = keybindings;
|
||||
merge_keybindings(default.keybindings.clone(), &new.keybindings);
|
||||
new.keybindings = keybindings;
|
||||
|
||||
Config {
|
||||
application: user.application,
|
||||
keybindings: user.keybindings,
|
||||
ui: user.ui,
|
||||
shell_integration: user.shell_integration,
|
||||
application: new.application,
|
||||
keybindings: new.keybindings,
|
||||
ui: new.ui,
|
||||
shell_integration: new.shell_integration,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn merge_keybindings(&mut self, other: &KeyBindings) {
|
||||
self.keybindings = merge_keybindings(self.keybindings.clone(), other);
|
||||
}
|
||||
|
||||
pub fn apply_prototype_ui_spec(&mut self, ui_spec: &UiSpec) {
|
||||
if let Some(ui_scale) = &ui_spec.ui_scale {
|
||||
self.ui.ui_scale = *ui_scale;
|
||||
}
|
||||
if let Some(show_help_bar) = &ui_spec.show_help_bar {
|
||||
self.ui.show_help_bar = *show_help_bar;
|
||||
}
|
||||
if let Some(show_preview_panel) = &ui_spec.show_preview_panel {
|
||||
self.ui.show_preview_panel = *show_preview_panel;
|
||||
}
|
||||
if let Some(orientation) = &ui_spec.orientation {
|
||||
self.ui.orientation = *orientation;
|
||||
}
|
||||
if let Some(input_position) = &ui_spec.input_bar_position {
|
||||
self.ui.input_bar_position = *input_position;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
88
television/gh.rs
Normal file
88
television/gh.rs
Normal file
@ -0,0 +1,88 @@
|
||||
use anyhow::Result;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tracing::debug;
|
||||
use ureq::get;
|
||||
|
||||
use crate::channels::prototypes::ChannelPrototype;
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
struct ApiResponse {
|
||||
nodes: Vec<Node>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
struct Node {
|
||||
_name: String,
|
||||
_path: PathBuf,
|
||||
kind: NodeType,
|
||||
download_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
enum NodeType {
|
||||
#[serde(rename = "file")]
|
||||
File,
|
||||
#[serde(rename = "dir")]
|
||||
Directory,
|
||||
}
|
||||
|
||||
const GITHUB_API_BASE_URL: &str =
|
||||
"https://api.github.com/repos/alexpasmantier/television/contents/";
|
||||
|
||||
fn make_gh_content_request(gh_dir: &Path) -> Result<ApiResponse> {
|
||||
let url = format!("{}{}", GITHUB_API_BASE_URL, gh_dir.to_str().unwrap());
|
||||
debug!("Making GitHub API request to: {}", url);
|
||||
get(&url)
|
||||
.header("User-Agent", "television-client")
|
||||
.header("Accept", "application/vnd.github+json")
|
||||
.call()
|
||||
.map(|response| {
|
||||
if response.status().is_success() {
|
||||
serde_json::from_str::<ApiResponse>(
|
||||
&response.into_body().read_to_string()?,
|
||||
)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to parse JSON: {}", e))
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Failed to fetch data from GitHub API"))
|
||||
}
|
||||
})?
|
||||
}
|
||||
|
||||
fn fetch_raw_content_from_url(url: &str) -> Result<String> {
|
||||
let response =
|
||||
get(url).header("User-Agent", "television-client").call()?;
|
||||
|
||||
if response.status().is_success() {
|
||||
Ok(response.into_body().read_to_string()?)
|
||||
} else {
|
||||
Err(anyhow::anyhow!(
|
||||
"Failed to fetch raw content from URL: {}",
|
||||
url
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
const DEFAULT_CABLE_DIR_PATH: &str = "cable/unix";
|
||||
#[cfg(windows)]
|
||||
const DEFAULT_CABLE_DIR_PATH: &str = "cable/windows";
|
||||
|
||||
pub fn get_default_prototypes_from_repo() -> Result<Vec<ChannelPrototype>> {
|
||||
let response = make_gh_content_request(Path::new(DEFAULT_CABLE_DIR_PATH))?;
|
||||
debug!("Response from GitHub API: {:?}", response);
|
||||
Ok(response
|
||||
.nodes
|
||||
.iter()
|
||||
.filter_map(|node| {
|
||||
if let NodeType::File = node.kind {
|
||||
node.download_url.clone()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.filter_map(|url| fetch_raw_content_from_url(&url).ok())
|
||||
.filter_map(|content| {
|
||||
toml::from_str::<ChannelPrototype>(&content).ok()
|
||||
})
|
||||
.collect())
|
||||
}
|
@ -7,6 +7,7 @@ pub mod config;
|
||||
pub mod draw;
|
||||
pub mod errors;
|
||||
pub mod event;
|
||||
pub mod gh;
|
||||
pub mod input;
|
||||
pub mod keymap;
|
||||
pub mod logging;
|
||||
|
@ -5,10 +5,11 @@ use std::process::exit;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use television::cable::load_cable;
|
||||
use television::cable::{CABLE_DIR_NAME, CHANNEL_FILE_FORMAT, load_cable};
|
||||
use television::cli::post_process;
|
||||
use television::gh::get_default_prototypes_from_repo;
|
||||
use television::{
|
||||
channels::prototypes::{Cable, ChannelPrototype},
|
||||
cable::Cable, channels::prototypes::ChannelPrototype,
|
||||
utils::clipboard::CLIPBOARD,
|
||||
};
|
||||
use tracing::{debug, error, info};
|
||||
@ -20,7 +21,9 @@ use television::cli::{
|
||||
guess_channel_from_prompt, list_channels,
|
||||
};
|
||||
|
||||
use television::config::{Config, ConfigEnv, merge_keybindings};
|
||||
use television::config::{
|
||||
Config, ConfigEnv, get_config_dir, merge_keybindings,
|
||||
};
|
||||
use television::utils::shell::render_autocomplete_script_template;
|
||||
use television::utils::{
|
||||
shell::{Shell, completion_script},
|
||||
@ -38,21 +41,21 @@ async fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
debug!("CLI: {:?}", cli);
|
||||
|
||||
let args = post_process(cli);
|
||||
debug!("PostProcessedCli: {:?}", args);
|
||||
|
||||
// load the configuration file
|
||||
debug!("Loading configuration...");
|
||||
let mut config = Config::new(&ConfigEnv::init()?)?;
|
||||
|
||||
debug!("Loading cable channels...");
|
||||
let cable = load_cable().unwrap_or_default();
|
||||
|
||||
let args = post_process(cli, &cable);
|
||||
debug!("PostProcessedCli: {:?}", args);
|
||||
|
||||
// optionally handle subcommands
|
||||
// handle subcommands
|
||||
debug!("Handling subcommands...");
|
||||
args.command
|
||||
.as_ref()
|
||||
.map(|x| handle_subcommands(x, &config));
|
||||
if let Some(subcommand) = &args.command {
|
||||
handle_subcommand(subcommand, &config)?;
|
||||
}
|
||||
|
||||
debug!("Loading cable channels...");
|
||||
let cable = load_cable().unwrap_or_else(|| exit(1));
|
||||
|
||||
// optionally change the working directory
|
||||
args.working_directory.as_ref().map(set_current_dir);
|
||||
@ -77,7 +80,7 @@ async fn main() -> Result<()> {
|
||||
config.application.tick_rate,
|
||||
);
|
||||
let mut app =
|
||||
App::new(&channel_prototype, config, args.input, options, &cable);
|
||||
App::new(channel_prototype, config, args.input, options, cable);
|
||||
stdout().flush()?;
|
||||
debug!("Running application...");
|
||||
let output = app.run(stdout().is_terminal(), false).await?;
|
||||
@ -127,7 +130,7 @@ pub fn set_current_dir(path: &String) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_subcommands(command: &Command, config: &Config) -> Result<()> {
|
||||
pub fn handle_subcommand(command: &Command, config: &Config) -> Result<()> {
|
||||
match command {
|
||||
Command::ListChannels => {
|
||||
list_channels();
|
||||
@ -146,6 +149,30 @@ pub fn handle_subcommands(command: &Command, config: &Config) -> Result<()> {
|
||||
println!("{script}");
|
||||
exit(0);
|
||||
}
|
||||
// TODO: better error handling
|
||||
Command::UpdateChannels => {
|
||||
println!("Fetching latest cable channels...");
|
||||
let cable =
|
||||
Cable::from_prototypes(get_default_prototypes_from_repo()?);
|
||||
println!("Writing channels locally...");
|
||||
let cable_path = get_config_dir().join(CABLE_DIR_NAME);
|
||||
if !cable_path.exists() {
|
||||
println!(
|
||||
"Creating cable directory at {}",
|
||||
cable_path.display()
|
||||
);
|
||||
std::fs::create_dir_all(&cable_path)?;
|
||||
}
|
||||
for (name, prototype) in cable.iter() {
|
||||
let file_path =
|
||||
cable_path.join(name).with_extension(CHANNEL_FILE_FORMAT);
|
||||
let content = toml::to_string(&prototype)?;
|
||||
std::fs::write(&file_path, content)?;
|
||||
println!("Saved channel {} to {}", name, file_path.display());
|
||||
}
|
||||
info!("Cable channels updated successfully.");
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -157,7 +184,7 @@ pub fn determine_channel(
|
||||
) -> ChannelPrototype {
|
||||
if readable_stdin {
|
||||
debug!("Using stdin channel");
|
||||
ChannelPrototype::stdin(args.preview_command.clone())
|
||||
ChannelPrototype::stdin(args.preview_spec.clone())
|
||||
} else if let Some(prompt) = &args.autocomplete_prompt {
|
||||
debug!("Using autocomplete prompt: {:?}", prompt);
|
||||
let channel_prototype = guess_channel_from_prompt(
|
||||
@ -177,8 +204,8 @@ pub fn determine_channel(
|
||||
|
||||
let mut prototype = cable.get_channel(&channel);
|
||||
// use cli preview command if any
|
||||
if let Some(pc) = &args.preview_command {
|
||||
prototype.preview_command = Some(pc.clone());
|
||||
if let Some(pc) = &args.preview_spec {
|
||||
prototype.preview = Some(pc.clone());
|
||||
}
|
||||
|
||||
prototype
|
||||
@ -188,9 +215,9 @@ pub fn determine_channel(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rustc_hash::FxHashMap;
|
||||
use television::{
|
||||
cable::load_cable,
|
||||
channels::{preview::PreviewCommand, prototypes::ChannelPrototype},
|
||||
use string_pipeline::MultiTemplate;
|
||||
use television::channels::prototypes::{
|
||||
ChannelPrototype, CommandSpec, PreviewSpec,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
@ -203,14 +230,18 @@ mod tests {
|
||||
cable_channels: Option<Cable>,
|
||||
) {
|
||||
let channels: Cable =
|
||||
cable_channels.unwrap_or_else(|| load_cable().unwrap_or_default());
|
||||
cable_channels.unwrap_or(Cable::from_prototypes(vec![
|
||||
ChannelPrototype::new("files", "fd -t f"),
|
||||
ChannelPrototype::new("dirs", "ls"),
|
||||
ChannelPrototype::new("git", "git status"),
|
||||
]));
|
||||
let channel =
|
||||
determine_channel(args, config, readable_stdin, &channels);
|
||||
|
||||
assert_eq!(
|
||||
channel.name, expected_channel.name,
|
||||
channel.metadata.name, expected_channel.metadata.name,
|
||||
"Expected {:?} but got {:?}",
|
||||
expected_channel.name, channel.name
|
||||
expected_channel.metadata.name, channel.metadata.name
|
||||
);
|
||||
}
|
||||
|
||||
@ -223,7 +254,7 @@ mod tests {
|
||||
&args,
|
||||
&config,
|
||||
true,
|
||||
&ChannelPrototype::new("stdin", "cat", false, None),
|
||||
&ChannelPrototype::new("stdin", "cat"),
|
||||
None,
|
||||
);
|
||||
}
|
||||
@ -231,8 +262,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_determine_channel_autocomplete_prompt() {
|
||||
let autocomplete_prompt = Some("cd".to_string());
|
||||
let expected_channel =
|
||||
ChannelPrototype::new("dirs", "ls {}", false, None);
|
||||
let expected_channel = ChannelPrototype::new("dirs", "ls {}");
|
||||
let args = PostProcessedCli {
|
||||
autocomplete_prompt,
|
||||
..Default::default()
|
||||
@ -274,14 +304,13 @@ mod tests {
|
||||
&args,
|
||||
&config,
|
||||
false,
|
||||
&ChannelPrototype::new("dirs", "", false, None),
|
||||
&ChannelPrototype::new("dirs", "ls {}"),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_determine_channel_config_fallback() {
|
||||
let cable = Cable::default();
|
||||
let args = PostProcessedCli {
|
||||
channel: None,
|
||||
..Default::default()
|
||||
@ -292,34 +321,38 @@ mod tests {
|
||||
&args,
|
||||
&config,
|
||||
false,
|
||||
&cable.get_channel("dirs"),
|
||||
Some(cable),
|
||||
&ChannelPrototype::new("dirs", "ls"),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_determine_channel_with_cli_preview() {
|
||||
let cable = Cable::default();
|
||||
|
||||
let preview_command = PreviewCommand::new("echo hello", ",", None);
|
||||
let preview_spec = PreviewSpec::new(
|
||||
CommandSpec::new(
|
||||
MultiTemplate::parse("echo hello").unwrap(),
|
||||
false,
|
||||
FxHashMap::default(),
|
||||
),
|
||||
None,
|
||||
);
|
||||
|
||||
let args = PostProcessedCli {
|
||||
channel: Some(String::from("dirs")),
|
||||
preview_command: Some(preview_command),
|
||||
preview_spec: Some(preview_spec),
|
||||
..Default::default()
|
||||
};
|
||||
let config = Config::default();
|
||||
|
||||
let expected_prototype = cable
|
||||
.get_channel("dirs")
|
||||
.set_preview(args.preview_command.clone());
|
||||
let expected_prototype = ChannelPrototype::new("dirs", "ls")
|
||||
.with_preview(args.preview_spec.clone());
|
||||
|
||||
assert_is_correct_channel(
|
||||
&args,
|
||||
&config,
|
||||
false,
|
||||
&expected_prototype,
|
||||
Some(cable),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,10 @@ use tokio::{
|
||||
use tracing::debug;
|
||||
|
||||
use crate::{
|
||||
channels::{entry::Entry, preview::PreviewCommand},
|
||||
channels::{
|
||||
entry::Entry,
|
||||
prototypes::{CommandSpec, PreviewSpec},
|
||||
},
|
||||
utils::{
|
||||
command::shell_command,
|
||||
strings::{ReplaceNonPrintableConfig, replace_non_printable},
|
||||
@ -134,13 +137,13 @@ pub struct Previewer {
|
||||
// FIXME: maybe use a bounded channel here with a single slot
|
||||
requests: UnboundedReceiver<Request>,
|
||||
last_job_entry: Option<Entry>,
|
||||
preview_command: PreviewCommand,
|
||||
preview_spec: PreviewSpec,
|
||||
results: UnboundedSender<Preview>,
|
||||
}
|
||||
|
||||
impl Previewer {
|
||||
pub fn new(
|
||||
preview_command: PreviewCommand,
|
||||
preview_command: &PreviewSpec,
|
||||
config: Config,
|
||||
receiver: UnboundedReceiver<Request>,
|
||||
sender: UnboundedSender<Preview>,
|
||||
@ -149,7 +152,7 @@ impl Previewer {
|
||||
config,
|
||||
requests: receiver,
|
||||
last_job_entry: None,
|
||||
preview_command,
|
||||
preview_spec: preview_command.clone(),
|
||||
results: sender,
|
||||
}
|
||||
}
|
||||
@ -168,15 +171,15 @@ impl Previewer {
|
||||
continue;
|
||||
}
|
||||
let results_handle = self.results.clone();
|
||||
let command =
|
||||
self.preview_command.format_with(&ticket.entry);
|
||||
self.last_job_entry = Some(ticket.entry.clone());
|
||||
// try to execute the preview with a timeout
|
||||
let preview_command =
|
||||
self.preview_spec.command.clone();
|
||||
match timeout(
|
||||
self.config.job_timeout,
|
||||
tokio::spawn(async move {
|
||||
try_preview(
|
||||
&command,
|
||||
&preview_command,
|
||||
&ticket.entry,
|
||||
&results_handle,
|
||||
);
|
||||
@ -210,16 +213,22 @@ impl Previewer {
|
||||
}
|
||||
|
||||
pub fn try_preview(
|
||||
command: &str,
|
||||
command: &CommandSpec,
|
||||
entry: &Entry,
|
||||
results_handle: &UnboundedSender<Preview>,
|
||||
) {
|
||||
debug!("Preview command: {}", command);
|
||||
|
||||
let child = shell_command(false)
|
||||
.arg(command)
|
||||
.output()
|
||||
.expect("failed to execute process");
|
||||
let child = shell_command(
|
||||
&command
|
||||
.inner
|
||||
.format(&entry.raw)
|
||||
.expect("Failed to format command"),
|
||||
command.interactive,
|
||||
&command.env,
|
||||
)
|
||||
.output()
|
||||
.expect("failed to execute process");
|
||||
|
||||
let preview: Preview = {
|
||||
if child.status.success() {
|
||||
@ -230,7 +239,7 @@ pub fn try_preview(
|
||||
.keep_control_characters(),
|
||||
);
|
||||
Preview::new(
|
||||
&entry.name,
|
||||
&entry.raw,
|
||||
content.to_string(),
|
||||
None,
|
||||
u16::try_from(content.lines().count()).unwrap_or(u16::MAX),
|
||||
@ -243,7 +252,7 @@ pub fn try_preview(
|
||||
.keep_control_characters(),
|
||||
);
|
||||
Preview::new(
|
||||
&entry.name,
|
||||
&entry.raw,
|
||||
content.to_string(),
|
||||
None,
|
||||
u16::try_from(content.lines().count()).unwrap_or(u16::MAX),
|
||||
|
@ -20,43 +20,13 @@ const POINTER_SYMBOL: &str = "> ";
|
||||
const SELECTED_SYMBOL: &str = "● ";
|
||||
const DESELECTED_SYMBOL: &str = " ";
|
||||
|
||||
/// The max width for each part of the entry (name and value) depending on various factors.
|
||||
fn max_widths(
|
||||
entry: &Entry,
|
||||
available_width: u16,
|
||||
use_icons: bool,
|
||||
is_selected: bool,
|
||||
) -> (u16, u16) {
|
||||
let available_width = available_width.saturating_sub(
|
||||
fn max_width(available_width: u16, use_icons: bool, is_selected: bool) -> u16 {
|
||||
available_width.saturating_sub(
|
||||
2 // pointer and space
|
||||
+ 2 * (u16::from(use_icons))
|
||||
+ 2 * (u16::from(is_selected))
|
||||
+ 2 // borders
|
||||
+ entry
|
||||
.line_number
|
||||
// ":{line_number}: "
|
||||
.map_or(0, |l| 1 + u16::try_from(l.checked_ilog10().unwrap_or(0)).unwrap() + 3),
|
||||
);
|
||||
|
||||
if entry.value.is_none() {
|
||||
return (available_width, 0);
|
||||
}
|
||||
|
||||
// otherwise, use up the available space for both name and value as nicely as possible
|
||||
let name_len =
|
||||
u16::try_from(entry.name.chars().count()).unwrap_or(u16::MAX);
|
||||
let value_len = entry
|
||||
.value
|
||||
.as_ref()
|
||||
.map_or(0, |v| u16::try_from(v.chars().count()).unwrap_or(u16::MAX));
|
||||
|
||||
if name_len < available_width / 2 {
|
||||
(name_len, available_width - name_len)
|
||||
} else if value_len < available_width / 2 {
|
||||
(available_width - value_len, value_len)
|
||||
} else {
|
||||
(available_width / 2, available_width / 2)
|
||||
}
|
||||
+ 2, // borders
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: could we not just iterate on chars here instead of using the indices?
|
||||
@ -70,8 +40,7 @@ fn build_result_line<'a>(
|
||||
area_width: u16,
|
||||
) -> Line<'a> {
|
||||
let mut spans = Vec::new();
|
||||
let (name_max_width, value_max_width) = max_widths(
|
||||
entry,
|
||||
let name_max_width = max_width(
|
||||
area_width,
|
||||
use_icons,
|
||||
selected_entries
|
||||
@ -104,7 +73,7 @@ fn build_result_line<'a>(
|
||||
// entry name
|
||||
let (mut entry_name, mut name_match_ranges) =
|
||||
make_matched_string_printable(
|
||||
&entry.name,
|
||||
entry.display(),
|
||||
entry.name_match_ranges.as_deref(),
|
||||
);
|
||||
// if the name is too long, we need to truncate it and add an ellipsis
|
||||
@ -160,56 +129,6 @@ fn build_result_line<'a>(
|
||||
Style::default().fg(colorscheme.result_line_number_fg),
|
||||
));
|
||||
}
|
||||
// optional value
|
||||
if let Some(value) = &entry.value {
|
||||
spans.push(Span::raw(": "));
|
||||
|
||||
let (mut value, mut value_match_ranges) =
|
||||
make_matched_string_printable(
|
||||
value,
|
||||
entry.value_match_ranges.as_deref(),
|
||||
);
|
||||
// if the value is too long, we need to truncate it and add an ellipsis
|
||||
if value.as_str().width() > value_max_width as usize {
|
||||
(value, value_match_ranges) = truncate_highlighted_string(
|
||||
&value,
|
||||
&value_match_ranges,
|
||||
value_max_width,
|
||||
);
|
||||
}
|
||||
|
||||
let mut last_match_end = 0;
|
||||
let value_chars = value.chars();
|
||||
let value_len = value.chars().count();
|
||||
for (start, end) in value_match_ranges
|
||||
.iter()
|
||||
.map(|(s, e)| (*s as usize, *e as usize))
|
||||
{
|
||||
spans.push(Span::styled(
|
||||
value_chars
|
||||
.clone()
|
||||
.skip(last_match_end)
|
||||
.take(start - last_match_end)
|
||||
.collect::<String>(),
|
||||
Style::default().fg(colorscheme.result_preview_fg),
|
||||
));
|
||||
spans.push(Span::styled(
|
||||
value_chars
|
||||
.clone()
|
||||
.skip(start)
|
||||
.take(end - start)
|
||||
.collect::<String>(),
|
||||
Style::default().fg(colorscheme.match_foreground_color),
|
||||
));
|
||||
last_match_end = end;
|
||||
}
|
||||
if last_match_end < value_len {
|
||||
spans.push(Span::styled(
|
||||
value_chars.skip(last_match_end).collect::<String>(),
|
||||
Style::default().fg(colorscheme.result_preview_fg),
|
||||
));
|
||||
}
|
||||
}
|
||||
Line::from(spans)
|
||||
}
|
||||
|
||||
@ -301,7 +220,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_build_result_line() {
|
||||
let entry = Entry::new(String::from("something nice"))
|
||||
.with_name_match_indices(
|
||||
.with_match_indices(
|
||||
// something nice
|
||||
// 012345678901234
|
||||
// om ni
|
||||
@ -331,7 +250,7 @@ mod tests {
|
||||
let entry =
|
||||
// See https://github.com/alexpasmantier/television/issues/439
|
||||
Entry::new(String::from("ジェイムス下地 - REDLINE Original Soundtrack - 06 - ROBOWORLD TV.mp3"))
|
||||
.with_name_match_indices(&[27, 28, 29, 30, 31]);
|
||||
.with_match_indices(&[27, 28, 29, 30, 31]);
|
||||
let result_line = build_result_line(
|
||||
&entry,
|
||||
None,
|
||||
|
@ -1,10 +1,9 @@
|
||||
use crate::{
|
||||
action::Action,
|
||||
cable::Cable,
|
||||
channels::{
|
||||
cable::Channel as CableChannel,
|
||||
entry::Entry,
|
||||
prototypes::{Cable, ChannelPrototype},
|
||||
remote_control::RemoteControl,
|
||||
channel::Channel as CableChannel, entry::Entry,
|
||||
prototypes::ChannelPrototype, remote_control::RemoteControl,
|
||||
},
|
||||
config::{Config, Theme},
|
||||
draw::{ChannelState, Ctx, TvState},
|
||||
@ -47,6 +46,7 @@ pub enum MatchingMode {
|
||||
|
||||
pub struct Television {
|
||||
action_tx: UnboundedSender<Action>,
|
||||
base_config: Config,
|
||||
pub config: Config,
|
||||
pub channel: CableChannel,
|
||||
pub remote_control: Option<RemoteControl>,
|
||||
@ -73,21 +73,32 @@ impl Television {
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
action_tx: UnboundedSender<Action>,
|
||||
channel_prototype: &ChannelPrototype,
|
||||
mut config: Config,
|
||||
channel_prototype: ChannelPrototype,
|
||||
mut base_config: Config,
|
||||
input: Option<String>,
|
||||
no_remote: bool,
|
||||
no_help: bool,
|
||||
exact: bool,
|
||||
cable_channels: Cable,
|
||||
) -> Self {
|
||||
if no_help {
|
||||
base_config.ui.show_help_bar = false;
|
||||
base_config.ui.no_help = true;
|
||||
}
|
||||
|
||||
let config = Self::merge_base_config_with_prototype_specs(
|
||||
&base_config,
|
||||
&channel_prototype,
|
||||
);
|
||||
debug!("Merged config: {:?}", config);
|
||||
|
||||
let mut results_picker = Picker::new(input.clone());
|
||||
if config.ui.input_bar_position == InputPosition::Bottom {
|
||||
results_picker = results_picker.inverted();
|
||||
}
|
||||
|
||||
// previewer
|
||||
let preview_handles = Self::setup_previewer(channel_prototype);
|
||||
let preview_handles = Self::setup_previewer(&channel_prototype);
|
||||
|
||||
let mut channel = CableChannel::new(channel_prototype);
|
||||
|
||||
@ -113,14 +124,9 @@ impl Television {
|
||||
let remote_control = if no_remote {
|
||||
None
|
||||
} else {
|
||||
Some(RemoteControl::new(Some(cable_channels)))
|
||||
Some(RemoteControl::new(cable_channels))
|
||||
};
|
||||
|
||||
if no_help {
|
||||
config.ui.show_help_bar = false;
|
||||
config.ui.no_help = true;
|
||||
}
|
||||
|
||||
let matching_mode = if exact {
|
||||
MatchingMode::Substring
|
||||
} else {
|
||||
@ -129,6 +135,7 @@ impl Television {
|
||||
|
||||
Self {
|
||||
action_tx,
|
||||
base_config,
|
||||
config,
|
||||
channel,
|
||||
remote_control,
|
||||
@ -150,15 +157,31 @@ impl Television {
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_base_config_with_prototype_specs(
|
||||
base_config: &Config,
|
||||
channel_prototype: &ChannelPrototype,
|
||||
) -> Config {
|
||||
let mut config = base_config.clone();
|
||||
// keybindings
|
||||
if let Some(keybindings) = &channel_prototype.keybindings {
|
||||
config.merge_keybindings(keybindings);
|
||||
}
|
||||
// ui
|
||||
if let Some(ui_spec) = &channel_prototype.ui {
|
||||
config.apply_prototype_ui_spec(ui_spec);
|
||||
}
|
||||
config
|
||||
}
|
||||
|
||||
fn setup_previewer(
|
||||
channel_prototype: &ChannelPrototype,
|
||||
) -> Option<(UnboundedSender<PreviewRequest>, UnboundedReceiver<Preview>)>
|
||||
{
|
||||
if channel_prototype.preview_command.is_some() {
|
||||
if let Some(preview_spec) = &channel_prototype.preview {
|
||||
let (pv_request_tx, pv_request_rx) = unbounded_channel();
|
||||
let (pv_preview_tx, pv_preview_rx) = unbounded_channel();
|
||||
let previewer = Previewer::new(
|
||||
channel_prototype.preview_command.clone().unwrap(),
|
||||
preview_spec,
|
||||
PreviewerConfig::default(),
|
||||
pv_request_rx,
|
||||
pv_preview_tx,
|
||||
@ -176,7 +199,7 @@ impl Television {
|
||||
|
||||
pub fn dump_context(&self) -> Ctx {
|
||||
let channel_state = ChannelState::new(
|
||||
self.channel.name.clone(),
|
||||
self.channel.prototype.metadata.name.clone(),
|
||||
self.channel.selected_entries().clone(),
|
||||
self.channel.total_count(),
|
||||
self.channel.running(),
|
||||
@ -202,13 +225,12 @@ impl Television {
|
||||
}
|
||||
|
||||
pub fn current_channel(&self) -> String {
|
||||
self.channel.name.clone()
|
||||
self.channel.prototype.metadata.name.clone()
|
||||
}
|
||||
|
||||
pub fn change_channel(&mut self, channel_prototype: &ChannelPrototype) {
|
||||
pub fn change_channel(&mut self, channel_prototype: ChannelPrototype) {
|
||||
self.preview_state.reset();
|
||||
self.preview_state.enabled =
|
||||
channel_prototype.preview_command.is_some();
|
||||
self.preview_state.enabled = channel_prototype.preview.is_some();
|
||||
self.reset_picker_selection();
|
||||
self.reset_picker_input();
|
||||
self.current_pattern = EMPTY_STRING.to_string();
|
||||
@ -218,9 +240,13 @@ impl Television {
|
||||
.send(PreviewRequest::Shutdown)
|
||||
.expect("Failed to send shutdown signal to previewer");
|
||||
}
|
||||
self.preview_handles = Self::setup_previewer(channel_prototype);
|
||||
debug!("Changing channel to {:?}", channel_prototype);
|
||||
self.preview_handles = Self::setup_previewer(&channel_prototype);
|
||||
self.config = Self::merge_base_config_with_prototype_specs(
|
||||
&self.base_config,
|
||||
&channel_prototype,
|
||||
);
|
||||
self.channel = CableChannel::new(channel_prototype);
|
||||
debug!("Changed channel to {:?}", channel_prototype);
|
||||
}
|
||||
|
||||
pub fn find(&mut self, pattern: &str) {
|
||||
@ -544,13 +570,13 @@ impl Television {
|
||||
.remote_control
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.zap(entry.name.as_str())?;
|
||||
.zap(entry.raw.as_str());
|
||||
// this resets the RC picker
|
||||
self.reset_picker_selection();
|
||||
self.reset_picker_input();
|
||||
self.remote_control.as_mut().unwrap().find(EMPTY_STRING);
|
||||
self.mode = Mode::Channel;
|
||||
self.change_channel(&new_channel);
|
||||
self.change_channel(new_channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -562,7 +588,7 @@ impl Television {
|
||||
if let Some(entries) = self.get_selected_entries(None) {
|
||||
let copied_string = entries
|
||||
.iter()
|
||||
.map(|e| e.name.clone())
|
||||
.map(|e| e.raw.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
|
||||
|
@ -1,11 +1,15 @@
|
||||
use std::process::Command;
|
||||
use std::{collections::HashMap, process::Command};
|
||||
|
||||
#[cfg(not(unix))]
|
||||
use tracing::warn;
|
||||
|
||||
use super::shell::Shell;
|
||||
|
||||
pub fn shell_command(interactive: bool) -> Command {
|
||||
pub fn shell_command<S>(
|
||||
command: &str,
|
||||
interactive: bool,
|
||||
envs: &HashMap<String, String, S>,
|
||||
) -> Command {
|
||||
let shell = Shell::from_env().unwrap_or_default();
|
||||
let mut cmd = Command::new(shell.executable());
|
||||
|
||||
@ -25,5 +29,6 @@ pub fn shell_command(interactive: bool) -> Command {
|
||||
warn!("Interactive mode is not supported on Windows.");
|
||||
}
|
||||
|
||||
cmd.envs(envs).arg(command);
|
||||
cmd
|
||||
}
|
||||
|
@ -1,37 +1,5 @@
|
||||
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||||
|
||||
pub fn sep_name_and_value_indices(
|
||||
indices: Vec<u32>,
|
||||
name_len: u32,
|
||||
) -> (Vec<u32>, Vec<u32>, bool, bool) {
|
||||
let mut name_indices = Vec::new();
|
||||
let mut value_indices = Vec::new();
|
||||
let mut should_add_name_indices = false;
|
||||
let mut should_add_value_indices = false;
|
||||
|
||||
for i in indices {
|
||||
if i < name_len {
|
||||
name_indices.push(i);
|
||||
should_add_name_indices = true;
|
||||
} else {
|
||||
value_indices.push(i - name_len);
|
||||
should_add_value_indices = true;
|
||||
}
|
||||
}
|
||||
|
||||
name_indices.sort_unstable();
|
||||
name_indices.dedup();
|
||||
value_indices.sort_unstable();
|
||||
value_indices.dedup();
|
||||
|
||||
(
|
||||
name_indices,
|
||||
value_indices,
|
||||
should_add_name_indices,
|
||||
should_add_value_indices,
|
||||
)
|
||||
}
|
||||
|
||||
const ELLIPSIS: &str = "…";
|
||||
const ELLIPSIS_CHAR_WIDTH_U16: u16 = 1;
|
||||
const ELLIPSIS_CHAR_WIDTH_U32: u32 = 1;
|
||||
|
65
tests/app.rs
65
tests/app.rs
@ -3,7 +3,8 @@ use std::{collections::HashSet, path::PathBuf, time::Duration};
|
||||
use television::{
|
||||
action::Action,
|
||||
app::{App, AppOptions},
|
||||
channels::prototypes::{Cable, ChannelPrototype},
|
||||
cable::Cable,
|
||||
channels::prototypes::ChannelPrototype,
|
||||
config::default_config_from_file,
|
||||
};
|
||||
use tokio::{task::JoinHandle, time::timeout};
|
||||
@ -28,13 +29,13 @@ fn setup_app(
|
||||
JoinHandle<television::app::AppOutput>,
|
||||
tokio::sync::mpsc::UnboundedSender<Action>,
|
||||
) {
|
||||
let chan: ChannelPrototype = channel_prototype.unwrap_or_else(|| {
|
||||
let target_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests")
|
||||
.join("target_dir");
|
||||
std::env::set_current_dir(&target_dir).unwrap();
|
||||
Cable::default().get("files").unwrap().clone()
|
||||
});
|
||||
let target_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests")
|
||||
.join("target_dir");
|
||||
std::env::set_current_dir(&target_dir).unwrap();
|
||||
|
||||
let chan: ChannelPrototype =
|
||||
channel_prototype.unwrap_or(ChannelPrototype::new("files", "fd -t f"));
|
||||
let mut config = default_config_from_file().unwrap();
|
||||
// this speeds up the tests
|
||||
config.application.tick_rate = 100.0;
|
||||
@ -47,7 +48,17 @@ fn setup_app(
|
||||
false,
|
||||
config.application.tick_rate,
|
||||
);
|
||||
let mut app = App::new(&chan, config, input, options, &Cable::default());
|
||||
let mut app = App::new(
|
||||
chan,
|
||||
config,
|
||||
input,
|
||||
options,
|
||||
Cable::from_prototypes(vec![
|
||||
ChannelPrototype::new("files", "fd -t f"),
|
||||
ChannelPrototype::new("dirs", "fd -t d"),
|
||||
ChannelPrototype::new("env", "printenv"),
|
||||
]),
|
||||
);
|
||||
|
||||
// retrieve the app's action channel handle in order to send a quit action
|
||||
let tx = app.action_tx.clone();
|
||||
@ -102,13 +113,7 @@ async fn test_app_basic_search() {
|
||||
|
||||
assert!(output.selected_entries.is_some());
|
||||
assert_eq!(
|
||||
&output
|
||||
.selected_entries
|
||||
.unwrap()
|
||||
.drain()
|
||||
.next()
|
||||
.unwrap()
|
||||
.name,
|
||||
&output.selected_entries.unwrap().drain().next().unwrap().raw,
|
||||
"file1.txt"
|
||||
);
|
||||
}
|
||||
@ -142,7 +147,7 @@ async fn test_app_basic_search_multiselect() {
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|e| &e.name)
|
||||
.map(|e| &e.raw)
|
||||
.collect::<HashSet<_>>(),
|
||||
HashSet::from([&"file1.txt".to_string(), &"file2.txt".to_string()])
|
||||
);
|
||||
@ -169,10 +174,7 @@ async fn test_app_exact_search_multiselect() {
|
||||
assert!(selected_entries.is_some());
|
||||
// should contain a single entry with the prompt
|
||||
assert!(!selected_entries.as_ref().unwrap().is_empty());
|
||||
assert_eq!(
|
||||
selected_entries.unwrap().drain().next().unwrap().name,
|
||||
"fie"
|
||||
);
|
||||
assert_eq!(selected_entries.unwrap().drain().next().unwrap().raw, "fie");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
|
||||
@ -204,7 +206,7 @@ async fn test_app_exact_search_positive() {
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|e| &e.name)
|
||||
.map(|e| &e.raw)
|
||||
.collect::<HashSet<_>>(),
|
||||
HashSet::from([&"file1.txt".to_string(), &"file2.txt".to_string()])
|
||||
);
|
||||
@ -212,8 +214,7 @@ async fn test_app_exact_search_positive() {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
|
||||
async fn test_app_exits_when_select_1_and_only_one_result() {
|
||||
let prototype =
|
||||
ChannelPrototype::new("some_channel", "echo file1.txt", false, None);
|
||||
let prototype = ChannelPrototype::new("some_channel", "echo file1.txt");
|
||||
let (f, tx) = setup_app(Some(prototype), true, false);
|
||||
|
||||
// tick a few times to get the results
|
||||
@ -231,25 +232,15 @@ async fn test_app_exits_when_select_1_and_only_one_result() {
|
||||
|
||||
assert!(output.selected_entries.is_some());
|
||||
assert_eq!(
|
||||
&output
|
||||
.selected_entries
|
||||
.unwrap()
|
||||
.drain()
|
||||
.next()
|
||||
.unwrap()
|
||||
.name,
|
||||
&output.selected_entries.unwrap().drain().next().unwrap().raw,
|
||||
"file1.txt"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
|
||||
async fn test_app_does_not_exit_when_select_1_and_more_than_one_result() {
|
||||
let prototype = ChannelPrototype::new(
|
||||
"some_channel",
|
||||
"echo 'file1.txt\nfile2.txt'",
|
||||
false,
|
||||
None,
|
||||
);
|
||||
let prototype =
|
||||
ChannelPrototype::new("some_channel", "echo 'file1.txt\nfile2.txt'");
|
||||
let (f, tx) = setup_app(Some(prototype), true, false);
|
||||
|
||||
// tick a few times to get the results
|
||||
|
Loading…
x
Reference in New Issue
Block a user