mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-06 19:45:23 +00:00
refactor(screen): extract UI related code to separate crate (#106)
Co-authored-by: Bertrand Chardon <bertrand.chardon@doctrine.fr>
This commit is contained in:
parent
6e35e1a50c
commit
54399e3777
114
Cargo.lock
generated
114
Cargo.lock
generated
@ -344,7 +344,7 @@ dependencies = [
|
|||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 2.0.4",
|
"thiserror 2.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -364,9 +364,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.2"
|
version = "1.2.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc"
|
checksum = "27f657647bcff5394bf56c7317665bbf790a137a50eaaa5c6bfbb9e27a518f2d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"shlex",
|
"shlex",
|
||||||
]
|
]
|
||||||
@ -379,9 +379,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.22"
|
version = "4.5.23"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "69371e34337c4c984bbe322360c2547210bf632eb2814bbe78a6e87a2935bd2b"
|
checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
@ -389,9 +389,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.5.22"
|
version = "4.5.23"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6e24c1b4099818523236a8ca881d2b45db98dadfb4625cf6608c12069fcbbde1"
|
checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
@ -413,9 +413,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_lex"
|
name = "clap_lex"
|
||||||
version = "0.7.3"
|
version = "0.7.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7"
|
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clipboard-win"
|
name = "clipboard-win"
|
||||||
@ -1041,7 +1041,7 @@ dependencies = [
|
|||||||
"parking_lot",
|
"parking_lot",
|
||||||
"signal-hook",
|
"signal-hook",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thiserror 2.0.4",
|
"thiserror 2.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1054,7 +1054,7 @@ dependencies = [
|
|||||||
"gix-date",
|
"gix-date",
|
||||||
"gix-utils",
|
"gix-utils",
|
||||||
"itoa",
|
"itoa",
|
||||||
"thiserror 2.0.4",
|
"thiserror 2.0.5",
|
||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1064,7 +1064,7 @@ version = "0.2.13"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d48b897b4bbc881aea994b4a5bbb340a04979d7be9089791304e04a9fbc66b53"
|
checksum = "d48b897b4bbc881aea994b4a5bbb340a04979d7be9089791304e04a9fbc66b53"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror 2.0.4",
|
"thiserror 2.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1073,7 +1073,7 @@ version = "0.4.10"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c6ffbeb3a5c0b8b84c3fe4133a6f8c82fa962f4caefe8d0762eced025d3eb4f7"
|
checksum = "c6ffbeb3a5c0b8b84c3fe4133a6f8c82fa962f4caefe8d0762eced025d3eb4f7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror 2.0.4",
|
"thiserror 2.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1087,7 +1087,7 @@ dependencies = [
|
|||||||
"gix-features",
|
"gix-features",
|
||||||
"gix-hash",
|
"gix-hash",
|
||||||
"memmap2",
|
"memmap2",
|
||||||
"thiserror 2.0.4",
|
"thiserror 2.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1106,7 +1106,7 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thiserror 2.0.4",
|
"thiserror 2.0.5",
|
||||||
"unicode-bom",
|
"unicode-bom",
|
||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
@ -1121,7 +1121,7 @@ dependencies = [
|
|||||||
"bstr",
|
"bstr",
|
||||||
"gix-path",
|
"gix-path",
|
||||||
"libc",
|
"libc",
|
||||||
"thiserror 2.0.4",
|
"thiserror 2.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1133,7 +1133,7 @@ dependencies = [
|
|||||||
"bstr",
|
"bstr",
|
||||||
"itoa",
|
"itoa",
|
||||||
"jiff",
|
"jiff",
|
||||||
"thiserror 2.0.4",
|
"thiserror 2.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1145,7 +1145,7 @@ dependencies = [
|
|||||||
"bstr",
|
"bstr",
|
||||||
"gix-hash",
|
"gix-hash",
|
||||||
"gix-object",
|
"gix-object",
|
||||||
"thiserror 2.0.4",
|
"thiserror 2.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1161,7 +1161,7 @@ dependencies = [
|
|||||||
"gix-path",
|
"gix-path",
|
||||||
"gix-ref",
|
"gix-ref",
|
||||||
"gix-sec",
|
"gix-sec",
|
||||||
"thiserror 2.0.4",
|
"thiserror 2.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1179,7 +1179,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"prodash",
|
"prodash",
|
||||||
"sha1_smol",
|
"sha1_smol",
|
||||||
"thiserror 2.0.4",
|
"thiserror 2.0.5",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1213,7 +1213,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "0b5eccc17194ed0e67d49285e4853307e4147e95407f91c1c3e4a13ba9f4e4ce"
|
checksum = "0b5eccc17194ed0e67d49285e4853307e4147e95407f91c1c3e4a13ba9f4e4ce"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"faster-hex",
|
"faster-hex",
|
||||||
"thiserror 2.0.4",
|
"thiserror 2.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1252,7 +1252,7 @@ dependencies = [
|
|||||||
"memmap2",
|
"memmap2",
|
||||||
"rustix",
|
"rustix",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thiserror 2.0.4",
|
"thiserror 2.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1263,7 +1263,7 @@ checksum = "1cd3ab68a452db63d9f3ebdacb10f30dba1fa0d31ac64f4203d395ed1102d940"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"gix-tempfile",
|
"gix-tempfile",
|
||||||
"gix-utils",
|
"gix-utils",
|
||||||
"thiserror 2.0.4",
|
"thiserror 2.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1282,7 +1282,7 @@ dependencies = [
|
|||||||
"gix-validate",
|
"gix-validate",
|
||||||
"itoa",
|
"itoa",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thiserror 2.0.4",
|
"thiserror 2.0.5",
|
||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1304,7 +1304,7 @@ dependencies = [
|
|||||||
"gix-quote",
|
"gix-quote",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 2.0.4",
|
"thiserror 2.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1322,7 +1322,7 @@ dependencies = [
|
|||||||
"gix-path",
|
"gix-path",
|
||||||
"memmap2",
|
"memmap2",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thiserror 2.0.4",
|
"thiserror 2.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1335,7 +1335,7 @@ dependencies = [
|
|||||||
"gix-trace",
|
"gix-trace",
|
||||||
"home",
|
"home",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"thiserror 2.0.4",
|
"thiserror 2.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1346,7 +1346,7 @@ checksum = "64a1e282216ec2ab2816cd57e6ed88f8009e634aec47562883c05ac8a7009a63"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bstr",
|
"bstr",
|
||||||
"gix-utils",
|
"gix-utils",
|
||||||
"thiserror 2.0.4",
|
"thiserror 2.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1366,7 +1366,7 @@ dependencies = [
|
|||||||
"gix-utils",
|
"gix-utils",
|
||||||
"gix-validate",
|
"gix-validate",
|
||||||
"memmap2",
|
"memmap2",
|
||||||
"thiserror 2.0.4",
|
"thiserror 2.0.5",
|
||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1381,7 +1381,7 @@ dependencies = [
|
|||||||
"gix-revision",
|
"gix-revision",
|
||||||
"gix-validate",
|
"gix-validate",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thiserror 2.0.4",
|
"thiserror 2.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1399,7 +1399,7 @@ dependencies = [
|
|||||||
"gix-object",
|
"gix-object",
|
||||||
"gix-revwalk",
|
"gix-revwalk",
|
||||||
"gix-trace",
|
"gix-trace",
|
||||||
"thiserror 2.0.4",
|
"thiserror 2.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1414,7 +1414,7 @@ dependencies = [
|
|||||||
"gix-hashtable",
|
"gix-hashtable",
|
||||||
"gix-object",
|
"gix-object",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thiserror 2.0.4",
|
"thiserror 2.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1464,7 +1464,7 @@ dependencies = [
|
|||||||
"gix-object",
|
"gix-object",
|
||||||
"gix-revwalk",
|
"gix-revwalk",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thiserror 2.0.4",
|
"thiserror 2.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1476,7 +1476,7 @@ dependencies = [
|
|||||||
"bstr",
|
"bstr",
|
||||||
"gix-features",
|
"gix-features",
|
||||||
"gix-path",
|
"gix-path",
|
||||||
"thiserror 2.0.4",
|
"thiserror 2.0.5",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1497,7 +1497,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "cd520d09f9f585b34b32aba1d0b36ada89ab7fefb54a8ca3fe37fc482a750937"
|
checksum = "cd520d09f9f585b34b32aba1d0b36ada89ab7fefb54a8ca3fe37fc482a750937"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bstr",
|
"bstr",
|
||||||
"thiserror 2.0.4",
|
"thiserror 2.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2207,20 +2207,20 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pest"
|
name = "pest"
|
||||||
version = "2.7.14"
|
version = "2.7.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "879952a81a83930934cbf1786752d6dedc3b1f29e8f8fb2ad1d0a36f377cf442"
|
checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
"thiserror 1.0.69",
|
"thiserror 2.0.5",
|
||||||
"ucd-trie",
|
"ucd-trie",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pest_derive"
|
name = "pest_derive"
|
||||||
version = "2.7.14"
|
version = "2.7.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d214365f632b123a47fd913301e14c946c61d1c183ee245fa76eb752e59a02dd"
|
checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pest",
|
"pest",
|
||||||
"pest_generator",
|
"pest_generator",
|
||||||
@ -2228,9 +2228,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pest_generator"
|
name = "pest_generator"
|
||||||
version = "2.7.14"
|
version = "2.7.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eb55586734301717aea2ac313f50b2eb8f60d2fc3dc01d190eefa2e625f60c4e"
|
checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pest",
|
"pest",
|
||||||
"pest_meta",
|
"pest_meta",
|
||||||
@ -2241,9 +2241,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pest_meta"
|
name = "pest_meta"
|
||||||
version = "2.7.14"
|
version = "2.7.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b75da2a70cf4d9cb76833c990ac9cd3923c9a8905a8929789ce347c84564d03d"
|
checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"pest",
|
"pest",
|
||||||
@ -2854,6 +2854,7 @@ dependencies = [
|
|||||||
"television-derive",
|
"television-derive",
|
||||||
"television-fuzzy",
|
"television-fuzzy",
|
||||||
"television-previewers",
|
"television-previewers",
|
||||||
|
"television-screen",
|
||||||
"television-utils",
|
"television-utils",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
@ -2917,6 +2918,20 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "television-screen"
|
||||||
|
version = "0.0.10"
|
||||||
|
dependencies = [
|
||||||
|
"ansi-to-tui",
|
||||||
|
"color-eyre",
|
||||||
|
"ratatui",
|
||||||
|
"serde",
|
||||||
|
"syntect",
|
||||||
|
"television-channels",
|
||||||
|
"television-previewers",
|
||||||
|
"television-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "television-utils"
|
name = "television-utils"
|
||||||
version = "0.0.10"
|
version = "0.0.10"
|
||||||
@ -2929,6 +2944,7 @@ dependencies = [
|
|||||||
"lazy_static",
|
"lazy_static",
|
||||||
"syntect",
|
"syntect",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"unicode-width 0.2.0",
|
||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2956,11 +2972,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.4"
|
version = "2.0.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2f49a1853cf82743e3b7950f77e0f4d622ca36cf4317cba00c767838bac8d490"
|
checksum = "643caef17e3128658ff44d85923ef2d28af81bb71e0d67bbfe1d76f19a73e053"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl 2.0.4",
|
"thiserror-impl 2.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2976,9 +2992,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "2.0.4"
|
version = "2.0.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8381894bb3efe0c4acac3ded651301ceee58a15d47c2e34885ed1908ad667061"
|
checksum = "995d0bbc9995d1f19d28b7215a9352b0fc3cd3a2d2ec95c2cadc485cdedbcdde"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
27
Cargo.toml
27
Cargo.toml
@ -10,17 +10,17 @@ repository = "https://github.com/alexpasmantier/television"
|
|||||||
homepage = "https://github.com/alexpasmantier/television"
|
homepage = "https://github.com/alexpasmantier/television"
|
||||||
keywords = ["search", "fuzzy", "preview", "tui", "terminal"]
|
keywords = ["search", "fuzzy", "preview", "tui", "terminal"]
|
||||||
categories = [
|
categories = [
|
||||||
"command-line-utilities",
|
"command-line-utilities",
|
||||||
"command-line-interface",
|
"command-line-interface",
|
||||||
"concurrency",
|
"concurrency",
|
||||||
"development-tools",
|
"development-tools",
|
||||||
]
|
]
|
||||||
include = [
|
include = [
|
||||||
"LICENSE",
|
"LICENSE",
|
||||||
"README.md",
|
"README.md",
|
||||||
"crates/television/**/*.rs",
|
"crates/television/**/*.rs",
|
||||||
"build.rs",
|
"build.rs",
|
||||||
".config/config.toml",
|
".config/config.toml",
|
||||||
]
|
]
|
||||||
rust-version = "1.80.0"
|
rust-version = "1.80.0"
|
||||||
|
|
||||||
@ -37,10 +37,10 @@ repository = "https://github.com/alexpasmantier/television"
|
|||||||
homepage = "https://github.com/alexpasmantier/television"
|
homepage = "https://github.com/alexpasmantier/television"
|
||||||
keywords = ["search", "fuzzy", "preview", "tui", "terminal"]
|
keywords = ["search", "fuzzy", "preview", "tui", "terminal"]
|
||||||
categories = [
|
categories = [
|
||||||
"command-line-utilities",
|
"command-line-utilities",
|
||||||
"command-line-interface",
|
"command-line-interface",
|
||||||
"concurrency",
|
"concurrency",
|
||||||
"development-tools",
|
"development-tools",
|
||||||
]
|
]
|
||||||
include = ["LICENSE", "README.md", "crates/television/**/*.rs", "build.rs"]
|
include = ["LICENSE", "README.md", "crates/television/**/*.rs", "build.rs"]
|
||||||
rust-version = "1.80.0"
|
rust-version = "1.80.0"
|
||||||
@ -56,6 +56,7 @@ name = "tv"
|
|||||||
# workspace dependencies
|
# workspace dependencies
|
||||||
television-fuzzy = { path = "crates/television-fuzzy", version = "0.0.10" }
|
television-fuzzy = { path = "crates/television-fuzzy", version = "0.0.10" }
|
||||||
television-derive = { path = "crates/television-derive", version = "0.0.10" }
|
television-derive = { path = "crates/television-derive", version = "0.0.10" }
|
||||||
|
television-screen = { path = "crates/television-screen", version = "0.0.10" }
|
||||||
television-channels = { path = "crates/television-channels", version = "0.0.10" }
|
television-channels = { path = "crates/television-channels", version = "0.0.10" }
|
||||||
television-previewers = { path = "crates/television-previewers", version = "0.0.10" }
|
television-previewers = { path = "crates/television-previewers", version = "0.0.10" }
|
||||||
television-utils = { path = "crates/television-utils", version = "0.0.10" }
|
television-utils = { path = "crates/television-utils", version = "0.0.10" }
|
||||||
|
24
crates/television-screen/Cargo.toml
Normal file
24
crates/television-screen/Cargo.toml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
[package]
|
||||||
|
name = "television-screen"
|
||||||
|
version = "0.0.10"
|
||||||
|
edition.workspace = true
|
||||||
|
description.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
keywords.workspace = true
|
||||||
|
categories.workspace = true
|
||||||
|
include.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
readme.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
ratatui = "0.29.0"
|
||||||
|
serde = "1.0.215"
|
||||||
|
television-utils = { path = "../television-utils", version="0.0.10" }
|
||||||
|
television-channels = { path = "../television-channels", version="0.0.10" }
|
||||||
|
television-previewers = { path = "../television-previewers", version="0.0.10" }
|
||||||
|
color-eyre = "0.6.3"
|
||||||
|
ansi-to-tui = "7.0.0"
|
||||||
|
syntect = "5.2.0"
|
63
crates/television-screen/src/colors.rs
Normal file
63
crates/television-screen/src/colors.rs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
use ratatui::style::Color;
|
||||||
|
|
||||||
|
pub const BORDER_COLOR: Color = Color::Blue;
|
||||||
|
pub const ACTION_COLOR: Color = Color::DarkGray;
|
||||||
|
// Styles
|
||||||
|
// input
|
||||||
|
pub const DEFAULT_INPUT_FG: Color = Color::LightRed;
|
||||||
|
pub const DEFAULT_RESULTS_COUNT_FG: Color = Color::LightRed;
|
||||||
|
// preview
|
||||||
|
pub const DEFAULT_PREVIEW_TITLE_FG: Color = Color::Blue;
|
||||||
|
pub const DEFAULT_SELECTED_PREVIEW_BG: Color = Color::Rgb(50, 50, 50);
|
||||||
|
pub const DEFAULT_PREVIEW_CONTENT_FG: Color = Color::Rgb(150, 150, 180);
|
||||||
|
pub const DEFAULT_PREVIEW_GUTTER_FG: Color = Color::Rgb(70, 70, 70);
|
||||||
|
pub const DEFAULT_PREVIEW_GUTTER_SELECTED_FG: Color =
|
||||||
|
Color::Rgb(255, 150, 150);
|
||||||
|
// Styles
|
||||||
|
pub const DEFAULT_RESULT_NAME_FG: Color = Color::Blue;
|
||||||
|
pub const DEFAULT_RESULT_PREVIEW_FG: Color = Color::Rgb(150, 150, 150);
|
||||||
|
pub const DEFAULT_RESULT_LINE_NUMBER_FG: Color = Color::Yellow;
|
||||||
|
pub const DEFAULT_RESULT_SELECTED_BG: Color = Color::Rgb(50, 50, 50);
|
||||||
|
|
||||||
|
pub const DEFAULT_RESULTS_LIST_MATCH_FOREGROUND_COLOR: Color = Color::Red;
|
||||||
|
|
||||||
|
pub struct ResultsListColors {
|
||||||
|
pub result_name_fg: Color,
|
||||||
|
pub result_preview_fg: Color,
|
||||||
|
pub result_line_number_fg: Color,
|
||||||
|
pub result_selected_bg: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ResultsListColors {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
result_name_fg: DEFAULT_RESULT_NAME_FG,
|
||||||
|
result_preview_fg: DEFAULT_RESULT_PREVIEW_FG,
|
||||||
|
result_line_number_fg: DEFAULT_RESULT_LINE_NUMBER_FG,
|
||||||
|
result_selected_bg: DEFAULT_RESULT_SELECTED_BG,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl ResultsListColors {
|
||||||
|
pub fn result_name_fg(mut self, color: Color) -> Self {
|
||||||
|
self.result_name_fg = color;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn result_preview_fg(mut self, color: Color) -> Self {
|
||||||
|
self.result_preview_fg = color;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn result_line_number_fg(mut self, color: Color) -> Self {
|
||||||
|
self.result_line_number_fg = color;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn result_selected_bg(mut self, color: Color) -> Self {
|
||||||
|
self.result_selected_bg = color;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
79
crates/television-screen/src/help.rs
Normal file
79
crates/television-screen/src/help.rs
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
use super::layout::HelpBarLayout;
|
||||||
|
use crate::colors::BORDER_COLOR;
|
||||||
|
use crate::logo::build_logo_paragraph;
|
||||||
|
use crate::metadata::build_metadata_table;
|
||||||
|
use crate::mode::{mode_color, Mode};
|
||||||
|
use ratatui::layout::Rect;
|
||||||
|
use ratatui::prelude::{Color, Style};
|
||||||
|
use ratatui::widgets::{Block, BorderType, Borders, Padding, Table};
|
||||||
|
use ratatui::Frame;
|
||||||
|
use television_channels::channels::UnitChannel;
|
||||||
|
use television_utils::metadata::AppMetadata;
|
||||||
|
|
||||||
|
pub fn draw_logo_block(f: &mut Frame, area: Rect, color: Color) {
|
||||||
|
let logo_block = Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
|
.border_style(Style::default().fg(BORDER_COLOR))
|
||||||
|
.style(Style::default().fg(color))
|
||||||
|
.padding(Padding::horizontal(1));
|
||||||
|
|
||||||
|
let logo_paragraph = build_logo_paragraph().block(logo_block);
|
||||||
|
|
||||||
|
f.render_widget(logo_paragraph, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_metadata_block(
|
||||||
|
f: &mut Frame,
|
||||||
|
area: Rect,
|
||||||
|
mode: Mode,
|
||||||
|
current_channel: UnitChannel,
|
||||||
|
app_metadata: &AppMetadata,
|
||||||
|
) {
|
||||||
|
let metadata_block = Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
|
.border_style(Style::default().fg(Color::Blue))
|
||||||
|
.padding(Padding::horizontal(1))
|
||||||
|
.style(Style::default());
|
||||||
|
|
||||||
|
let metadata_table =
|
||||||
|
build_metadata_table(mode, current_channel, app_metadata)
|
||||||
|
.block(metadata_block);
|
||||||
|
|
||||||
|
f.render_widget(metadata_table, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_keymaps_block(f: &mut Frame, area: Rect, keymap_table: Table) {
|
||||||
|
let keymaps_block = Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
|
.border_style(Style::default().fg(Color::Blue))
|
||||||
|
.style(Style::default())
|
||||||
|
.padding(Padding::horizontal(1));
|
||||||
|
|
||||||
|
let keymaps_table = keymap_table.block(keymaps_block);
|
||||||
|
|
||||||
|
f.render_widget(keymaps_table, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_help_bar(
|
||||||
|
f: &mut Frame,
|
||||||
|
layout: &Option<HelpBarLayout>,
|
||||||
|
current_channel: UnitChannel,
|
||||||
|
keymap_table: Table,
|
||||||
|
mode: Mode,
|
||||||
|
app_metadata: &AppMetadata,
|
||||||
|
) {
|
||||||
|
if let Some(help_bar) = layout {
|
||||||
|
draw_metadata_block(
|
||||||
|
f,
|
||||||
|
help_bar.left,
|
||||||
|
mode,
|
||||||
|
current_channel,
|
||||||
|
app_metadata,
|
||||||
|
);
|
||||||
|
draw_keymaps_block(f, help_bar.middle, keymap_table);
|
||||||
|
draw_logo_block(f, help_bar.right, mode_color(mode));
|
||||||
|
}
|
||||||
|
}
|
116
crates/television-screen/src/input.rs
Normal file
116
crates/television-screen/src/input.rs
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
use color_eyre::Result;
|
||||||
|
use ratatui::{
|
||||||
|
layout::{
|
||||||
|
Alignment, Constraint, Direction, Layout as RatatuiLayout, Rect,
|
||||||
|
},
|
||||||
|
style::{Style, Stylize},
|
||||||
|
text::{Line, Span},
|
||||||
|
widgets::{Block, BorderType, Borders, ListState, Paragraph},
|
||||||
|
Frame,
|
||||||
|
};
|
||||||
|
use television_utils::input::Input;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
colors::{BORDER_COLOR, DEFAULT_INPUT_FG, DEFAULT_RESULTS_COUNT_FG},
|
||||||
|
spinner::{Spinner, SpinnerState},
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: refactor arguments (e.g. use a struct for the spinner+state, same
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn draw_input_box(
|
||||||
|
f: &mut Frame,
|
||||||
|
rect: Rect,
|
||||||
|
results_count: u32,
|
||||||
|
total_count: u32,
|
||||||
|
input_state: &mut Input,
|
||||||
|
results_picker_state: &mut ListState,
|
||||||
|
matcher_running: bool,
|
||||||
|
spinner: &Spinner,
|
||||||
|
spinner_state: &mut SpinnerState,
|
||||||
|
) -> Result<()> {
|
||||||
|
let input_block = Block::default()
|
||||||
|
.title_top(Line::from(" Pattern ").alignment(Alignment::Center))
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
|
.border_style(Style::default().fg(BORDER_COLOR))
|
||||||
|
.style(Style::default());
|
||||||
|
|
||||||
|
let input_block_inner = input_block.inner(rect);
|
||||||
|
if input_block_inner.area() == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
f.render_widget(input_block, rect);
|
||||||
|
|
||||||
|
// split input block into 4 parts: prompt symbol, input, result count, spinner
|
||||||
|
let inner_input_chunks = RatatuiLayout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([
|
||||||
|
// prompt symbol
|
||||||
|
Constraint::Length(2),
|
||||||
|
// input field
|
||||||
|
Constraint::Fill(1),
|
||||||
|
// result count
|
||||||
|
Constraint::Length(
|
||||||
|
3 * ((total_count as f32).log10().ceil() as u16 + 1) + 3,
|
||||||
|
),
|
||||||
|
// spinner
|
||||||
|
Constraint::Length(1),
|
||||||
|
])
|
||||||
|
.split(input_block_inner);
|
||||||
|
|
||||||
|
let arrow_block = Block::default();
|
||||||
|
let arrow = Paragraph::new(Span::styled(
|
||||||
|
"> ",
|
||||||
|
Style::default().fg(DEFAULT_INPUT_FG).bold(),
|
||||||
|
))
|
||||||
|
.block(arrow_block);
|
||||||
|
f.render_widget(arrow, inner_input_chunks[0]);
|
||||||
|
|
||||||
|
let interactive_input_block = Block::default();
|
||||||
|
// keep 2 for borders and 1 for cursor
|
||||||
|
let width = inner_input_chunks[1].width.max(3) - 3;
|
||||||
|
let scroll = input_state.visual_scroll(width as usize);
|
||||||
|
let input = Paragraph::new(input_state.value())
|
||||||
|
.scroll((0, u16::try_from(scroll)?))
|
||||||
|
.block(interactive_input_block)
|
||||||
|
.style(Style::default().fg(DEFAULT_INPUT_FG).bold().italic())
|
||||||
|
.alignment(Alignment::Left);
|
||||||
|
f.render_widget(input, inner_input_chunks[1]);
|
||||||
|
|
||||||
|
if matcher_running {
|
||||||
|
f.render_stateful_widget(
|
||||||
|
spinner,
|
||||||
|
inner_input_chunks[3],
|
||||||
|
spinner_state,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result_count_block = Block::default();
|
||||||
|
let result_count_paragraph = Paragraph::new(Span::styled(
|
||||||
|
format!(
|
||||||
|
" {} / {} ",
|
||||||
|
if results_count == 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
results_picker_state.selected().unwrap_or(0) + 1
|
||||||
|
},
|
||||||
|
results_count,
|
||||||
|
),
|
||||||
|
Style::default().fg(DEFAULT_RESULTS_COUNT_FG).italic(),
|
||||||
|
))
|
||||||
|
.block(result_count_block)
|
||||||
|
.alignment(Alignment::Right);
|
||||||
|
f.render_widget(result_count_paragraph, inner_input_chunks[2]);
|
||||||
|
|
||||||
|
// Make the cursor visible and ask tui-rs to put it at the
|
||||||
|
// specified coordinates after rendering
|
||||||
|
f.set_cursor_position((
|
||||||
|
// Put cursor past the end of the input text
|
||||||
|
inner_input_chunks[1].x
|
||||||
|
+ u16::try_from(input_state.visual_cursor().max(scroll) - scroll)?,
|
||||||
|
// Move one line down, from the border to the input line
|
||||||
|
inner_input_chunks[1].y,
|
||||||
|
));
|
||||||
|
Ok(())
|
||||||
|
}
|
279
crates/television-screen/src/keybindings.rs
Normal file
279
crates/television-screen/src/keybindings.rs
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
use std::{collections::HashMap, fmt::Display};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
colors::ACTION_COLOR,
|
||||||
|
mode::{Mode, CHANNEL_COLOR, REMOTE_CONTROL_COLOR, SEND_TO_CHANNEL_COLOR},
|
||||||
|
};
|
||||||
|
use ratatui::{
|
||||||
|
layout::Constraint,
|
||||||
|
style::{Color, Style},
|
||||||
|
text::{Line, Span},
|
||||||
|
widgets::{Cell, Row, Table},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DisplayableKeybindings {
|
||||||
|
bindings: HashMap<DisplayableAction, Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DisplayableKeybindings {
|
||||||
|
pub fn new(bindings: HashMap<DisplayableAction, Vec<String>>) -> Self {
|
||||||
|
Self { bindings }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub enum DisplayableAction {
|
||||||
|
ResultsNavigation,
|
||||||
|
PreviewNavigation,
|
||||||
|
SelectEntry,
|
||||||
|
CopyEntryToClipboard,
|
||||||
|
SendToChannel,
|
||||||
|
ToggleRemoteControl,
|
||||||
|
Cancel,
|
||||||
|
Quit,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for DisplayableAction {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let action = match self {
|
||||||
|
DisplayableAction::ResultsNavigation => "Results navigation",
|
||||||
|
DisplayableAction::PreviewNavigation => "Preview navigation",
|
||||||
|
DisplayableAction::SelectEntry => "Select entry",
|
||||||
|
DisplayableAction::CopyEntryToClipboard => {
|
||||||
|
"Copy entry to clipboard"
|
||||||
|
}
|
||||||
|
DisplayableAction::SendToChannel => "Send to channel",
|
||||||
|
DisplayableAction::ToggleRemoteControl => "Toggle Remote control",
|
||||||
|
DisplayableAction::Cancel => "Cancel",
|
||||||
|
DisplayableAction::Quit => "Quit",
|
||||||
|
};
|
||||||
|
write!(f, "{}", action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_keybindings_table(
|
||||||
|
keybindings: &HashMap<Mode, DisplayableKeybindings>,
|
||||||
|
mode: Mode,
|
||||||
|
) -> Table<'_> {
|
||||||
|
match mode {
|
||||||
|
Mode::Channel => {
|
||||||
|
build_keybindings_table_for_channel(&keybindings[&mode])
|
||||||
|
}
|
||||||
|
Mode::RemoteControl => {
|
||||||
|
build_keybindings_table_for_channel_selection(&keybindings[&mode])
|
||||||
|
}
|
||||||
|
Mode::SendToChannel => {
|
||||||
|
build_keybindings_table_for_channel_transitions(
|
||||||
|
&keybindings[&mode],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_keybindings_table_for_channel(
|
||||||
|
keybindings: &DisplayableKeybindings,
|
||||||
|
) -> Table<'_> {
|
||||||
|
// Results navigation
|
||||||
|
let results_navigation_keys = keybindings
|
||||||
|
.bindings
|
||||||
|
.get(&DisplayableAction::ResultsNavigation)
|
||||||
|
.unwrap();
|
||||||
|
let results_row = Row::new(build_cells_for_group(
|
||||||
|
"Results navigation",
|
||||||
|
results_navigation_keys,
|
||||||
|
CHANNEL_COLOR,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Preview navigation
|
||||||
|
let preview_navigation_keys = keybindings
|
||||||
|
.bindings
|
||||||
|
.get(&DisplayableAction::PreviewNavigation)
|
||||||
|
.unwrap();
|
||||||
|
let preview_row = Row::new(build_cells_for_group(
|
||||||
|
"Preview navigation",
|
||||||
|
preview_navigation_keys,
|
||||||
|
CHANNEL_COLOR,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Select entry
|
||||||
|
let select_entry_keys = keybindings
|
||||||
|
.bindings
|
||||||
|
.get(&DisplayableAction::SelectEntry)
|
||||||
|
.unwrap();
|
||||||
|
let select_entry_row = Row::new(build_cells_for_group(
|
||||||
|
"Select entry",
|
||||||
|
select_entry_keys,
|
||||||
|
CHANNEL_COLOR,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Copy entry to clipboard
|
||||||
|
let copy_entry_keys = keybindings
|
||||||
|
.bindings
|
||||||
|
.get(&DisplayableAction::CopyEntryToClipboard)
|
||||||
|
.unwrap();
|
||||||
|
let copy_entry_row = Row::new(build_cells_for_group(
|
||||||
|
"Copy entry to clipboard",
|
||||||
|
copy_entry_keys,
|
||||||
|
CHANNEL_COLOR,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Send to channel
|
||||||
|
let send_to_channel_keys = keybindings
|
||||||
|
.bindings
|
||||||
|
.get(&DisplayableAction::SendToChannel)
|
||||||
|
.unwrap();
|
||||||
|
let send_to_channel_row = Row::new(build_cells_for_group(
|
||||||
|
"Send results to",
|
||||||
|
send_to_channel_keys,
|
||||||
|
CHANNEL_COLOR,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Switch channels
|
||||||
|
let switch_channels_keys = keybindings
|
||||||
|
.bindings
|
||||||
|
.get(&DisplayableAction::ToggleRemoteControl)
|
||||||
|
.unwrap();
|
||||||
|
let switch_channels_row = Row::new(build_cells_for_group(
|
||||||
|
"Toggle Remote control",
|
||||||
|
switch_channels_keys,
|
||||||
|
CHANNEL_COLOR,
|
||||||
|
));
|
||||||
|
|
||||||
|
// MISC line (quit, help, etc.)
|
||||||
|
// Quit ⏼
|
||||||
|
let quit_keys =
|
||||||
|
keybindings.bindings.get(&DisplayableAction::Quit).unwrap();
|
||||||
|
let quit_row =
|
||||||
|
Row::new(build_cells_for_group("Quit", quit_keys, CHANNEL_COLOR));
|
||||||
|
|
||||||
|
let widths = vec![Constraint::Fill(1), Constraint::Fill(2)];
|
||||||
|
|
||||||
|
Table::new(
|
||||||
|
vec![
|
||||||
|
results_row,
|
||||||
|
preview_row,
|
||||||
|
select_entry_row,
|
||||||
|
copy_entry_row,
|
||||||
|
send_to_channel_row,
|
||||||
|
switch_channels_row,
|
||||||
|
quit_row,
|
||||||
|
],
|
||||||
|
widths,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_keybindings_table_for_channel_selection(
|
||||||
|
keybindings: &DisplayableKeybindings,
|
||||||
|
) -> Table<'_> {
|
||||||
|
// Results navigation
|
||||||
|
let navigation_keys = keybindings
|
||||||
|
.bindings
|
||||||
|
.get(&DisplayableAction::ResultsNavigation)
|
||||||
|
.unwrap();
|
||||||
|
let results_row = Row::new(build_cells_for_group(
|
||||||
|
"Browse channels",
|
||||||
|
navigation_keys,
|
||||||
|
REMOTE_CONTROL_COLOR,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Select entry
|
||||||
|
let select_entry_keys = keybindings
|
||||||
|
.bindings
|
||||||
|
.get(&DisplayableAction::SelectEntry)
|
||||||
|
.unwrap();
|
||||||
|
let select_entry_row = Row::new(build_cells_for_group(
|
||||||
|
"Select channel",
|
||||||
|
select_entry_keys,
|
||||||
|
REMOTE_CONTROL_COLOR,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Remote control
|
||||||
|
let switch_channels_keys = keybindings
|
||||||
|
.bindings
|
||||||
|
.get(&DisplayableAction::ToggleRemoteControl)
|
||||||
|
.unwrap();
|
||||||
|
let switch_channels_row = Row::new(build_cells_for_group(
|
||||||
|
"Toggle Remote control",
|
||||||
|
switch_channels_keys,
|
||||||
|
REMOTE_CONTROL_COLOR,
|
||||||
|
));
|
||||||
|
|
||||||
|
Table::new(
|
||||||
|
vec![results_row, select_entry_row, switch_channels_row],
|
||||||
|
vec![Constraint::Fill(1), Constraint::Fill(2)],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_keybindings_table_for_channel_transitions(
|
||||||
|
keybindings: &DisplayableKeybindings,
|
||||||
|
) -> Table<'_> {
|
||||||
|
// Results navigation
|
||||||
|
let results_navigation_keys = keybindings
|
||||||
|
.bindings
|
||||||
|
.get(&DisplayableAction::ResultsNavigation)
|
||||||
|
.unwrap();
|
||||||
|
let results_row = Row::new(build_cells_for_group(
|
||||||
|
"Browse channels",
|
||||||
|
results_navigation_keys,
|
||||||
|
SEND_TO_CHANNEL_COLOR,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Select entry
|
||||||
|
let select_entry_keys = keybindings
|
||||||
|
.bindings
|
||||||
|
.get(&DisplayableAction::SelectEntry)
|
||||||
|
.unwrap();
|
||||||
|
let select_entry_row = Row::new(build_cells_for_group(
|
||||||
|
"Send to channel",
|
||||||
|
select_entry_keys,
|
||||||
|
SEND_TO_CHANNEL_COLOR,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Cancel
|
||||||
|
let cancel_keys = keybindings
|
||||||
|
.bindings
|
||||||
|
.get(&DisplayableAction::Cancel)
|
||||||
|
.unwrap();
|
||||||
|
let cancel_row = Row::new(build_cells_for_group(
|
||||||
|
"Cancel",
|
||||||
|
cancel_keys,
|
||||||
|
SEND_TO_CHANNEL_COLOR,
|
||||||
|
));
|
||||||
|
|
||||||
|
Table::new(
|
||||||
|
vec![results_row, select_entry_row, cancel_row],
|
||||||
|
vec![Constraint::Fill(1), Constraint::Fill(2)],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_cells_for_group<'a>(
|
||||||
|
group_name: &str,
|
||||||
|
keys: &'a [String],
|
||||||
|
key_color: Color,
|
||||||
|
) -> Vec<Cell<'a>> {
|
||||||
|
// group name
|
||||||
|
let mut cells = vec![Cell::from(Span::styled(
|
||||||
|
group_name.to_owned() + ": ",
|
||||||
|
Style::default().fg(ACTION_COLOR),
|
||||||
|
))];
|
||||||
|
|
||||||
|
let spans = keys.iter().skip(1).fold(
|
||||||
|
vec![Span::styled(
|
||||||
|
keys[0].clone(),
|
||||||
|
Style::default().fg(key_color),
|
||||||
|
)],
|
||||||
|
|mut acc, key| {
|
||||||
|
acc.push(Span::raw(" / "));
|
||||||
|
acc.push(Span::styled(
|
||||||
|
key.to_owned(),
|
||||||
|
Style::default().fg(key_color),
|
||||||
|
));
|
||||||
|
acc
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
cells.push(Cell::from(Line::from(spans)));
|
||||||
|
|
||||||
|
cells
|
||||||
|
}
|
13
crates/television-screen/src/lib.rs
Normal file
13
crates/television-screen/src/lib.rs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
pub mod cache;
|
||||||
|
pub mod colors;
|
||||||
|
pub mod help;
|
||||||
|
pub mod input;
|
||||||
|
pub mod keybindings;
|
||||||
|
pub mod layout;
|
||||||
|
pub mod logo;
|
||||||
|
pub mod metadata;
|
||||||
|
pub mod mode;
|
||||||
|
pub mod preview;
|
||||||
|
pub mod remote_control;
|
||||||
|
pub mod results;
|
||||||
|
pub mod spinner;
|
125
crates/television-screen/src/metadata.rs
Normal file
125
crates/television-screen/src/metadata.rs
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use crate::mode::{mode_color, Mode};
|
||||||
|
use ratatui::{
|
||||||
|
layout::Constraint,
|
||||||
|
style::{Color, Style},
|
||||||
|
text::Span,
|
||||||
|
widgets::{Cell, Row, Table},
|
||||||
|
};
|
||||||
|
use television_channels::channels::UnitChannel;
|
||||||
|
use television_utils::metadata::AppMetadata;
|
||||||
|
|
||||||
|
const METADATA_FIELD_NAME_COLOR: Color = Color::DarkGray;
|
||||||
|
const METADATA_FIELD_VALUE_COLOR: Color = Color::Gray;
|
||||||
|
|
||||||
|
impl Display for Mode {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Mode::Channel => write!(f, "Channel"),
|
||||||
|
Mode::RemoteControl => write!(f, "Remote Control"),
|
||||||
|
Mode::SendToChannel => write!(f, "Send to Channel"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_metadata_table(
|
||||||
|
mode: Mode,
|
||||||
|
current_channel: UnitChannel,
|
||||||
|
app_metadata: &AppMetadata,
|
||||||
|
) -> Table<'_> {
|
||||||
|
let version_row = Row::new(vec![
|
||||||
|
Cell::from(Span::styled(
|
||||||
|
"version: ",
|
||||||
|
Style::default().fg(METADATA_FIELD_NAME_COLOR),
|
||||||
|
)),
|
||||||
|
Cell::from(Span::styled(
|
||||||
|
&app_metadata.version,
|
||||||
|
Style::default().fg(METADATA_FIELD_VALUE_COLOR),
|
||||||
|
)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let target_triple_row = Row::new(vec![
|
||||||
|
Cell::from(Span::styled(
|
||||||
|
"target triple: ",
|
||||||
|
Style::default().fg(METADATA_FIELD_NAME_COLOR),
|
||||||
|
)),
|
||||||
|
Cell::from(Span::styled(
|
||||||
|
&app_metadata.build.target_triple,
|
||||||
|
Style::default().fg(METADATA_FIELD_VALUE_COLOR),
|
||||||
|
)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let build_row = Row::new(vec![
|
||||||
|
Cell::from(Span::styled(
|
||||||
|
"build: ",
|
||||||
|
Style::default().fg(METADATA_FIELD_NAME_COLOR),
|
||||||
|
)),
|
||||||
|
Cell::from(Span::styled(
|
||||||
|
&app_metadata.build.rustc_version,
|
||||||
|
Style::default().fg(METADATA_FIELD_VALUE_COLOR),
|
||||||
|
)),
|
||||||
|
Cell::from(Span::styled(
|
||||||
|
" (",
|
||||||
|
Style::default().fg(METADATA_FIELD_NAME_COLOR),
|
||||||
|
)),
|
||||||
|
Cell::from(Span::styled(
|
||||||
|
&app_metadata.build.build_date,
|
||||||
|
Style::default().fg(METADATA_FIELD_VALUE_COLOR),
|
||||||
|
)),
|
||||||
|
Cell::from(Span::styled(
|
||||||
|
")",
|
||||||
|
Style::default().fg(METADATA_FIELD_NAME_COLOR),
|
||||||
|
)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let current_dir_row = Row::new(vec![
|
||||||
|
Cell::from(Span::styled(
|
||||||
|
"current directory: ",
|
||||||
|
Style::default().fg(METADATA_FIELD_NAME_COLOR),
|
||||||
|
)),
|
||||||
|
Cell::from(Span::styled(
|
||||||
|
std::env::current_dir()
|
||||||
|
.expect("Could not get current directory")
|
||||||
|
.display()
|
||||||
|
.to_string(),
|
||||||
|
Style::default().fg(METADATA_FIELD_VALUE_COLOR),
|
||||||
|
)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let current_channel_row = Row::new(vec![
|
||||||
|
Cell::from(Span::styled(
|
||||||
|
"current channel: ",
|
||||||
|
Style::default().fg(METADATA_FIELD_NAME_COLOR),
|
||||||
|
)),
|
||||||
|
Cell::from(Span::styled(
|
||||||
|
current_channel.to_string(),
|
||||||
|
Style::default().fg(METADATA_FIELD_VALUE_COLOR),
|
||||||
|
)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let current_mode_row = Row::new(vec![
|
||||||
|
Cell::from(Span::styled(
|
||||||
|
"current mode: ",
|
||||||
|
Style::default().fg(METADATA_FIELD_NAME_COLOR),
|
||||||
|
)),
|
||||||
|
Cell::from(Span::styled(
|
||||||
|
mode.to_string(),
|
||||||
|
Style::default().fg(mode_color(mode)),
|
||||||
|
)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let widths = vec![Constraint::Fill(1), Constraint::Fill(2)];
|
||||||
|
|
||||||
|
Table::new(
|
||||||
|
vec![
|
||||||
|
version_row,
|
||||||
|
target_triple_row,
|
||||||
|
build_row,
|
||||||
|
current_dir_row,
|
||||||
|
current_channel_row,
|
||||||
|
current_mode_row,
|
||||||
|
],
|
||||||
|
widths,
|
||||||
|
)
|
||||||
|
}
|
22
crates/television-screen/src/mode.rs
Normal file
22
crates/television-screen/src/mode.rs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
use ratatui::style::Color;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub const CHANNEL_COLOR: Color = Color::Indexed(222);
|
||||||
|
pub const REMOTE_CONTROL_COLOR: Color = Color::Indexed(1);
|
||||||
|
pub const SEND_TO_CHANNEL_COLOR: Color = Color::Indexed(105);
|
||||||
|
|
||||||
|
pub fn mode_color(mode: Mode) -> Color {
|
||||||
|
match mode {
|
||||||
|
Mode::Channel => CHANNEL_COLOR,
|
||||||
|
Mode::RemoteControl => REMOTE_CONTROL_COLOR,
|
||||||
|
Mode::SendToChannel => SEND_TO_CHANNEL_COLOR,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Mode shouldn't be in the screen crate
|
||||||
|
#[derive(PartialEq, Copy, Clone, Hash, Eq, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum Mode {
|
||||||
|
Channel,
|
||||||
|
RemoteControl,
|
||||||
|
SendToChannel,
|
||||||
|
}
|
413
crates/television-screen/src/preview.rs
Normal file
413
crates/television-screen/src/preview.rs
Normal file
@ -0,0 +1,413 @@
|
|||||||
|
use crate::cache::RenderedPreviewCache;
|
||||||
|
use crate::colors::{
|
||||||
|
BORDER_COLOR, DEFAULT_PREVIEW_CONTENT_FG, DEFAULT_PREVIEW_GUTTER_FG,
|
||||||
|
DEFAULT_PREVIEW_GUTTER_SELECTED_FG, DEFAULT_PREVIEW_TITLE_FG,
|
||||||
|
DEFAULT_SELECTED_PREVIEW_BG,
|
||||||
|
};
|
||||||
|
use ansi_to_tui::IntoText;
|
||||||
|
use color_eyre::eyre::Result;
|
||||||
|
use ratatui::layout::{Alignment, Rect};
|
||||||
|
use ratatui::prelude::{Color, Line, Modifier, Span, Style, Stylize, Text};
|
||||||
|
use ratatui::widgets::{Block, BorderType, Borders, Padding, Paragraph, Wrap};
|
||||||
|
use ratatui::Frame;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use syntect::highlighting::Color as SyntectColor;
|
||||||
|
use television_channels::entry::Entry;
|
||||||
|
use television_previewers::previewers::{
|
||||||
|
Preview, PreviewContent, FILE_TOO_LARGE_MSG, PREVIEW_NOT_SUPPORTED_MSG,
|
||||||
|
};
|
||||||
|
use television_utils::strings::{
|
||||||
|
replace_non_printable, shrink_with_ellipsis, ReplaceNonPrintableConfig,
|
||||||
|
EMPTY_STRING,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
const FILL_CHAR_SLANTED: char = '╱';
|
||||||
|
const FILL_CHAR_EMPTY: char = ' ';
|
||||||
|
|
||||||
|
pub fn build_preview_paragraph(
|
||||||
|
preview_block: Block,
|
||||||
|
inner: Rect,
|
||||||
|
preview_content: PreviewContent,
|
||||||
|
target_line: Option<u16>,
|
||||||
|
preview_scroll: u16,
|
||||||
|
) -> Paragraph {
|
||||||
|
match preview_content {
|
||||||
|
PreviewContent::AnsiText(text) => {
|
||||||
|
build_ansi_text_paragraph(text, preview_block, preview_scroll)
|
||||||
|
}
|
||||||
|
PreviewContent::PlainText(content) => build_plain_text_paragraph(
|
||||||
|
content,
|
||||||
|
preview_block,
|
||||||
|
target_line,
|
||||||
|
preview_scroll,
|
||||||
|
),
|
||||||
|
PreviewContent::PlainTextWrapped(content) => {
|
||||||
|
build_plain_text_wrapped_paragraph(content, preview_block)
|
||||||
|
}
|
||||||
|
PreviewContent::SyntectHighlightedText(highlighted_lines) => {
|
||||||
|
build_syntect_highlighted_paragraph(
|
||||||
|
highlighted_lines,
|
||||||
|
preview_block,
|
||||||
|
target_line,
|
||||||
|
preview_scroll,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// meta
|
||||||
|
PreviewContent::Loading => {
|
||||||
|
build_meta_preview_paragraph(inner, "Loading...", FILL_CHAR_EMPTY)
|
||||||
|
.block(preview_block)
|
||||||
|
.alignment(Alignment::Left)
|
||||||
|
.style(Style::default().add_modifier(Modifier::ITALIC))
|
||||||
|
}
|
||||||
|
PreviewContent::NotSupported => build_meta_preview_paragraph(
|
||||||
|
inner,
|
||||||
|
PREVIEW_NOT_SUPPORTED_MSG,
|
||||||
|
FILL_CHAR_EMPTY,
|
||||||
|
)
|
||||||
|
.block(preview_block)
|
||||||
|
.alignment(Alignment::Left)
|
||||||
|
.style(Style::default().add_modifier(Modifier::ITALIC)),
|
||||||
|
PreviewContent::FileTooLarge => build_meta_preview_paragraph(
|
||||||
|
inner,
|
||||||
|
FILE_TOO_LARGE_MSG,
|
||||||
|
FILL_CHAR_EMPTY,
|
||||||
|
)
|
||||||
|
.block(preview_block)
|
||||||
|
.alignment(Alignment::Left)
|
||||||
|
.style(Style::default().add_modifier(Modifier::ITALIC)),
|
||||||
|
PreviewContent::Empty => Paragraph::new(Text::raw(EMPTY_STRING)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_ansi_text_paragraph(
|
||||||
|
text: String,
|
||||||
|
preview_block: Block,
|
||||||
|
preview_scroll: u16,
|
||||||
|
) -> Paragraph {
|
||||||
|
let text = replace_non_printable(
|
||||||
|
text.as_bytes(),
|
||||||
|
&ReplaceNonPrintableConfig {
|
||||||
|
replace_line_feed: false,
|
||||||
|
replace_control_characters: false,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.0
|
||||||
|
.into_text()
|
||||||
|
.unwrap();
|
||||||
|
Paragraph::new(text)
|
||||||
|
.block(preview_block)
|
||||||
|
.scroll((preview_scroll, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_plain_text_paragraph(
|
||||||
|
text: Vec<String>,
|
||||||
|
preview_block: Block,
|
||||||
|
target_line: Option<u16>,
|
||||||
|
preview_scroll: u16,
|
||||||
|
) -> Paragraph {
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
for (i, line) in text.iter().enumerate() {
|
||||||
|
lines.push(Line::from(vec![
|
||||||
|
build_line_number_span(i + 1).style(Style::default().fg(
|
||||||
|
if matches!(
|
||||||
|
target_line,
|
||||||
|
Some(l) if l == u16::try_from(i).unwrap_or(0) + 1
|
||||||
|
)
|
||||||
|
{
|
||||||
|
DEFAULT_PREVIEW_GUTTER_SELECTED_FG
|
||||||
|
} else {
|
||||||
|
DEFAULT_PREVIEW_GUTTER_FG
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
Span::styled(" │ ",
|
||||||
|
Style::default().fg(DEFAULT_PREVIEW_GUTTER_FG).dim()),
|
||||||
|
Span::styled(
|
||||||
|
line.to_string(),
|
||||||
|
Style::default().fg(DEFAULT_PREVIEW_CONTENT_FG).bg(
|
||||||
|
if matches!(target_line, Some(l) if l == u16::try_from(i).unwrap() + 1) {
|
||||||
|
DEFAULT_SELECTED_PREVIEW_BG
|
||||||
|
} else {
|
||||||
|
Color::Reset
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
let text = Text::from(lines);
|
||||||
|
Paragraph::new(text)
|
||||||
|
.block(preview_block)
|
||||||
|
.scroll((preview_scroll, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_plain_text_wrapped_paragraph(
|
||||||
|
text: String,
|
||||||
|
preview_block: Block,
|
||||||
|
) -> Paragraph {
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
for line in text.lines() {
|
||||||
|
lines.push(Line::styled(
|
||||||
|
line.to_string(),
|
||||||
|
Style::default().fg(DEFAULT_PREVIEW_CONTENT_FG),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let text = Text::from(lines);
|
||||||
|
Paragraph::new(text)
|
||||||
|
.block(preview_block)
|
||||||
|
.wrap(Wrap { trim: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_syntect_highlighted_paragraph(
|
||||||
|
highlighted_lines: Vec<Vec<(syntect::highlighting::Style, String)>>,
|
||||||
|
preview_block: Block,
|
||||||
|
target_line: Option<u16>,
|
||||||
|
preview_scroll: u16,
|
||||||
|
) -> Paragraph {
|
||||||
|
compute_paragraph_from_highlighted_lines(
|
||||||
|
&highlighted_lines,
|
||||||
|
target_line.map(|l| l as usize),
|
||||||
|
)
|
||||||
|
.block(preview_block)
|
||||||
|
.alignment(Alignment::Left)
|
||||||
|
.scroll((preview_scroll, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_meta_preview_paragraph<'a>(
|
||||||
|
inner: Rect,
|
||||||
|
message: &str,
|
||||||
|
fill_char: char,
|
||||||
|
) -> Paragraph<'a> {
|
||||||
|
let message_len = message.len();
|
||||||
|
if message_len + 8 > inner.width as usize {
|
||||||
|
return Paragraph::new(Text::from(EMPTY_STRING));
|
||||||
|
}
|
||||||
|
let fill_char_str = fill_char.to_string();
|
||||||
|
let fill_line = fill_char_str.repeat(inner.width as usize);
|
||||||
|
|
||||||
|
// Build the paragraph content with slanted lines and center the custom message
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
|
||||||
|
// Calculate the vertical center
|
||||||
|
let vertical_center = inner.height as usize / 2;
|
||||||
|
let horizontal_padding = (inner.width as usize - message_len) / 2 - 4;
|
||||||
|
|
||||||
|
// Fill the paragraph with slanted lines and insert the centered custom message
|
||||||
|
for i in 0..inner.height {
|
||||||
|
if i as usize == vertical_center {
|
||||||
|
// Center the message horizontally in the middle line
|
||||||
|
let line = format!(
|
||||||
|
"{} {} {}",
|
||||||
|
fill_char_str.repeat(horizontal_padding),
|
||||||
|
message,
|
||||||
|
fill_char_str.repeat(
|
||||||
|
inner.width as usize - horizontal_padding - message_len
|
||||||
|
)
|
||||||
|
);
|
||||||
|
lines.push(Line::from(line));
|
||||||
|
} else if i as usize + 1 == vertical_center
|
||||||
|
|| (i as usize).saturating_sub(1) == vertical_center
|
||||||
|
{
|
||||||
|
let line = format!(
|
||||||
|
"{} {} {}",
|
||||||
|
fill_char_str.repeat(horizontal_padding),
|
||||||
|
" ".repeat(message_len),
|
||||||
|
fill_char_str.repeat(
|
||||||
|
inner.width as usize - horizontal_padding - message_len
|
||||||
|
)
|
||||||
|
);
|
||||||
|
lines.push(Line::from(line));
|
||||||
|
} else {
|
||||||
|
lines.push(Line::from(fill_line.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a paragraph with the generated content
|
||||||
|
Paragraph::new(Text::from(lines))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_preview_title_block(
|
||||||
|
f: &mut Frame,
|
||||||
|
rect: Rect,
|
||||||
|
preview: &Arc<Preview>,
|
||||||
|
use_nerd_font_icons: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut preview_title_spans = Vec::new();
|
||||||
|
if preview.icon.is_some() && use_nerd_font_icons {
|
||||||
|
let icon = preview.icon.as_ref().unwrap();
|
||||||
|
preview_title_spans.push(Span::styled(
|
||||||
|
{
|
||||||
|
let mut icon_str = String::from(icon.icon);
|
||||||
|
icon_str.push(' ');
|
||||||
|
icon_str
|
||||||
|
},
|
||||||
|
Style::default().fg(Color::from_str(icon.color)?),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
preview_title_spans.push(Span::styled(
|
||||||
|
shrink_with_ellipsis(
|
||||||
|
&replace_non_printable(
|
||||||
|
preview.title.as_bytes(),
|
||||||
|
&ReplaceNonPrintableConfig::default(),
|
||||||
|
)
|
||||||
|
.0,
|
||||||
|
rect.width.saturating_sub(4) as usize,
|
||||||
|
),
|
||||||
|
Style::default().fg(DEFAULT_PREVIEW_TITLE_FG).bold(),
|
||||||
|
));
|
||||||
|
let preview_title = Paragraph::new(Line::from(preview_title_spans))
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.padding(Padding::horizontal(1))
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
|
.border_style(Style::default().fg(BORDER_COLOR)),
|
||||||
|
)
|
||||||
|
.alignment(Alignment::Left);
|
||||||
|
f.render_widget(preview_title, rect);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_preview_content_block(
|
||||||
|
f: &mut Frame,
|
||||||
|
rect: Rect,
|
||||||
|
entry: &Entry,
|
||||||
|
preview: &Arc<Preview>,
|
||||||
|
rendered_preview_cache: &Arc<Mutex<RenderedPreviewCache<'static>>>,
|
||||||
|
preview_scroll: u16,
|
||||||
|
) {
|
||||||
|
let preview_outer_block = Block::default()
|
||||||
|
.title_top(Line::from(" Preview ").alignment(Alignment::Center))
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
|
.border_style(Style::default().fg(BORDER_COLOR))
|
||||||
|
.style(Style::default())
|
||||||
|
.padding(Padding::right(1));
|
||||||
|
|
||||||
|
let preview_inner_block =
|
||||||
|
Block::default().style(Style::default()).padding(Padding {
|
||||||
|
top: 0,
|
||||||
|
right: 1,
|
||||||
|
bottom: 0,
|
||||||
|
left: 1,
|
||||||
|
});
|
||||||
|
let inner = preview_outer_block.inner(rect);
|
||||||
|
f.render_widget(preview_outer_block, rect);
|
||||||
|
|
||||||
|
let target_line = entry.line_number.map(|l| u16::try_from(l).unwrap_or(0));
|
||||||
|
let cache_key = compute_cache_key(entry);
|
||||||
|
|
||||||
|
// Check if the rendered preview content is already in the cache
|
||||||
|
if let Some(preview_paragraph) =
|
||||||
|
rendered_preview_cache.lock().unwrap().get(&cache_key)
|
||||||
|
{
|
||||||
|
let p = preview_paragraph.as_ref().clone();
|
||||||
|
f.render_widget(p.scroll((preview_scroll, 0)), inner);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// If not, render the preview content and cache it if not empty
|
||||||
|
let rp = build_preview_paragraph(
|
||||||
|
preview_inner_block,
|
||||||
|
inner,
|
||||||
|
preview.content.clone(),
|
||||||
|
target_line,
|
||||||
|
preview_scroll,
|
||||||
|
);
|
||||||
|
if !preview.stale {
|
||||||
|
rendered_preview_cache
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.insert(cache_key, &Arc::new(rp.clone()));
|
||||||
|
}
|
||||||
|
f.render_widget(
|
||||||
|
Arc::new(rp).as_ref().clone().scroll((preview_scroll, 0)),
|
||||||
|
inner,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_line_number_span<'a>(line_number: usize) -> Span<'a> {
|
||||||
|
Span::from(format!("{line_number:5} "))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_paragraph_from_highlighted_lines(
|
||||||
|
highlighted_lines: &[Vec<(syntect::highlighting::Style, String)>],
|
||||||
|
line_specifier: Option<usize>,
|
||||||
|
) -> Paragraph<'static> {
|
||||||
|
let preview_lines: Vec<Line> = highlighted_lines
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, l)| {
|
||||||
|
let line_number =
|
||||||
|
build_line_number_span(i + 1).style(Style::default().fg(
|
||||||
|
if line_specifier.is_some()
|
||||||
|
&& i == line_specifier.unwrap().saturating_sub(1)
|
||||||
|
{
|
||||||
|
DEFAULT_PREVIEW_GUTTER_SELECTED_FG
|
||||||
|
} else {
|
||||||
|
DEFAULT_PREVIEW_GUTTER_FG
|
||||||
|
},
|
||||||
|
));
|
||||||
|
Line::from_iter(
|
||||||
|
std::iter::once(line_number)
|
||||||
|
.chain(std::iter::once(Span::styled(
|
||||||
|
" │ ",
|
||||||
|
Style::default().fg(DEFAULT_PREVIEW_GUTTER_FG).dim(),
|
||||||
|
)))
|
||||||
|
.chain(l.iter().cloned().map(|sr| {
|
||||||
|
convert_syn_region_to_span(
|
||||||
|
&(sr.0, sr.1),
|
||||||
|
if line_specifier.is_some()
|
||||||
|
&& i == line_specifier
|
||||||
|
.unwrap()
|
||||||
|
.saturating_sub(1)
|
||||||
|
{
|
||||||
|
Some(SyntectColor {
|
||||||
|
r: 50,
|
||||||
|
g: 50,
|
||||||
|
b: 50,
|
||||||
|
a: 255,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Paragraph::new(preview_lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn convert_syn_region_to_span<'a>(
|
||||||
|
syn_region: &(syntect::highlighting::Style, String),
|
||||||
|
background: Option<syntect::highlighting::Color>,
|
||||||
|
) -> Span<'a> {
|
||||||
|
let mut style = Style::default()
|
||||||
|
.fg(convert_syn_color_to_ratatui_color(syn_region.0.foreground));
|
||||||
|
if let Some(background) = background {
|
||||||
|
style = style.bg(convert_syn_color_to_ratatui_color(background));
|
||||||
|
}
|
||||||
|
style = match syn_region.0.font_style {
|
||||||
|
syntect::highlighting::FontStyle::BOLD => style.bold(),
|
||||||
|
syntect::highlighting::FontStyle::ITALIC => style.italic(),
|
||||||
|
syntect::highlighting::FontStyle::UNDERLINE => style.underlined(),
|
||||||
|
_ => style,
|
||||||
|
};
|
||||||
|
Span::styled(syn_region.1.clone(), style)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_syn_color_to_ratatui_color(
|
||||||
|
color: syntect::highlighting::Color,
|
||||||
|
) -> Color {
|
||||||
|
Color::Rgb(color.r, color.g, color.b)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_cache_key(entry: &Entry) -> String {
|
||||||
|
let mut cache_key = entry.name.clone();
|
||||||
|
if let Some(line_number) = entry.line_number {
|
||||||
|
cache_key.push_str(&line_number.to_string());
|
||||||
|
}
|
||||||
|
cache_key
|
||||||
|
}
|
144
crates/television-screen/src/remote_control.rs
Normal file
144
crates/television-screen/src/remote_control.rs
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::logo::build_remote_logo_paragraph;
|
||||||
|
use crate::mode::REMOTE_CONTROL_COLOR;
|
||||||
|
use crate::results::build_results_list;
|
||||||
|
use television_channels::entry::Entry;
|
||||||
|
use television_utils::input::Input;
|
||||||
|
|
||||||
|
use crate::colors::{ResultsListColors, BORDER_COLOR, DEFAULT_INPUT_FG};
|
||||||
|
use color_eyre::eyre::Result;
|
||||||
|
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
|
||||||
|
use ratatui::prelude::Style;
|
||||||
|
use ratatui::style::{Color, Stylize};
|
||||||
|
use ratatui::text::{Line, Span};
|
||||||
|
use ratatui::widgets::{
|
||||||
|
Block, BorderType, Borders, ListDirection, ListState, Padding, Paragraph,
|
||||||
|
};
|
||||||
|
use ratatui::Frame;
|
||||||
|
|
||||||
|
pub fn draw_remote_control(
|
||||||
|
f: &mut Frame,
|
||||||
|
rect: Rect,
|
||||||
|
entries: &[Entry],
|
||||||
|
use_nerd_font_icons: bool,
|
||||||
|
picker_state: &mut ListState,
|
||||||
|
input_state: &mut Input,
|
||||||
|
icon_color_cache: &mut HashMap<String, Color>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let layout = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Min(3),
|
||||||
|
Constraint::Length(3),
|
||||||
|
Constraint::Length(20),
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.split(rect);
|
||||||
|
draw_rc_channels(
|
||||||
|
f,
|
||||||
|
layout[0],
|
||||||
|
entries,
|
||||||
|
use_nerd_font_icons,
|
||||||
|
picker_state,
|
||||||
|
icon_color_cache,
|
||||||
|
);
|
||||||
|
draw_rc_input(f, layout[1], input_state)?;
|
||||||
|
draw_rc_logo(f, layout[2]);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_rc_channels(
|
||||||
|
f: &mut Frame,
|
||||||
|
area: Rect,
|
||||||
|
entries: &[Entry],
|
||||||
|
use_nerd_font_icons: bool,
|
||||||
|
picker_state: &mut ListState,
|
||||||
|
icon_color_cache: &mut HashMap<String, Color>,
|
||||||
|
) {
|
||||||
|
let rc_block = Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
|
.border_style(Style::default().fg(BORDER_COLOR))
|
||||||
|
.style(Style::default())
|
||||||
|
.padding(Padding::right(1));
|
||||||
|
|
||||||
|
let channel_list = build_results_list(
|
||||||
|
rc_block,
|
||||||
|
entries,
|
||||||
|
ListDirection::TopToBottom,
|
||||||
|
Some(
|
||||||
|
ResultsListColors::default().result_name_fg(REMOTE_CONTROL_COLOR),
|
||||||
|
),
|
||||||
|
use_nerd_font_icons,
|
||||||
|
icon_color_cache,
|
||||||
|
);
|
||||||
|
|
||||||
|
f.render_stateful_widget(channel_list, area, picker_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_rc_input(f: &mut Frame, area: Rect, input: &mut Input) -> Result<()> {
|
||||||
|
let input_block = Block::default()
|
||||||
|
.title_top(Line::from("Remote Control").alignment(Alignment::Center))
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
|
.border_style(Style::default().fg(BORDER_COLOR))
|
||||||
|
.style(Style::default());
|
||||||
|
|
||||||
|
let input_block_inner = input_block.inner(area);
|
||||||
|
|
||||||
|
f.render_widget(input_block, area);
|
||||||
|
|
||||||
|
// split input block into 2 parts: prompt symbol, input
|
||||||
|
let inner_input_chunks = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([
|
||||||
|
// prompt symbol
|
||||||
|
Constraint::Length(2),
|
||||||
|
// input field
|
||||||
|
Constraint::Fill(1),
|
||||||
|
])
|
||||||
|
.split(input_block_inner);
|
||||||
|
|
||||||
|
let prompt_symbol_block = Block::default();
|
||||||
|
let arrow = Paragraph::new(Span::styled(
|
||||||
|
"> ",
|
||||||
|
Style::default().fg(DEFAULT_INPUT_FG).bold(),
|
||||||
|
))
|
||||||
|
.block(prompt_symbol_block);
|
||||||
|
f.render_widget(arrow, inner_input_chunks[0]);
|
||||||
|
|
||||||
|
let interactive_input_block = Block::default();
|
||||||
|
// keep 2 for borders and 1 for cursor
|
||||||
|
let width = inner_input_chunks[1].width.max(3) - 3;
|
||||||
|
let scroll = input.visual_scroll(width as usize);
|
||||||
|
let input_paragraph = Paragraph::new(input.value())
|
||||||
|
.scroll((0, u16::try_from(scroll)?))
|
||||||
|
.block(interactive_input_block)
|
||||||
|
.style(Style::default().fg(DEFAULT_INPUT_FG).bold().italic())
|
||||||
|
.alignment(Alignment::Left);
|
||||||
|
f.render_widget(input_paragraph, inner_input_chunks[1]);
|
||||||
|
|
||||||
|
// Make the cursor visible and ask tui-rs to put it at the
|
||||||
|
// specified coordinates after rendering
|
||||||
|
f.set_cursor_position((
|
||||||
|
// Put cursor past the end of the input text
|
||||||
|
inner_input_chunks[1].x
|
||||||
|
+ u16::try_from(input.visual_cursor().max(scroll) - scroll)?,
|
||||||
|
// Move one line down, from the border to the input line
|
||||||
|
inner_input_chunks[1].y,
|
||||||
|
));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn draw_rc_logo(f: &mut Frame, area: Rect) {
|
||||||
|
let logo_block =
|
||||||
|
Block::default().style(Style::default().fg(REMOTE_CONTROL_COLOR));
|
||||||
|
|
||||||
|
let logo_paragraph = build_remote_logo_paragraph()
|
||||||
|
.alignment(Alignment::Center)
|
||||||
|
.block(logo_block);
|
||||||
|
|
||||||
|
f.render_widget(logo_paragraph, area);
|
||||||
|
}
|
@ -1,71 +1,23 @@
|
|||||||
use crate::television::Television;
|
use crate::colors::{
|
||||||
use crate::ui::layout::InputPosition;
|
ResultsListColors, BORDER_COLOR,
|
||||||
use crate::ui::BORDER_COLOR;
|
DEFAULT_RESULTS_LIST_MATCH_FOREGROUND_COLOR,
|
||||||
|
};
|
||||||
|
use crate::layout::InputPosition;
|
||||||
use color_eyre::eyre::Result;
|
use color_eyre::eyre::Result;
|
||||||
use ratatui::layout::{Alignment, Rect};
|
use ratatui::layout::{Alignment, Rect};
|
||||||
use ratatui::prelude::{Color, Line, Span, Style};
|
use ratatui::prelude::{Color, Line, Span, Style};
|
||||||
use ratatui::widgets::{
|
use ratatui::widgets::{
|
||||||
Block, BorderType, Borders, List, ListDirection, Padding,
|
Block, BorderType, Borders, List, ListDirection, ListState, Padding,
|
||||||
};
|
};
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use television_channels::channels::OnAir;
|
|
||||||
use television_channels::entry::Entry;
|
use television_channels::entry::Entry;
|
||||||
use television_utils::strings::{
|
use television_utils::strings::{
|
||||||
make_matched_string_printable, next_char_boundary,
|
make_matched_string_printable, next_char_boundary,
|
||||||
slice_at_char_boundaries,
|
slice_at_char_boundaries,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Styles
|
|
||||||
const DEFAULT_RESULT_NAME_FG: Color = Color::Blue;
|
|
||||||
const DEFAULT_RESULT_PREVIEW_FG: Color = Color::Rgb(150, 150, 150);
|
|
||||||
const DEFAULT_RESULT_LINE_NUMBER_FG: Color = Color::Yellow;
|
|
||||||
const DEFAULT_RESULT_SELECTED_BG: Color = Color::Rgb(50, 50, 50);
|
|
||||||
|
|
||||||
const DEFAULT_RESULTS_LIST_MATCH_FOREGROUND_COLOR: Color = Color::Red;
|
|
||||||
|
|
||||||
pub struct ResultsListColors {
|
|
||||||
pub result_name_fg: Color,
|
|
||||||
pub result_preview_fg: Color,
|
|
||||||
pub result_line_number_fg: Color,
|
|
||||||
pub result_selected_bg: Color,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ResultsListColors {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
result_name_fg: DEFAULT_RESULT_NAME_FG,
|
|
||||||
result_preview_fg: DEFAULT_RESULT_PREVIEW_FG,
|
|
||||||
result_line_number_fg: DEFAULT_RESULT_LINE_NUMBER_FG,
|
|
||||||
result_selected_bg: DEFAULT_RESULT_SELECTED_BG,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
impl ResultsListColors {
|
|
||||||
pub fn result_name_fg(mut self, color: Color) -> Self {
|
|
||||||
self.result_name_fg = color;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn result_preview_fg(mut self, color: Color) -> Self {
|
|
||||||
self.result_preview_fg = color;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn result_line_number_fg(mut self, color: Color) -> Self {
|
|
||||||
self.result_line_number_fg = color;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn result_selected_bg(mut self, color: Color) -> Self {
|
|
||||||
self.result_selected_bg = color;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build_results_list<'a, 'b>(
|
pub fn build_results_list<'a, 'b>(
|
||||||
results_block: Block<'b>,
|
results_block: Block<'b>,
|
||||||
entries: &'a [Entry],
|
entries: &'a [Entry],
|
||||||
@ -186,48 +138,35 @@ where
|
|||||||
.block(results_block)
|
.block(results_block)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Television {
|
pub fn draw_results_list(
|
||||||
pub(crate) fn draw_results_list(
|
f: &mut Frame,
|
||||||
&mut self,
|
rect: Rect,
|
||||||
f: &mut Frame,
|
entries: &[Entry],
|
||||||
rect: Rect,
|
relative_picker_state: &mut ListState,
|
||||||
) -> Result<()> {
|
input_bar_position: InputPosition,
|
||||||
let results_block = Block::default()
|
use_nerd_font_icons: bool,
|
||||||
.title_top(Line::from(" Results ").alignment(Alignment::Center))
|
icon_color_cache: &mut HashMap<String, Color>,
|
||||||
.borders(Borders::ALL)
|
) -> Result<()> {
|
||||||
.border_type(BorderType::Rounded)
|
let results_block = Block::default()
|
||||||
.border_style(Style::default().fg(BORDER_COLOR))
|
.title_top(Line::from(" Results ").alignment(Alignment::Center))
|
||||||
.style(Style::default())
|
.borders(Borders::ALL)
|
||||||
.padding(Padding::right(1));
|
.border_type(BorderType::Rounded)
|
||||||
|
.border_style(Style::default().fg(BORDER_COLOR))
|
||||||
|
.style(Style::default())
|
||||||
|
.padding(Padding::right(1));
|
||||||
|
|
||||||
let result_count = self.channel.result_count();
|
let results_list = build_results_list(
|
||||||
if result_count > 0 && self.results_picker.selected().is_none() {
|
results_block,
|
||||||
self.results_picker.select(Some(0));
|
entries,
|
||||||
self.results_picker.relative_select(Some(0));
|
match input_bar_position {
|
||||||
}
|
InputPosition::Bottom => ListDirection::BottomToTop,
|
||||||
|
InputPosition::Top => ListDirection::TopToBottom,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
use_nerd_font_icons,
|
||||||
|
icon_color_cache,
|
||||||
|
);
|
||||||
|
|
||||||
let entries = self.channel.results(
|
f.render_stateful_widget(results_list, rect, relative_picker_state);
|
||||||
rect.height.saturating_sub(2).into(),
|
Ok(())
|
||||||
u32::try_from(self.results_picker.offset())?,
|
|
||||||
);
|
|
||||||
|
|
||||||
let results_list = build_results_list(
|
|
||||||
results_block,
|
|
||||||
&entries,
|
|
||||||
match self.config.ui.input_bar_position {
|
|
||||||
InputPosition::Bottom => ListDirection::BottomToTop,
|
|
||||||
InputPosition::Top => ListDirection::TopToBottom,
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
self.config.ui.use_nerd_font_icons,
|
|
||||||
&mut self.icon_color_cache,
|
|
||||||
);
|
|
||||||
|
|
||||||
f.render_stateful_widget(
|
|
||||||
results_list,
|
|
||||||
rect,
|
|
||||||
&mut self.results_picker.relative_state,
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -2,10 +2,6 @@ use ratatui::{
|
|||||||
buffer::Buffer, layout::Rect, style::Style, widgets::StatefulWidget,
|
buffer::Buffer, layout::Rect, style::Style, widgets::StatefulWidget,
|
||||||
};
|
};
|
||||||
|
|
||||||
//const FRAMES: &[char] = &[
|
|
||||||
// '⠄', '⠆', '⠇', '⠋', '⠙', '⠸', '⠰', '⠠', '⠰', '⠸', '⠙', '⠋', '⠇', '⠆',
|
|
||||||
//];
|
|
||||||
|
|
||||||
const FRAMES: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
const FRAMES: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
||||||
|
|
||||||
/// A spinner widget.
|
/// A spinner widget.
|
||||||
@ -69,3 +65,17 @@ impl StatefulWidget for Spinner {
|
|||||||
state.tick();
|
state.tick();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl StatefulWidget for &Spinner {
|
||||||
|
type State = SpinnerState;
|
||||||
|
|
||||||
|
/// Renders the spinner in the given area.
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||||
|
buf.set_string(
|
||||||
|
area.left(),
|
||||||
|
area.top(),
|
||||||
|
self.frame(state.current_frame),
|
||||||
|
Style::default(),
|
||||||
|
);
|
||||||
|
state.tick();
|
||||||
|
}
|
||||||
|
}
|
@ -21,6 +21,7 @@ bat = { version = "0.24.0", default-features = false, features = ["regex-onig"]
|
|||||||
directories = "5.0.1"
|
directories = "5.0.1"
|
||||||
syntect = "5.2.0"
|
syntect = "5.2.0"
|
||||||
gag = "1.0.0"
|
gag = "1.0.0"
|
||||||
|
unicode-width = "0.2.0"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
winapi-util = "0.1.9"
|
winapi-util = "0.1.9"
|
||||||
|
@ -1,19 +1,3 @@
|
|||||||
use crate::television::Television;
|
|
||||||
use crate::ui::BORDER_COLOR;
|
|
||||||
use color_eyre::eyre::Result;
|
|
||||||
use ratatui::layout::{
|
|
||||||
Alignment, Constraint, Direction, Layout as RatatuiLayout, Rect,
|
|
||||||
};
|
|
||||||
use ratatui::prelude::{Span, Style};
|
|
||||||
use ratatui::style::Stylize;
|
|
||||||
use ratatui::text::Line;
|
|
||||||
use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
|
|
||||||
use ratatui::Frame;
|
|
||||||
use television_channels::channels::OnAir;
|
|
||||||
|
|
||||||
pub mod actions;
|
|
||||||
pub mod backend;
|
|
||||||
|
|
||||||
/// Input requests are used to change the input state.
|
/// Input requests are used to change the input state.
|
||||||
///
|
///
|
||||||
/// Different backends can be used to convert events into requests.
|
/// Different backends can be used to convert events into requests.
|
||||||
@ -45,18 +29,7 @@ pub struct StateChanged {
|
|||||||
#[allow(clippy::module_name_repetitions)]
|
#[allow(clippy::module_name_repetitions)]
|
||||||
pub type InputResponse = Option<StateChanged>;
|
pub type InputResponse = Option<StateChanged>;
|
||||||
|
|
||||||
/// The input buffer with cursor support.
|
/// An input buffer with cursor support.
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use tui_input::Input;
|
|
||||||
///
|
|
||||||
/// let input: Input = "Hello World".into();
|
|
||||||
///
|
|
||||||
/// assert_eq!(input.cursor(), 11);
|
|
||||||
/// assert_eq!(input.to_string(), "Hello World");
|
|
||||||
/// ```
|
|
||||||
#[derive(Default, Debug, Clone)]
|
#[derive(Default, Debug, Clone)]
|
||||||
pub struct Input {
|
pub struct Input {
|
||||||
value: String,
|
value: String,
|
||||||
@ -411,114 +384,6 @@ impl std::fmt::Display for Input {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Television {
|
|
||||||
pub(crate) fn draw_input_box(
|
|
||||||
&mut self,
|
|
||||||
f: &mut Frame,
|
|
||||||
rect: Rect,
|
|
||||||
) -> Result<()> {
|
|
||||||
let input_block = Block::default()
|
|
||||||
.title_top(Line::from(" Pattern ").alignment(Alignment::Center))
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_type(BorderType::Rounded)
|
|
||||||
.border_style(Style::default().fg(BORDER_COLOR))
|
|
||||||
.style(Style::default());
|
|
||||||
|
|
||||||
let input_block_inner = input_block.inner(rect);
|
|
||||||
if input_block_inner.area() == 0 {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
f.render_widget(input_block, rect);
|
|
||||||
|
|
||||||
// split input block into 4 parts: prompt symbol, input, result count, spinner
|
|
||||||
let total_count = self.channel.total_count();
|
|
||||||
let inner_input_chunks = RatatuiLayout::default()
|
|
||||||
.direction(Direction::Horizontal)
|
|
||||||
.constraints([
|
|
||||||
// prompt symbol
|
|
||||||
Constraint::Length(2),
|
|
||||||
// input field
|
|
||||||
Constraint::Fill(1),
|
|
||||||
// result count
|
|
||||||
Constraint::Length(
|
|
||||||
3 * ((total_count as f32).log10().ceil() as u16 + 1) + 3,
|
|
||||||
),
|
|
||||||
// spinner
|
|
||||||
Constraint::Length(1),
|
|
||||||
])
|
|
||||||
.split(input_block_inner);
|
|
||||||
|
|
||||||
let arrow_block = Block::default();
|
|
||||||
let arrow = Paragraph::new(Span::styled(
|
|
||||||
"> ",
|
|
||||||
Style::default()
|
|
||||||
.fg(crate::television::DEFAULT_INPUT_FG)
|
|
||||||
.bold(),
|
|
||||||
))
|
|
||||||
.block(arrow_block);
|
|
||||||
f.render_widget(arrow, inner_input_chunks[0]);
|
|
||||||
|
|
||||||
let interactive_input_block = Block::default();
|
|
||||||
// keep 2 for borders and 1 for cursor
|
|
||||||
let width = inner_input_chunks[1].width.max(3) - 3;
|
|
||||||
let scroll = self.results_picker.input.visual_scroll(width as usize);
|
|
||||||
let input = Paragraph::new(self.results_picker.input.value())
|
|
||||||
.scroll((0, u16::try_from(scroll)?))
|
|
||||||
.block(interactive_input_block)
|
|
||||||
.style(
|
|
||||||
Style::default()
|
|
||||||
.fg(crate::television::DEFAULT_INPUT_FG)
|
|
||||||
.bold()
|
|
||||||
.italic(),
|
|
||||||
)
|
|
||||||
.alignment(Alignment::Left);
|
|
||||||
f.render_widget(input, inner_input_chunks[1]);
|
|
||||||
|
|
||||||
if self.channel.running() {
|
|
||||||
f.render_stateful_widget(
|
|
||||||
self.spinner,
|
|
||||||
inner_input_chunks[3],
|
|
||||||
&mut self.spinner_state,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let result_count = self.channel.result_count();
|
|
||||||
let result_count_block = Block::default();
|
|
||||||
let result_count_paragraph = Paragraph::new(Span::styled(
|
|
||||||
format!(
|
|
||||||
" {} / {} ",
|
|
||||||
if result_count == 0 {
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
self.results_picker.selected().unwrap_or(0) + 1
|
|
||||||
},
|
|
||||||
result_count,
|
|
||||||
),
|
|
||||||
Style::default()
|
|
||||||
.fg(crate::television::DEFAULT_RESULTS_COUNT_FG)
|
|
||||||
.italic(),
|
|
||||||
))
|
|
||||||
.block(result_count_block)
|
|
||||||
.alignment(Alignment::Right);
|
|
||||||
f.render_widget(result_count_paragraph, inner_input_chunks[2]);
|
|
||||||
|
|
||||||
// Make the cursor visible and ask tui-rs to put it at the
|
|
||||||
// specified coordinates after rendering
|
|
||||||
f.set_cursor_position((
|
|
||||||
// Put cursor past the end of the input text
|
|
||||||
inner_input_chunks[1].x
|
|
||||||
+ u16::try_from(
|
|
||||||
self.results_picker.input.visual_cursor().max(scroll)
|
|
||||||
- scroll,
|
|
||||||
)?,
|
|
||||||
// Move one line down, from the border to the input line
|
|
||||||
inner_input_chunks[1].y,
|
|
||||||
));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
const TEXT: &str = "first second, third.";
|
const TEXT: &str = "first second, third.";
|
@ -2,6 +2,8 @@ pub mod cache;
|
|||||||
pub mod command;
|
pub mod command;
|
||||||
pub mod files;
|
pub mod files;
|
||||||
pub mod indices;
|
pub mod indices;
|
||||||
|
pub mod input;
|
||||||
|
pub mod metadata;
|
||||||
pub mod stdin;
|
pub mod stdin;
|
||||||
pub mod strings;
|
pub mod strings;
|
||||||
pub mod syntax;
|
pub mod syntax;
|
||||||
|
39
crates/television-utils/src/metadata.rs
Normal file
39
crates/television-utils/src/metadata.rs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
pub struct BuildMetadata {
|
||||||
|
pub rustc_version: String,
|
||||||
|
pub build_date: String,
|
||||||
|
pub target_triple: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BuildMetadata {
|
||||||
|
pub fn new(
|
||||||
|
rustc_version: String,
|
||||||
|
build_date: String,
|
||||||
|
target_triple: String,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
rustc_version,
|
||||||
|
build_date,
|
||||||
|
target_triple,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AppMetadata {
|
||||||
|
pub version: String,
|
||||||
|
pub build: BuildMetadata,
|
||||||
|
pub current_directory: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppMetadata {
|
||||||
|
pub fn new(
|
||||||
|
version: String,
|
||||||
|
build: BuildMetadata,
|
||||||
|
current_directory: String,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
version,
|
||||||
|
build,
|
||||||
|
current_directory,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,13 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
use std::ops::Deref;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
|
use television_screen::mode::Mode;
|
||||||
use tokio::sync::{mpsc, Mutex};
|
use tokio::sync::{mpsc, Mutex};
|
||||||
use tracing::{debug, info};
|
use tracing::{debug, info};
|
||||||
|
|
||||||
use crate::config::{parse_key, KeyBindings};
|
use crate::config::parse_key;
|
||||||
use crate::television::{Mode, Television};
|
use crate::keymap::Keymap;
|
||||||
|
use crate::television::Television;
|
||||||
use crate::{
|
use crate::{
|
||||||
action::Action,
|
action::Action,
|
||||||
config::Config,
|
config::Config,
|
||||||
@ -17,46 +17,6 @@ use crate::{
|
|||||||
use television_channels::channels::TelevisionChannel;
|
use television_channels::channels::TelevisionChannel;
|
||||||
use television_channels::entry::Entry;
|
use television_channels::entry::Entry;
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
|
||||||
pub struct Keymap(pub HashMap<Mode, HashMap<Key, Action>>);
|
|
||||||
|
|
||||||
impl Deref for Keymap {
|
|
||||||
type Target = HashMap<Mode, HashMap<Key, Action>>;
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&KeyBindings> for Keymap {
|
|
||||||
fn from(keybindings: &KeyBindings) -> Self {
|
|
||||||
let mut keymap = HashMap::new();
|
|
||||||
for (mode, bindings) in keybindings.iter() {
|
|
||||||
let mut mode_keymap = HashMap::new();
|
|
||||||
for (action, key) in bindings {
|
|
||||||
mode_keymap.insert(*key, action.clone());
|
|
||||||
}
|
|
||||||
keymap.insert(*mode, mode_keymap);
|
|
||||||
}
|
|
||||||
Self(keymap)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Keymap {
|
|
||||||
pub fn with_mode_mappings(
|
|
||||||
mut self,
|
|
||||||
mode: Mode,
|
|
||||||
mappings: Vec<(Key, Action)>,
|
|
||||||
) -> Result<Self> {
|
|
||||||
let mode_keymap = self.0.get_mut(&mode).ok_or_else(|| {
|
|
||||||
color_eyre::eyre::eyre!("Mode {:?} not found", mode)
|
|
||||||
})?;
|
|
||||||
for (key, action) in mappings {
|
|
||||||
mode_keymap.insert(key, action);
|
|
||||||
}
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The main application struct that holds the state of the application.
|
/// The main application struct that holds the state of the application.
|
||||||
pub struct App {
|
pub struct App {
|
||||||
keymap: Keymap,
|
keymap: Keymap,
|
||||||
|
@ -155,7 +155,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::action::Action;
|
use crate::action::Action;
|
||||||
use crate::config::keybindings::parse_key;
|
use crate::config::keybindings::parse_key;
|
||||||
use crate::television::Mode;
|
use television_screen::mode::Mode;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_config() -> Result<()> {
|
fn test_config() -> Result<()> {
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
use crate::action::Action;
|
use crate::action::Action;
|
||||||
use crate::event::{convert_raw_event_to_key, Key};
|
use crate::event::{convert_raw_event_to_key, Key};
|
||||||
use crate::television::Mode;
|
|
||||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||||
use serde::{Deserialize, Deserializer};
|
use serde::{Deserialize, Deserializer};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
|
use television_screen::mode::Mode;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct KeyBindings(pub config::Map<Mode, config::Map<Action, Key>>);
|
pub struct KeyBindings(pub config::Map<Mode, config::Map<Action, Key>>);
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
use crate::television::Mode;
|
|
||||||
use ratatui::prelude::{Color, Modifier, Style};
|
use ratatui::prelude::{Color, Modifier, Style};
|
||||||
use serde::{Deserialize, Deserializer};
|
use serde::{Deserialize, Deserializer};
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
ops::{Deref, DerefMut},
|
ops::{Deref, DerefMut},
|
||||||
};
|
};
|
||||||
|
use television_screen::mode::Mode;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct Styles(pub HashMap<Mode, HashMap<String, Style>>);
|
pub struct Styles(pub HashMap<Mode, HashMap<String, Style>>);
|
||||||
|
@ -2,7 +2,7 @@ use config::ValueKind;
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::ui::layout::InputPosition;
|
use television_screen::layout::InputPosition;
|
||||||
|
|
||||||
const DEFAULT_UI_SCALE: u16 = 90;
|
const DEFAULT_UI_SCALE: u16 = 90;
|
||||||
|
|
||||||
|
17
crates/television/input.rs
Normal file
17
crates/television/input.rs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
use crate::action::Action;
|
||||||
|
use television_utils::input::InputRequest;
|
||||||
|
|
||||||
|
pub fn convert_action_to_input_request(
|
||||||
|
action: &Action,
|
||||||
|
) -> Option<InputRequest> {
|
||||||
|
match action {
|
||||||
|
Action::AddInputChar(c) => Some(InputRequest::InsertChar(*c)),
|
||||||
|
Action::DeletePrevChar => Some(InputRequest::DeletePrevChar),
|
||||||
|
Action::DeleteNextChar => Some(InputRequest::DeleteNextChar),
|
||||||
|
Action::GoToPrevChar => Some(InputRequest::GoToPrevChar),
|
||||||
|
Action::GoToNextChar => Some(InputRequest::GoToNextChar),
|
||||||
|
Action::GoToInputStart => Some(InputRequest::GoToStart),
|
||||||
|
Action::GoToInputEnd => Some(InputRequest::GoToEnd),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
49
crates/television/keymap.rs
Normal file
49
crates/television/keymap.rs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
use color_eyre::Result;
|
||||||
|
use television_screen::mode::Mode;
|
||||||
|
|
||||||
|
use crate::action::Action;
|
||||||
|
use crate::config::KeyBindings;
|
||||||
|
use crate::event::Key;
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct Keymap(pub HashMap<Mode, HashMap<Key, Action>>);
|
||||||
|
|
||||||
|
impl Deref for Keymap {
|
||||||
|
type Target = HashMap<Mode, HashMap<Key, Action>>;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&KeyBindings> for Keymap {
|
||||||
|
fn from(keybindings: &KeyBindings) -> Self {
|
||||||
|
let mut keymap = HashMap::new();
|
||||||
|
for (mode, bindings) in keybindings.iter() {
|
||||||
|
let mut mode_keymap = HashMap::new();
|
||||||
|
for (action, key) in bindings {
|
||||||
|
mode_keymap.insert(*key, action.clone());
|
||||||
|
}
|
||||||
|
keymap.insert(*mode, mode_keymap);
|
||||||
|
}
|
||||||
|
Self(keymap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Keymap {
|
||||||
|
pub fn with_mode_mappings(
|
||||||
|
mut self,
|
||||||
|
mode: Mode,
|
||||||
|
mappings: Vec<(Key, Action)>,
|
||||||
|
) -> Result<Self> {
|
||||||
|
let mode_keymap = self.0.get_mut(&mode).ok_or_else(|| {
|
||||||
|
color_eyre::eyre::eyre!("Mode {:?} not found", mode)
|
||||||
|
})?;
|
||||||
|
for (key, action) in mappings {
|
||||||
|
mode_keymap.insert(key, action);
|
||||||
|
}
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
}
|
@ -20,12 +20,13 @@ pub mod cli;
|
|||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
pub mod event;
|
pub mod event;
|
||||||
|
pub mod input;
|
||||||
|
pub mod keymap;
|
||||||
pub mod logging;
|
pub mod logging;
|
||||||
pub mod picker;
|
pub mod picker;
|
||||||
pub mod render;
|
pub mod render;
|
||||||
pub mod television;
|
pub mod television;
|
||||||
pub mod tui;
|
pub mod tui;
|
||||||
pub mod ui;
|
|
||||||
|
|
||||||
#[tokio::main(flavor = "multi_thread")]
|
#[tokio::main(flavor = "multi_thread")]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
use crate::ui::input::Input;
|
|
||||||
use ratatui::widgets::ListState;
|
use ratatui::widgets::ListState;
|
||||||
use television_utils::strings::EMPTY_STRING;
|
use television_utils::{input::Input, strings::EMPTY_STRING};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Picker {
|
pub struct Picker {
|
||||||
|
@ -1,16 +1,11 @@
|
|||||||
|
use crate::config::KeyBindings;
|
||||||
|
use crate::input::convert_action_to_input_request;
|
||||||
use crate::picker::Picker;
|
use crate::picker::Picker;
|
||||||
use crate::ui::{
|
use crate::{action::Action, config::Config};
|
||||||
cache::RenderedPreviewCache,
|
use crate::{cable::load_cable_channels, keymap::Keymap};
|
||||||
input::actions::InputActionHandler,
|
|
||||||
layout::{Dimensions, InputPosition, Layout},
|
|
||||||
spinner::Spinner,
|
|
||||||
};
|
|
||||||
use crate::{action::Action, config::Config, ui::spinner::SpinnerState};
|
|
||||||
use crate::{app::Keymap, cable::load_cable_channels};
|
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use copypasta::{ClipboardContext, ClipboardProvider};
|
use copypasta::{ClipboardContext, ClipboardProvider};
|
||||||
use ratatui::{layout::Rect, style::Color, Frame};
|
use ratatui::{layout::Rect, style::Color, Frame};
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use television_channels::channels::{
|
use television_channels::channels::{
|
||||||
@ -19,16 +14,24 @@ use television_channels::channels::{
|
|||||||
};
|
};
|
||||||
use television_channels::entry::{Entry, ENTRY_PLACEHOLDER};
|
use television_channels::entry::{Entry, ENTRY_PLACEHOLDER};
|
||||||
use television_previewers::previewers::Previewer;
|
use television_previewers::previewers::Previewer;
|
||||||
|
use television_screen::cache::RenderedPreviewCache;
|
||||||
|
use television_screen::help::draw_help_bar;
|
||||||
|
use television_screen::input::draw_input_box;
|
||||||
|
use television_screen::keybindings::{
|
||||||
|
build_keybindings_table, DisplayableAction, DisplayableKeybindings,
|
||||||
|
};
|
||||||
|
use television_screen::layout::{Dimensions, InputPosition, Layout};
|
||||||
|
use television_screen::mode::Mode;
|
||||||
|
use television_screen::preview::{
|
||||||
|
draw_preview_content_block, draw_preview_title_block,
|
||||||
|
};
|
||||||
|
use television_screen::remote_control::draw_remote_control;
|
||||||
|
use television_screen::results::draw_results_list;
|
||||||
|
use television_screen::spinner::{Spinner, SpinnerState};
|
||||||
|
use television_utils::metadata::{AppMetadata, BuildMetadata};
|
||||||
use television_utils::strings::EMPTY_STRING;
|
use television_utils::strings::EMPTY_STRING;
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
||||||
#[derive(PartialEq, Copy, Clone, Hash, Eq, Debug, Serialize, Deserialize)]
|
|
||||||
pub enum Mode {
|
|
||||||
Channel,
|
|
||||||
RemoteControl,
|
|
||||||
SendToChannel,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Television {
|
pub struct Television {
|
||||||
action_tx: Option<UnboundedSender<Action>>,
|
action_tx: Option<UnboundedSender<Action>>,
|
||||||
pub config: Config,
|
pub config: Config,
|
||||||
@ -48,6 +51,7 @@ pub struct Television {
|
|||||||
pub rendered_preview_cache: Arc<Mutex<RenderedPreviewCache<'static>>>,
|
pub rendered_preview_cache: Arc<Mutex<RenderedPreviewCache<'static>>>,
|
||||||
pub(crate) spinner: Spinner,
|
pub(crate) spinner: Spinner,
|
||||||
pub(crate) spinner_state: SpinnerState,
|
pub(crate) spinner_state: SpinnerState,
|
||||||
|
pub app_metadata: AppMetadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Television {
|
impl Television {
|
||||||
@ -61,6 +65,18 @@ impl Television {
|
|||||||
let keymap = Keymap::from(&config.keybindings);
|
let keymap = Keymap::from(&config.keybindings);
|
||||||
let builtin_channels = load_builtin_channels();
|
let builtin_channels = load_builtin_channels();
|
||||||
let cable_channels = load_cable_channels().unwrap_or_default();
|
let cable_channels = load_cable_channels().unwrap_or_default();
|
||||||
|
let app_metadata = AppMetadata::new(
|
||||||
|
env!("CARGO_PKG_VERSION").to_string(),
|
||||||
|
BuildMetadata::new(
|
||||||
|
env!("VERGEN_RUSTC_SEMVER").to_string(),
|
||||||
|
env!("VERGEN_BUILD_DATE").to_string(),
|
||||||
|
env!("VERGEN_CARGO_TARGET_TRIPLE").to_string(),
|
||||||
|
),
|
||||||
|
std::env::current_dir()
|
||||||
|
.expect("Could not get current directory")
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
channel.find(EMPTY_STRING);
|
channel.find(EMPTY_STRING);
|
||||||
let spinner = Spinner::default();
|
let spinner = Spinner::default();
|
||||||
@ -87,9 +103,18 @@ impl Television {
|
|||||||
)),
|
)),
|
||||||
spinner,
|
spinner,
|
||||||
spinner_state: SpinnerState::from(&spinner),
|
spinner_state: SpinnerState::from(&spinner),
|
||||||
|
app_metadata,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn init_remote_control(&mut self) {
|
||||||
|
let builtin_channels = load_builtin_channels();
|
||||||
|
let cable_channels = load_cable_channels().unwrap_or_default();
|
||||||
|
self.remote_control = TelevisionChannel::RemoteControl(
|
||||||
|
RemoteControl::new(builtin_channels, Some(cable_channels)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn current_channel(&self) -> UnitChannel {
|
pub fn current_channel(&self) -> UnitChannel {
|
||||||
UnitChannel::from(&self.channel)
|
UnitChannel::from(&self.channel)
|
||||||
}
|
}
|
||||||
@ -174,7 +199,7 @@ impl Television {
|
|||||||
match self.mode {
|
match self.mode {
|
||||||
Mode::Channel => self.results_picker.reset_selection(),
|
Mode::Channel => self.results_picker.reset_selection(),
|
||||||
Mode::RemoteControl | Mode::SendToChannel => {
|
Mode::RemoteControl | Mode::SendToChannel => {
|
||||||
self.rc_picker.reset_selection()
|
self.rc_picker.reset_selection();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -183,7 +208,7 @@ impl Television {
|
|||||||
match self.mode {
|
match self.mode {
|
||||||
Mode::Channel => self.results_picker.reset_input(),
|
Mode::Channel => self.results_picker.reset_input(),
|
||||||
Mode::RemoteControl | Mode::SendToChannel => {
|
Mode::RemoteControl | Mode::SendToChannel => {
|
||||||
self.rc_picker.reset_input()
|
self.rc_picker.reset_input();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -209,11 +234,6 @@ impl Television {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Styles
|
|
||||||
// input
|
|
||||||
pub(crate) const DEFAULT_INPUT_FG: Color = Color::LightRed;
|
|
||||||
pub(crate) const DEFAULT_RESULTS_COUNT_FG: Color = Color::LightRed;
|
|
||||||
|
|
||||||
impl Television {
|
impl Television {
|
||||||
/// Register an action handler that can send actions for processing if necessary.
|
/// Register an action handler that can send actions for processing if necessary.
|
||||||
///
|
///
|
||||||
@ -253,7 +273,8 @@ impl Television {
|
|||||||
&mut self.rc_picker.input
|
&mut self.rc_picker.input
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
input.handle_action(&action);
|
input
|
||||||
|
.handle(convert_action_to_input_request(&action).unwrap());
|
||||||
match action {
|
match action {
|
||||||
Action::AddInputChar(_)
|
Action::AddInputChar(_)
|
||||||
| Action::DeletePrevChar
|
| Action::DeletePrevChar
|
||||||
@ -292,10 +313,12 @@ impl Television {
|
|||||||
Action::ToggleRemoteControl => match self.mode {
|
Action::ToggleRemoteControl => match self.mode {
|
||||||
Mode::Channel => {
|
Mode::Channel => {
|
||||||
self.mode = Mode::RemoteControl;
|
self.mode = Mode::RemoteControl;
|
||||||
|
self.init_remote_control();
|
||||||
}
|
}
|
||||||
Mode::RemoteControl => {
|
Mode::RemoteControl => {
|
||||||
// this resets the RC picker
|
// this resets the RC picker
|
||||||
self.reset_picker_input();
|
self.reset_picker_input();
|
||||||
|
self.init_remote_control();
|
||||||
self.remote_control.find(EMPTY_STRING);
|
self.remote_control.find(EMPTY_STRING);
|
||||||
self.reset_picker_selection();
|
self.reset_picker_selection();
|
||||||
self.mode = Mode::Channel;
|
self.mode = Mode::Channel;
|
||||||
@ -382,16 +405,54 @@ impl Television {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// help bar (metadata, keymaps, logo)
|
// help bar (metadata, keymaps, logo)
|
||||||
self.draw_help_bar(f, &layout.help_bar)?;
|
draw_help_bar(
|
||||||
|
f,
|
||||||
|
&layout.help_bar,
|
||||||
|
self.current_channel(),
|
||||||
|
build_keybindings_table(
|
||||||
|
&self.config.keybindings.to_displayable(),
|
||||||
|
self.mode,
|
||||||
|
),
|
||||||
|
self.mode,
|
||||||
|
&self.app_metadata,
|
||||||
|
);
|
||||||
|
|
||||||
self.results_area_height = u32::from(layout.results.height - 2); // 2 for the borders
|
self.results_area_height =
|
||||||
|
u32::from(layout.results.height.saturating_sub(2)); // 2 for the borders
|
||||||
self.preview_pane_height = layout.preview_window.height;
|
self.preview_pane_height = layout.preview_window.height;
|
||||||
|
|
||||||
// results list
|
// results list
|
||||||
self.draw_results_list(f, layout.results)?;
|
let result_count = self.channel.result_count();
|
||||||
|
if result_count > 0 && self.results_picker.selected().is_none() {
|
||||||
|
self.results_picker.select(Some(0));
|
||||||
|
self.results_picker.relative_select(Some(0));
|
||||||
|
}
|
||||||
|
let entries = self.channel.results(
|
||||||
|
self.results_area_height,
|
||||||
|
u32::try_from(self.results_picker.offset())?,
|
||||||
|
);
|
||||||
|
draw_results_list(
|
||||||
|
f,
|
||||||
|
layout.results,
|
||||||
|
&entries,
|
||||||
|
&mut self.results_picker.relative_state,
|
||||||
|
self.config.ui.input_bar_position,
|
||||||
|
self.config.ui.use_nerd_font_icons,
|
||||||
|
&mut self.icon_color_cache,
|
||||||
|
)?;
|
||||||
|
|
||||||
// input box
|
// input box
|
||||||
self.draw_input_box(f, layout.input)?;
|
draw_input_box(
|
||||||
|
f,
|
||||||
|
layout.input,
|
||||||
|
result_count,
|
||||||
|
self.channel.total_count(),
|
||||||
|
&mut self.results_picker.input,
|
||||||
|
&mut self.results_picker.state,
|
||||||
|
self.channel.running(),
|
||||||
|
&self.spinner,
|
||||||
|
&mut self.spinner_state,
|
||||||
|
)?;
|
||||||
|
|
||||||
let selected_entry = self
|
let selected_entry = self
|
||||||
.get_selected_entry(Some(Mode::Channel))
|
.get_selected_entry(Some(Mode::Channel))
|
||||||
@ -400,20 +461,199 @@ impl Television {
|
|||||||
|
|
||||||
// preview title
|
// preview title
|
||||||
self.current_preview_total_lines = preview.total_lines();
|
self.current_preview_total_lines = preview.total_lines();
|
||||||
self.draw_preview_title_block(f, layout.preview_title, &preview)?;
|
draw_preview_title_block(
|
||||||
|
f,
|
||||||
|
layout.preview_title,
|
||||||
|
&preview,
|
||||||
|
self.config.ui.use_nerd_font_icons,
|
||||||
|
)?;
|
||||||
|
|
||||||
// preview content
|
// preview content
|
||||||
self.draw_preview_content_block(
|
// initialize preview scroll
|
||||||
|
self.maybe_init_preview_scroll(
|
||||||
|
selected_entry
|
||||||
|
.line_number
|
||||||
|
.map(|l| u16::try_from(l).unwrap_or(0)),
|
||||||
|
layout.preview_window.height,
|
||||||
|
);
|
||||||
|
draw_preview_content_block(
|
||||||
f,
|
f,
|
||||||
layout.preview_window,
|
layout.preview_window,
|
||||||
&selected_entry,
|
&selected_entry,
|
||||||
&preview,
|
&preview,
|
||||||
|
&self.rendered_preview_cache,
|
||||||
|
self.preview_scroll.unwrap_or(0),
|
||||||
);
|
);
|
||||||
|
|
||||||
// remote control
|
// remote control
|
||||||
if matches!(self.mode, Mode::RemoteControl | Mode::SendToChannel) {
|
if matches!(self.mode, Mode::RemoteControl | Mode::SendToChannel) {
|
||||||
self.draw_remote_control(f, layout.remote_control.unwrap())?;
|
// NOTE: this should be done in the `update` method
|
||||||
|
let result_count = self.remote_control.result_count();
|
||||||
|
if result_count > 0 && self.rc_picker.selected().is_none() {
|
||||||
|
self.rc_picker.select(Some(0));
|
||||||
|
self.rc_picker.relative_select(Some(0));
|
||||||
|
}
|
||||||
|
let entries = self.remote_control.results(
|
||||||
|
area.height.saturating_sub(2).into(),
|
||||||
|
u32::try_from(self.rc_picker.offset())?,
|
||||||
|
);
|
||||||
|
draw_remote_control(
|
||||||
|
f,
|
||||||
|
layout.remote_control.unwrap(),
|
||||||
|
&entries,
|
||||||
|
self.config.ui.use_nerd_font_icons,
|
||||||
|
&mut self.rc_picker.state,
|
||||||
|
&mut self.rc_picker.input,
|
||||||
|
&mut self.icon_color_cache,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn maybe_init_preview_scroll(
|
||||||
|
&mut self,
|
||||||
|
target_line: Option<u16>,
|
||||||
|
height: u16,
|
||||||
|
) {
|
||||||
|
if self.preview_scroll.is_none() && !self.channel.running() {
|
||||||
|
self.preview_scroll =
|
||||||
|
Some(target_line.unwrap_or(0).saturating_sub(height / 3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyBindings {
|
||||||
|
pub fn to_displayable(&self) -> HashMap<Mode, DisplayableKeybindings> {
|
||||||
|
// channel mode keybindings
|
||||||
|
let channel_bindings: HashMap<DisplayableAction, Vec<String>> =
|
||||||
|
HashMap::from_iter(vec![
|
||||||
|
(
|
||||||
|
DisplayableAction::ResultsNavigation,
|
||||||
|
serialized_keys_for_actions(
|
||||||
|
self,
|
||||||
|
&[
|
||||||
|
Action::SelectPrevEntry,
|
||||||
|
Action::SelectNextEntry,
|
||||||
|
Action::SelectPrevPage,
|
||||||
|
Action::SelectNextPage,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
DisplayableAction::PreviewNavigation,
|
||||||
|
serialized_keys_for_actions(
|
||||||
|
self,
|
||||||
|
&[
|
||||||
|
Action::ScrollPreviewHalfPageUp,
|
||||||
|
Action::ScrollPreviewHalfPageDown,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
DisplayableAction::SelectEntry,
|
||||||
|
serialized_keys_for_actions(self, &[Action::SelectEntry]),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
DisplayableAction::CopyEntryToClipboard,
|
||||||
|
serialized_keys_for_actions(
|
||||||
|
self,
|
||||||
|
&[Action::CopyEntryToClipboard],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
DisplayableAction::SendToChannel,
|
||||||
|
serialized_keys_for_actions(
|
||||||
|
self,
|
||||||
|
&[Action::ToggleSendToChannel],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
DisplayableAction::ToggleRemoteControl,
|
||||||
|
serialized_keys_for_actions(
|
||||||
|
self,
|
||||||
|
&[Action::ToggleRemoteControl],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
DisplayableAction::Quit,
|
||||||
|
serialized_keys_for_actions(self, &[Action::Quit]),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// remote control mode keybindings
|
||||||
|
let remote_control_bindings: HashMap<DisplayableAction, Vec<String>> =
|
||||||
|
HashMap::from_iter(vec![
|
||||||
|
(
|
||||||
|
DisplayableAction::ResultsNavigation,
|
||||||
|
serialized_keys_for_actions(
|
||||||
|
self,
|
||||||
|
&[Action::SelectPrevEntry, Action::SelectNextEntry],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
DisplayableAction::SelectEntry,
|
||||||
|
serialized_keys_for_actions(self, &[Action::SelectEntry]),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
DisplayableAction::ToggleRemoteControl,
|
||||||
|
serialized_keys_for_actions(
|
||||||
|
self,
|
||||||
|
&[Action::ToggleRemoteControl],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// send to channel mode keybindings
|
||||||
|
let send_to_channel_bindings: HashMap<DisplayableAction, Vec<String>> =
|
||||||
|
HashMap::from_iter(vec![
|
||||||
|
(
|
||||||
|
DisplayableAction::ResultsNavigation,
|
||||||
|
serialized_keys_for_actions(
|
||||||
|
self,
|
||||||
|
&[Action::SelectPrevEntry, Action::SelectNextEntry],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
DisplayableAction::SelectEntry,
|
||||||
|
serialized_keys_for_actions(self, &[Action::SelectEntry]),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
DisplayableAction::Cancel,
|
||||||
|
serialized_keys_for_actions(
|
||||||
|
self,
|
||||||
|
&[Action::ToggleSendToChannel],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
HashMap::from_iter(vec![
|
||||||
|
(Mode::Channel, DisplayableKeybindings::new(channel_bindings)),
|
||||||
|
(
|
||||||
|
Mode::RemoteControl,
|
||||||
|
DisplayableKeybindings::new(remote_control_bindings),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Mode::SendToChannel,
|
||||||
|
DisplayableKeybindings::new(send_to_channel_bindings),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialized_keys_for_actions(
|
||||||
|
keybindings: &KeyBindings,
|
||||||
|
actions: &[Action],
|
||||||
|
) -> Vec<String> {
|
||||||
|
actions
|
||||||
|
.iter()
|
||||||
|
.map(|a| {
|
||||||
|
keybindings
|
||||||
|
.get(&Mode::Channel)
|
||||||
|
.unwrap()
|
||||||
|
.get(a)
|
||||||
|
.unwrap()
|
||||||
|
.clone()
|
||||||
|
.to_string()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
use ratatui::style::Color;
|
|
||||||
|
|
||||||
pub mod cache;
|
|
||||||
pub(crate) mod help;
|
|
||||||
pub mod input;
|
|
||||||
pub mod keymap;
|
|
||||||
pub mod layout;
|
|
||||||
pub mod logo;
|
|
||||||
pub mod metadata;
|
|
||||||
mod mode;
|
|
||||||
pub mod preview;
|
|
||||||
mod remote_control;
|
|
||||||
pub mod results;
|
|
||||||
pub mod spinner;
|
|
||||||
|
|
||||||
pub const BORDER_COLOR: Color = Color::Blue;
|
|
@ -1,68 +0,0 @@
|
|||||||
use super::layout::HelpBarLayout;
|
|
||||||
use crate::television::Television;
|
|
||||||
use crate::ui::logo::build_logo_paragraph;
|
|
||||||
use crate::ui::mode::mode_color;
|
|
||||||
use crate::ui::BORDER_COLOR;
|
|
||||||
use ratatui::layout::Rect;
|
|
||||||
use ratatui::prelude::{Color, Style};
|
|
||||||
use ratatui::widgets::{Block, BorderType, Borders, Padding};
|
|
||||||
use ratatui::Frame;
|
|
||||||
|
|
||||||
pub fn draw_logo_block(f: &mut Frame, area: Rect, color: Color) {
|
|
||||||
let logo_block = Block::default()
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_type(BorderType::Rounded)
|
|
||||||
.border_style(Style::default().fg(BORDER_COLOR))
|
|
||||||
.style(Style::default().fg(color))
|
|
||||||
.padding(Padding::horizontal(1));
|
|
||||||
|
|
||||||
let logo_paragraph = build_logo_paragraph().block(logo_block);
|
|
||||||
|
|
||||||
f.render_widget(logo_paragraph, area);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Television {
|
|
||||||
pub(crate) fn draw_help_bar(
|
|
||||||
&self,
|
|
||||||
f: &mut Frame,
|
|
||||||
layout: &Option<HelpBarLayout>,
|
|
||||||
) -> color_eyre::Result<()> {
|
|
||||||
if let Some(help_bar) = layout {
|
|
||||||
self.draw_metadata_block(f, help_bar.left);
|
|
||||||
self.draw_keymaps_block(f, help_bar.middle)?;
|
|
||||||
draw_logo_block(f, help_bar.right, mode_color(self.mode));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_metadata_block(&self, f: &mut Frame, area: Rect) {
|
|
||||||
let metadata_block = Block::default()
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_type(BorderType::Rounded)
|
|
||||||
.border_style(Style::default().fg(BORDER_COLOR))
|
|
||||||
.padding(Padding::horizontal(1))
|
|
||||||
.style(Style::default());
|
|
||||||
|
|
||||||
let metadata_table = self.build_metadata_table().block(metadata_block);
|
|
||||||
|
|
||||||
f.render_widget(metadata_table, area);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_keymaps_block(
|
|
||||||
&self,
|
|
||||||
f: &mut Frame,
|
|
||||||
area: Rect,
|
|
||||||
) -> color_eyre::Result<()> {
|
|
||||||
let keymaps_block = Block::default()
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_type(BorderType::Rounded)
|
|
||||||
.border_style(Style::default().fg(BORDER_COLOR))
|
|
||||||
.style(Style::default())
|
|
||||||
.padding(Padding::horizontal(1));
|
|
||||||
|
|
||||||
let keymaps_table = self.build_keymap_table()?.block(keymaps_block);
|
|
||||||
|
|
||||||
f.render_widget(keymaps_table, area);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
use crate::action::Action;
|
|
||||||
use crate::ui::input::{Input, InputRequest, StateChanged};
|
|
||||||
|
|
||||||
/// This makes the `Action` type compatible with the `Input` logic.
|
|
||||||
pub trait InputActionHandler {
|
|
||||||
// Handle Key event.
|
|
||||||
fn handle_action(&mut self, action: &Action) -> Option<StateChanged>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl InputActionHandler for Input {
|
|
||||||
/// Handle Key event.
|
|
||||||
fn handle_action(&mut self, action: &Action) -> Option<StateChanged> {
|
|
||||||
match action {
|
|
||||||
Action::AddInputChar(c) => {
|
|
||||||
self.handle(InputRequest::InsertChar(*c))
|
|
||||||
}
|
|
||||||
Action::DeletePrevChar => {
|
|
||||||
self.handle(InputRequest::DeletePrevChar)
|
|
||||||
}
|
|
||||||
Action::DeleteNextChar => {
|
|
||||||
self.handle(InputRequest::DeleteNextChar)
|
|
||||||
}
|
|
||||||
Action::GoToPrevChar => self.handle(InputRequest::GoToPrevChar),
|
|
||||||
Action::GoToNextChar => self.handle(InputRequest::GoToNextChar),
|
|
||||||
Action::GoToInputStart => self.handle(InputRequest::GoToStart),
|
|
||||||
Action::GoToInputEnd => self.handle(InputRequest::GoToEnd),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,72 +0,0 @@
|
|||||||
use super::{Input, InputRequest, StateChanged};
|
|
||||||
use ratatui::crossterm::event::{
|
|
||||||
Event as CrosstermEvent, KeyCode, KeyEvent, KeyEventKind, KeyModifiers,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Converts crossterm event into input requests.
|
|
||||||
/// TODO: make these keybindings configurable.
|
|
||||||
pub fn to_input_request(evt: &CrosstermEvent) -> Option<InputRequest> {
|
|
||||||
use InputRequest::*;
|
|
||||||
use KeyCode::*;
|
|
||||||
match evt {
|
|
||||||
CrosstermEvent::Key(KeyEvent {
|
|
||||||
code,
|
|
||||||
modifiers,
|
|
||||||
kind,
|
|
||||||
state: _,
|
|
||||||
}) if *kind == KeyEventKind::Press => match (*code, *modifiers) {
|
|
||||||
(Backspace, KeyModifiers::NONE) => Some(DeletePrevChar),
|
|
||||||
(Delete, KeyModifiers::NONE) => Some(DeleteNextChar),
|
|
||||||
(Tab, KeyModifiers::NONE) => None,
|
|
||||||
(Left, KeyModifiers::NONE) => Some(GoToPrevChar),
|
|
||||||
(Right, KeyModifiers::NONE) => Some(GoToNextChar),
|
|
||||||
(Char('w'), KeyModifiers::CONTROL)
|
|
||||||
| (Backspace, KeyModifiers::ALT) => Some(DeletePrevWord),
|
|
||||||
(Delete, KeyModifiers::CONTROL) => Some(DeleteNextWord),
|
|
||||||
(Char('a'), KeyModifiers::CONTROL)
|
|
||||||
| (Home, KeyModifiers::NONE) => Some(GoToStart),
|
|
||||||
(Char('e'), KeyModifiers::CONTROL) | (End, KeyModifiers::NONE) => {
|
|
||||||
Some(GoToEnd)
|
|
||||||
}
|
|
||||||
(Char(c), KeyModifiers::NONE) => Some(InsertChar(c)),
|
|
||||||
(Char(c), KeyModifiers::SHIFT) => Some(InsertChar(c)),
|
|
||||||
(_, _) => None,
|
|
||||||
},
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
/// Import this trait to implement `Input::handle_event()` for crossterm.
|
|
||||||
pub trait EventHandler {
|
|
||||||
/// Handle crossterm event.
|
|
||||||
fn handle_event(&mut self, evt: &CrosstermEvent) -> Option<StateChanged>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventHandler for Input {
|
|
||||||
/// Handle crossterm event.
|
|
||||||
fn handle_event(&mut self, evt: &CrosstermEvent) -> Option<StateChanged> {
|
|
||||||
to_input_request(evt).and_then(|req| self.handle(req))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use ratatui::crossterm::event::{KeyEventKind, KeyEventState};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn handle_tab() {
|
|
||||||
let evt = CrosstermEvent::Key(KeyEvent {
|
|
||||||
code: KeyCode::Tab,
|
|
||||||
modifiers: KeyModifiers::NONE,
|
|
||||||
kind: KeyEventKind::Press,
|
|
||||||
state: KeyEventState::NONE,
|
|
||||||
});
|
|
||||||
|
|
||||||
let req = to_input_request(&evt);
|
|
||||||
|
|
||||||
assert!(req.is_none());
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,297 +0,0 @@
|
|||||||
use color_eyre::eyre::{OptionExt, Result};
|
|
||||||
use ratatui::{
|
|
||||||
layout::Constraint,
|
|
||||||
style::{Color, Style},
|
|
||||||
text::{Line, Span},
|
|
||||||
widgets::{Cell, Row, Table},
|
|
||||||
};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use crate::ui::mode::mode_color;
|
|
||||||
use crate::{
|
|
||||||
action::Action,
|
|
||||||
event::Key,
|
|
||||||
television::{Mode, Television},
|
|
||||||
};
|
|
||||||
|
|
||||||
const ACTION_COLOR: Color = Color::DarkGray;
|
|
||||||
|
|
||||||
impl Television {
|
|
||||||
pub fn build_keymap_table<'a>(&self) -> Result<Table<'a>> {
|
|
||||||
match self.mode {
|
|
||||||
Mode::Channel => self.build_keymap_table_for_channel(),
|
|
||||||
Mode::RemoteControl => {
|
|
||||||
self.build_keymap_table_for_channel_selection()
|
|
||||||
}
|
|
||||||
Mode::SendToChannel => {
|
|
||||||
self.build_keymap_table_for_channel_transitions()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_keymap_table_for_channel<'a>(&self) -> Result<Table<'a>> {
|
|
||||||
let keymap = self.keymap_for_mode()?;
|
|
||||||
let key_color = mode_color(self.mode);
|
|
||||||
|
|
||||||
// Results navigation
|
|
||||||
let prev = keys_for_action(keymap, &Action::SelectPrevEntry);
|
|
||||||
let next = keys_for_action(keymap, &Action::SelectNextEntry);
|
|
||||||
let results_row = Row::new(build_cells_for_key_groups(
|
|
||||||
"Results navigation",
|
|
||||||
vec![prev, next],
|
|
||||||
key_color,
|
|
||||||
));
|
|
||||||
|
|
||||||
// Preview navigation
|
|
||||||
let up_keys =
|
|
||||||
keys_for_action(keymap, &Action::ScrollPreviewHalfPageUp);
|
|
||||||
let down_keys =
|
|
||||||
keys_for_action(keymap, &Action::ScrollPreviewHalfPageDown);
|
|
||||||
let preview_row = Row::new(build_cells_for_key_groups(
|
|
||||||
"Preview navigation",
|
|
||||||
vec![up_keys, down_keys],
|
|
||||||
key_color,
|
|
||||||
));
|
|
||||||
|
|
||||||
// Select entry
|
|
||||||
let select_entry_keys = keys_for_action(keymap, &Action::SelectEntry);
|
|
||||||
let select_entry_row = Row::new(build_cells_for_key_groups(
|
|
||||||
"Select entry",
|
|
||||||
vec![select_entry_keys],
|
|
||||||
key_color,
|
|
||||||
));
|
|
||||||
|
|
||||||
// Copy entry to clipboard
|
|
||||||
let copy_entry_keys =
|
|
||||||
keys_for_action(keymap, &Action::CopyEntryToClipboard);
|
|
||||||
let copy_entry_row = Row::new(build_cells_for_key_groups(
|
|
||||||
"Copy entry to clipboard",
|
|
||||||
vec![copy_entry_keys],
|
|
||||||
key_color,
|
|
||||||
));
|
|
||||||
|
|
||||||
// Send to channel
|
|
||||||
let send_to_channel_keys =
|
|
||||||
keys_for_action(keymap, &Action::ToggleSendToChannel);
|
|
||||||
let send_to_channel_row = Row::new(build_cells_for_key_groups(
|
|
||||||
"Send results to",
|
|
||||||
vec![send_to_channel_keys],
|
|
||||||
key_color,
|
|
||||||
));
|
|
||||||
|
|
||||||
// Switch channels
|
|
||||||
let switch_channels_keys =
|
|
||||||
keys_for_action(keymap, &Action::ToggleRemoteControl);
|
|
||||||
let switch_channels_row = Row::new(build_cells_for_key_groups(
|
|
||||||
"Toggle Remote control",
|
|
||||||
vec![switch_channels_keys],
|
|
||||||
key_color,
|
|
||||||
));
|
|
||||||
|
|
||||||
// MISC line (quit, help, etc.)
|
|
||||||
// Quit ⏼
|
|
||||||
let quit_keys = keys_for_action(keymap, &Action::Quit);
|
|
||||||
let quit_row = Row::new(build_cells_for_key_groups(
|
|
||||||
"Quit",
|
|
||||||
vec![quit_keys],
|
|
||||||
key_color,
|
|
||||||
));
|
|
||||||
|
|
||||||
let widths = vec![Constraint::Fill(1), Constraint::Fill(2)];
|
|
||||||
|
|
||||||
Ok(Table::new(
|
|
||||||
vec![
|
|
||||||
results_row,
|
|
||||||
preview_row,
|
|
||||||
select_entry_row,
|
|
||||||
copy_entry_row,
|
|
||||||
send_to_channel_row,
|
|
||||||
switch_channels_row,
|
|
||||||
quit_row,
|
|
||||||
],
|
|
||||||
widths,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_keymap_table_for_channel_selection<'a>(
|
|
||||||
&self,
|
|
||||||
) -> Result<Table<'a>> {
|
|
||||||
let keymap = self.keymap_for_mode()?;
|
|
||||||
let key_color = mode_color(self.mode);
|
|
||||||
|
|
||||||
// Results navigation
|
|
||||||
let prev = keys_for_action(keymap, &Action::SelectPrevEntry);
|
|
||||||
let next = keys_for_action(keymap, &Action::SelectNextEntry);
|
|
||||||
let results_row = Row::new(build_cells_for_key_groups(
|
|
||||||
"Browse channels",
|
|
||||||
vec![prev, next],
|
|
||||||
key_color,
|
|
||||||
));
|
|
||||||
|
|
||||||
// Select entry
|
|
||||||
let select_entry_keys = keys_for_action(keymap, &Action::SelectEntry);
|
|
||||||
let select_entry_row = Row::new(build_cells_for_key_groups(
|
|
||||||
"Select channel",
|
|
||||||
vec![select_entry_keys],
|
|
||||||
key_color,
|
|
||||||
));
|
|
||||||
|
|
||||||
// Remote control
|
|
||||||
let switch_channels_keys =
|
|
||||||
keys_for_action(keymap, &Action::ToggleRemoteControl);
|
|
||||||
let switch_channels_row = Row::new(build_cells_for_key_groups(
|
|
||||||
"Toggle Remote control",
|
|
||||||
vec![switch_channels_keys],
|
|
||||||
key_color,
|
|
||||||
));
|
|
||||||
|
|
||||||
// Quit
|
|
||||||
let quit_keys = keys_for_action(keymap, &Action::Quit);
|
|
||||||
let quit_row = Row::new(build_cells_for_key_groups(
|
|
||||||
"Quit",
|
|
||||||
vec![quit_keys],
|
|
||||||
key_color,
|
|
||||||
));
|
|
||||||
|
|
||||||
Ok(Table::new(
|
|
||||||
vec![results_row, select_entry_row, switch_channels_row, quit_row],
|
|
||||||
vec![Constraint::Fill(1), Constraint::Fill(2)],
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_keymap_table_for_channel_transitions<'a>(
|
|
||||||
&self,
|
|
||||||
) -> Result<Table<'a>> {
|
|
||||||
let keymap = self.keymap_for_mode()?;
|
|
||||||
let key_color = mode_color(self.mode);
|
|
||||||
|
|
||||||
// Results navigation
|
|
||||||
let prev = keys_for_action(keymap, &Action::SelectPrevEntry);
|
|
||||||
let next = keys_for_action(keymap, &Action::SelectNextEntry);
|
|
||||||
let results_row = Row::new(build_cells_for_key_groups(
|
|
||||||
"Browse channels",
|
|
||||||
vec![prev, next],
|
|
||||||
key_color,
|
|
||||||
));
|
|
||||||
|
|
||||||
// Select entry
|
|
||||||
let select_entry_keys = keys_for_action(keymap, &Action::SelectEntry);
|
|
||||||
let select_entry_row = Row::new(build_cells_for_key_groups(
|
|
||||||
"Send to channel",
|
|
||||||
vec![select_entry_keys],
|
|
||||||
key_color,
|
|
||||||
));
|
|
||||||
|
|
||||||
// Cancel
|
|
||||||
let cancel_keys =
|
|
||||||
keys_for_action(keymap, &Action::ToggleSendToChannel);
|
|
||||||
let cancel_row = Row::new(build_cells_for_key_groups(
|
|
||||||
"Cancel",
|
|
||||||
vec![cancel_keys],
|
|
||||||
key_color,
|
|
||||||
));
|
|
||||||
|
|
||||||
// Quit
|
|
||||||
let quit_keys = keys_for_action(keymap, &Action::Quit);
|
|
||||||
let quit_row = Row::new(build_cells_for_key_groups(
|
|
||||||
"Quit",
|
|
||||||
vec![quit_keys],
|
|
||||||
key_color,
|
|
||||||
));
|
|
||||||
|
|
||||||
Ok(Table::new(
|
|
||||||
vec![results_row, select_entry_row, cancel_row, quit_row],
|
|
||||||
vec![Constraint::Fill(1), Constraint::Fill(2)],
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the keymap for the current mode.
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// A reference to the keymap for the current mode.
|
|
||||||
fn keymap_for_mode(&self) -> Result<&HashMap<Key, Action>> {
|
|
||||||
let keymap = self
|
|
||||||
.keymap
|
|
||||||
.get(&self.mode)
|
|
||||||
.ok_or_eyre("No keybindings found for the current Mode")?;
|
|
||||||
Ok(keymap)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build the corresponding spans for a group of keys.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```rust
|
|
||||||
/// use ratatui::text::Span;
|
|
||||||
/// use television::ui::help::build_spans_for_key_groups;
|
|
||||||
///
|
|
||||||
/// let key_groups = vec![
|
|
||||||
/// // alternate keys for the `SelectNextEntry` action
|
|
||||||
/// vec!["j".to_string(), "n".to_string()],
|
|
||||||
/// // alternate keys for the `SelectPrevEntry` action
|
|
||||||
/// vec!["k".to_string(), "p".to_string()],
|
|
||||||
/// ];
|
|
||||||
/// let spans = build_spans_for_key_groups("↕ Results", key_groups);
|
|
||||||
///
|
|
||||||
/// assert_eq!(spans.len(), 5);
|
|
||||||
/// ```
|
|
||||||
fn build_cells_for_key_groups(
|
|
||||||
group_name: &str,
|
|
||||||
key_groups: Vec<Vec<String>>,
|
|
||||||
key_color: Color,
|
|
||||||
) -> Vec<Cell> {
|
|
||||||
if key_groups.is_empty() || key_groups.iter().all(Vec::is_empty) {
|
|
||||||
return vec![group_name.into(), "No keybindings".into()];
|
|
||||||
}
|
|
||||||
let non_empty_groups = key_groups.iter().filter(|keys| !keys.is_empty());
|
|
||||||
let mut cells = vec![Cell::from(Span::styled(
|
|
||||||
group_name.to_owned() + ": ",
|
|
||||||
Style::default().fg(ACTION_COLOR),
|
|
||||||
))];
|
|
||||||
|
|
||||||
let mut spans = Vec::new();
|
|
||||||
|
|
||||||
let key_group_spans: Vec<Span> = non_empty_groups
|
|
||||||
.map(|keys| {
|
|
||||||
let key_group = keys.join(", ");
|
|
||||||
Span::styled(key_group, Style::default().fg(key_color))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
key_group_spans.iter().enumerate().for_each(|(i, span)| {
|
|
||||||
spans.push(span.clone());
|
|
||||||
if i < key_group_spans.len() - 1 {
|
|
||||||
spans.push(Span::styled(" / ", Style::default().fg(key_color)));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
cells.push(Cell::from(Line::from(spans)));
|
|
||||||
|
|
||||||
cells
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the keys for a given action.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```rust
|
|
||||||
/// use std::collections::HashMap;
|
|
||||||
/// use television::action::Action;
|
|
||||||
/// use television::ui::help::keys_for_action;
|
|
||||||
///
|
|
||||||
/// let mut keymap = HashMap::new();
|
|
||||||
/// keymap.insert('j', Action::SelectNextEntry);
|
|
||||||
/// keymap.insert('k', Action::SelectPrevEntry);
|
|
||||||
///
|
|
||||||
/// let keys = keys_for_action(&keymap, Action::SelectNextEntry);
|
|
||||||
///
|
|
||||||
/// assert_eq!(keys, vec!["j"]);
|
|
||||||
/// ```
|
|
||||||
fn keys_for_action(
|
|
||||||
keymap: &HashMap<Key, Action>,
|
|
||||||
action: &Action,
|
|
||||||
) -> Vec<String> {
|
|
||||||
keymap
|
|
||||||
.iter()
|
|
||||||
.filter(|(_key, act)| *act == action)
|
|
||||||
.map(|(key, _act)| format!("{key}"))
|
|
||||||
.collect()
|
|
||||||
}
|
|
@ -1,123 +0,0 @@
|
|||||||
use std::fmt::Display;
|
|
||||||
|
|
||||||
use ratatui::{
|
|
||||||
layout::Constraint,
|
|
||||||
style::{Color, Style},
|
|
||||||
text::Span,
|
|
||||||
widgets::{Cell, Row, Table},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::television::{Mode, Television};
|
|
||||||
use crate::ui::mode::mode_color;
|
|
||||||
|
|
||||||
const METADATA_FIELD_NAME_COLOR: Color = Color::DarkGray;
|
|
||||||
const METADATA_FIELD_VALUE_COLOR: Color = Color::Gray;
|
|
||||||
|
|
||||||
impl Display for Mode {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
Mode::Channel => write!(f, "Channel"),
|
|
||||||
Mode::RemoteControl => write!(f, "Remote Control"),
|
|
||||||
Mode::SendToChannel => write!(f, "Send to Channel"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Television {
|
|
||||||
pub fn build_metadata_table<'a>(&self) -> Table<'a> {
|
|
||||||
let version_row = Row::new(vec![
|
|
||||||
Cell::from(Span::styled(
|
|
||||||
"version: ",
|
|
||||||
Style::default().fg(METADATA_FIELD_NAME_COLOR),
|
|
||||||
)),
|
|
||||||
Cell::from(Span::styled(
|
|
||||||
env!("CARGO_PKG_VERSION"),
|
|
||||||
Style::default().fg(METADATA_FIELD_VALUE_COLOR),
|
|
||||||
)),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let target_triple_row = Row::new(vec![
|
|
||||||
Cell::from(Span::styled(
|
|
||||||
"target triple: ",
|
|
||||||
Style::default().fg(METADATA_FIELD_NAME_COLOR),
|
|
||||||
)),
|
|
||||||
Cell::from(Span::styled(
|
|
||||||
env!("VERGEN_CARGO_TARGET_TRIPLE"),
|
|
||||||
Style::default().fg(METADATA_FIELD_VALUE_COLOR),
|
|
||||||
)),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let build_row = Row::new(vec![
|
|
||||||
Cell::from(Span::styled(
|
|
||||||
"build: ",
|
|
||||||
Style::default().fg(METADATA_FIELD_NAME_COLOR),
|
|
||||||
)),
|
|
||||||
Cell::from(Span::styled(
|
|
||||||
env!("VERGEN_RUSTC_SEMVER"),
|
|
||||||
Style::default().fg(METADATA_FIELD_VALUE_COLOR),
|
|
||||||
)),
|
|
||||||
Cell::from(Span::styled(
|
|
||||||
" (",
|
|
||||||
Style::default().fg(METADATA_FIELD_NAME_COLOR),
|
|
||||||
)),
|
|
||||||
Cell::from(Span::styled(
|
|
||||||
env!("VERGEN_BUILD_DATE"),
|
|
||||||
Style::default().fg(METADATA_FIELD_VALUE_COLOR),
|
|
||||||
)),
|
|
||||||
Cell::from(Span::styled(
|
|
||||||
")",
|
|
||||||
Style::default().fg(METADATA_FIELD_NAME_COLOR),
|
|
||||||
)),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let current_dir_row = Row::new(vec![
|
|
||||||
Cell::from(Span::styled(
|
|
||||||
"current directory: ",
|
|
||||||
Style::default().fg(METADATA_FIELD_NAME_COLOR),
|
|
||||||
)),
|
|
||||||
Cell::from(Span::styled(
|
|
||||||
std::env::current_dir()
|
|
||||||
.expect("Could not get current directory")
|
|
||||||
.display()
|
|
||||||
.to_string(),
|
|
||||||
Style::default().fg(METADATA_FIELD_VALUE_COLOR),
|
|
||||||
)),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let current_channel_row = Row::new(vec![
|
|
||||||
Cell::from(Span::styled(
|
|
||||||
"current channel: ",
|
|
||||||
Style::default().fg(METADATA_FIELD_NAME_COLOR),
|
|
||||||
)),
|
|
||||||
Cell::from(Span::styled(
|
|
||||||
self.current_channel().to_string(),
|
|
||||||
Style::default().fg(METADATA_FIELD_VALUE_COLOR),
|
|
||||||
)),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let current_mode_row = Row::new(vec![
|
|
||||||
Cell::from(Span::styled(
|
|
||||||
"current mode: ",
|
|
||||||
Style::default().fg(METADATA_FIELD_NAME_COLOR),
|
|
||||||
)),
|
|
||||||
Cell::from(Span::styled(
|
|
||||||
self.mode.to_string(),
|
|
||||||
Style::default().fg(mode_color(self.mode)),
|
|
||||||
)),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let widths = vec![Constraint::Fill(1), Constraint::Fill(2)];
|
|
||||||
|
|
||||||
Table::new(
|
|
||||||
vec![
|
|
||||||
version_row,
|
|
||||||
target_triple_row,
|
|
||||||
build_row,
|
|
||||||
current_dir_row,
|
|
||||||
current_channel_row,
|
|
||||||
current_mode_row,
|
|
||||||
],
|
|
||||||
widths,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
use crate::television::Mode;
|
|
||||||
use ratatui::style::Color;
|
|
||||||
|
|
||||||
const CHANNEL_COLOR: Color = Color::Indexed(222);
|
|
||||||
const REMOTE_CONTROL_COLOR: Color = Color::Indexed(1);
|
|
||||||
const SEND_TO_CHANNEL_COLOR: Color = Color::Indexed(105);
|
|
||||||
|
|
||||||
pub fn mode_color(mode: Mode) -> Color {
|
|
||||||
match mode {
|
|
||||||
Mode::Channel => CHANNEL_COLOR,
|
|
||||||
Mode::RemoteControl => REMOTE_CONTROL_COLOR,
|
|
||||||
Mode::SendToChannel => SEND_TO_CHANNEL_COLOR,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,457 +0,0 @@
|
|||||||
use crate::television::Television;
|
|
||||||
use crate::ui::BORDER_COLOR;
|
|
||||||
use ansi_to_tui::IntoText;
|
|
||||||
use color_eyre::eyre::Result;
|
|
||||||
use ratatui::layout::{Alignment, Rect};
|
|
||||||
use ratatui::prelude::{Color, Line, Modifier, Span, Style, Stylize, Text};
|
|
||||||
use ratatui::widgets::{Block, BorderType, Borders, Padding, Paragraph, Wrap};
|
|
||||||
use ratatui::Frame;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use syntect::highlighting::Color as SyntectColor;
|
|
||||||
use television_channels::channels::OnAir;
|
|
||||||
use television_channels::entry::Entry;
|
|
||||||
use television_previewers::previewers::{
|
|
||||||
Preview, PreviewContent, FILE_TOO_LARGE_MSG, PREVIEW_NOT_SUPPORTED_MSG,
|
|
||||||
};
|
|
||||||
use television_utils::strings::{
|
|
||||||
replace_non_printable, shrink_with_ellipsis, ReplaceNonPrintableConfig,
|
|
||||||
EMPTY_STRING,
|
|
||||||
};
|
|
||||||
|
|
||||||
// preview
|
|
||||||
pub const DEFAULT_PREVIEW_TITLE_FG: Color = Color::Blue;
|
|
||||||
const DEFAULT_SELECTED_PREVIEW_BG: Color = Color::Rgb(50, 50, 50);
|
|
||||||
const DEFAULT_PREVIEW_CONTENT_FG: Color = Color::Rgb(150, 150, 180);
|
|
||||||
const DEFAULT_PREVIEW_GUTTER_FG: Color = Color::Rgb(70, 70, 70);
|
|
||||||
const DEFAULT_PREVIEW_GUTTER_SELECTED_FG: Color = Color::Rgb(255, 150, 150);
|
|
||||||
|
|
||||||
impl Television {
|
|
||||||
pub(crate) fn draw_preview_title_block(
|
|
||||||
&self,
|
|
||||||
f: &mut Frame,
|
|
||||||
rect: Rect,
|
|
||||||
preview: &Arc<Preview>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let mut preview_title_spans = Vec::new();
|
|
||||||
if preview.icon.is_some() && self.config.ui.use_nerd_font_icons {
|
|
||||||
let icon = preview.icon.as_ref().unwrap();
|
|
||||||
preview_title_spans.push(Span::styled(
|
|
||||||
{
|
|
||||||
let mut icon_str = String::from(icon.icon);
|
|
||||||
icon_str.push(' ');
|
|
||||||
icon_str
|
|
||||||
},
|
|
||||||
Style::default().fg(Color::from_str(icon.color)?),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
preview_title_spans.push(Span::styled(
|
|
||||||
shrink_with_ellipsis(
|
|
||||||
&replace_non_printable(
|
|
||||||
preview.title.as_bytes(),
|
|
||||||
&ReplaceNonPrintableConfig::default(),
|
|
||||||
)
|
|
||||||
.0,
|
|
||||||
rect.width.saturating_sub(4) as usize,
|
|
||||||
),
|
|
||||||
Style::default().fg(DEFAULT_PREVIEW_TITLE_FG).bold(),
|
|
||||||
));
|
|
||||||
let preview_title = Paragraph::new(Line::from(preview_title_spans))
|
|
||||||
.block(
|
|
||||||
Block::default()
|
|
||||||
.padding(Padding::horizontal(1))
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_type(BorderType::Rounded)
|
|
||||||
.border_style(Style::default().fg(BORDER_COLOR)),
|
|
||||||
)
|
|
||||||
.alignment(Alignment::Left);
|
|
||||||
f.render_widget(preview_title, rect);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn draw_preview_content_block(
|
|
||||||
&mut self,
|
|
||||||
f: &mut Frame,
|
|
||||||
rect: Rect,
|
|
||||||
entry: &Entry,
|
|
||||||
preview: &Arc<Preview>,
|
|
||||||
) {
|
|
||||||
let preview_outer_block = Block::default()
|
|
||||||
.title_top(Line::from(" Preview ").alignment(Alignment::Center))
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_type(BorderType::Rounded)
|
|
||||||
.border_style(Style::default().fg(BORDER_COLOR))
|
|
||||||
.style(Style::default())
|
|
||||||
.padding(Padding::right(1));
|
|
||||||
|
|
||||||
let preview_inner_block =
|
|
||||||
Block::default().style(Style::default()).padding(Padding {
|
|
||||||
top: 0,
|
|
||||||
right: 1,
|
|
||||||
bottom: 0,
|
|
||||||
left: 1,
|
|
||||||
});
|
|
||||||
let inner = preview_outer_block.inner(rect);
|
|
||||||
f.render_widget(preview_outer_block, rect);
|
|
||||||
|
|
||||||
let target_line =
|
|
||||||
entry.line_number.map(|l| u16::try_from(l).unwrap_or(0));
|
|
||||||
let cache_key = compute_cache_key(entry);
|
|
||||||
|
|
||||||
self.maybe_init_preview_scroll(target_line, inner.height);
|
|
||||||
|
|
||||||
// Check if the rendered preview content is already in the cache
|
|
||||||
if let Some(preview_paragraph) =
|
|
||||||
self.rendered_preview_cache.lock().unwrap().get(&cache_key)
|
|
||||||
{
|
|
||||||
let p = preview_paragraph.as_ref().clone();
|
|
||||||
f.render_widget(
|
|
||||||
p.scroll((self.preview_scroll.unwrap_or(0), 0)),
|
|
||||||
inner,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// If not, render the preview content and cache it if not empty
|
|
||||||
let rp = Self::build_preview_paragraph(
|
|
||||||
preview_inner_block,
|
|
||||||
inner,
|
|
||||||
preview.content.clone(),
|
|
||||||
target_line,
|
|
||||||
self.preview_scroll,
|
|
||||||
);
|
|
||||||
if !preview.stale {
|
|
||||||
self.rendered_preview_cache
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.insert(cache_key, &Arc::new(rp.clone()));
|
|
||||||
}
|
|
||||||
f.render_widget(
|
|
||||||
Arc::new(rp)
|
|
||||||
.as_ref()
|
|
||||||
.clone()
|
|
||||||
.scroll((self.preview_scroll.unwrap_or(0), 0)),
|
|
||||||
inner,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
const FILL_CHAR_SLANTED: char = '╱';
|
|
||||||
const FILL_CHAR_EMPTY: char = ' ';
|
|
||||||
|
|
||||||
// FIXME: I broke the previewer (srolling is not working as intended)
|
|
||||||
// and it looks like the previewer displays the wrong previews
|
|
||||||
pub fn build_preview_paragraph(
|
|
||||||
preview_block: Block,
|
|
||||||
inner: Rect,
|
|
||||||
preview_content: PreviewContent,
|
|
||||||
target_line: Option<u16>,
|
|
||||||
preview_scroll: Option<u16>,
|
|
||||||
) -> Paragraph {
|
|
||||||
match preview_content {
|
|
||||||
PreviewContent::AnsiText(text) => Self::build_ansi_text_paragraph(
|
|
||||||
text,
|
|
||||||
preview_block,
|
|
||||||
preview_scroll,
|
|
||||||
),
|
|
||||||
PreviewContent::PlainText(content) => {
|
|
||||||
Self::build_plain_text_paragraph(
|
|
||||||
content,
|
|
||||||
preview_block,
|
|
||||||
target_line,
|
|
||||||
preview_scroll,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
PreviewContent::PlainTextWrapped(content) => {
|
|
||||||
Self::build_plain_text_wrapped_paragraph(
|
|
||||||
content,
|
|
||||||
preview_block,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
PreviewContent::SyntectHighlightedText(highlighted_lines) => {
|
|
||||||
Self::build_syntect_highlighted_paragraph(
|
|
||||||
highlighted_lines,
|
|
||||||
preview_block,
|
|
||||||
target_line,
|
|
||||||
preview_scroll,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// meta
|
|
||||||
PreviewContent::Loading => Self::build_meta_preview_paragraph(
|
|
||||||
inner,
|
|
||||||
"Loading...",
|
|
||||||
Self::FILL_CHAR_EMPTY,
|
|
||||||
)
|
|
||||||
.block(preview_block)
|
|
||||||
.alignment(Alignment::Left)
|
|
||||||
.style(Style::default().add_modifier(Modifier::ITALIC)),
|
|
||||||
PreviewContent::NotSupported => {
|
|
||||||
Self::build_meta_preview_paragraph(
|
|
||||||
inner,
|
|
||||||
PREVIEW_NOT_SUPPORTED_MSG,
|
|
||||||
Self::FILL_CHAR_EMPTY,
|
|
||||||
)
|
|
||||||
.block(preview_block)
|
|
||||||
.alignment(Alignment::Left)
|
|
||||||
.style(Style::default().add_modifier(Modifier::ITALIC))
|
|
||||||
}
|
|
||||||
PreviewContent::FileTooLarge => {
|
|
||||||
Self::build_meta_preview_paragraph(
|
|
||||||
inner,
|
|
||||||
FILE_TOO_LARGE_MSG,
|
|
||||||
Self::FILL_CHAR_EMPTY,
|
|
||||||
)
|
|
||||||
.block(preview_block)
|
|
||||||
.alignment(Alignment::Left)
|
|
||||||
.style(Style::default().add_modifier(Modifier::ITALIC))
|
|
||||||
}
|
|
||||||
PreviewContent::Empty => Paragraph::new(Text::raw(EMPTY_STRING)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_ansi_text_paragraph(
|
|
||||||
text: String,
|
|
||||||
preview_block: Block,
|
|
||||||
preview_scroll: Option<u16>,
|
|
||||||
) -> Paragraph {
|
|
||||||
let text = replace_non_printable(
|
|
||||||
text.as_bytes(),
|
|
||||||
&ReplaceNonPrintableConfig {
|
|
||||||
replace_line_feed: false,
|
|
||||||
replace_control_characters: false,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.0
|
|
||||||
.into_text()
|
|
||||||
.unwrap();
|
|
||||||
Paragraph::new(text)
|
|
||||||
.block(preview_block)
|
|
||||||
.scroll((preview_scroll.unwrap_or(0), 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_plain_text_paragraph(
|
|
||||||
text: Vec<String>,
|
|
||||||
preview_block: Block,
|
|
||||||
target_line: Option<u16>,
|
|
||||||
preview_scroll: Option<u16>,
|
|
||||||
) -> Paragraph {
|
|
||||||
let mut lines = Vec::new();
|
|
||||||
for (i, line) in text.iter().enumerate() {
|
|
||||||
lines.push(Line::from(vec![
|
|
||||||
build_line_number_span(i + 1).style(Style::default().fg(
|
|
||||||
if matches!(
|
|
||||||
target_line,
|
|
||||||
Some(l) if l == u16::try_from(i).unwrap_or(0) + 1
|
|
||||||
)
|
|
||||||
{
|
|
||||||
DEFAULT_PREVIEW_GUTTER_SELECTED_FG
|
|
||||||
} else {
|
|
||||||
DEFAULT_PREVIEW_GUTTER_FG
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
Span::styled(" │ ",
|
|
||||||
Style::default().fg(DEFAULT_PREVIEW_GUTTER_FG).dim()),
|
|
||||||
Span::styled(
|
|
||||||
line.to_string(),
|
|
||||||
Style::default().fg(DEFAULT_PREVIEW_CONTENT_FG).bg(
|
|
||||||
if matches!(target_line, Some(l) if l == u16::try_from(i).unwrap() + 1) {
|
|
||||||
DEFAULT_SELECTED_PREVIEW_BG
|
|
||||||
} else {
|
|
||||||
Color::Reset
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]));
|
|
||||||
}
|
|
||||||
let text = Text::from(lines);
|
|
||||||
Paragraph::new(text)
|
|
||||||
.block(preview_block)
|
|
||||||
.scroll((preview_scroll.unwrap_or(0), 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_plain_text_wrapped_paragraph(
|
|
||||||
text: String,
|
|
||||||
preview_block: Block,
|
|
||||||
) -> Paragraph {
|
|
||||||
let mut lines = Vec::new();
|
|
||||||
for line in text.lines() {
|
|
||||||
lines.push(Line::styled(
|
|
||||||
line.to_string(),
|
|
||||||
Style::default().fg(DEFAULT_PREVIEW_CONTENT_FG),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let text = Text::from(lines);
|
|
||||||
Paragraph::new(text)
|
|
||||||
.block(preview_block)
|
|
||||||
.wrap(Wrap { trim: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_syntect_highlighted_paragraph(
|
|
||||||
highlighted_lines: Vec<Vec<(syntect::highlighting::Style, String)>>,
|
|
||||||
preview_block: Block,
|
|
||||||
target_line: Option<u16>,
|
|
||||||
preview_scroll: Option<u16>,
|
|
||||||
) -> Paragraph {
|
|
||||||
compute_paragraph_from_highlighted_lines(
|
|
||||||
&highlighted_lines,
|
|
||||||
target_line.map(|l| l as usize),
|
|
||||||
)
|
|
||||||
.block(preview_block)
|
|
||||||
.alignment(Alignment::Left)
|
|
||||||
.scroll((preview_scroll.unwrap_or(0), 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn maybe_init_preview_scroll(
|
|
||||||
&mut self,
|
|
||||||
target_line: Option<u16>,
|
|
||||||
height: u16,
|
|
||||||
) {
|
|
||||||
if self.preview_scroll.is_none() && !self.channel.running() {
|
|
||||||
self.preview_scroll =
|
|
||||||
Some(target_line.unwrap_or(0).saturating_sub(height / 3));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build_meta_preview_paragraph<'a>(
|
|
||||||
inner: Rect,
|
|
||||||
message: &str,
|
|
||||||
fill_char: char,
|
|
||||||
) -> Paragraph<'a> {
|
|
||||||
let message_len = message.len();
|
|
||||||
if message_len + 8 > inner.width as usize {
|
|
||||||
return Paragraph::new(Text::from(EMPTY_STRING));
|
|
||||||
}
|
|
||||||
let fill_char_str = fill_char.to_string();
|
|
||||||
let fill_line = fill_char_str.repeat(inner.width as usize);
|
|
||||||
|
|
||||||
// Build the paragraph content with slanted lines and center the custom message
|
|
||||||
let mut lines = Vec::new();
|
|
||||||
|
|
||||||
// Calculate the vertical center
|
|
||||||
let vertical_center = inner.height as usize / 2;
|
|
||||||
let horizontal_padding = (inner.width as usize - message_len) / 2 - 4;
|
|
||||||
|
|
||||||
// Fill the paragraph with slanted lines and insert the centered custom message
|
|
||||||
for i in 0..inner.height {
|
|
||||||
if i as usize == vertical_center {
|
|
||||||
// Center the message horizontally in the middle line
|
|
||||||
let line = format!(
|
|
||||||
"{} {} {}",
|
|
||||||
fill_char_str.repeat(horizontal_padding),
|
|
||||||
message,
|
|
||||||
fill_char_str.repeat(
|
|
||||||
inner.width as usize
|
|
||||||
- horizontal_padding
|
|
||||||
- message_len
|
|
||||||
)
|
|
||||||
);
|
|
||||||
lines.push(Line::from(line));
|
|
||||||
} else if i as usize + 1 == vertical_center
|
|
||||||
|| (i as usize).saturating_sub(1) == vertical_center
|
|
||||||
{
|
|
||||||
let line = format!(
|
|
||||||
"{} {} {}",
|
|
||||||
fill_char_str.repeat(horizontal_padding),
|
|
||||||
" ".repeat(message_len),
|
|
||||||
fill_char_str.repeat(
|
|
||||||
inner.width as usize
|
|
||||||
- horizontal_padding
|
|
||||||
- message_len
|
|
||||||
)
|
|
||||||
);
|
|
||||||
lines.push(Line::from(line));
|
|
||||||
} else {
|
|
||||||
lines.push(Line::from(fill_line.clone()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a paragraph with the generated content
|
|
||||||
Paragraph::new(Text::from(lines))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_line_number_span<'a>(line_number: usize) -> Span<'a> {
|
|
||||||
Span::from(format!("{line_number:5} "))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_paragraph_from_highlighted_lines(
|
|
||||||
highlighted_lines: &[Vec<(syntect::highlighting::Style, String)>],
|
|
||||||
line_specifier: Option<usize>,
|
|
||||||
) -> Paragraph<'static> {
|
|
||||||
let preview_lines: Vec<Line> = highlighted_lines
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, l)| {
|
|
||||||
let line_number =
|
|
||||||
build_line_number_span(i + 1).style(Style::default().fg(
|
|
||||||
if line_specifier.is_some()
|
|
||||||
&& i == line_specifier.unwrap().saturating_sub(1)
|
|
||||||
{
|
|
||||||
DEFAULT_PREVIEW_GUTTER_SELECTED_FG
|
|
||||||
} else {
|
|
||||||
DEFAULT_PREVIEW_GUTTER_FG
|
|
||||||
},
|
|
||||||
));
|
|
||||||
Line::from_iter(
|
|
||||||
std::iter::once(line_number)
|
|
||||||
.chain(std::iter::once(Span::styled(
|
|
||||||
" │ ",
|
|
||||||
Style::default().fg(DEFAULT_PREVIEW_GUTTER_FG).dim(),
|
|
||||||
)))
|
|
||||||
.chain(l.iter().cloned().map(|sr| {
|
|
||||||
convert_syn_region_to_span(
|
|
||||||
&(sr.0, sr.1),
|
|
||||||
if line_specifier.is_some()
|
|
||||||
&& i == line_specifier
|
|
||||||
.unwrap()
|
|
||||||
.saturating_sub(1)
|
|
||||||
{
|
|
||||||
Some(SyntectColor {
|
|
||||||
r: 50,
|
|
||||||
g: 50,
|
|
||||||
b: 50,
|
|
||||||
a: 255,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Paragraph::new(preview_lines)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn convert_syn_region_to_span<'a>(
|
|
||||||
syn_region: &(syntect::highlighting::Style, String),
|
|
||||||
background: Option<syntect::highlighting::Color>,
|
|
||||||
) -> Span<'a> {
|
|
||||||
let mut style = Style::default()
|
|
||||||
.fg(convert_syn_color_to_ratatui_color(syn_region.0.foreground));
|
|
||||||
if let Some(background) = background {
|
|
||||||
style = style.bg(convert_syn_color_to_ratatui_color(background));
|
|
||||||
}
|
|
||||||
style = match syn_region.0.font_style {
|
|
||||||
syntect::highlighting::FontStyle::BOLD => style.bold(),
|
|
||||||
syntect::highlighting::FontStyle::ITALIC => style.italic(),
|
|
||||||
syntect::highlighting::FontStyle::UNDERLINE => style.underlined(),
|
|
||||||
_ => style,
|
|
||||||
};
|
|
||||||
Span::styled(syn_region.1.clone(), style)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn convert_syn_color_to_ratatui_color(
|
|
||||||
color: syntect::highlighting::Color,
|
|
||||||
) -> Color {
|
|
||||||
Color::Rgb(color.r, color.g, color.b)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_cache_key(entry: &Entry) -> String {
|
|
||||||
let mut cache_key = entry.name.clone();
|
|
||||||
if let Some(line_number) = entry.line_number {
|
|
||||||
cache_key.push_str(&line_number.to_string());
|
|
||||||
}
|
|
||||||
cache_key
|
|
||||||
}
|
|
@ -1,153 +0,0 @@
|
|||||||
use crate::television::Television;
|
|
||||||
use crate::ui::logo::build_remote_logo_paragraph;
|
|
||||||
use crate::ui::mode::mode_color;
|
|
||||||
use crate::ui::results::{build_results_list, ResultsListColors};
|
|
||||||
use crate::ui::BORDER_COLOR;
|
|
||||||
use color_eyre::eyre::Result;
|
|
||||||
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
|
|
||||||
use ratatui::prelude::Style;
|
|
||||||
use ratatui::style::{Color, Stylize};
|
|
||||||
use ratatui::text::{Line, Span};
|
|
||||||
use ratatui::widgets::{
|
|
||||||
Block, BorderType, Borders, ListDirection, Padding, Paragraph,
|
|
||||||
};
|
|
||||||
use ratatui::Frame;
|
|
||||||
use television_channels::channels::OnAir;
|
|
||||||
|
|
||||||
impl Television {
|
|
||||||
pub fn draw_remote_control(
|
|
||||||
&mut self,
|
|
||||||
f: &mut Frame,
|
|
||||||
rect: Rect,
|
|
||||||
) -> Result<()> {
|
|
||||||
let layout = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints(
|
|
||||||
[
|
|
||||||
Constraint::Min(3),
|
|
||||||
Constraint::Length(3),
|
|
||||||
Constraint::Length(20),
|
|
||||||
]
|
|
||||||
.as_ref(),
|
|
||||||
)
|
|
||||||
.split(rect);
|
|
||||||
self.draw_rc_channels(f, &layout[0])?;
|
|
||||||
self.draw_rc_input(f, &layout[1])?;
|
|
||||||
draw_rc_logo(f, layout[2], mode_color(self.mode));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_rc_channels(&mut self, f: &mut Frame, area: &Rect) -> Result<()> {
|
|
||||||
let rc_block = Block::default()
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_type(BorderType::Rounded)
|
|
||||||
.border_style(Style::default().fg(BORDER_COLOR))
|
|
||||||
.style(Style::default())
|
|
||||||
.padding(Padding::right(1));
|
|
||||||
|
|
||||||
let result_count = self.remote_control.result_count();
|
|
||||||
if result_count > 0 && self.rc_picker.selected().is_none() {
|
|
||||||
self.rc_picker.select(Some(0));
|
|
||||||
self.rc_picker.relative_select(Some(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
let entries = self.remote_control.results(
|
|
||||||
area.height.saturating_sub(2).into(),
|
|
||||||
u32::try_from(self.rc_picker.offset())?,
|
|
||||||
);
|
|
||||||
|
|
||||||
let channel_list = build_results_list(
|
|
||||||
rc_block,
|
|
||||||
&entries,
|
|
||||||
ListDirection::TopToBottom,
|
|
||||||
Some(
|
|
||||||
ResultsListColors::default()
|
|
||||||
.result_name_fg(mode_color(self.mode)),
|
|
||||||
),
|
|
||||||
self.config.ui.use_nerd_font_icons,
|
|
||||||
&mut self.icon_color_cache,
|
|
||||||
);
|
|
||||||
|
|
||||||
f.render_stateful_widget(
|
|
||||||
channel_list,
|
|
||||||
*area,
|
|
||||||
&mut self.rc_picker.state,
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_rc_input(&mut self, f: &mut Frame, area: &Rect) -> Result<()> {
|
|
||||||
let input_block = Block::default()
|
|
||||||
.title_top(
|
|
||||||
Line::from("Remote Control").alignment(Alignment::Center),
|
|
||||||
)
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_type(BorderType::Rounded)
|
|
||||||
.border_style(Style::default().fg(BORDER_COLOR))
|
|
||||||
.style(Style::default());
|
|
||||||
|
|
||||||
let input_block_inner = input_block.inner(*area);
|
|
||||||
|
|
||||||
f.render_widget(input_block, *area);
|
|
||||||
|
|
||||||
// split input block into 2 parts: prompt symbol, input
|
|
||||||
let inner_input_chunks = Layout::default()
|
|
||||||
.direction(Direction::Horizontal)
|
|
||||||
.constraints([
|
|
||||||
// prompt symbol
|
|
||||||
Constraint::Length(2),
|
|
||||||
// input field
|
|
||||||
Constraint::Fill(1),
|
|
||||||
])
|
|
||||||
.split(input_block_inner);
|
|
||||||
|
|
||||||
let prompt_symbol_block = Block::default();
|
|
||||||
let arrow = Paragraph::new(Span::styled(
|
|
||||||
"> ",
|
|
||||||
Style::default()
|
|
||||||
.fg(crate::television::DEFAULT_INPUT_FG)
|
|
||||||
.bold(),
|
|
||||||
))
|
|
||||||
.block(prompt_symbol_block);
|
|
||||||
f.render_widget(arrow, inner_input_chunks[0]);
|
|
||||||
|
|
||||||
let interactive_input_block = Block::default();
|
|
||||||
// keep 2 for borders and 1 for cursor
|
|
||||||
let width = inner_input_chunks[1].width.max(3) - 3;
|
|
||||||
let scroll = self.rc_picker.input.visual_scroll(width as usize);
|
|
||||||
let input = Paragraph::new(self.rc_picker.input.value())
|
|
||||||
.scroll((0, u16::try_from(scroll)?))
|
|
||||||
.block(interactive_input_block)
|
|
||||||
.style(
|
|
||||||
Style::default()
|
|
||||||
.fg(crate::television::DEFAULT_INPUT_FG)
|
|
||||||
.bold()
|
|
||||||
.italic(),
|
|
||||||
)
|
|
||||||
.alignment(Alignment::Left);
|
|
||||||
f.render_widget(input, inner_input_chunks[1]);
|
|
||||||
|
|
||||||
// Make the cursor visible and ask tui-rs to put it at the
|
|
||||||
// specified coordinates after rendering
|
|
||||||
f.set_cursor_position((
|
|
||||||
// Put cursor past the end of the input text
|
|
||||||
inner_input_chunks[1].x
|
|
||||||
+ u16::try_from(
|
|
||||||
self.rc_picker.input.visual_cursor().max(scroll) - scroll,
|
|
||||||
)?,
|
|
||||||
// Move one line down, from the border to the input line
|
|
||||||
inner_input_chunks[1].y,
|
|
||||||
));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_rc_logo(f: &mut Frame, area: Rect, color: Color) {
|
|
||||||
let logo_block = Block::default().style(Style::default().fg(color));
|
|
||||||
|
|
||||||
let logo_paragraph = build_remote_logo_paragraph()
|
|
||||||
.alignment(Alignment::Center)
|
|
||||||
.block(logo_block);
|
|
||||||
|
|
||||||
f.render_widget(logo_paragraph, area);
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user