mirror of
https://github.com/alexpasmantier/television.git
synced 2025-07-29 06:11:37 +00:00
feat(ui): add support for customizing input_header
, preview_header
and preview_footer
This PR adds support for customizing * input_header * preview_header * preview_footer all the items are of type MultiTemplate, this gives some flexibility when showing the selected value, let's say on the preview header, if it's long or you want only a section --------- Co-authored-by: alexandre pasmantier <alex.pasmant@gmail.com>
This commit is contained in:
parent
ab1efed88d
commit
510e7b6338
153
Cargo.lock
generated
153
Cargo.lock
generated
@ -545,6 +545,16 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deranged"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
||||||
|
dependencies = [
|
||||||
|
"powerfmt",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "devicons"
|
name = "devicons"
|
||||||
version = "0.6.12"
|
version = "0.6.12"
|
||||||
@ -591,6 +601,12 @@ version = "1.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dyn-clone"
|
||||||
|
version = "1.0.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.15.0"
|
version = "1.15.0"
|
||||||
@ -774,6 +790,12 @@ dependencies = [
|
|||||||
"crunchy",
|
"crunchy",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.15.2"
|
version = "0.15.2"
|
||||||
@ -797,6 +819,12 @@ version = "0.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e"
|
checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hex"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
@ -860,6 +888,17 @@ version = "1.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "1.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"hashbrown 0.12.3",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.9.0"
|
version = "2.9.0"
|
||||||
@ -867,7 +906,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
|
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown 0.15.2",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1019,7 +1059,7 @@ version = "0.12.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
|
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hashbrown",
|
"hashbrown 0.15.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1117,6 +1157,12 @@ dependencies = [
|
|||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-conv"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.19"
|
version = "0.2.19"
|
||||||
@ -1311,6 +1357,12 @@ dependencies = [
|
|||||||
"winreg",
|
"winreg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "powerfmt"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.94"
|
version = "1.0.94"
|
||||||
@ -1397,6 +1449,26 @@ dependencies = [
|
|||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ref-cast"
|
||||||
|
version = "1.0.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf"
|
||||||
|
dependencies = [
|
||||||
|
"ref-cast-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ref-cast-impl"
|
||||||
|
version = "1.0.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.11.1"
|
version = "1.11.1"
|
||||||
@ -1570,6 +1642,18 @@ dependencies = [
|
|||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "schemars"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
|
||||||
|
dependencies = [
|
||||||
|
"dyn-clone",
|
||||||
|
"ref-cast",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@ -1617,6 +1701,37 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_with"
|
||||||
|
version = "3.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf65a400f8f66fb7b0552869ad70157166676db75ed8181f8104ea91cf9d0b42"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"chrono",
|
||||||
|
"hex",
|
||||||
|
"indexmap 1.9.3",
|
||||||
|
"indexmap 2.9.0",
|
||||||
|
"schemars",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"serde_with_macros",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_with_macros"
|
||||||
|
version = "3.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "81679d9ed988d5e9a5e6531dc3f2c28efbd639cbd1dfb628df08edea6004da77"
|
||||||
|
dependencies = [
|
||||||
|
"darling",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serial2"
|
name = "serial2"
|
||||||
version = "0.2.29"
|
version = "0.2.29"
|
||||||
@ -1824,6 +1939,7 @@ dependencies = [
|
|||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serde_with",
|
||||||
"signal-hook",
|
"signal-hook",
|
||||||
"string_pipeline",
|
"string_pipeline",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
@ -1900,6 +2016,37 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time"
|
||||||
|
version = "0.3.41"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
|
||||||
|
dependencies = [
|
||||||
|
"deranged",
|
||||||
|
"itoa",
|
||||||
|
"num-conv",
|
||||||
|
"powerfmt",
|
||||||
|
"serde",
|
||||||
|
"time-core",
|
||||||
|
"time-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-core"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-macros"
|
||||||
|
version = "0.2.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
|
||||||
|
dependencies = [
|
||||||
|
"num-conv",
|
||||||
|
"time-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinytemplate"
|
name = "tinytemplate"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@ -1966,7 +2113,7 @@ version = "0.22.24"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
|
checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap 2.9.0",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_spanned",
|
"serde_spanned",
|
||||||
"toml_datetime",
|
"toml_datetime",
|
||||||
|
@ -58,6 +58,7 @@ string_pipeline = "0.11.1"
|
|||||||
ureq = "3.0.11"
|
ureq = "3.0.11"
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.140"
|
||||||
colored = "3.0.0"
|
colored = "3.0.0"
|
||||||
|
serde_with = "3.13.0"
|
||||||
|
|
||||||
|
|
||||||
# target specific dependencies
|
# target specific dependencies
|
||||||
|
@ -12,3 +12,4 @@ command = "echo '{split:=:1..}'"
|
|||||||
[ui]
|
[ui]
|
||||||
layout = "portrait"
|
layout = "portrait"
|
||||||
preview_size = 20
|
preview_size = 20
|
||||||
|
preview_header = "{split:=:0}"
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use std::fmt::{self, Display, Formatter};
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::KeyBindings,
|
config::KeyBindings,
|
||||||
screen::layout::{InputPosition, Orientation},
|
screen::layout::{InputPosition, Orientation},
|
||||||
};
|
};
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use serde::ser::SerializeSeq;
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_with::{OneOrMany, serde_as};
|
||||||
use string_pipeline::MultiTemplate;
|
use string_pipeline::MultiTemplate;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@ -53,13 +55,49 @@ impl Display for Template {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Template {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.raw() == other.raw()
|
||||||
|
&& matches!(
|
||||||
|
(self, other),
|
||||||
|
(Template::StringPipeline(_), Template::StringPipeline(_))
|
||||||
|
| (Template::Raw(_), Template::Raw(_))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for Template {}
|
||||||
|
|
||||||
|
impl Hash for Template {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
self.raw().hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for Template {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(self.raw())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Template {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let raw = String::deserialize(deserializer)?;
|
||||||
|
Template::parse(&raw).map_err(serde::de::Error::custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
||||||
pub struct CommandSpec {
|
pub struct CommandSpec {
|
||||||
#[serde(
|
#[serde(rename = "command")]
|
||||||
rename = "command",
|
#[serde_as(as = "OneOrMany<_>")]
|
||||||
deserialize_with = "deserialize_commands",
|
|
||||||
serialize_with = "serialize_commands"
|
|
||||||
)]
|
|
||||||
pub inner: Vec<Template>,
|
pub inner: Vec<Template>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub interactive: bool,
|
pub interactive: bool,
|
||||||
@ -67,13 +105,6 @@ pub struct CommandSpec {
|
|||||||
pub env: FxHashMap<String, String>,
|
pub env: FxHashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
enum SerializedCommand {
|
|
||||||
Single(String),
|
|
||||||
Multiple(Vec<String>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for CommandSpec {
|
impl Display for CommandSpec {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
write!(
|
write!(
|
||||||
@ -118,142 +149,6 @@ impl CommandSpec {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn serialize_command<S>(
|
|
||||||
command: &Template,
|
|
||||||
serializer: S,
|
|
||||||
) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: serde::Serializer,
|
|
||||||
{
|
|
||||||
serializer.serialize_str(command.raw())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::ref_option)]
|
|
||||||
fn serialize_maybe_command<S>(
|
|
||||||
command: &Option<Template>,
|
|
||||||
serializer: S,
|
|
||||||
) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: serde::Serializer,
|
|
||||||
{
|
|
||||||
match command {
|
|
||||||
Some(cmd) => serialize_command(cmd, serializer),
|
|
||||||
None => serializer.serialize_none(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn deserialize_command<'de, D>(deserializer: D) -> Result<Template, D::Error>
|
|
||||||
where
|
|
||||||
D: serde::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let raw: String = serde::Deserialize::deserialize(deserializer)?;
|
|
||||||
Template::parse(&raw).map_err(serde::de::Error::custom)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_maybe_command<'de, D>(
|
|
||||||
deserializer: D,
|
|
||||||
) -> Result<Option<Template>, D::Error>
|
|
||||||
where
|
|
||||||
D: serde::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let raw: Option<String> = serde::Deserialize::deserialize(deserializer)?;
|
|
||||||
match raw {
|
|
||||||
Some(cmd) => Template::parse(&cmd)
|
|
||||||
.map(Some)
|
|
||||||
.map_err(serde::de::Error::custom),
|
|
||||||
None => Ok(None),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_commands<S>(
|
|
||||||
commands: &[Template],
|
|
||||||
serializer: S,
|
|
||||||
) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: serde::Serializer,
|
|
||||||
{
|
|
||||||
if commands.len() == 1 {
|
|
||||||
let raw = commands[0].raw();
|
|
||||||
serializer.serialize_str(raw)
|
|
||||||
} else {
|
|
||||||
let raw: Vec<String> =
|
|
||||||
commands.iter().map(|c| c.raw().to_string()).collect();
|
|
||||||
let mut seq = serializer.serialize_seq(Some(raw.len()))?;
|
|
||||||
for item in raw {
|
|
||||||
seq.serialize_element(&item)?;
|
|
||||||
}
|
|
||||||
seq.end()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::ref_option, dead_code)]
|
|
||||||
fn serialize_maybe_commands<S>(
|
|
||||||
commands: Option<&[Template]>,
|
|
||||||
serializer: S,
|
|
||||||
) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: serde::Serializer,
|
|
||||||
{
|
|
||||||
match commands {
|
|
||||||
Some(m) => serialize_commands(m, serializer),
|
|
||||||
None => serializer.serialize_none(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_commands<'de, D>(
|
|
||||||
deserializer: D,
|
|
||||||
) -> Result<Vec<Template>, D::Error>
|
|
||||||
where
|
|
||||||
D: serde::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let res = match serde::Deserialize::deserialize(deserializer)? {
|
|
||||||
SerializedCommand::Single(cmd) => {
|
|
||||||
Template::parse(&cmd).map(|m| vec![m])
|
|
||||||
}
|
|
||||||
SerializedCommand::Multiple(cmds) => cmds
|
|
||||||
.iter()
|
|
||||||
.map(|cmd| Template::parse(cmd))
|
|
||||||
.collect::<Result<Vec<_>, _>>(),
|
|
||||||
}
|
|
||||||
.map_err(serde::de::Error::custom);
|
|
||||||
|
|
||||||
if let Ok(ref cmds) = res {
|
|
||||||
if cmds.is_empty() {
|
|
||||||
return Err(serde::de::Error::custom(
|
|
||||||
"Command list cannot be empty",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn deserialize_maybe_commands<'de, D>(
|
|
||||||
deserializer: D,
|
|
||||||
) -> Result<Option<Vec<Template>>, D::Error>
|
|
||||||
where
|
|
||||||
D: serde::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let raw: Option<SerializedCommand> =
|
|
||||||
serde::Deserialize::deserialize(deserializer)?;
|
|
||||||
match raw {
|
|
||||||
Some(template) => {
|
|
||||||
let cmd = match template {
|
|
||||||
SerializedCommand::Single(cmd) => {
|
|
||||||
Template::parse(&cmd).map(|m| vec![m])
|
|
||||||
}
|
|
||||||
SerializedCommand::Multiple(cmds) => {
|
|
||||||
cmds.iter().map(|cmd| Template::parse(cmd)).collect()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
cmd.map_err(serde::de::Error::custom).map(Some)
|
|
||||||
}
|
|
||||||
None => Ok(None),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
||||||
pub struct ChannelPrototype {
|
pub struct ChannelPrototype {
|
||||||
pub metadata: Metadata,
|
pub metadata: Metadata,
|
||||||
@ -342,17 +237,9 @@ pub struct Metadata {
|
|||||||
pub struct SourceSpec {
|
pub struct SourceSpec {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub command: CommandSpec,
|
pub command: CommandSpec,
|
||||||
#[serde(
|
#[serde(default)]
|
||||||
default,
|
|
||||||
deserialize_with = "deserialize_maybe_command",
|
|
||||||
serialize_with = "serialize_maybe_command"
|
|
||||||
)]
|
|
||||||
pub display: Option<Template>,
|
pub display: Option<Template>,
|
||||||
#[serde(
|
#[serde(default)]
|
||||||
default,
|
|
||||||
deserialize_with = "deserialize_maybe_command",
|
|
||||||
serialize_with = "serialize_maybe_command"
|
|
||||||
)]
|
|
||||||
pub output: Option<Template>,
|
pub output: Option<Template>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -360,11 +247,7 @@ pub struct SourceSpec {
|
|||||||
pub struct PreviewSpec {
|
pub struct PreviewSpec {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub command: CommandSpec,
|
pub command: CommandSpec,
|
||||||
#[serde(
|
#[serde(default)]
|
||||||
default,
|
|
||||||
deserialize_with = "deserialize_maybe_command",
|
|
||||||
serialize_with = "serialize_maybe_command"
|
|
||||||
)]
|
|
||||||
pub offset: Option<Template>,
|
pub offset: Option<Template>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -397,12 +280,18 @@ pub struct UiSpec {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub show_preview_panel: Option<bool>,
|
pub show_preview_panel: Option<bool>,
|
||||||
// `layout` is clearer for the user but collides with the overall `Layout` type.
|
// `layout` is clearer for the user but collides with the overall `Layout` type.
|
||||||
#[serde(rename = "layout", default)]
|
#[serde(rename = "layout", alias = "orientation", default)]
|
||||||
pub orientation: Option<Orientation>,
|
pub orientation: Option<Orientation>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub input_bar_position: Option<InputPosition>,
|
pub input_bar_position: Option<InputPosition>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub preview_size: Option<u16>,
|
pub preview_size: Option<u16>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub input_header: Option<Template>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub preview_header: Option<Template>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub preview_footer: Option<Template>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const DEFAULT_PROTOTYPE_NAME: &str = "files";
|
pub const DEFAULT_PROTOTYPE_NAME: &str = "files";
|
||||||
@ -432,6 +321,41 @@ mod tests {
|
|||||||
assert_eq!(command_spec.get_nth(3).raw(), "cmd1"); // wraps around
|
assert_eq!(command_spec.get_nth(3).raw(), "cmd1"); // wraps around
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_template_serialization() {
|
||||||
|
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
||||||
|
struct TestStruct {
|
||||||
|
template: Template,
|
||||||
|
}
|
||||||
|
let raw_1 = r#"template = "Hello, {}""#;
|
||||||
|
let raw_2 = r#"template = "Hello, World""#;
|
||||||
|
let raw_3 = r#"template = "docker images --format '{{.Repository}}:{{.Tag}} {{.ID}}'""#;
|
||||||
|
|
||||||
|
let test_1: TestStruct = from_str(raw_1).unwrap();
|
||||||
|
let test_2: TestStruct = from_str(raw_2).unwrap();
|
||||||
|
let test_3: TestStruct = from_str(raw_3).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
test_1.template,
|
||||||
|
Template::StringPipeline(
|
||||||
|
MultiTemplate::parse("Hello, {}").unwrap()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
test_2.template,
|
||||||
|
Template::StringPipeline(
|
||||||
|
MultiTemplate::parse("Hello, World").unwrap()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
test_3.template,
|
||||||
|
Template::Raw(
|
||||||
|
"docker images --format '{{.Repository}}:{{.Tag}} {{.ID}}'"
|
||||||
|
.to_string()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_channel_prototype_deserialization() {
|
fn test_channel_prototype_deserialization() {
|
||||||
let toml_data = r#"
|
let toml_data = r#"
|
||||||
@ -445,13 +369,14 @@ mod tests {
|
|||||||
interactive = false
|
interactive = false
|
||||||
env = {}
|
env = {}
|
||||||
display = "{split:/:-1}" # only show the last path segment ('/a/b/c' -> 'c')
|
display = "{split:/:-1}" # only show the last path segment ('/a/b/c' -> 'c')
|
||||||
ansi = false
|
|
||||||
output = "{}" # output the full path
|
output = "{}" # output the full path
|
||||||
|
unknown_field = "ignored" # should be ignored
|
||||||
|
|
||||||
[preview]
|
[preview]
|
||||||
command = "bat -n --color=always {}"
|
command = "bat -n --color=always {}"
|
||||||
env = { "BAT_THEME" = "ansi" }
|
env = { "BAT_THEME" = "ansi" }
|
||||||
interactive = false
|
interactive = false
|
||||||
|
offset = "3" # why not
|
||||||
|
|
||||||
[ui]
|
[ui]
|
||||||
layout = "landscape"
|
layout = "landscape"
|
||||||
@ -459,6 +384,10 @@ mod tests {
|
|||||||
show_help_bar = false
|
show_help_bar = false
|
||||||
show_preview_panel = true
|
show_preview_panel = true
|
||||||
input_bar_position = "bottom"
|
input_bar_position = "bottom"
|
||||||
|
preview_size = 66
|
||||||
|
input_header = "Input: {}"
|
||||||
|
preview_header = "Preview: {}"
|
||||||
|
preview_footer = "Press 'q' to quit"
|
||||||
|
|
||||||
[keybindings]
|
[keybindings]
|
||||||
quit = ["esc", "ctrl-c"]
|
quit = ["esc", "ctrl-c"]
|
||||||
@ -478,19 +407,37 @@ mod tests {
|
|||||||
format!("{}", prototype.source.command.inner[0]),
|
format!("{}", prototype.source.command.inner[0]),
|
||||||
"fd -t f"
|
"fd -t f"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(!prototype.source.command.interactive);
|
assert!(!prototype.source.command.interactive);
|
||||||
assert_eq!(prototype.source.display.unwrap().raw(), "{split:/:-1}");
|
assert_eq!(prototype.source.display.unwrap().raw(), "{split:/:-1}");
|
||||||
assert_eq!(prototype.source.output.unwrap().raw(), "{}");
|
assert_eq!(prototype.source.output.unwrap().raw(), "{}");
|
||||||
|
|
||||||
|
let preview = prototype.preview.as_ref().unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format!("{}", prototype.preview.unwrap().command.inner[0]),
|
format!("{}", preview.command.inner[0]),
|
||||||
"bat -n --color=always {}"
|
"bat -n --color=always {}"
|
||||||
);
|
);
|
||||||
|
assert!(!preview.command.interactive);
|
||||||
|
assert_eq!(
|
||||||
|
preview.command.env.get("BAT_THEME"),
|
||||||
|
Some(&"ansi".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(preview.offset.as_ref().unwrap().raw(), "3");
|
||||||
|
|
||||||
let ui = prototype.ui.unwrap();
|
let ui = prototype.ui.unwrap();
|
||||||
assert_eq!(ui.orientation, Some(Orientation::Landscape));
|
assert_eq!(ui.orientation, Some(Orientation::Landscape));
|
||||||
assert_eq!(ui.ui_scale, Some(100));
|
assert_eq!(ui.ui_scale, Some(100));
|
||||||
assert!(!(ui.show_help_bar.unwrap()));
|
assert!(!(ui.show_help_bar.unwrap()));
|
||||||
assert!(ui.show_preview_panel.unwrap());
|
assert!(ui.show_preview_panel.unwrap());
|
||||||
assert_eq!(ui.input_bar_position, Some(InputPosition::Bottom));
|
assert_eq!(ui.input_bar_position, Some(InputPosition::Bottom));
|
||||||
|
assert_eq!(ui.preview_size, Some(66));
|
||||||
|
assert_eq!(ui.input_header.as_ref().unwrap().raw(), "Input: {}");
|
||||||
|
assert_eq!(ui.preview_header.as_ref().unwrap().raw(), "Preview: {}");
|
||||||
|
assert_eq!(
|
||||||
|
ui.preview_footer.as_ref().unwrap().raw(),
|
||||||
|
"Press 'q' to quit"
|
||||||
|
);
|
||||||
|
|
||||||
let keybindings = prototype.keybindings.unwrap();
|
let keybindings = prototype.keybindings.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keybindings.0.get(&Action::Quit),
|
keybindings.0.get(&Action::Quit),
|
||||||
@ -599,6 +546,7 @@ mod tests {
|
|||||||
[ui]
|
[ui]
|
||||||
layout = "landscape"
|
layout = "landscape"
|
||||||
ui_scale = 40
|
ui_scale = 40
|
||||||
|
preview_footer = "Press 'q' to quit"
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
let prototype: ChannelPrototype = from_str(toml_data).unwrap();
|
let prototype: ChannelPrototype = from_str(toml_data).unwrap();
|
||||||
@ -623,5 +571,12 @@ mod tests {
|
|||||||
assert!(ui.show_help_bar.is_none());
|
assert!(ui.show_help_bar.is_none());
|
||||||
assert!(ui.show_preview_panel.is_none());
|
assert!(ui.show_preview_panel.is_none());
|
||||||
assert!(ui.input_bar_position.is_none());
|
assert!(ui.input_bar_position.is_none());
|
||||||
|
assert!(ui.preview_size.is_none());
|
||||||
|
assert!(ui.input_header.is_none());
|
||||||
|
assert!(ui.preview_header.is_none());
|
||||||
|
assert_eq!(
|
||||||
|
ui.preview_footer.as_ref().unwrap().raw(),
|
||||||
|
"Press 'q' to quit"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -69,13 +69,35 @@ pub struct Cli {
|
|||||||
#[arg(short, long, value_name = "STRING", verbatim_doc_comment)]
|
#[arg(short, long, value_name = "STRING", verbatim_doc_comment)]
|
||||||
pub input: Option<String>,
|
pub input: Option<String>,
|
||||||
|
|
||||||
/// Input fields header title
|
/// Input field header template.
|
||||||
///
|
///
|
||||||
/// This can be used to give the input field a custom title e.g. the current
|
/// The given value is parsed as a `MultiTemplate`. It is evaluated against
|
||||||
/// working directory.
|
/// the current channel name and the resulting text is shown as the input
|
||||||
/// The default value for the input header is the current channel.
|
/// field title. Defaults to the current channel name when omitted.
|
||||||
#[arg(long, value_name = "STRING", verbatim_doc_comment)]
|
#[arg(long = "input-header", value_name = "STRING", verbatim_doc_comment)]
|
||||||
pub custom_header: Option<String>,
|
pub input_header: Option<String>,
|
||||||
|
|
||||||
|
/// Preview header template
|
||||||
|
///
|
||||||
|
/// The given value is parsed as a `MultiTemplate`. It is evaluated for every
|
||||||
|
/// entry and its result is displayed above the preview panel.
|
||||||
|
#[arg(
|
||||||
|
long = "preview-header",
|
||||||
|
value_name = "STRING",
|
||||||
|
verbatim_doc_comment
|
||||||
|
)]
|
||||||
|
pub preview_header: Option<String>,
|
||||||
|
|
||||||
|
/// Preview footer template
|
||||||
|
///
|
||||||
|
/// The given value is parsed as a `MultiTemplate`. It is evaluated for every
|
||||||
|
/// entry and its result is displayed below the preview panel.
|
||||||
|
#[arg(
|
||||||
|
long = "preview-footer",
|
||||||
|
value_name = "STRING",
|
||||||
|
verbatim_doc_comment
|
||||||
|
)]
|
||||||
|
pub preview_footer: Option<String>,
|
||||||
|
|
||||||
/// The working directory to start the application in.
|
/// The working directory to start the application in.
|
||||||
///
|
///
|
||||||
|
@ -28,7 +28,9 @@ pub struct PostProcessedCli {
|
|||||||
pub tick_rate: Option<f64>,
|
pub tick_rate: Option<f64>,
|
||||||
pub frame_rate: Option<f64>,
|
pub frame_rate: Option<f64>,
|
||||||
pub input: Option<String>,
|
pub input: Option<String>,
|
||||||
pub custom_header: Option<String>,
|
pub input_header: Option<String>,
|
||||||
|
pub preview_header: Option<String>,
|
||||||
|
pub preview_footer: Option<String>,
|
||||||
pub command: Option<Command>,
|
pub command: Option<Command>,
|
||||||
pub working_directory: Option<String>,
|
pub working_directory: Option<String>,
|
||||||
pub autocomplete_prompt: Option<String>,
|
pub autocomplete_prompt: Option<String>,
|
||||||
@ -52,7 +54,9 @@ impl Default for PostProcessedCli {
|
|||||||
tick_rate: None,
|
tick_rate: None,
|
||||||
frame_rate: None,
|
frame_rate: None,
|
||||||
input: None,
|
input: None,
|
||||||
custom_header: None,
|
input_header: None,
|
||||||
|
preview_header: None,
|
||||||
|
preview_footer: None,
|
||||||
command: None,
|
command: None,
|
||||||
working_directory: None,
|
working_directory: None,
|
||||||
autocomplete_prompt: None,
|
autocomplete_prompt: None,
|
||||||
@ -115,7 +119,9 @@ pub fn post_process(cli: Cli) -> PostProcessedCli {
|
|||||||
tick_rate: cli.tick_rate,
|
tick_rate: cli.tick_rate,
|
||||||
frame_rate: cli.frame_rate,
|
frame_rate: cli.frame_rate,
|
||||||
input: cli.input,
|
input: cli.input,
|
||||||
custom_header: cli.custom_header,
|
input_header: cli.input_header,
|
||||||
|
preview_header: cli.preview_header,
|
||||||
|
preview_footer: cli.preview_footer,
|
||||||
command: cli.command,
|
command: cli.command,
|
||||||
working_directory,
|
working_directory,
|
||||||
autocomplete_prompt: cli.autocomplete_prompt,
|
autocomplete_prompt: cli.autocomplete_prompt,
|
||||||
|
@ -242,6 +242,17 @@ impl Config {
|
|||||||
if let Some(preview_size) = &ui_spec.preview_size {
|
if let Some(preview_size) = &ui_spec.preview_size {
|
||||||
self.ui.preview_size = *preview_size;
|
self.ui.preview_size = *preview_size;
|
||||||
}
|
}
|
||||||
|
if let Some(input_header) = &ui_spec.input_header {
|
||||||
|
self.ui.input_header = Some(input_header.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(preview_header) = &ui_spec.preview_header {
|
||||||
|
self.ui.preview_header = Some(preview_header.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(preview_footer) = &ui_spec.preview_footer {
|
||||||
|
self.ui.preview_footer = Some(preview_footer.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use crate::channels::prototypes::Template;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::screen::layout::{
|
use crate::screen::layout::{
|
||||||
@ -23,9 +24,13 @@ pub struct UiConfig {
|
|||||||
pub orientation: Orientation,
|
pub orientation: Orientation,
|
||||||
pub preview_title_position: Option<PreviewTitlePosition>,
|
pub preview_title_position: Option<PreviewTitlePosition>,
|
||||||
pub theme: String,
|
pub theme: String,
|
||||||
pub custom_header: Option<String>,
|
|
||||||
|
|
||||||
pub preview_size: u16,
|
pub preview_size: u16,
|
||||||
|
#[serde(default)]
|
||||||
|
pub input_header: Option<Template>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub preview_header: Option<Template>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub preview_footer: Option<Template>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for UiConfig {
|
impl Default for UiConfig {
|
||||||
@ -40,8 +45,10 @@ impl Default for UiConfig {
|
|||||||
orientation: Orientation::Landscape,
|
orientation: Orientation::Landscape,
|
||||||
preview_title_position: None,
|
preview_title_position: None,
|
||||||
theme: String::from(DEFAULT_THEME),
|
theme: String::from(DEFAULT_THEME),
|
||||||
custom_header: None,
|
|
||||||
preview_size: DEFAULT_PREVIEW_SIZE,
|
preview_size: DEFAULT_PREVIEW_SIZE,
|
||||||
|
input_header: None,
|
||||||
|
preview_header: None,
|
||||||
|
preview_footer: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -247,7 +247,7 @@ pub fn draw(ctx: &Ctx, f: &mut Frame<'_>, area: Rect) -> Result<Layout> {
|
|||||||
&ctx.tv_state.channel_state.current_channel_name,
|
&ctx.tv_state.channel_state.current_channel_name,
|
||||||
&ctx.tv_state.spinner,
|
&ctx.tv_state.spinner,
|
||||||
&ctx.colorscheme,
|
&ctx.colorscheme,
|
||||||
&ctx.config.ui.custom_header,
|
&ctx.config.ui.input_header,
|
||||||
&ctx.config.ui.input_bar_position,
|
&ctx.config.ui.input_bar_position,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ use television::cli::post_process;
|
|||||||
use television::gh::update_local_channels;
|
use television::gh::update_local_channels;
|
||||||
use television::{
|
use television::{
|
||||||
cable::Cable, channels::prototypes::ChannelPrototype,
|
cable::Cable, channels::prototypes::ChannelPrototype,
|
||||||
utils::clipboard::CLIPBOARD,
|
channels::prototypes::Template, utils::clipboard::CLIPBOARD,
|
||||||
};
|
};
|
||||||
use tracing::{debug, error, info};
|
use tracing::{debug, error, info};
|
||||||
|
|
||||||
@ -81,8 +81,34 @@ async fn main() -> Result<()> {
|
|||||||
args.preview_size,
|
args.preview_size,
|
||||||
config.application.tick_rate,
|
config.application.tick_rate,
|
||||||
);
|
);
|
||||||
let mut app =
|
let mut app = App::new(
|
||||||
App::new(channel_prototype, config, args.input, options, cable);
|
channel_prototype,
|
||||||
|
config,
|
||||||
|
args.input.clone(),
|
||||||
|
options,
|
||||||
|
cable,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Apply CLI template overrides last (highest priority). This must run
|
||||||
|
// AFTER the channel-specific UI spec has been merged (inside
|
||||||
|
// `Television::new`) so that CLI flags win over both global and channel
|
||||||
|
// configuration.
|
||||||
|
if let Some(ref s) = args.input_header {
|
||||||
|
if let Ok(t) = Template::parse(s) {
|
||||||
|
app.television.config.ui.input_header = Some(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(ref s) = args.preview_header {
|
||||||
|
if let Ok(t) = Template::parse(s) {
|
||||||
|
app.television.config.ui.preview_header = Some(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(ref s) = args.preview_footer {
|
||||||
|
if let Ok(t) = Template::parse(s) {
|
||||||
|
app.television.config.ui.preview_footer = Some(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
stdout().flush()?;
|
stdout().flush()?;
|
||||||
debug!("Running application...");
|
debug!("Running application...");
|
||||||
let output = app.run(stdout().is_terminal(), false).await?;
|
let output = app.run(stdout().is_terminal(), false).await?;
|
||||||
@ -128,9 +154,6 @@ fn apply_cli_overrides(args: &PostProcessedCli, config: &mut Config) {
|
|||||||
config.keybindings =
|
config.keybindings =
|
||||||
merge_keybindings(config.keybindings.clone(), keybindings);
|
merge_keybindings(config.keybindings.clone(), keybindings);
|
||||||
}
|
}
|
||||||
if let Some(header) = &args.custom_header {
|
|
||||||
config.ui.custom_header = Some(header.to_string());
|
|
||||||
}
|
|
||||||
config.ui.ui_scale = args.ui_scale;
|
config.ui.ui_scale = args.ui_scale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,6 +101,7 @@ pub struct Preview {
|
|||||||
pub content: String,
|
pub content: String,
|
||||||
pub icon: Option<FileIcon>,
|
pub icon: Option<FileIcon>,
|
||||||
pub total_lines: u16,
|
pub total_lines: u16,
|
||||||
|
pub footer: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_PREVIEW_TITLE: &str = "Select an entry to preview";
|
const DEFAULT_PREVIEW_TITLE: &str = "Select an entry to preview";
|
||||||
@ -112,6 +113,7 @@ impl Default for Preview {
|
|||||||
content: String::new(),
|
content: String::new(),
|
||||||
icon: None,
|
icon: None,
|
||||||
total_lines: 1,
|
total_lines: 1,
|
||||||
|
footer: String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -122,12 +124,14 @@ impl Preview {
|
|||||||
content: String,
|
content: String,
|
||||||
icon: Option<FileIcon>,
|
icon: Option<FileIcon>,
|
||||||
total_lines: u16,
|
total_lines: u16,
|
||||||
|
footer: String,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
title: title.to_string(),
|
title: title.to_string(),
|
||||||
content,
|
content,
|
||||||
icon,
|
icon,
|
||||||
total_lines,
|
total_lines,
|
||||||
|
footer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -243,6 +247,7 @@ pub fn try_preview(
|
|||||||
content.to_string(),
|
content.to_string(),
|
||||||
None,
|
None,
|
||||||
u16::try_from(content.lines().count()).unwrap_or(u16::MAX),
|
u16::try_from(content.lines().count()).unwrap_or(u16::MAX),
|
||||||
|
String::new(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
let (content, _) = replace_non_printable(
|
let (content, _) = replace_non_printable(
|
||||||
@ -256,6 +261,7 @@ pub fn try_preview(
|
|||||||
content.to_string(),
|
content.to_string(),
|
||||||
None,
|
None,
|
||||||
u16::try_from(content.lines().count()).unwrap_or(u16::MAX),
|
u16::try_from(content.lines().count()).unwrap_or(u16::MAX),
|
||||||
|
String::new(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -51,7 +51,11 @@ impl PreviewState {
|
|||||||
scroll: u16,
|
scroll: u16,
|
||||||
target_line: Option<u16>,
|
target_line: Option<u16>,
|
||||||
) {
|
) {
|
||||||
if self.preview.title != preview.title || self.scroll != scroll {
|
if self.preview.title != preview.title
|
||||||
|
|| self.preview.content != preview.content
|
||||||
|
|| self.preview.footer != preview.footer
|
||||||
|
|| self.scroll != scroll
|
||||||
|
{
|
||||||
self.preview = preview;
|
self.preview = preview;
|
||||||
self.scroll = scroll;
|
self.scroll = scroll;
|
||||||
self.target_line = target_line;
|
self.target_line = target_line;
|
||||||
@ -91,6 +95,7 @@ impl PreviewState {
|
|||||||
cropped_content,
|
cropped_content,
|
||||||
self.preview.icon,
|
self.preview.icon,
|
||||||
self.preview.total_lines,
|
self.preview.total_lines,
|
||||||
|
self.preview.footer.clone(),
|
||||||
),
|
),
|
||||||
num_skipped_lines,
|
num_skipped_lines,
|
||||||
target_line,
|
target_line,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use crate::channels::prototypes::Template;
|
||||||
use crate::utils::input::Input;
|
use crate::utils::input::Input;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
@ -28,10 +29,13 @@ pub fn draw_input_box(
|
|||||||
channel_name: &str,
|
channel_name: &str,
|
||||||
spinner: &Spinner,
|
spinner: &Spinner,
|
||||||
colorscheme: &Colorscheme,
|
colorscheme: &Colorscheme,
|
||||||
custom_header: &Option<String>,
|
input_header: &Option<Template>,
|
||||||
input_bar_position: &InputPosition,
|
input_bar_position: &InputPosition,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let header = custom_header.as_deref().unwrap_or(channel_name);
|
let header = input_header
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|tpl| tpl.format(channel_name).ok())
|
||||||
|
.unwrap_or_else(|| channel_name.to_string());
|
||||||
let input_block = Block::default()
|
let input_block = Block::default()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_type(BorderType::Rounded)
|
.border_type(BorderType::Rounded)
|
||||||
@ -41,7 +45,7 @@ pub fn draw_input_box(
|
|||||||
InputPosition::Bottom => Position::Bottom,
|
InputPosition::Bottom => Position::Bottom,
|
||||||
})
|
})
|
||||||
.title(
|
.title(
|
||||||
Line::from(String::from(" ") + header + " ")
|
Line::from(format!(" {} ", header))
|
||||||
.style(Style::default().fg(colorscheme.mode.channel).bold())
|
.style(Style::default().fg(colorscheme.mode.channel).bold())
|
||||||
.centered(),
|
.centered(),
|
||||||
)
|
)
|
||||||
|
@ -29,6 +29,7 @@ pub fn draw_preview_content_block(
|
|||||||
colorscheme,
|
colorscheme,
|
||||||
preview_state.preview.icon,
|
preview_state.preview.icon,
|
||||||
&preview_state.preview.title,
|
&preview_state.preview.title,
|
||||||
|
&preview_state.preview.footer,
|
||||||
use_nerd_font_icons,
|
use_nerd_font_icons,
|
||||||
)?;
|
)?;
|
||||||
// render the preview content
|
// render the preview content
|
||||||
@ -138,6 +139,7 @@ fn draw_content_outer_block(
|
|||||||
colorscheme: &Colorscheme,
|
colorscheme: &Colorscheme,
|
||||||
icon: Option<FileIcon>,
|
icon: Option<FileIcon>,
|
||||||
title: &str,
|
title: &str,
|
||||||
|
footer: &str,
|
||||||
use_nerd_font_icons: bool,
|
use_nerd_font_icons: bool,
|
||||||
) -> Result<Rect> {
|
) -> Result<Rect> {
|
||||||
let mut preview_title_spans = vec![Span::from(" ")];
|
let mut preview_title_spans = vec![Span::from(" ")];
|
||||||
@ -153,7 +155,7 @@ fn draw_content_outer_block(
|
|||||||
Style::default().fg(Color::from_str(icon.color)?),
|
Style::default().fg(Color::from_str(icon.color)?),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
// preview title
|
// preview header
|
||||||
preview_title_spans.push(Span::styled(
|
preview_title_spans.push(Span::styled(
|
||||||
shrink_with_ellipsis(
|
shrink_with_ellipsis(
|
||||||
&replace_non_printable(
|
&replace_non_printable(
|
||||||
@ -167,13 +169,26 @@ fn draw_content_outer_block(
|
|||||||
));
|
));
|
||||||
preview_title_spans.push(Span::from(" "));
|
preview_title_spans.push(Span::from(" "));
|
||||||
|
|
||||||
// build the preview block
|
let mut block = Block::default();
|
||||||
let preview_outer_block = Block::default()
|
block = block.title_top(
|
||||||
.title_top(
|
Line::from(preview_title_spans)
|
||||||
Line::from(preview_title_spans)
|
.alignment(Alignment::Center)
|
||||||
.alignment(Alignment::Center)
|
.style(Style::default().fg(colorscheme.preview.title_fg)),
|
||||||
.style(Style::default().fg(colorscheme.preview.title_fg)),
|
);
|
||||||
)
|
|
||||||
|
// preview footer
|
||||||
|
if !footer.is_empty() {
|
||||||
|
let footer_line = Line::from(vec![
|
||||||
|
Span::from(" "),
|
||||||
|
Span::from(footer),
|
||||||
|
Span::from(" "),
|
||||||
|
])
|
||||||
|
.alignment(Alignment::Center)
|
||||||
|
.style(Style::default().fg(colorscheme.preview.title_fg));
|
||||||
|
block = block.title_bottom(footer_line);
|
||||||
|
}
|
||||||
|
|
||||||
|
let preview_outer_block = block
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_type(BorderType::Rounded)
|
.border_type(BorderType::Rounded)
|
||||||
.border_style(Style::default().fg(colorscheme.general.border_fg))
|
.border_style(Style::default().fg(colorscheme.general.border_fg))
|
||||||
|
@ -184,6 +184,21 @@ impl Television {
|
|||||||
// ui
|
// ui
|
||||||
if let Some(ui_spec) = &channel_prototype.ui {
|
if let Some(ui_spec) = &channel_prototype.ui {
|
||||||
config.apply_prototype_ui_spec(ui_spec);
|
config.apply_prototype_ui_spec(ui_spec);
|
||||||
|
if config.ui.input_header.is_none() {
|
||||||
|
if let Some(header_tpl) = &ui_spec.input_header {
|
||||||
|
config.ui.input_header = Some(header_tpl.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if config.ui.preview_header.is_none() {
|
||||||
|
if let Some(ph) = &ui_spec.preview_header {
|
||||||
|
config.ui.preview_header = Some(ph.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if config.ui.preview_footer.is_none() {
|
||||||
|
if let Some(pf) = &ui_spec.preview_footer {
|
||||||
|
config.ui.preview_footer = Some(pf.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
@ -491,7 +506,21 @@ impl Television {
|
|||||||
}
|
}
|
||||||
// available previews
|
// available previews
|
||||||
let entry = selected_entry.as_ref().unwrap();
|
let entry = selected_entry.as_ref().unwrap();
|
||||||
if let Ok(preview) = receiver.try_recv() {
|
if let Ok(mut preview) = receiver.try_recv() {
|
||||||
|
if let Some(tpl) = &self.config.ui.preview_header {
|
||||||
|
preview.title = tpl
|
||||||
|
.format(&entry.raw)
|
||||||
|
.unwrap_or_else(|_| entry.raw.clone());
|
||||||
|
} else {
|
||||||
|
preview.title.clone_from(&entry.raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ftpl) = &self.config.ui.preview_footer {
|
||||||
|
preview.footer = ftpl
|
||||||
|
.format(&entry.raw)
|
||||||
|
.unwrap_or_else(|_| String::new());
|
||||||
|
}
|
||||||
|
|
||||||
let scroll = entry
|
let scroll = entry
|
||||||
.line_number
|
.line_number
|
||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
|
28
tests/e2e.rs
28
tests/e2e.rs
@ -211,6 +211,34 @@ fn tv_remote_control() {
|
|||||||
assert_exit_ok(&mut child, DEFAULT_TIMEOUT);
|
assert_exit_ok(&mut child, DEFAULT_TIMEOUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tv_custom_input_header_and_preview_size() {
|
||||||
|
let pty_system = native_pty_system();
|
||||||
|
// Create a new pty
|
||||||
|
let pair = pty_system.openpty(DEFAULT_PTY_SIZE).unwrap();
|
||||||
|
|
||||||
|
// Spawn a tv process in the pty
|
||||||
|
let mut cmd = tv_command("files");
|
||||||
|
cmd.args(["--input-header", "bagels"]);
|
||||||
|
let mut child = pair.slave.spawn_command(cmd).unwrap();
|
||||||
|
sleep(Duration::from_millis(200));
|
||||||
|
|
||||||
|
// Read the output from the pty
|
||||||
|
let mut buf = [0; 5096];
|
||||||
|
let mut reader = pair.master.try_clone_reader().unwrap();
|
||||||
|
let _ = reader.read(&mut buf).unwrap();
|
||||||
|
let output = String::from_utf8_lossy(&buf);
|
||||||
|
|
||||||
|
assert!(output.contains("bagels"));
|
||||||
|
|
||||||
|
// Send Ctrl-C to the process
|
||||||
|
let mut writer = pair.master.take_writer().unwrap();
|
||||||
|
writeln!(writer, "\x03").unwrap(); // Ctrl-C
|
||||||
|
sleep(Duration::from_millis(200));
|
||||||
|
|
||||||
|
assert_exit_ok(&mut child, DEFAULT_TIMEOUT);
|
||||||
|
}
|
||||||
|
|
||||||
macro_rules! test_channel {
|
macro_rules! test_channel {
|
||||||
($($name:ident: $channel_name:expr,)*) => {
|
($($name:ident: $channel_name:expr,)*) => {
|
||||||
$(
|
$(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user