mirror of
https://github.com/alexpasmantier/television.git
synced 2025-07-29 06:11:37 +00:00
feat(config): border styles and padding are now configurable (#642)
## 📺 PR Description this allows changing (or removing) the border styles tv 0.12.3: <img width="773" height="407" alt="image" src="https://github.com/user-attachments/assets/8b74fc2f-7abc-4bb8-b645-c6243f40230c" /> this PR with ```toml [ui.input_bar] border_type = 'none' [ui.results_panel] border_type = 'none' [ui.preview_panel] border_type = 'plain' ``` <img width="778" height="407" alt="image" src="https://github.com/user-attachments/assets/481fd0fa-a90a-4be8-bbe8-6e0ee5b002e3" /> see #638 ## Checklist - [ ] my commits **and PR title** follow the [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) format - [x] if this is a new feature, I have added tests to consolidate the feature and prevent regressions - [ ] if this is a bug fix, I have added a test that reproduces the bug (if applicable) - [x] I have added a reasonable amount of documentation to the code where appropriate --------- Co-authored-by: alexandre pasmantier <alex.pasmant@gmail.com>
This commit is contained in:
parent
39610e145d
commit
a0e3063702
@ -54,10 +54,6 @@ use_nerd_font_icons = false
|
||||
# └───────────────────────────────────────┘
|
||||
ui_scale = 100
|
||||
|
||||
# Where to place the input bar in the UI (top or bottom)
|
||||
input_bar_position = "top"
|
||||
# The input prompt string (defaults to ">" if not specified)
|
||||
input_prompt = ">"
|
||||
# What orientation should tv be (landscape or portrait)
|
||||
orientation = "landscape"
|
||||
# The theme to use for the UI
|
||||
@ -100,6 +96,14 @@ remote_control = { enabled = true, visible = false }
|
||||
|
||||
# Feature-specific configurations
|
||||
# Each feature can have its own configuration section
|
||||
[ui.input_bar]
|
||||
# Where to place the input bar in the UI (top or bottom)
|
||||
position = "top"
|
||||
# The input prompt string (defaults to ">" if not specified)
|
||||
prompt = ">"
|
||||
# header = "{}"
|
||||
border_type = "rounded" # https://docs.rs/ratatui/latest/ratatui/widgets/block/enum.BorderType.html#variants
|
||||
|
||||
[ui.status_bar]
|
||||
# Status bar separators (bubble):
|
||||
#separator_open = ""
|
||||
@ -108,12 +112,16 @@ remote_control = { enabled = true, visible = false }
|
||||
separator_open = ""
|
||||
separator_close = ""
|
||||
|
||||
[ui.results_panel]
|
||||
border_type = "rounded"
|
||||
|
||||
[ui.preview_panel]
|
||||
# Preview panel size (percentage of screen width/height)
|
||||
size = 50
|
||||
#header = "{}"
|
||||
#footer = ""
|
||||
scrollbar = true
|
||||
border_type = "rounded"
|
||||
|
||||
[ui.remote_control]
|
||||
# Whether to show channel descriptions in remote control mode
|
||||
|
@ -1,9 +1,10 @@
|
||||
use crate::cli::parse_source_entry_delimiter;
|
||||
use crate::config::ui::InputBarConfig;
|
||||
use crate::{
|
||||
config::{KeyBindings, ui},
|
||||
event::Key,
|
||||
features::Features,
|
||||
screen::layout::{InputPosition, Orientation},
|
||||
screen::layout::Orientation,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use rustc_hash::FxHashMap;
|
||||
@ -387,16 +388,14 @@ pub struct UiSpec {
|
||||
// `layout` is clearer for the user but collides with the overall `Layout` type.
|
||||
#[serde(rename = "layout", alias = "orientation", default)]
|
||||
pub orientation: Option<Orientation>,
|
||||
#[serde(default)]
|
||||
pub input_bar_position: Option<InputPosition>,
|
||||
#[serde(default)]
|
||||
pub input_header: Option<Template>,
|
||||
#[serde(default)]
|
||||
pub input_prompt: Option<String>,
|
||||
// Feature-specific configurations
|
||||
#[serde(default)]
|
||||
pub input_bar: Option<InputBarConfig>,
|
||||
#[serde(default)]
|
||||
pub preview_panel: Option<ui::PreviewPanelConfig>,
|
||||
#[serde(default)]
|
||||
pub results_panel: Option<ui::ResultsPanelConfig>,
|
||||
#[serde(default)]
|
||||
pub status_bar: Option<ui::StatusBarConfig>,
|
||||
#[serde(default)]
|
||||
pub help_panel: Option<ui::HelpPanelConfig>,
|
||||
@ -412,10 +411,9 @@ impl From<&crate::config::UiConfig> for UiSpec {
|
||||
ui_scale: Some(config.ui_scale),
|
||||
features: Some(config.features.clone()),
|
||||
orientation: Some(config.orientation),
|
||||
input_bar_position: Some(config.input_bar_position),
|
||||
input_header: config.input_header.clone(),
|
||||
input_prompt: Some(config.input_prompt.clone()),
|
||||
input_bar: Some(config.input_bar.clone()),
|
||||
preview_panel: Some(config.preview_panel.clone()),
|
||||
results_panel: Some(config.results_panel.clone()),
|
||||
status_bar: Some(config.status_bar.clone()),
|
||||
help_panel: Some(config.help_panel.clone()),
|
||||
remote_control: Some(config.remote_control.clone()),
|
||||
@ -425,7 +423,10 @@ impl From<&crate::config::UiConfig> for UiSpec {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{action::Action, event::Key};
|
||||
use crate::{
|
||||
action::Action, config::ui::BorderType, event::Key,
|
||||
screen::layout::InputPosition,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use toml::from_str;
|
||||
@ -508,16 +509,23 @@ mod tests {
|
||||
[ui]
|
||||
layout = "landscape"
|
||||
ui_scale = 100
|
||||
input_bar_position = "bottom"
|
||||
input_header = "Input: {}"
|
||||
|
||||
[ui.features]
|
||||
preview_panel = { enabled = true, visible = true }
|
||||
|
||||
[ui.input_bar]
|
||||
position = "bottom"
|
||||
header = "Input: {}"
|
||||
border_type = "plain"
|
||||
|
||||
[ui.preview_panel]
|
||||
size = 66
|
||||
header = "Preview: {}"
|
||||
footer = "Press 'q' to quit"
|
||||
border_type = "thick"
|
||||
|
||||
[ui.results_panel]
|
||||
border_type = "none"
|
||||
|
||||
[keybindings]
|
||||
esc = "quit"
|
||||
@ -565,29 +573,23 @@ mod tests {
|
||||
assert!(ui.features.is_some());
|
||||
let features = ui.features.as_ref().unwrap();
|
||||
assert!(features.preview_panel.enabled);
|
||||
assert_eq!(ui.input_bar_position, Some(InputPosition::Bottom));
|
||||
assert_eq!(ui.preview_panel.as_ref().unwrap().size, 66);
|
||||
assert_eq!(ui.input_header.as_ref().unwrap().raw(), "Input: {}");
|
||||
let input_bar = ui.input_bar.as_ref().unwrap();
|
||||
assert_eq!(input_bar.position, InputPosition::Bottom);
|
||||
assert_eq!(input_bar.header.as_ref().unwrap().raw(), "Input: {}");
|
||||
assert_eq!(input_bar.border_type, BorderType::Plain);
|
||||
let preview_panel = ui.preview_panel.as_ref().unwrap();
|
||||
assert_eq!(
|
||||
ui.preview_panel
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.header
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.raw(),
|
||||
preview_panel.header.as_ref().unwrap().raw(),
|
||||
"Preview: {}"
|
||||
);
|
||||
assert_eq!(
|
||||
ui.preview_panel
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.footer
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.raw(),
|
||||
preview_panel.footer.as_ref().unwrap().raw(),
|
||||
"Press 'q' to quit"
|
||||
);
|
||||
assert_eq!(preview_panel.border_type, BorderType::Thick);
|
||||
|
||||
assert_eq!(ui.results_panel.unwrap().border_type, BorderType::None);
|
||||
|
||||
let keybindings = prototype.keybindings.unwrap();
|
||||
assert_eq!(
|
||||
@ -710,6 +712,9 @@ mod tests {
|
||||
layout = "landscape"
|
||||
ui_scale = 40
|
||||
|
||||
[ui.input_bar]
|
||||
border_type = "none"
|
||||
|
||||
[ui.preview_panel]
|
||||
footer = "Press 'q' to quit"
|
||||
"#;
|
||||
@ -734,9 +739,11 @@ mod tests {
|
||||
assert_eq!(ui.orientation, Some(Orientation::Landscape));
|
||||
assert_eq!(ui.ui_scale, Some(40));
|
||||
assert!(ui.features.is_none());
|
||||
assert!(ui.input_bar_position.is_none());
|
||||
assert_eq!(
|
||||
ui.input_bar.as_ref().unwrap().border_type,
|
||||
BorderType::None
|
||||
);
|
||||
assert!(ui.preview_panel.is_some());
|
||||
assert!(ui.input_header.is_none());
|
||||
assert_eq!(
|
||||
ui.preview_panel
|
||||
.as_ref()
|
||||
|
@ -33,7 +33,7 @@ pub struct Cli {
|
||||
/// A list of the available channels can be displayed using the
|
||||
/// `list-channels` command. The channel can also be changed from within
|
||||
/// the application.
|
||||
#[arg(value_enum, index = 1, verbatim_doc_comment)]
|
||||
#[arg(index = 1, verbatim_doc_comment)]
|
||||
pub channel: Option<String>,
|
||||
|
||||
/// A preview line number offset template to use to scroll the preview to for each
|
||||
@ -151,6 +151,20 @@ pub struct Cli {
|
||||
#[arg(long, value_name = "STRING", verbatim_doc_comment)]
|
||||
pub input_prompt: Option<String>,
|
||||
|
||||
/// Sets the input panel border type.
|
||||
///
|
||||
/// Available options are: `none`, `plain`, `rounded`, `thick`.
|
||||
#[arg(long, value_enum, verbatim_doc_comment)]
|
||||
pub input_border: Option<BorderType>,
|
||||
|
||||
/// Sets the input panel padding.
|
||||
///
|
||||
/// Format: `top=INTEGER;left=INTEGER;bottom=INTEGER;right=INTEGER`
|
||||
///
|
||||
/// Example: `--input-padding='top=1;left=2;bottom=1;right=2'`
|
||||
#[arg(long, value_name = "STRING", verbatim_doc_comment)]
|
||||
pub input_padding: Option<String>,
|
||||
|
||||
/// Preview header template
|
||||
///
|
||||
/// When a channel is specified: This overrides the header defined in the channel prototype.
|
||||
@ -181,6 +195,30 @@ pub struct Cli {
|
||||
)]
|
||||
pub preview_footer: Option<String>,
|
||||
|
||||
/// Sets the preview panel border type.
|
||||
///
|
||||
/// Available options are: `none`, `plain`, `rounded`, `thick`.
|
||||
#[arg(
|
||||
long,
|
||||
value_enum,
|
||||
verbatim_doc_comment,
|
||||
conflicts_with = "no_preview"
|
||||
)]
|
||||
pub preview_border: Option<BorderType>,
|
||||
|
||||
/// Sets the preview panel padding.
|
||||
///
|
||||
/// Format: `top=INTEGER;left=INTEGER;bottom=INTEGER;right=INTEGER`
|
||||
///
|
||||
/// Example: `--preview-padding='top=1;left=2;bottom=1;right=2'`
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "STRING",
|
||||
verbatim_doc_comment,
|
||||
conflicts_with = "no_preview"
|
||||
)]
|
||||
pub preview_padding: Option<String>,
|
||||
|
||||
/// Source command to use for the current channel.
|
||||
///
|
||||
/// When a channel is specified: This overrides the command defined in the channel prototype.
|
||||
@ -220,6 +258,20 @@ pub struct Cli {
|
||||
#[arg(long, value_name = "STRING", verbatim_doc_comment)]
|
||||
pub source_output: Option<String>,
|
||||
|
||||
/// Sets the results panel border type.
|
||||
///
|
||||
/// Available options are: `none`, `plain`, `rounded`, `thick`.
|
||||
#[arg(long, value_enum, verbatim_doc_comment)]
|
||||
pub results_border: Option<BorderType>,
|
||||
|
||||
/// Sets the results panel padding.
|
||||
///
|
||||
/// Format: `top=INTEGER;left=INTEGER;bottom=INTEGER;right=INTEGER`
|
||||
///
|
||||
/// Example: `--results-padding='top=1;left=2;bottom=1;right=2'`
|
||||
#[arg(long, value_name = "STRING", verbatim_doc_comment)]
|
||||
pub results_padding: Option<String>,
|
||||
|
||||
/// The delimiter byte to use for splitting the source's command output into entries.
|
||||
///
|
||||
/// This can be useful when the source command outputs multiline entries and you want to
|
||||
@ -509,6 +561,14 @@ pub enum LayoutOrientation {
|
||||
Portrait,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, ValueEnum)]
|
||||
pub enum BorderType {
|
||||
None,
|
||||
Plain,
|
||||
Rounded,
|
||||
Thick,
|
||||
}
|
||||
|
||||
// Add validator functions
|
||||
fn validate_positive_float(s: &str) -> Result<f64, String> {
|
||||
match s.parse::<f64>() {
|
||||
|
@ -4,6 +4,7 @@ use crate::{
|
||||
cli::args::{Cli, Command},
|
||||
config::{
|
||||
DEFAULT_PREVIEW_SIZE, KeyBindings, get_config_dir, get_data_dir,
|
||||
ui::{BorderType, Padding},
|
||||
},
|
||||
errors::cli_parsing_error_exit,
|
||||
screen::layout::Orientation,
|
||||
@ -62,6 +63,12 @@ pub struct PostProcessedCli {
|
||||
pub preview_size: Option<u16>,
|
||||
pub preview_header: Option<String>,
|
||||
pub preview_footer: Option<String>,
|
||||
pub preview_border: Option<BorderType>,
|
||||
pub preview_padding: Option<Padding>,
|
||||
|
||||
// Results panel configuration
|
||||
pub results_border: Option<BorderType>,
|
||||
pub results_padding: Option<Padding>,
|
||||
|
||||
// Status bar configuration
|
||||
pub no_status_bar: bool,
|
||||
@ -82,6 +89,8 @@ pub struct PostProcessedCli {
|
||||
pub input: Option<String>,
|
||||
pub input_header: Option<String>,
|
||||
pub input_prompt: Option<String>,
|
||||
pub input_border: Option<BorderType>,
|
||||
pub input_padding: Option<Padding>,
|
||||
|
||||
// UI and layout configuration
|
||||
pub layout: Option<Orientation>,
|
||||
@ -112,6 +121,8 @@ pub struct PostProcessedCli {
|
||||
pub command: Option<Command>,
|
||||
}
|
||||
|
||||
const DEFAULT_BORDER_TYPE: BorderType = BorderType::Rounded;
|
||||
|
||||
impl Default for PostProcessedCli {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@ -134,6 +145,12 @@ impl Default for PostProcessedCli {
|
||||
preview_size: Some(DEFAULT_PREVIEW_SIZE),
|
||||
preview_header: None,
|
||||
preview_footer: None,
|
||||
preview_border: Some(DEFAULT_BORDER_TYPE),
|
||||
preview_padding: None,
|
||||
|
||||
// Results panel configuration
|
||||
results_border: Some(DEFAULT_BORDER_TYPE),
|
||||
results_padding: None,
|
||||
|
||||
// Status bar configuration
|
||||
no_status_bar: false,
|
||||
@ -154,6 +171,8 @@ impl Default for PostProcessedCli {
|
||||
input: None,
|
||||
input_header: None,
|
||||
input_prompt: None,
|
||||
input_border: Some(DEFAULT_BORDER_TYPE),
|
||||
input_padding: None,
|
||||
|
||||
// UI and layout configuration
|
||||
layout: None,
|
||||
@ -300,10 +319,25 @@ pub fn post_process(cli: Cli, readable_stdin: bool) -> PostProcessedCli {
|
||||
});
|
||||
|
||||
// Determine layout
|
||||
let layout: Option<Orientation> =
|
||||
cli.layout.map(|layout_enum| match layout_enum {
|
||||
args::LayoutOrientation::Landscape => Orientation::Landscape,
|
||||
args::LayoutOrientation::Portrait => Orientation::Portrait,
|
||||
let layout: Option<Orientation> = cli.layout.map(Orientation::from);
|
||||
|
||||
// borders
|
||||
let input_border = cli.input_border.map(BorderType::from);
|
||||
let results_border = cli.results_border.map(BorderType::from);
|
||||
let preview_border = cli.preview_border.map(BorderType::from);
|
||||
|
||||
// padding
|
||||
let input_padding = cli.input_padding.map(|p| {
|
||||
parse_padding_literal(&p, CLI_PADDING_DELIMITER)
|
||||
.unwrap_or_else(|e| cli_parsing_error_exit(&e.to_string()))
|
||||
});
|
||||
let results_padding = cli.results_padding.map(|p| {
|
||||
parse_padding_literal(&p, CLI_PADDING_DELIMITER)
|
||||
.unwrap_or_else(|e| cli_parsing_error_exit(&e.to_string()))
|
||||
});
|
||||
let preview_padding = cli.preview_padding.map(|p| {
|
||||
parse_padding_literal(&p, CLI_PADDING_DELIMITER)
|
||||
.unwrap_or_else(|e| cli_parsing_error_exit(&e.to_string()))
|
||||
});
|
||||
|
||||
PostProcessedCli {
|
||||
@ -326,6 +360,12 @@ pub fn post_process(cli: Cli, readable_stdin: bool) -> PostProcessedCli {
|
||||
preview_size: cli.preview_size,
|
||||
preview_header: cli.preview_header,
|
||||
preview_footer: cli.preview_footer,
|
||||
preview_border,
|
||||
preview_padding,
|
||||
|
||||
// Results configuration
|
||||
results_border,
|
||||
results_padding,
|
||||
|
||||
// Status bar configuration
|
||||
no_status_bar: cli.no_status_bar,
|
||||
@ -346,6 +386,8 @@ pub fn post_process(cli: Cli, readable_stdin: bool) -> PostProcessedCli {
|
||||
input: cli.input,
|
||||
input_header: cli.input_header,
|
||||
input_prompt: cli.input_prompt,
|
||||
input_border,
|
||||
input_padding,
|
||||
|
||||
// UI and layout configuration
|
||||
layout,
|
||||
@ -432,6 +474,7 @@ fn validate_adhoc_mode_constraints(cli: &Cli, readable_stdin: bool) {
|
||||
}
|
||||
|
||||
const CLI_KEYBINDINGS_DELIMITER: char = ';';
|
||||
const CLI_PADDING_DELIMITER: char = ';';
|
||||
|
||||
/// Parse a keybindings literal into a `KeyBindings` struct.
|
||||
///
|
||||
@ -452,6 +495,17 @@ fn parse_keybindings_literal(
|
||||
toml::from_str(&toml_definition).map_err(|e| anyhow!(e))
|
||||
}
|
||||
|
||||
/// Parses the CLI padding values into `config::ui::Padding`
|
||||
fn parse_padding_literal(
|
||||
cli_padding: &str,
|
||||
delimiter: char,
|
||||
) -> Result<Padding> {
|
||||
let toml_definition = cli_padding
|
||||
.split(delimiter)
|
||||
.fold(String::new(), |acc, p| acc + p + "\n");
|
||||
toml::from_str(&toml_definition).map_err(|e| anyhow!(e))
|
||||
}
|
||||
|
||||
pub fn list_channels<P>(cable_dir: P)
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
|
@ -255,9 +255,6 @@ impl Config {
|
||||
if let Some(value) = ui_spec.orientation {
|
||||
self.ui.orientation = value;
|
||||
}
|
||||
if let Some(value) = ui_spec.input_bar_position {
|
||||
self.ui.input_bar_position = value;
|
||||
}
|
||||
|
||||
// Apply clone fields
|
||||
if let Some(value) = &ui_spec.features {
|
||||
@ -273,17 +270,26 @@ impl Config {
|
||||
self.ui.remote_control = value.clone();
|
||||
}
|
||||
|
||||
// Apply input_header
|
||||
if let Some(value) = &ui_spec.input_header {
|
||||
self.ui.input_header = Some(value.clone());
|
||||
// Handle input, results, and preview with field merging
|
||||
if let Some(input_bar) = &ui_spec.input_bar {
|
||||
self.ui.input_bar.position = input_bar.position;
|
||||
if input_bar.header.is_some() {
|
||||
self.ui.input_bar.header.clone_from(&input_bar.header);
|
||||
}
|
||||
|
||||
// Apply input_prompt
|
||||
if let Some(value) = &ui_spec.input_prompt {
|
||||
self.ui.input_prompt.clone_from(value);
|
||||
self.ui.input_bar.prompt.clone_from(&input_bar.prompt);
|
||||
self.ui
|
||||
.input_bar
|
||||
.border_type
|
||||
.clone_from(&input_bar.border_type);
|
||||
self.ui.input_bar.padding = input_bar.padding;
|
||||
}
|
||||
if let Some(results_panel) = &ui_spec.results_panel {
|
||||
self.ui
|
||||
.results_panel
|
||||
.border_type
|
||||
.clone_from(&results_panel.border_type);
|
||||
self.ui.results_panel.padding = results_panel.padding;
|
||||
}
|
||||
|
||||
// Handle preview_panel with field merging
|
||||
if let Some(preview_panel) = &ui_spec.preview_panel {
|
||||
self.ui.preview_panel.size = preview_panel.size;
|
||||
if let Some(header) = &preview_panel.header {
|
||||
@ -293,6 +299,11 @@ impl Config {
|
||||
self.ui.preview_panel.footer = Some(footer.clone());
|
||||
}
|
||||
self.ui.preview_panel.scrollbar = preview_panel.scrollbar;
|
||||
self.ui
|
||||
.preview_panel
|
||||
.border_type
|
||||
.clone_from(&preview_panel.border_type);
|
||||
self.ui.preview_panel.padding = preview_panel.padding;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -364,6 +375,7 @@ pub use ui::{DEFAULT_PREVIEW_SIZE, DEFAULT_UI_SCALE};
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::action::Action;
|
||||
use crate::config::ui::Padding;
|
||||
use crate::event::Key;
|
||||
|
||||
use super::*;
|
||||
@ -498,8 +510,8 @@ mod tests {
|
||||
}
|
||||
|
||||
const USER_CONFIG_INPUT_PROMPT: &str = r#"
|
||||
[ui]
|
||||
input_prompt = "❯"
|
||||
[ui.input_bar]
|
||||
prompt = "❯"
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
@ -518,7 +530,7 @@ mod tests {
|
||||
let config = Config::new(&config_env, None).unwrap();
|
||||
|
||||
// Verify that input_prompt was loaded from user config
|
||||
assert_eq!(config.ui.input_prompt, "❯");
|
||||
assert_eq!(config.ui.input_bar.prompt, "❯");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -584,4 +596,91 @@ mod tests {
|
||||
|
||||
assert_eq!(config.shell_integration.keybindings, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apply_prototype_ui_spec() {
|
||||
use crate::channels::prototypes::Template;
|
||||
use crate::features::Features;
|
||||
use crate::screen::layout::{InputPosition, Orientation};
|
||||
use ui::{
|
||||
BorderType, HelpPanelConfig, InputBarConfig, PreviewPanelConfig,
|
||||
RemoteControlConfig, ResultsPanelConfig, StatusBarConfig,
|
||||
};
|
||||
|
||||
let mut features = Features::default();
|
||||
features.help_panel.disable();
|
||||
|
||||
let mut config = Config::default();
|
||||
config.ui.preview_panel.header =
|
||||
Some(Template::Raw("cow".to_string()));
|
||||
|
||||
let ui_spec = UiSpec {
|
||||
ui_scale: Some(12),
|
||||
features: Some(features),
|
||||
orientation: Some(Orientation::Portrait),
|
||||
input_bar: Some(InputBarConfig {
|
||||
position: InputPosition::Bottom,
|
||||
header: Some(Template::Raw("hello".to_string())),
|
||||
prompt: "world".to_string(),
|
||||
border_type: BorderType::Thick,
|
||||
padding: Padding::uniform(2),
|
||||
}),
|
||||
results_panel: Some(ResultsPanelConfig {
|
||||
border_type: BorderType::None,
|
||||
padding: Padding::uniform(2),
|
||||
}),
|
||||
preview_panel: Some(PreviewPanelConfig {
|
||||
size: 42,
|
||||
header: None, // does not overwrite "cow"
|
||||
footer: Some(Template::Raw("moo".to_string())),
|
||||
scrollbar: true,
|
||||
border_type: BorderType::Plain,
|
||||
padding: Padding::uniform(2),
|
||||
}),
|
||||
status_bar: Some(StatusBarConfig {
|
||||
separator_open: "open".to_string(),
|
||||
separator_close: "close".to_string(),
|
||||
}),
|
||||
help_panel: Some(HelpPanelConfig {
|
||||
show_categories: true,
|
||||
}),
|
||||
remote_control: Some(RemoteControlConfig {
|
||||
show_channel_descriptions: true,
|
||||
sort_alphabetically: true,
|
||||
}),
|
||||
};
|
||||
config.apply_prototype_ui_spec(&ui_spec);
|
||||
|
||||
assert_eq!(config.ui.ui_scale, 12);
|
||||
assert!(!config.ui.features.help_panel.enabled);
|
||||
assert_eq!(config.ui.input_bar.position, InputPosition::Bottom);
|
||||
assert_eq!(
|
||||
config.ui.input_bar.header.as_ref().unwrap().raw(),
|
||||
"hello"
|
||||
);
|
||||
assert_eq!(config.ui.input_bar.prompt, "world");
|
||||
assert_eq!(config.ui.input_bar.border_type, BorderType::Thick);
|
||||
assert_eq!(config.ui.input_bar.padding, Padding::uniform(2));
|
||||
|
||||
assert_eq!(config.ui.results_panel.border_type, BorderType::None);
|
||||
assert_eq!(config.ui.results_panel.padding, Padding::uniform(2));
|
||||
|
||||
assert_eq!(config.ui.preview_panel.size, 42);
|
||||
assert_eq!(
|
||||
config.ui.preview_panel.header.as_ref().unwrap().raw(),
|
||||
"cow"
|
||||
);
|
||||
assert_eq!(
|
||||
config.ui.preview_panel.footer.as_ref().unwrap().raw(),
|
||||
"moo"
|
||||
);
|
||||
assert!(config.ui.preview_panel.scrollbar);
|
||||
assert_eq!(config.ui.preview_panel.border_type, BorderType::Plain);
|
||||
assert_eq!(config.ui.preview_panel.padding, Padding::uniform(2));
|
||||
|
||||
assert_eq!(config.ui.status_bar.separator_open, "open");
|
||||
assert_eq!(config.ui.status_bar.separator_close, "close");
|
||||
assert!(config.ui.remote_control.show_channel_descriptions);
|
||||
assert!(config.ui.remote_control.sort_alphabetically);
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,28 @@ use serde::{Deserialize, Serialize};
|
||||
pub const DEFAULT_UI_SCALE: u16 = 100;
|
||||
pub const DEFAULT_PREVIEW_SIZE: u16 = 50;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Hash)]
|
||||
#[serde(default)]
|
||||
pub struct InputBarConfig {
|
||||
pub position: InputPosition,
|
||||
pub header: Option<Template>,
|
||||
pub prompt: String,
|
||||
pub border_type: BorderType,
|
||||
pub padding: Padding,
|
||||
}
|
||||
|
||||
impl Default for InputBarConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
position: InputPosition::default(),
|
||||
header: None,
|
||||
prompt: ">".to_string(),
|
||||
border_type: BorderType::default(),
|
||||
padding: Padding::uniform(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Hash, Default)]
|
||||
#[serde(default)]
|
||||
pub struct StatusBarConfig {
|
||||
@ -16,6 +38,13 @@ pub struct StatusBarConfig {
|
||||
pub separator_close: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Hash, Default)]
|
||||
#[serde(default)]
|
||||
pub struct ResultsPanelConfig {
|
||||
pub border_type: BorderType,
|
||||
pub padding: Padding,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Hash)]
|
||||
#[serde(default)]
|
||||
pub struct PreviewPanelConfig {
|
||||
@ -23,6 +52,8 @@ pub struct PreviewPanelConfig {
|
||||
pub header: Option<Template>,
|
||||
pub footer: Option<Template>,
|
||||
pub scrollbar: bool,
|
||||
pub border_type: BorderType,
|
||||
pub padding: Padding,
|
||||
}
|
||||
|
||||
impl Default for PreviewPanelConfig {
|
||||
@ -32,6 +63,8 @@ impl Default for PreviewPanelConfig {
|
||||
header: None,
|
||||
footer: None,
|
||||
scrollbar: true,
|
||||
border_type: BorderType::default(),
|
||||
padding: Padding::uniform(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -104,17 +137,15 @@ pub struct ThemeOverrides {
|
||||
pub struct UiConfig {
|
||||
pub use_nerd_font_icons: bool,
|
||||
pub ui_scale: u16,
|
||||
pub input_bar_position: InputPosition,
|
||||
pub orientation: Orientation,
|
||||
pub theme: String,
|
||||
pub input_header: Option<Template>,
|
||||
#[serde(default = "default_input_prompt")]
|
||||
pub input_prompt: String,
|
||||
pub features: Features,
|
||||
|
||||
// Feature-specific configurations
|
||||
pub input_bar: InputBarConfig,
|
||||
pub status_bar: StatusBarConfig,
|
||||
pub preview_panel: PreviewPanelConfig,
|
||||
pub results_panel: ResultsPanelConfig,
|
||||
pub help_panel: HelpPanelConfig,
|
||||
pub remote_control: RemoteControlConfig,
|
||||
|
||||
@ -123,27 +154,117 @@ pub struct UiConfig {
|
||||
pub theme_overrides: ThemeOverrides,
|
||||
}
|
||||
|
||||
const DEFAULT_INPUT_PROMPT: &str = ">";
|
||||
fn default_input_prompt() -> String {
|
||||
String::from(DEFAULT_INPUT_PROMPT)
|
||||
}
|
||||
|
||||
impl Default for UiConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
use_nerd_font_icons: false,
|
||||
ui_scale: DEFAULT_UI_SCALE,
|
||||
input_bar_position: InputPosition::Top,
|
||||
orientation: Orientation::Landscape,
|
||||
theme: String::from(DEFAULT_THEME),
|
||||
input_header: None,
|
||||
input_prompt: String::from(DEFAULT_INPUT_PROMPT),
|
||||
features: Features::default(),
|
||||
input_bar: InputBarConfig::default(),
|
||||
status_bar: StatusBarConfig::default(),
|
||||
preview_panel: PreviewPanelConfig::default(),
|
||||
results_panel: ResultsPanelConfig::default(),
|
||||
help_panel: HelpPanelConfig::default(),
|
||||
remote_control: RemoteControlConfig::default(),
|
||||
theme_overrides: ThemeOverrides::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Hash, Default,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum BorderType {
|
||||
None,
|
||||
Plain,
|
||||
#[default]
|
||||
Rounded,
|
||||
Thick,
|
||||
}
|
||||
|
||||
impl BorderType {
|
||||
pub fn to_ratatui_border_type(
|
||||
&self,
|
||||
) -> Option<ratatui::widgets::BorderType> {
|
||||
match self {
|
||||
BorderType::None => None,
|
||||
BorderType::Plain => Some(ratatui::widgets::BorderType::Plain),
|
||||
BorderType::Rounded => Some(ratatui::widgets::BorderType::Rounded),
|
||||
BorderType::Thick => Some(ratatui::widgets::BorderType::Thick),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::cli::args::BorderType> for BorderType {
|
||||
fn from(border_type: crate::cli::args::BorderType) -> Self {
|
||||
match border_type {
|
||||
crate::cli::args::BorderType::None => BorderType::None,
|
||||
crate::cli::args::BorderType::Plain => BorderType::Plain,
|
||||
crate::cli::args::BorderType::Rounded => BorderType::Rounded,
|
||||
crate::cli::args::BorderType::Thick => BorderType::Thick,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Hash, Default,
|
||||
)]
|
||||
#[serde(default)]
|
||||
pub struct Padding {
|
||||
pub top: u16,
|
||||
pub bottom: u16,
|
||||
pub left: u16,
|
||||
pub right: u16,
|
||||
}
|
||||
|
||||
impl Padding {
|
||||
pub fn new(top: u16, bottom: u16, left: u16, right: u16) -> Self {
|
||||
Self {
|
||||
top,
|
||||
bottom,
|
||||
left,
|
||||
right,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn uniform(padding: u16) -> Self {
|
||||
Self {
|
||||
top: padding,
|
||||
bottom: padding,
|
||||
left: padding,
|
||||
right: padding,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn horizontal(padding: u16) -> Self {
|
||||
Self {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: padding,
|
||||
right: padding,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn vertical(padding: u16) -> Self {
|
||||
Self {
|
||||
top: padding,
|
||||
bottom: padding,
|
||||
left: 0,
|
||||
right: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Padding> for ratatui::widgets::Padding {
|
||||
fn from(padding: Padding) -> Self {
|
||||
ratatui::widgets::Padding {
|
||||
top: padding.top,
|
||||
bottom: padding.bottom,
|
||||
left: padding.left,
|
||||
right: padding.right,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -191,14 +191,12 @@ pub fn draw(ctx: Box<Ctx>, f: &mut Frame<'_>, area: Rect) -> Result<Layout> {
|
||||
&ctx.tv_state.results_picker.entries,
|
||||
&ctx.tv_state.channel_state.selected_entries,
|
||||
&mut ctx.tv_state.results_picker.relative_state.clone(),
|
||||
ctx.config.ui.input_bar_position,
|
||||
ctx.config.ui.input_bar.position,
|
||||
ctx.config.ui.use_nerd_font_icons,
|
||||
&ctx.colorscheme,
|
||||
&ctx.config.ui.results_panel,
|
||||
)?;
|
||||
|
||||
// input box
|
||||
let input_prompt = ctx.config.ui.input_prompt.clone();
|
||||
|
||||
draw_input_box(
|
||||
f,
|
||||
layout.input,
|
||||
@ -210,9 +208,7 @@ pub fn draw(ctx: Box<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.input_header,
|
||||
&input_prompt,
|
||||
&ctx.config.ui.input_bar_position,
|
||||
&ctx.config.ui.input_bar,
|
||||
)?;
|
||||
|
||||
// status bar at the bottom
|
||||
@ -228,7 +224,7 @@ pub fn draw(ctx: Box<Ctx>, f: &mut Frame<'_>, area: Rect) -> Result<Layout> {
|
||||
ctx.tv_state.preview_state,
|
||||
ctx.config.ui.use_nerd_font_icons,
|
||||
&ctx.colorscheme,
|
||||
ctx.config.ui.preview_panel.scrollbar,
|
||||
&ctx.config.ui.preview_panel,
|
||||
)?;
|
||||
}
|
||||
|
||||
|
@ -4,11 +4,10 @@ use std::env;
|
||||
use std::io::{BufWriter, IsTerminal, Write, stdout};
|
||||
use std::path::PathBuf;
|
||||
use std::process::exit;
|
||||
use television::cable::cable_empty_exit;
|
||||
use television::{
|
||||
action::Action,
|
||||
app::{App, AppOptions},
|
||||
cable::{Cable, load_cable},
|
||||
cable::{Cable, cable_empty_exit, load_cable},
|
||||
channels::prototypes::{
|
||||
ChannelPrototype, CommandSpec, PreviewSpec, Template, UiSpec,
|
||||
},
|
||||
@ -18,7 +17,7 @@ use television::{
|
||||
args::{Cli, Command},
|
||||
guess_channel_from_prompt, list_channels,
|
||||
},
|
||||
config::{Config, ConfigEnv, merge_bindings},
|
||||
config::{Config, ConfigEnv, merge_bindings, ui::InputBarConfig},
|
||||
errors::os_error_exit,
|
||||
features::FeatureFlags,
|
||||
gh::update_local_channels,
|
||||
@ -212,11 +211,11 @@ fn apply_cli_overrides(args: &PostProcessedCli, config: &mut Config) {
|
||||
config.ui.ui_scale = args.ui_scale.unwrap_or(config.ui.ui_scale);
|
||||
if let Some(input_header) = &args.input_header {
|
||||
if let Ok(t) = Template::parse(input_header) {
|
||||
config.ui.input_header = Some(t);
|
||||
config.ui.input_bar.header = Some(t);
|
||||
}
|
||||
}
|
||||
if let Some(input_prompt) = &args.input_prompt {
|
||||
config.ui.input_prompt.clone_from(input_prompt);
|
||||
config.ui.input_bar.prompt.clone_from(input_prompt);
|
||||
}
|
||||
if let Some(preview_header) = &args.preview_header {
|
||||
if let Ok(t) = Template::parse(preview_header) {
|
||||
@ -231,6 +230,24 @@ fn apply_cli_overrides(args: &PostProcessedCli, config: &mut Config) {
|
||||
if let Some(layout) = args.layout {
|
||||
config.ui.orientation = layout;
|
||||
}
|
||||
if let Some(input_border) = args.input_border {
|
||||
config.ui.input_bar.border_type = input_border;
|
||||
}
|
||||
if let Some(preview_border) = args.preview_border {
|
||||
config.ui.preview_panel.border_type = preview_border;
|
||||
}
|
||||
if let Some(results_border) = args.results_border {
|
||||
config.ui.results_panel.border_type = results_border;
|
||||
}
|
||||
if let Some(input_padding) = args.input_padding {
|
||||
config.ui.input_bar.padding = input_padding;
|
||||
}
|
||||
if let Some(preview_padding) = args.preview_padding {
|
||||
config.ui.preview_panel.padding = preview_padding;
|
||||
}
|
||||
if let Some(results_padding) = args.results_padding {
|
||||
config.ui.results_panel.padding = results_padding;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_current_dir(path: &PathBuf) -> Result<()> {
|
||||
@ -330,7 +347,10 @@ fn create_adhoc_channel(
|
||||
|
||||
// Set UI specification
|
||||
let mut ui_spec = UiSpec::from(&config.ui);
|
||||
ui_spec.input_header = Some(input_header);
|
||||
let input_bar = ui_spec
|
||||
.input_bar
|
||||
.get_or_insert_with(InputBarConfig::default);
|
||||
input_bar.header = Some(input_header);
|
||||
ui_spec.features = Some(features);
|
||||
prototype.ui = Some(ui_spec);
|
||||
|
||||
@ -389,10 +409,9 @@ fn apply_ui_overrides(
|
||||
ui_scale: None,
|
||||
features: None,
|
||||
orientation: None,
|
||||
input_bar_position: None,
|
||||
input_header: None,
|
||||
input_prompt: None,
|
||||
input_bar: None,
|
||||
preview_panel: None,
|
||||
results_panel: None,
|
||||
status_bar: None,
|
||||
help_panel: None,
|
||||
remote_control: None,
|
||||
@ -401,14 +420,74 @@ fn apply_ui_overrides(
|
||||
// Apply input header override
|
||||
if let Some(input_header_str) = &args.input_header {
|
||||
if let Ok(template) = Template::parse(input_header_str) {
|
||||
ui_spec.input_header = Some(template);
|
||||
let input_bar = ui_spec
|
||||
.input_bar
|
||||
.get_or_insert_with(InputBarConfig::default);
|
||||
input_bar.header = Some(template);
|
||||
ui_changes_needed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply input prompt override
|
||||
if let Some(input_prompt_str) = &args.input_prompt {
|
||||
ui_spec.input_prompt = Some(input_prompt_str.clone());
|
||||
let input_bar = ui_spec
|
||||
.input_bar
|
||||
.get_or_insert_with(InputBarConfig::default);
|
||||
input_bar.prompt.clone_from(input_prompt_str);
|
||||
ui_changes_needed = true;
|
||||
}
|
||||
|
||||
// Apply input bar border override
|
||||
if let Some(input_border) = args.input_border {
|
||||
let input_bar = ui_spec
|
||||
.input_bar
|
||||
.get_or_insert_with(InputBarConfig::default);
|
||||
input_bar.border_type = input_border;
|
||||
ui_changes_needed = true;
|
||||
}
|
||||
|
||||
// Apply input bar padding override
|
||||
if let Some(input_padding) = &args.input_padding {
|
||||
let input_bar = ui_spec
|
||||
.input_bar
|
||||
.get_or_insert_with(InputBarConfig::default);
|
||||
input_bar.padding = *input_padding;
|
||||
ui_changes_needed = true;
|
||||
}
|
||||
|
||||
// Apply preview panel border override
|
||||
if let Some(preview_border) = args.preview_border {
|
||||
let preview_panel = ui_spec.preview_panel.get_or_insert_with(
|
||||
television::config::ui::PreviewPanelConfig::default,
|
||||
);
|
||||
preview_panel.border_type = preview_border;
|
||||
ui_changes_needed = true;
|
||||
}
|
||||
|
||||
// Apply preview panel padding override
|
||||
if let Some(preview_padding) = &args.preview_padding {
|
||||
let preview_panel = ui_spec.preview_panel.get_or_insert_with(
|
||||
television::config::ui::PreviewPanelConfig::default,
|
||||
);
|
||||
preview_panel.padding = *preview_padding;
|
||||
ui_changes_needed = true;
|
||||
}
|
||||
|
||||
// Apply results panel border override
|
||||
if let Some(results_border) = args.results_border {
|
||||
let results_panel = ui_spec.results_panel.get_or_insert_with(
|
||||
television::config::ui::ResultsPanelConfig::default,
|
||||
);
|
||||
results_panel.border_type = results_border;
|
||||
ui_changes_needed = true;
|
||||
}
|
||||
|
||||
// Apply results panel padding override
|
||||
if let Some(results_padding) = &args.results_padding {
|
||||
let results_panel = ui_spec.results_panel.get_or_insert_with(
|
||||
television::config::ui::ResultsPanelConfig::default,
|
||||
);
|
||||
results_panel.padding = *results_padding;
|
||||
ui_changes_needed = true;
|
||||
}
|
||||
|
||||
@ -507,8 +586,12 @@ pub fn determine_channel(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rustc_hash::FxHashMap;
|
||||
use television::channels::prototypes::{
|
||||
use television::{
|
||||
channels::prototypes::{
|
||||
ChannelPrototype, CommandSpec, PreviewSpec, Template,
|
||||
},
|
||||
config::ui::{BorderType, InputBarConfig, Padding},
|
||||
screen::layout::InputPosition,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
@ -716,7 +799,7 @@ mod tests {
|
||||
// Preview should be disabled since no preview command was provided
|
||||
assert!(!features.is_enabled(FeatureFlags::PreviewPanel));
|
||||
assert_eq!(
|
||||
ui_spec.input_header,
|
||||
ui_spec.input_bar.as_ref().unwrap().header,
|
||||
Some(Template::parse("Custom Channel").unwrap())
|
||||
);
|
||||
}
|
||||
@ -730,6 +813,12 @@ mod tests {
|
||||
input_header: Some("Input Header".to_string()),
|
||||
preview_header: Some("Preview Header".to_string()),
|
||||
preview_footer: Some("Preview Footer".to_string()),
|
||||
preview_border: Some(BorderType::Thick),
|
||||
input_border: Some(BorderType::Thick),
|
||||
results_border: Some(BorderType::Thick),
|
||||
input_padding: Some(Padding::new(1, 2, 3, 4)),
|
||||
preview_padding: Some(Padding::new(5, 6, 7, 8)),
|
||||
results_padding: Some(Padding::new(9, 10, 11, 12)),
|
||||
..Default::default()
|
||||
};
|
||||
apply_cli_overrides(&args, &mut config);
|
||||
@ -737,7 +826,7 @@ mod tests {
|
||||
assert_eq!(config.application.tick_rate, 100_f64);
|
||||
assert!(!config.ui.features.is_enabled(FeatureFlags::PreviewPanel));
|
||||
assert_eq!(
|
||||
config.ui.input_header,
|
||||
config.ui.input_bar.header,
|
||||
Some(Template::parse("Input Header").unwrap())
|
||||
);
|
||||
assert_eq!(
|
||||
@ -748,6 +837,15 @@ mod tests {
|
||||
config.ui.preview_panel.footer,
|
||||
Some(Template::parse("Preview Footer").unwrap())
|
||||
);
|
||||
assert_eq!(config.ui.preview_panel.border_type, BorderType::Thick);
|
||||
assert_eq!(config.ui.input_bar.border_type, BorderType::Thick);
|
||||
assert_eq!(config.ui.results_panel.border_type, BorderType::Thick);
|
||||
assert_eq!(config.ui.input_bar.padding, Padding::new(1, 2, 3, 4));
|
||||
assert_eq!(config.ui.preview_panel.padding, Padding::new(5, 6, 7, 8));
|
||||
assert_eq!(
|
||||
config.ui.results_panel.padding,
|
||||
Padding::new(9, 10, 11, 12)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -761,11 +859,15 @@ mod tests {
|
||||
ui_scale: None,
|
||||
features: None,
|
||||
orientation: Some(Orientation::Portrait),
|
||||
input_bar_position: None,
|
||||
input_header: Some(Template::parse("Original Header").unwrap()),
|
||||
input_prompt: None,
|
||||
input_bar: Some(InputBarConfig {
|
||||
position: InputPosition::default(),
|
||||
header: Some(Template::parse("Original Header").unwrap()),
|
||||
prompt: ">".to_string(),
|
||||
border_type: BorderType::Thick,
|
||||
padding: Padding::uniform(1),
|
||||
}),
|
||||
preview_panel: Some(television::config::ui::PreviewPanelConfig {
|
||||
size: 50,
|
||||
size: 60,
|
||||
header: Some(
|
||||
Template::parse("Original Preview Header").unwrap(),
|
||||
),
|
||||
@ -773,6 +875,12 @@ mod tests {
|
||||
Template::parse("Original Preview Footer").unwrap(),
|
||||
),
|
||||
scrollbar: false,
|
||||
border_type: BorderType::Thick,
|
||||
padding: Padding::uniform(2),
|
||||
}),
|
||||
results_panel: Some(television::config::ui::ResultsPanelConfig {
|
||||
border_type: BorderType::Thick,
|
||||
padding: Padding::uniform(2),
|
||||
}),
|
||||
status_bar: None,
|
||||
help_panel: None,
|
||||
@ -788,6 +896,12 @@ mod tests {
|
||||
preview_header: Some("CLI Preview Header".to_string()),
|
||||
preview_footer: Some("CLI Preview Footer".to_string()),
|
||||
layout: Some(Orientation::Landscape),
|
||||
input_border: Some(BorderType::Plain),
|
||||
preview_border: Some(BorderType::Plain),
|
||||
results_border: Some(BorderType::Plain),
|
||||
input_padding: Some(Padding::new(1, 2, 3, 4)),
|
||||
preview_padding: Some(Padding::new(5, 6, 7, 8)),
|
||||
results_padding: Some(Padding::new(9, 10, 11, 12)),
|
||||
..Default::default()
|
||||
};
|
||||
let config = Config::default();
|
||||
@ -800,7 +914,7 @@ mod tests {
|
||||
let ui_spec = result_channel.ui.as_ref().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
ui_spec.input_header,
|
||||
ui_spec.input_bar.as_ref().unwrap().header,
|
||||
Some(Template::parse("CLI Input Header").unwrap())
|
||||
);
|
||||
assert_eq!(ui_spec.orientation, Some(Orientation::Landscape));
|
||||
@ -815,6 +929,26 @@ mod tests {
|
||||
preview_panel.footer,
|
||||
Some(Template::parse("CLI Preview Footer").unwrap())
|
||||
);
|
||||
assert_eq!(preview_panel.border_type, BorderType::Plain);
|
||||
assert_eq!(preview_panel.padding, Padding::new(5, 6, 7, 8));
|
||||
|
||||
assert_eq!(
|
||||
ui_spec.results_panel.as_ref().unwrap().border_type,
|
||||
BorderType::Plain
|
||||
);
|
||||
assert_eq!(
|
||||
ui_spec.results_panel.as_ref().unwrap().padding,
|
||||
Padding::new(9, 10, 11, 12)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ui_spec.input_bar.as_ref().unwrap().border_type,
|
||||
BorderType::Plain
|
||||
);
|
||||
assert_eq!(
|
||||
ui_spec.input_bar.as_ref().unwrap().padding,
|
||||
Padding::new(1, 2, 3, 4)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
channels::prototypes::Template,
|
||||
config::ui::InputBarConfig,
|
||||
screen::{colors::Colorscheme, layout::InputPosition, spinner::Spinner},
|
||||
utils::input::Input,
|
||||
};
|
||||
@ -12,7 +12,7 @@ use ratatui::{
|
||||
style::{Style, Stylize},
|
||||
text::{Line, Span},
|
||||
widgets::{
|
||||
Block, BorderType, Borders, ListState, Paragraph, block::Position,
|
||||
Block, Borders, ListState, Padding, Paragraph, block::Position,
|
||||
},
|
||||
};
|
||||
|
||||
@ -28,19 +28,15 @@ pub fn draw_input_box(
|
||||
channel_name: &str,
|
||||
spinner: &Spinner,
|
||||
colorscheme: &Colorscheme,
|
||||
input_header: &Option<Template>,
|
||||
input_prompt: &str,
|
||||
input_bar_position: &InputPosition,
|
||||
input_bar_config: &InputBarConfig,
|
||||
) -> Result<()> {
|
||||
let header = input_header
|
||||
let header = input_bar_config
|
||||
.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)
|
||||
.border_style(Style::default().fg(colorscheme.general.border_fg))
|
||||
.title_position(match input_bar_position {
|
||||
let mut input_block = Block::default()
|
||||
.title_position(match input_bar_config.position {
|
||||
InputPosition::Top => Position::Top,
|
||||
InputPosition::Bottom => Position::Bottom,
|
||||
})
|
||||
@ -52,7 +48,16 @@ pub fn draw_input_box(
|
||||
.style(
|
||||
Style::default()
|
||||
.bg(colorscheme.general.background.unwrap_or_default()),
|
||||
);
|
||||
)
|
||||
.padding(Padding::from(input_bar_config.padding));
|
||||
if let Some(border_type) =
|
||||
input_bar_config.border_type.to_ratatui_border_type()
|
||||
{
|
||||
input_block = input_block
|
||||
.borders(Borders::ALL)
|
||||
.border_type(border_type)
|
||||
.border_style(Style::default().fg(colorscheme.general.border_fg));
|
||||
}
|
||||
|
||||
let input_block_inner = input_block.inner(rect);
|
||||
if input_block_inner.area() == 0 {
|
||||
@ -67,7 +72,8 @@ pub fn draw_input_box(
|
||||
.constraints([
|
||||
// prompt symbol + space
|
||||
Constraint::Length(
|
||||
u16::try_from(input_prompt.chars().count() + 1).unwrap_or(2),
|
||||
u16::try_from(input_bar_config.prompt.chars().count() + 1)
|
||||
.unwrap_or(2),
|
||||
),
|
||||
// input field
|
||||
Constraint::Fill(1),
|
||||
@ -84,7 +90,7 @@ pub fn draw_input_box(
|
||||
|
||||
let arrow_block = Block::default();
|
||||
let arrow = Paragraph::new(Span::styled(
|
||||
format!("{} ", input_prompt),
|
||||
format!("{} ", input_bar_config.prompt),
|
||||
Style::default().fg(colorscheme.input.input_fg).bold(),
|
||||
))
|
||||
.block(arrow_block);
|
||||
|
@ -70,6 +70,19 @@ pub enum Orientation {
|
||||
Portrait,
|
||||
}
|
||||
|
||||
impl From<crate::cli::args::LayoutOrientation> for Orientation {
|
||||
fn from(value: crate::cli::args::LayoutOrientation) -> Self {
|
||||
match value {
|
||||
crate::cli::args::LayoutOrientation::Landscape => {
|
||||
Orientation::Landscape
|
||||
}
|
||||
crate::cli::args::LayoutOrientation::Portrait => {
|
||||
Orientation::Portrait
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Layout {
|
||||
pub results: Rect,
|
||||
@ -194,7 +207,7 @@ impl Layout {
|
||||
// results percentage is whatever remains
|
||||
let results_percentage = 100u16.saturating_sub(preview_percentage);
|
||||
|
||||
match (ui_config.orientation, ui_config.input_bar_position) {
|
||||
match (ui_config.orientation, ui_config.input_bar.position) {
|
||||
// Preview is rendered on the right or bottom depending on orientation
|
||||
(Orientation::Landscape, _)
|
||||
| (Orientation::Portrait, InputPosition::Top) => {
|
||||
@ -243,7 +256,7 @@ impl Layout {
|
||||
// Now split the results window vertically into results list + input
|
||||
let result_chunks = layout::Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(match ui_config.input_bar_position {
|
||||
.constraints(match ui_config.input_bar.position {
|
||||
InputPosition::Top => results_constraints
|
||||
.clone()
|
||||
.into_iter()
|
||||
@ -254,7 +267,8 @@ impl Layout {
|
||||
.split(result_window);
|
||||
|
||||
let (input_rect, results_rect) = match ui_config
|
||||
.input_bar_position
|
||||
.input_bar
|
||||
.position
|
||||
{
|
||||
InputPosition::Bottom => {
|
||||
(result_chunks[1], result_chunks[0])
|
||||
@ -278,7 +292,7 @@ impl Layout {
|
||||
|
||||
let mut portrait_constraints: Vec<Constraint> = Vec::new();
|
||||
|
||||
match ui_config.input_bar_position {
|
||||
match ui_config.input_bar.position {
|
||||
InputPosition::Top => {
|
||||
// Input bar is always the first chunk
|
||||
portrait_constraints
|
||||
|
@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
previewer::state::PreviewState,
|
||||
config::ui::{BorderType, Padding, PreviewPanelConfig},
|
||||
previewer::{Preview, state::PreviewState},
|
||||
screen::colors::Colorscheme,
|
||||
utils::strings::{
|
||||
EMPTY_STRING, ReplaceNonPrintableConfig, replace_non_printable,
|
||||
@ -7,14 +8,13 @@ use crate::{
|
||||
},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use devicons::FileIcon;
|
||||
use ratatui::{
|
||||
Frame,
|
||||
layout::{Alignment, Rect},
|
||||
prelude::{Color, Line, Span, Style, Stylize, Text},
|
||||
widgets::{
|
||||
Block, BorderType, Borders, Clear, Padding, Paragraph, Scrollbar,
|
||||
ScrollbarOrientation, ScrollbarState, StatefulWidget,
|
||||
Block, Borders, Clear, Padding as RatatuiPadding, Paragraph,
|
||||
Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget,
|
||||
},
|
||||
};
|
||||
use std::str::FromStr;
|
||||
@ -26,15 +26,15 @@ pub fn draw_preview_content_block(
|
||||
preview_state: PreviewState,
|
||||
use_nerd_font_icons: bool,
|
||||
colorscheme: &Colorscheme,
|
||||
scrollbar_enabled: bool,
|
||||
preview_panel_config: &PreviewPanelConfig,
|
||||
) -> Result<()> {
|
||||
let inner = draw_content_outer_block(
|
||||
f,
|
||||
rect,
|
||||
colorscheme,
|
||||
preview_state.preview.icon,
|
||||
&preview_state.preview.title,
|
||||
&preview_state.preview.footer,
|
||||
preview_panel_config.border_type,
|
||||
preview_panel_config.padding,
|
||||
&preview_state.preview,
|
||||
use_nerd_font_icons,
|
||||
)?;
|
||||
let scroll = preview_state.scroll as usize;
|
||||
@ -50,7 +50,7 @@ pub fn draw_preview_content_block(
|
||||
f.render_widget(rp, inner);
|
||||
|
||||
// render scrollbar if enabled
|
||||
if scrollbar_enabled {
|
||||
if preview_panel_config.scrollbar {
|
||||
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
|
||||
.style(Style::default().fg(colorscheme.general.border_fg));
|
||||
|
||||
@ -76,7 +76,9 @@ pub fn build_preview_paragraph(
|
||||
highlight_bg: Color,
|
||||
) -> Paragraph<'static> {
|
||||
let preview_block =
|
||||
Block::default().style(Style::default()).padding(Padding {
|
||||
Block::default()
|
||||
.style(Style::default())
|
||||
.padding(RatatuiPadding {
|
||||
top: 0,
|
||||
right: 1,
|
||||
bottom: 0,
|
||||
@ -165,15 +167,15 @@ fn draw_content_outer_block(
|
||||
f: &mut Frame,
|
||||
rect: Rect,
|
||||
colorscheme: &Colorscheme,
|
||||
icon: Option<FileIcon>,
|
||||
title: &str,
|
||||
footer: &str,
|
||||
border_type: BorderType,
|
||||
padding: Padding,
|
||||
preview: &Preview,
|
||||
use_nerd_font_icons: bool,
|
||||
) -> Result<Rect> {
|
||||
let mut preview_title_spans = vec![Span::from(" ")];
|
||||
// optional icon
|
||||
if icon.is_some() && use_nerd_font_icons {
|
||||
let icon = icon.as_ref().unwrap();
|
||||
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);
|
||||
@ -187,7 +189,7 @@ fn draw_content_outer_block(
|
||||
preview_title_spans.push(Span::styled(
|
||||
shrink_with_ellipsis(
|
||||
&replace_non_printable(
|
||||
title.as_bytes(),
|
||||
preview.title.as_bytes(),
|
||||
&ReplaceNonPrintableConfig::default(),
|
||||
)
|
||||
.0,
|
||||
@ -205,10 +207,10 @@ fn draw_content_outer_block(
|
||||
);
|
||||
|
||||
// preview footer
|
||||
if !footer.is_empty() {
|
||||
if !preview.footer.is_empty() {
|
||||
let footer_line = Line::from(vec![
|
||||
Span::from(" "),
|
||||
Span::from(footer),
|
||||
Span::from(preview.footer.as_str()),
|
||||
Span::from(" "),
|
||||
])
|
||||
.alignment(Alignment::Center)
|
||||
@ -216,15 +218,18 @@ fn draw_content_outer_block(
|
||||
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))
|
||||
let mut preview_outer_block = block
|
||||
.style(
|
||||
Style::default()
|
||||
.bg(colorscheme.general.background.unwrap_or_default()),
|
||||
)
|
||||
.padding(Padding::new(0, 1, 1, 0));
|
||||
.padding(RatatuiPadding::from(padding));
|
||||
if let Some(border_type) = border_type.to_ratatui_border_type() {
|
||||
preview_outer_block = preview_outer_block
|
||||
.borders(Borders::ALL)
|
||||
.border_type(border_type)
|
||||
.border_style(Style::default().fg(colorscheme.general.border_fg));
|
||||
}
|
||||
|
||||
let inner = preview_outer_block.inner(rect);
|
||||
f.render_widget(preview_outer_block, rect);
|
||||
|
@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
channels::entry::Entry,
|
||||
config::ui::ResultsPanelConfig,
|
||||
screen::{colors::Colorscheme, layout::InputPosition, result_item},
|
||||
};
|
||||
use anyhow::Result;
|
||||
@ -8,7 +9,7 @@ use ratatui::{
|
||||
layout::{Alignment, Rect},
|
||||
prelude::Style,
|
||||
text::Line,
|
||||
widgets::{Block, BorderType, Borders, ListState, Padding},
|
||||
widgets::{Block, Borders, ListState, Padding},
|
||||
};
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
@ -22,17 +23,23 @@ pub fn draw_results_list(
|
||||
input_bar_position: InputPosition,
|
||||
use_nerd_font_icons: bool,
|
||||
colorscheme: &Colorscheme,
|
||||
results_panel_config: &ResultsPanelConfig,
|
||||
) -> Result<()> {
|
||||
let results_block = Block::default()
|
||||
let mut results_block = Block::default()
|
||||
.title_top(Line::from(" Results ").alignment(Alignment::Center))
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(Style::default().fg(colorscheme.general.border_fg))
|
||||
.style(
|
||||
Style::default()
|
||||
.bg(colorscheme.general.background.unwrap_or_default()),
|
||||
)
|
||||
.padding(Padding::right(1));
|
||||
.padding(Padding::from(results_panel_config.padding));
|
||||
if let Some(border_type) =
|
||||
results_panel_config.border_type.to_ratatui_border_type()
|
||||
{
|
||||
results_block = results_block
|
||||
.borders(Borders::ALL)
|
||||
.border_type(border_type)
|
||||
.border_style(Style::default().fg(colorscheme.general.border_fg));
|
||||
}
|
||||
|
||||
let list_direction = match input_bar_position {
|
||||
InputPosition::Bottom => ratatui::widgets::ListDirection::BottomToTop,
|
||||
|
@ -111,7 +111,7 @@ impl Television {
|
||||
debug!("Merged config: {:?}", config);
|
||||
|
||||
let mut results_picker = Picker::new(input.clone());
|
||||
if config.ui.input_bar_position == InputPosition::Bottom {
|
||||
if config.ui.input_bar.position == InputPosition::Bottom {
|
||||
results_picker = results_picker.inverted();
|
||||
}
|
||||
|
||||
@ -206,9 +206,11 @@ 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.input_bar.header.is_none() {
|
||||
if let Some(input_bar) = &ui_spec.input_bar {
|
||||
if let Some(header_tpl) = &input_bar.header {
|
||||
config.ui.input_bar.header = Some(header_tpl.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
if config.ui.preview_panel.header.is_none() {
|
||||
|
Loading…
x
Reference in New Issue
Block a user