mirror of
https://github.com/alexpasmantier/television.git
synced 2025-07-28 13:51:41 +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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "devicons"
|
||||
version = "0.6.12"
|
||||
@ -591,6 +601,12 @@ version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
version = "1.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
@ -774,6 +790,12 @@ dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.2"
|
||||
@ -797,6 +819,12 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.3.1"
|
||||
@ -860,6 +888,17 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "indexmap"
|
||||
version = "2.9.0"
|
||||
@ -867,7 +906,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
"hashbrown 0.15.2",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1019,7 +1059,7 @@ version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
"hashbrown 0.15.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1117,6 +1157,12 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@ -1311,6 +1357,12 @@ dependencies = [
|
||||
"winreg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.94"
|
||||
@ -1397,6 +1449,26 @@ dependencies = [
|
||||
"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]]
|
||||
name = "regex"
|
||||
version = "1.11.1"
|
||||
@ -1570,6 +1642,18 @@ dependencies = [
|
||||
"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]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
@ -1617,6 +1701,37 @@ dependencies = [
|
||||
"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]]
|
||||
name = "serial2"
|
||||
version = "0.2.29"
|
||||
@ -1824,6 +1939,7 @@ dependencies = [
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"signal-hook",
|
||||
"string_pipeline",
|
||||
"tempfile",
|
||||
@ -1900,6 +2016,37 @@ dependencies = [
|
||||
"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]]
|
||||
name = "tinytemplate"
|
||||
version = "1.2.1"
|
||||
@ -1966,7 +2113,7 @@ version = "0.22.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"indexmap 2.9.0",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
|
@ -58,6 +58,7 @@ string_pipeline = "0.11.1"
|
||||
ureq = "3.0.11"
|
||||
serde_json = "1.0.140"
|
||||
colored = "3.0.0"
|
||||
serde_with = "3.13.0"
|
||||
|
||||
|
||||
# target specific dependencies
|
||||
|
@ -12,3 +12,4 @@ command = "echo '{split:=:1..}'"
|
||||
[ui]
|
||||
layout = "portrait"
|
||||
preview_size = 20
|
||||
preview_header = "{split:=:0}"
|
||||
|
@ -1,12 +1,14 @@
|
||||
use anyhow::Result;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
use crate::{
|
||||
config::KeyBindings,
|
||||
screen::layout::{InputPosition, Orientation},
|
||||
};
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::ser::SerializeSeq;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::{OneOrMany, serde_as};
|
||||
use string_pipeline::MultiTemplate;
|
||||
|
||||
#[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)]
|
||||
pub struct CommandSpec {
|
||||
#[serde(
|
||||
rename = "command",
|
||||
deserialize_with = "deserialize_commands",
|
||||
serialize_with = "serialize_commands"
|
||||
)]
|
||||
#[serde(rename = "command")]
|
||||
#[serde_as(as = "OneOrMany<_>")]
|
||||
pub inner: Vec<Template>,
|
||||
#[serde(default)]
|
||||
pub interactive: bool,
|
||||
@ -67,13 +105,6 @@ pub struct CommandSpec {
|
||||
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 {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
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)]
|
||||
pub struct ChannelPrototype {
|
||||
pub metadata: Metadata,
|
||||
@ -342,17 +237,9 @@ pub struct Metadata {
|
||||
pub struct SourceSpec {
|
||||
#[serde(flatten)]
|
||||
pub command: CommandSpec,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "deserialize_maybe_command",
|
||||
serialize_with = "serialize_maybe_command"
|
||||
)]
|
||||
#[serde(default)]
|
||||
pub display: Option<Template>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "deserialize_maybe_command",
|
||||
serialize_with = "serialize_maybe_command"
|
||||
)]
|
||||
#[serde(default)]
|
||||
pub output: Option<Template>,
|
||||
}
|
||||
|
||||
@ -360,11 +247,7 @@ pub struct SourceSpec {
|
||||
pub struct PreviewSpec {
|
||||
#[serde(flatten)]
|
||||
pub command: CommandSpec,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "deserialize_maybe_command",
|
||||
serialize_with = "serialize_maybe_command"
|
||||
)]
|
||||
#[serde(default)]
|
||||
pub offset: Option<Template>,
|
||||
}
|
||||
|
||||
@ -397,12 +280,18 @@ pub struct UiSpec {
|
||||
#[serde(default)]
|
||||
pub show_preview_panel: Option<bool>,
|
||||
// `layout` is clearer for the user but collides with the overall `Layout` type.
|
||||
#[serde(rename = "layout", default)]
|
||||
#[serde(rename = "layout", alias = "orientation", default)]
|
||||
pub orientation: Option<Orientation>,
|
||||
#[serde(default)]
|
||||
pub input_bar_position: Option<InputPosition>,
|
||||
#[serde(default)]
|
||||
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";
|
||||
@ -432,6 +321,41 @@ mod tests {
|
||||
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]
|
||||
fn test_channel_prototype_deserialization() {
|
||||
let toml_data = r#"
|
||||
@ -445,13 +369,14 @@ mod tests {
|
||||
interactive = false
|
||||
env = {}
|
||||
display = "{split:/:-1}" # only show the last path segment ('/a/b/c' -> 'c')
|
||||
ansi = false
|
||||
output = "{}" # output the full path
|
||||
unknown_field = "ignored" # should be ignored
|
||||
|
||||
[preview]
|
||||
command = "bat -n --color=always {}"
|
||||
env = { "BAT_THEME" = "ansi" }
|
||||
interactive = false
|
||||
offset = "3" # why not
|
||||
|
||||
[ui]
|
||||
layout = "landscape"
|
||||
@ -459,6 +384,10 @@ mod tests {
|
||||
show_help_bar = false
|
||||
show_preview_panel = true
|
||||
input_bar_position = "bottom"
|
||||
preview_size = 66
|
||||
input_header = "Input: {}"
|
||||
preview_header = "Preview: {}"
|
||||
preview_footer = "Press 'q' to quit"
|
||||
|
||||
[keybindings]
|
||||
quit = ["esc", "ctrl-c"]
|
||||
@ -478,19 +407,37 @@ mod tests {
|
||||
format!("{}", prototype.source.command.inner[0]),
|
||||
"fd -t f"
|
||||
);
|
||||
|
||||
assert!(!prototype.source.command.interactive);
|
||||
assert_eq!(prototype.source.display.unwrap().raw(), "{split:/:-1}");
|
||||
assert_eq!(prototype.source.output.unwrap().raw(), "{}");
|
||||
|
||||
let preview = prototype.preview.as_ref().unwrap();
|
||||
assert_eq!(
|
||||
format!("{}", prototype.preview.unwrap().command.inner[0]),
|
||||
format!("{}", preview.command.inner[0]),
|
||||
"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();
|
||||
assert_eq!(ui.orientation, Some(Orientation::Landscape));
|
||||
assert_eq!(ui.ui_scale, Some(100));
|
||||
assert!(!(ui.show_help_bar.unwrap()));
|
||||
assert!(ui.show_preview_panel.unwrap());
|
||||
assert_eq!(ui.input_bar_position, Some(InputPosition::Bottom));
|
||||
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();
|
||||
assert_eq!(
|
||||
keybindings.0.get(&Action::Quit),
|
||||
@ -599,6 +546,7 @@ mod tests {
|
||||
[ui]
|
||||
layout = "landscape"
|
||||
ui_scale = 40
|
||||
preview_footer = "Press 'q' to quit"
|
||||
"#;
|
||||
|
||||
let prototype: ChannelPrototype = from_str(toml_data).unwrap();
|
||||
@ -623,5 +571,12 @@ mod tests {
|
||||
assert!(ui.show_help_bar.is_none());
|
||||
assert!(ui.show_preview_panel.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)]
|
||||
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
|
||||
/// working directory.
|
||||
/// The default value for the input header is the current channel.
|
||||
#[arg(long, value_name = "STRING", verbatim_doc_comment)]
|
||||
pub custom_header: Option<String>,
|
||||
/// The given value is parsed as a `MultiTemplate`. It is evaluated against
|
||||
/// the current channel name and the resulting text is shown as the input
|
||||
/// field title. Defaults to the current channel name when omitted.
|
||||
#[arg(long = "input-header", value_name = "STRING", verbatim_doc_comment)]
|
||||
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.
|
||||
///
|
||||
|
@ -28,7 +28,9 @@ pub struct PostProcessedCli {
|
||||
pub tick_rate: Option<f64>,
|
||||
pub frame_rate: Option<f64>,
|
||||
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 working_directory: Option<String>,
|
||||
pub autocomplete_prompt: Option<String>,
|
||||
@ -52,7 +54,9 @@ impl Default for PostProcessedCli {
|
||||
tick_rate: None,
|
||||
frame_rate: None,
|
||||
input: None,
|
||||
custom_header: None,
|
||||
input_header: None,
|
||||
preview_header: None,
|
||||
preview_footer: None,
|
||||
command: None,
|
||||
working_directory: None,
|
||||
autocomplete_prompt: None,
|
||||
@ -115,7 +119,9 @@ pub fn post_process(cli: Cli) -> PostProcessedCli {
|
||||
tick_rate: cli.tick_rate,
|
||||
frame_rate: cli.frame_rate,
|
||||
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,
|
||||
working_directory,
|
||||
autocomplete_prompt: cli.autocomplete_prompt,
|
||||
|
@ -242,6 +242,17 @@ impl Config {
|
||||
if let Some(preview_size) = &ui_spec.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 crate::screen::layout::{
|
||||
@ -23,9 +24,13 @@ pub struct UiConfig {
|
||||
pub orientation: Orientation,
|
||||
pub preview_title_position: Option<PreviewTitlePosition>,
|
||||
pub theme: String,
|
||||
pub custom_header: Option<String>,
|
||||
|
||||
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 {
|
||||
@ -40,8 +45,10 @@ impl Default for UiConfig {
|
||||
orientation: Orientation::Landscape,
|
||||
preview_title_position: None,
|
||||
theme: String::from(DEFAULT_THEME),
|
||||
custom_header: None,
|
||||
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.spinner,
|
||||
&ctx.colorscheme,
|
||||
&ctx.config.ui.custom_header,
|
||||
&ctx.config.ui.input_header,
|
||||
&ctx.config.ui.input_bar_position,
|
||||
)?;
|
||||
|
||||
|
@ -10,7 +10,7 @@ use television::cli::post_process;
|
||||
use television::gh::update_local_channels;
|
||||
use television::{
|
||||
cable::Cable, channels::prototypes::ChannelPrototype,
|
||||
utils::clipboard::CLIPBOARD,
|
||||
channels::prototypes::Template, utils::clipboard::CLIPBOARD,
|
||||
};
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
@ -81,8 +81,34 @@ async fn main() -> Result<()> {
|
||||
args.preview_size,
|
||||
config.application.tick_rate,
|
||||
);
|
||||
let mut app =
|
||||
App::new(channel_prototype, config, args.input, options, cable);
|
||||
let mut app = App::new(
|
||||
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()?;
|
||||
debug!("Running application...");
|
||||
let output = app.run(stdout().is_terminal(), false).await?;
|
||||
@ -128,9 +154,6 @@ fn apply_cli_overrides(args: &PostProcessedCli, config: &mut Config) {
|
||||
config.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;
|
||||
}
|
||||
|
||||
|
@ -101,6 +101,7 @@ pub struct Preview {
|
||||
pub content: String,
|
||||
pub icon: Option<FileIcon>,
|
||||
pub total_lines: u16,
|
||||
pub footer: String,
|
||||
}
|
||||
|
||||
const DEFAULT_PREVIEW_TITLE: &str = "Select an entry to preview";
|
||||
@ -112,6 +113,7 @@ impl Default for Preview {
|
||||
content: String::new(),
|
||||
icon: None,
|
||||
total_lines: 1,
|
||||
footer: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -122,12 +124,14 @@ impl Preview {
|
||||
content: String,
|
||||
icon: Option<FileIcon>,
|
||||
total_lines: u16,
|
||||
footer: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
title: title.to_string(),
|
||||
content,
|
||||
icon,
|
||||
total_lines,
|
||||
footer,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -243,6 +247,7 @@ pub fn try_preview(
|
||||
content.to_string(),
|
||||
None,
|
||||
u16::try_from(content.lines().count()).unwrap_or(u16::MAX),
|
||||
String::new(),
|
||||
)
|
||||
} else {
|
||||
let (content, _) = replace_non_printable(
|
||||
@ -256,6 +261,7 @@ pub fn try_preview(
|
||||
content.to_string(),
|
||||
None,
|
||||
u16::try_from(content.lines().count()).unwrap_or(u16::MAX),
|
||||
String::new(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
@ -51,7 +51,11 @@ impl PreviewState {
|
||||
scroll: 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.scroll = scroll;
|
||||
self.target_line = target_line;
|
||||
@ -91,6 +95,7 @@ impl PreviewState {
|
||||
cropped_content,
|
||||
self.preview.icon,
|
||||
self.preview.total_lines,
|
||||
self.preview.footer.clone(),
|
||||
),
|
||||
num_skipped_lines,
|
||||
target_line,
|
||||
|
@ -1,3 +1,4 @@
|
||||
use crate::channels::prototypes::Template;
|
||||
use crate::utils::input::Input;
|
||||
use anyhow::Result;
|
||||
use ratatui::{
|
||||
@ -28,10 +29,13 @@ pub fn draw_input_box(
|
||||
channel_name: &str,
|
||||
spinner: &Spinner,
|
||||
colorscheme: &Colorscheme,
|
||||
custom_header: &Option<String>,
|
||||
input_header: &Option<Template>,
|
||||
input_bar_position: &InputPosition,
|
||||
) -> 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()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
@ -41,7 +45,7 @@ pub fn draw_input_box(
|
||||
InputPosition::Bottom => Position::Bottom,
|
||||
})
|
||||
.title(
|
||||
Line::from(String::from(" ") + header + " ")
|
||||
Line::from(format!(" {} ", header))
|
||||
.style(Style::default().fg(colorscheme.mode.channel).bold())
|
||||
.centered(),
|
||||
)
|
||||
|
@ -29,6 +29,7 @@ pub fn draw_preview_content_block(
|
||||
colorscheme,
|
||||
preview_state.preview.icon,
|
||||
&preview_state.preview.title,
|
||||
&preview_state.preview.footer,
|
||||
use_nerd_font_icons,
|
||||
)?;
|
||||
// render the preview content
|
||||
@ -138,6 +139,7 @@ fn draw_content_outer_block(
|
||||
colorscheme: &Colorscheme,
|
||||
icon: Option<FileIcon>,
|
||||
title: &str,
|
||||
footer: &str,
|
||||
use_nerd_font_icons: bool,
|
||||
) -> Result<Rect> {
|
||||
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)?),
|
||||
));
|
||||
}
|
||||
// preview title
|
||||
// preview header
|
||||
preview_title_spans.push(Span::styled(
|
||||
shrink_with_ellipsis(
|
||||
&replace_non_printable(
|
||||
@ -167,13 +169,26 @@ fn draw_content_outer_block(
|
||||
));
|
||||
preview_title_spans.push(Span::from(" "));
|
||||
|
||||
// build the preview block
|
||||
let preview_outer_block = Block::default()
|
||||
.title_top(
|
||||
Line::from(preview_title_spans)
|
||||
.alignment(Alignment::Center)
|
||||
.style(Style::default().fg(colorscheme.preview.title_fg)),
|
||||
)
|
||||
let mut block = Block::default();
|
||||
block = block.title_top(
|
||||
Line::from(preview_title_spans)
|
||||
.alignment(Alignment::Center)
|
||||
.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)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(Style::default().fg(colorscheme.general.border_fg))
|
||||
|
@ -184,6 +184,21 @@ impl Television {
|
||||
// ui
|
||||
if let Some(ui_spec) = &channel_prototype.ui {
|
||||
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
|
||||
}
|
||||
@ -491,7 +506,21 @@ impl Television {
|
||||
}
|
||||
// available previews
|
||||
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
|
||||
.line_number
|
||||
.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);
|
||||
}
|
||||
|
||||
#[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 {
|
||||
($($name:ident: $channel_name:expr,)*) => {
|
||||
$(
|
||||
|
Loading…
x
Reference in New Issue
Block a user