feat(config): add cli options for border types and add optional Padding

This commit is contained in:
alexandre pasmantier 2025-07-24 19:26:36 +02:00
parent 7f7cea94c8
commit b507f6c8a0
10 changed files with 375 additions and 28 deletions

View File

@ -739,7 +739,10 @@ mod tests {
assert_eq!(ui.orientation, Some(Orientation::Landscape));
assert_eq!(ui.ui_scale, Some(40));
assert!(ui.features.is_none());
assert_eq!(ui.input_bar.as_ref().unwrap().border_type, BorderType::None);
assert_eq!(
ui.input_bar.as_ref().unwrap().border_type,
BorderType::None
);
assert!(ui.preview_panel.is_some());
assert_eq!(
ui.preview_panel

View File

@ -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>() {

View File

@ -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>,

View File

@ -281,12 +281,14 @@ impl Config {
.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;
}
if let Some(preview_panel) = &ui_spec.preview_panel {
self.ui.preview_panel.size = preview_panel.size;
@ -301,6 +303,7 @@ impl Config {
.preview_panel
.border_type
.clone_from(&preview_panel.border_type);
self.ui.preview_panel.padding = preview_panel.padding;
}
}
}
@ -372,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::*;
@ -619,9 +623,11 @@ mod tests {
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,
@ -629,6 +635,7 @@ mod tests {
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(),
@ -653,7 +660,11 @@ mod tests {
);
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(),
@ -665,6 +676,8 @@ mod tests {
);
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);

View File

@ -16,6 +16,7 @@ pub struct InputBarConfig {
pub header: Option<Template>,
pub prompt: String,
pub border_type: BorderType,
pub padding: Padding,
}
impl Default for InputBarConfig {
@ -25,6 +26,7 @@ impl Default for InputBarConfig {
header: None,
prompt: ">".to_string(),
border_type: BorderType::default(),
padding: Padding::uniform(0),
}
}
}
@ -40,6 +42,7 @@ pub struct StatusBarConfig {
#[serde(default)]
pub struct ResultsPanelConfig {
pub border_type: BorderType,
pub padding: Padding,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Hash)]
@ -50,6 +53,7 @@ pub struct PreviewPanelConfig {
pub footer: Option<Template>,
pub scrollbar: bool,
pub border_type: BorderType,
pub padding: Padding,
}
impl Default for PreviewPanelConfig {
@ -60,6 +64,7 @@ impl Default for PreviewPanelConfig {
footer: None,
scrollbar: true,
border_type: BorderType::default(),
padding: Padding::uniform(0),
}
}
}
@ -168,7 +173,9 @@ impl Default for UiConfig {
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Hash, Default)]
#[derive(
Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Hash, Default,
)]
#[serde(rename_all = "snake_case")]
pub enum BorderType {
None,
@ -177,6 +184,7 @@ pub enum BorderType {
Rounded,
Thick,
}
impl BorderType {
pub fn to_ratatui_border_type(
&self,
@ -189,3 +197,74 @@ impl BorderType {
}
}
}
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,
}
}
}

View File

@ -230,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<()> {
@ -419,6 +437,60 @@ fn apply_ui_overrides(
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;
}
// Apply layout/orientation override
if let Some(layout) = args.layout {
ui_spec.orientation = Some(layout);
@ -518,7 +590,7 @@ mod tests {
channels::prototypes::{
ChannelPrototype, CommandSpec, PreviewSpec, Template,
},
config::ui::{BorderType, InputBarConfig},
config::ui::{BorderType, InputBarConfig, Padding},
screen::layout::InputPosition,
};
@ -741,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);
@ -759,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]
@ -776,10 +863,11 @@ mod tests {
position: InputPosition::default(),
header: Some(Template::parse("Original Header").unwrap()),
prompt: ">".to_string(),
border_type: BorderType::Rounded,
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(),
),
@ -787,9 +875,13 @@ mod tests {
Template::parse("Original Preview Footer").unwrap(),
),
scrollbar: false,
border_type: BorderType::default(),
border_type: BorderType::Thick,
padding: Padding::uniform(2),
}),
results_panel: Some(television::config::ui::ResultsPanelConfig {
border_type: BorderType::Thick,
padding: Padding::uniform(2),
}),
results_panel: None,
status_bar: None,
help_panel: None,
remote_control: None,
@ -804,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();
@ -831,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]

View File

@ -11,7 +11,9 @@ use ratatui::{
},
style::{Style, Stylize},
text::{Line, Span},
widgets::{Block, Borders, ListState, Paragraph, block::Position},
widgets::{
Block, Borders, ListState, Padding, Paragraph, block::Position,
},
};
#[allow(clippy::too_many_arguments)]
@ -46,7 +48,8 @@ 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()
{

View File

@ -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,

View File

@ -1,5 +1,5 @@
use crate::{
config::ui::{BorderType, PreviewPanelConfig},
config::ui::{BorderType, Padding, PreviewPanelConfig},
previewer::{Preview, state::PreviewState},
screen::colors::Colorscheme,
utils::strings::{
@ -13,8 +13,8 @@ use ratatui::{
layout::{Alignment, Rect},
prelude::{Color, Line, Span, Style, Stylize, Text},
widgets::{
Block, Borders, Clear, Padding, Paragraph, Scrollbar,
ScrollbarOrientation, ScrollbarState, StatefulWidget,
Block, Borders, Clear, Padding as RatatuiPadding, Paragraph,
Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget,
},
};
use std::str::FromStr;
@ -32,7 +32,8 @@ pub fn draw_preview_content_block(
f,
rect,
colorscheme,
&preview_panel_config.border_type,
preview_panel_config.border_type,
preview_panel_config.padding,
&preview_state.preview,
use_nerd_font_icons,
)?;
@ -75,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,
@ -164,7 +167,8 @@ fn draw_content_outer_block(
f: &mut Frame,
rect: Rect,
colorscheme: &Colorscheme,
border_type: &BorderType,
border_type: BorderType,
padding: Padding,
preview: &Preview,
use_nerd_font_icons: bool,
) -> Result<Rect> {
@ -219,7 +223,7 @@ fn draw_content_outer_block(
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)

View File

@ -31,7 +31,7 @@ pub fn draw_results_list(
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()
{