feat(config): BorderType

This commit is contained in:
raylu 2025-07-13 13:00:56 -07:00 committed by alexandre pasmantier
parent 39610e145d
commit 784c3df283
12 changed files with 208 additions and 141 deletions

View File

@ -54,10 +54,6 @@ use_nerd_font_icons = false
# └───────────────────────────────────────┘ # └───────────────────────────────────────┘
ui_scale = 100 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) # What orientation should tv be (landscape or portrait)
orientation = "landscape" orientation = "landscape"
# The theme to use for the UI # The theme to use for the UI
@ -100,6 +96,14 @@ remote_control = { enabled = true, visible = false }
# Feature-specific configurations # Feature-specific configurations
# Each feature can have its own configuration section # 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] [ui.status_bar]
# Status bar separators (bubble): # Status bar separators (bubble):
#separator_open = "" #separator_open = ""
@ -108,12 +112,16 @@ remote_control = { enabled = true, visible = false }
separator_open = "" separator_open = ""
separator_close = "" separator_close = ""
[ui.results_panel]
border_type = "rounded"
[ui.preview_panel] [ui.preview_panel]
# Preview panel size (percentage of screen width/height) # Preview panel size (percentage of screen width/height)
size = 50 size = 50
#header = "{}" #header = "{}"
#footer = "" #footer = ""
scrollbar = true scrollbar = true
border_type = "rounded"
[ui.remote_control] [ui.remote_control]
# Whether to show channel descriptions in remote control mode # Whether to show channel descriptions in remote control mode

View File

@ -1,9 +1,10 @@
use crate::cli::parse_source_entry_delimiter; use crate::cli::parse_source_entry_delimiter;
use crate::config::ui::InputBarConfig;
use crate::{ use crate::{
config::{KeyBindings, ui}, config::{KeyBindings, ui},
event::Key, event::Key,
features::Features, features::Features,
screen::layout::{InputPosition, Orientation}, screen::layout::Orientation,
}; };
use anyhow::Result; use anyhow::Result;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
@ -387,16 +388,14 @@ pub struct UiSpec {
// `layout` is clearer for the user but collides with the overall `Layout` type. // `layout` is clearer for the user but collides with the overall `Layout` type.
#[serde(rename = "layout", alias = "orientation", default)] #[serde(rename = "layout", alias = "orientation", default)]
pub orientation: Option<Orientation>, 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 // Feature-specific configurations
#[serde(default)] #[serde(default)]
pub input_bar: Option<InputBarConfig>,
#[serde(default)]
pub preview_panel: Option<ui::PreviewPanelConfig>, pub preview_panel: Option<ui::PreviewPanelConfig>,
#[serde(default)] #[serde(default)]
pub results_panel: Option<ui::ResultsPanelConfig>,
#[serde(default)]
pub status_bar: Option<ui::StatusBarConfig>, pub status_bar: Option<ui::StatusBarConfig>,
#[serde(default)] #[serde(default)]
pub help_panel: Option<ui::HelpPanelConfig>, pub help_panel: Option<ui::HelpPanelConfig>,
@ -412,10 +411,9 @@ impl From<&crate::config::UiConfig> for UiSpec {
ui_scale: Some(config.ui_scale), ui_scale: Some(config.ui_scale),
features: Some(config.features.clone()), features: Some(config.features.clone()),
orientation: Some(config.orientation), orientation: Some(config.orientation),
input_bar_position: Some(config.input_bar_position), input_bar: Some(config.input_bar.clone()),
input_header: config.input_header.clone(),
input_prompt: Some(config.input_prompt.clone()),
preview_panel: Some(config.preview_panel.clone()), preview_panel: Some(config.preview_panel.clone()),
results_panel: Some(config.results_panel.clone()),
status_bar: Some(config.status_bar.clone()), status_bar: Some(config.status_bar.clone()),
help_panel: Some(config.help_panel.clone()), help_panel: Some(config.help_panel.clone()),
remote_control: Some(config.remote_control.clone()), remote_control: Some(config.remote_control.clone()),
@ -425,7 +423,10 @@ impl From<&crate::config::UiConfig> for UiSpec {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{action::Action, event::Key}; use crate::{
action::Action, config::ui::BorderType, event::Key,
screen::layout::InputPosition,
};
use super::*; use super::*;
use toml::from_str; use toml::from_str;
@ -508,16 +509,23 @@ mod tests {
[ui] [ui]
layout = "landscape" layout = "landscape"
ui_scale = 100 ui_scale = 100
input_bar_position = "bottom"
input_header = "Input: {}"
[ui.features] [ui.features]
preview_panel = { enabled = true, visible = true } preview_panel = { enabled = true, visible = true }
[ui.input_bar]
position = "bottom"
header = "Input: {}"
border_type = "plain"
[ui.preview_panel] [ui.preview_panel]
size = 66 size = 66
header = "Preview: {}" header = "Preview: {}"
footer = "Press 'q' to quit" footer = "Press 'q' to quit"
border_type = "thick"
[ui.results_panel]
border_type = "none"
[keybindings] [keybindings]
esc = "quit" esc = "quit"
@ -565,29 +573,23 @@ mod tests {
assert!(ui.features.is_some()); assert!(ui.features.is_some());
let features = ui.features.as_ref().unwrap(); let features = ui.features.as_ref().unwrap();
assert!(features.preview_panel.enabled); 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.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!( assert_eq!(
ui.preview_panel preview_panel.header.as_ref().unwrap().raw(),
.as_ref()
.unwrap()
.header
.as_ref()
.unwrap()
.raw(),
"Preview: {}" "Preview: {}"
); );
assert_eq!( assert_eq!(
ui.preview_panel preview_panel.footer.as_ref().unwrap().raw(),
.as_ref()
.unwrap()
.footer
.as_ref()
.unwrap()
.raw(),
"Press 'q' to quit" "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(); let keybindings = prototype.keybindings.unwrap();
assert_eq!( assert_eq!(
@ -734,9 +736,8 @@ mod tests {
assert_eq!(ui.orientation, Some(Orientation::Landscape)); assert_eq!(ui.orientation, Some(Orientation::Landscape));
assert_eq!(ui.ui_scale, Some(40)); assert_eq!(ui.ui_scale, Some(40));
assert!(ui.features.is_none()); assert!(ui.features.is_none());
assert!(ui.input_bar_position.is_none()); assert!(ui.input_bar.is_none());
assert!(ui.preview_panel.is_some()); assert!(ui.preview_panel.is_some());
assert!(ui.input_header.is_none());
assert_eq!( assert_eq!(
ui.preview_panel ui.preview_panel
.as_ref() .as_ref()

View File

@ -255,14 +255,14 @@ impl Config {
if let Some(value) = ui_spec.orientation { if let Some(value) = ui_spec.orientation {
self.ui.orientation = value; self.ui.orientation = value;
} }
if let Some(value) = ui_spec.input_bar_position {
self.ui.input_bar_position = value;
}
// Apply clone fields // Apply clone fields
if let Some(value) = &ui_spec.features { if let Some(value) = &ui_spec.features {
self.ui.features = value.clone(); self.ui.features = value.clone();
} }
if let Some(value) = &ui_spec.input_bar {
self.ui.input_bar = value.clone();
}
if let Some(value) = &ui_spec.status_bar { if let Some(value) = &ui_spec.status_bar {
self.ui.status_bar = value.clone(); self.ui.status_bar = value.clone();
} }
@ -273,16 +273,6 @@ impl Config {
self.ui.remote_control = value.clone(); self.ui.remote_control = value.clone();
} }
// Apply input_header
if let Some(value) = &ui_spec.input_header {
self.ui.input_header = Some(value.clone());
}
// Apply input_prompt
if let Some(value) = &ui_spec.input_prompt {
self.ui.input_prompt.clone_from(value);
}
// Handle preview_panel with field merging // Handle preview_panel with field merging
if let Some(preview_panel) = &ui_spec.preview_panel { if let Some(preview_panel) = &ui_spec.preview_panel {
self.ui.preview_panel.size = preview_panel.size; self.ui.preview_panel.size = preview_panel.size;
@ -498,8 +488,8 @@ mod tests {
} }
const USER_CONFIG_INPUT_PROMPT: &str = r#" const USER_CONFIG_INPUT_PROMPT: &str = r#"
[ui] [ui.input_bar]
input_prompt = "" prompt = ""
"#; "#;
#[test] #[test]
@ -518,7 +508,7 @@ mod tests {
let config = Config::new(&config_env, None).unwrap(); let config = Config::new(&config_env, None).unwrap();
// Verify that input_prompt was loaded from user config // Verify that input_prompt was loaded from user config
assert_eq!(config.ui.input_prompt, ""); assert_eq!(config.ui.input_bar.prompt, "");
} }
#[test] #[test]

View File

@ -232,7 +232,7 @@ struct Inner {
// input // input
input_text_fg: String, input_text_fg: String,
result_count_fg: String, result_count_fg: String,
//results // results
result_name_fg: String, result_name_fg: String,
result_line_number_fg: String, result_line_number_fg: String,
result_value_fg: String, result_value_fg: String,
@ -241,9 +241,9 @@ struct Inner {
// and falls back to match_fg // and falls back to match_fg
selection_fg: Option<String>, selection_fg: Option<String>,
match_fg: String, match_fg: String,
//preview // preview
preview_title_fg: String, preview_title_fg: String,
//modes // modes
channel_mode_fg: String, channel_mode_fg: String,
channel_mode_bg: Option<String>, channel_mode_bg: Option<String>,
remote_control_mode_fg: String, remote_control_mode_fg: String,

View File

@ -9,6 +9,26 @@ use serde::{Deserialize, Serialize};
pub const DEFAULT_UI_SCALE: u16 = 100; pub const DEFAULT_UI_SCALE: u16 = 100;
pub const DEFAULT_PREVIEW_SIZE: u16 = 50; 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,
}
impl Default for InputBarConfig {
fn default() -> Self {
Self {
position: InputPosition::default(),
header: None,
prompt: ">".to_string(),
border_type: BorderType::default(),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Hash, Default)] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Hash, Default)]
#[serde(default)] #[serde(default)]
pub struct StatusBarConfig { pub struct StatusBarConfig {
@ -16,6 +36,12 @@ pub struct StatusBarConfig {
pub separator_close: String, pub separator_close: String,
} }
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Hash, Default)]
#[serde(default)]
pub struct ResultsPanelConfig {
pub border_type: BorderType,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Hash)] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Hash)]
#[serde(default)] #[serde(default)]
pub struct PreviewPanelConfig { pub struct PreviewPanelConfig {
@ -23,6 +49,7 @@ pub struct PreviewPanelConfig {
pub header: Option<Template>, pub header: Option<Template>,
pub footer: Option<Template>, pub footer: Option<Template>,
pub scrollbar: bool, pub scrollbar: bool,
pub border_type: BorderType,
} }
impl Default for PreviewPanelConfig { impl Default for PreviewPanelConfig {
@ -32,6 +59,7 @@ impl Default for PreviewPanelConfig {
header: None, header: None,
footer: None, footer: None,
scrollbar: true, scrollbar: true,
border_type: BorderType::default(),
} }
} }
} }
@ -104,17 +132,15 @@ pub struct ThemeOverrides {
pub struct UiConfig { pub struct UiConfig {
pub use_nerd_font_icons: bool, pub use_nerd_font_icons: bool,
pub ui_scale: u16, pub ui_scale: u16,
pub input_bar_position: InputPosition,
pub orientation: Orientation, pub orientation: Orientation,
pub theme: String, pub theme: String,
pub input_header: Option<Template>,
#[serde(default = "default_input_prompt")]
pub input_prompt: String,
pub features: Features, pub features: Features,
// Feature-specific configurations // Feature-specific configurations
pub input_bar: InputBarConfig,
pub status_bar: StatusBarConfig, pub status_bar: StatusBarConfig,
pub preview_panel: PreviewPanelConfig, pub preview_panel: PreviewPanelConfig,
pub results_panel: ResultsPanelConfig,
pub help_panel: HelpPanelConfig, pub help_panel: HelpPanelConfig,
pub remote_control: RemoteControlConfig, pub remote_control: RemoteControlConfig,
@ -123,27 +149,43 @@ pub struct UiConfig {
pub theme_overrides: ThemeOverrides, pub theme_overrides: ThemeOverrides,
} }
const DEFAULT_INPUT_PROMPT: &str = ">";
fn default_input_prompt() -> String {
String::from(DEFAULT_INPUT_PROMPT)
}
impl Default for UiConfig { impl Default for UiConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
use_nerd_font_icons: false, use_nerd_font_icons: false,
ui_scale: DEFAULT_UI_SCALE, ui_scale: DEFAULT_UI_SCALE,
input_bar_position: InputPosition::Top,
orientation: Orientation::Landscape, orientation: Orientation::Landscape,
theme: String::from(DEFAULT_THEME), theme: String::from(DEFAULT_THEME),
input_header: None,
input_prompt: String::from(DEFAULT_INPUT_PROMPT),
features: Features::default(), features: Features::default(),
input_bar: InputBarConfig::default(),
status_bar: StatusBarConfig::default(), status_bar: StatusBarConfig::default(),
preview_panel: PreviewPanelConfig::default(), preview_panel: PreviewPanelConfig::default(),
results_panel: ResultsPanelConfig::default(),
help_panel: HelpPanelConfig::default(), help_panel: HelpPanelConfig::default(),
remote_control: RemoteControlConfig::default(), remote_control: RemoteControlConfig::default(),
theme_overrides: ThemeOverrides::default(), theme_overrides: ThemeOverrides::default(),
} }
} }
} }
#[derive(Clone, 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),
}
}
}

View File

@ -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.results_picker.entries,
&ctx.tv_state.channel_state.selected_entries, &ctx.tv_state.channel_state.selected_entries,
&mut ctx.tv_state.results_picker.relative_state.clone(), &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.config.ui.use_nerd_font_icons,
&ctx.colorscheme, &ctx.colorscheme,
&ctx.config.ui.results_panel,
)?; )?;
// input box
let input_prompt = ctx.config.ui.input_prompt.clone();
draw_input_box( draw_input_box(
f, f,
layout.input, 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.channel_state.current_channel_name,
&ctx.tv_state.spinner, &ctx.tv_state.spinner,
&ctx.colorscheme, &ctx.colorscheme,
&ctx.config.ui.input_header, &ctx.config.ui.input_bar,
&input_prompt,
&ctx.config.ui.input_bar_position,
)?; )?;
// status bar at the bottom // 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.tv_state.preview_state,
ctx.config.ui.use_nerd_font_icons, ctx.config.ui.use_nerd_font_icons,
&ctx.colorscheme, &ctx.colorscheme,
ctx.config.ui.preview_panel.scrollbar, &ctx.config.ui.preview_panel,
)?; )?;
} }

View File

@ -4,11 +4,10 @@ use std::env;
use std::io::{BufWriter, IsTerminal, Write, stdout}; use std::io::{BufWriter, IsTerminal, Write, stdout};
use std::path::PathBuf; use std::path::PathBuf;
use std::process::exit; use std::process::exit;
use television::cable::cable_empty_exit;
use television::{ use television::{
action::Action, action::Action,
app::{App, AppOptions}, app::{App, AppOptions},
cable::{Cable, load_cable}, cable::{Cable, cable_empty_exit, load_cable},
channels::prototypes::{ channels::prototypes::{
ChannelPrototype, CommandSpec, PreviewSpec, Template, UiSpec, ChannelPrototype, CommandSpec, PreviewSpec, Template, UiSpec,
}, },
@ -18,7 +17,7 @@ use television::{
args::{Cli, Command}, args::{Cli, Command},
guess_channel_from_prompt, list_channels, guess_channel_from_prompt, list_channels,
}, },
config::{Config, ConfigEnv, merge_bindings}, config::{Config, ConfigEnv, merge_bindings, ui::InputBarConfig},
errors::os_error_exit, errors::os_error_exit,
features::FeatureFlags, features::FeatureFlags,
gh::update_local_channels, 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); config.ui.ui_scale = args.ui_scale.unwrap_or(config.ui.ui_scale);
if let Some(input_header) = &args.input_header { if let Some(input_header) = &args.input_header {
if let Ok(t) = Template::parse(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 { 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 Some(preview_header) = &args.preview_header {
if let Ok(t) = Template::parse(preview_header) { if let Ok(t) = Template::parse(preview_header) {
@ -330,7 +329,10 @@ fn create_adhoc_channel(
// Set UI specification // Set UI specification
let mut ui_spec = UiSpec::from(&config.ui); 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); ui_spec.features = Some(features);
prototype.ui = Some(ui_spec); prototype.ui = Some(ui_spec);
@ -389,10 +391,9 @@ fn apply_ui_overrides(
ui_scale: None, ui_scale: None,
features: None, features: None,
orientation: None, orientation: None,
input_bar_position: None, input_bar: None,
input_header: None,
input_prompt: None,
preview_panel: None, preview_panel: None,
results_panel: None,
status_bar: None, status_bar: None,
help_panel: None, help_panel: None,
remote_control: None, remote_control: None,
@ -401,14 +402,20 @@ fn apply_ui_overrides(
// Apply input header override // Apply input header override
if let Some(input_header_str) = &args.input_header { if let Some(input_header_str) = &args.input_header {
if let Ok(template) = Template::parse(input_header_str) { 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; ui_changes_needed = true;
} }
} }
// Apply input prompt override // Apply input prompt override
if let Some(input_prompt_str) = &args.input_prompt { 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; ui_changes_needed = true;
} }
@ -507,8 +514,12 @@ pub fn determine_channel(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use television::channels::prototypes::{ use television::{
ChannelPrototype, CommandSpec, PreviewSpec, Template, channels::prototypes::{
ChannelPrototype, CommandSpec, PreviewSpec, Template,
},
config::ui::{BorderType, InputBarConfig},
screen::layout::InputPosition,
}; };
use super::*; use super::*;
@ -716,7 +727,7 @@ mod tests {
// Preview should be disabled since no preview command was provided // Preview should be disabled since no preview command was provided
assert!(!features.is_enabled(FeatureFlags::PreviewPanel)); assert!(!features.is_enabled(FeatureFlags::PreviewPanel));
assert_eq!( assert_eq!(
ui_spec.input_header, ui_spec.input_bar.as_ref().unwrap().header,
Some(Template::parse("Custom Channel").unwrap()) Some(Template::parse("Custom Channel").unwrap())
); );
} }
@ -737,7 +748,7 @@ mod tests {
assert_eq!(config.application.tick_rate, 100_f64); assert_eq!(config.application.tick_rate, 100_f64);
assert!(!config.ui.features.is_enabled(FeatureFlags::PreviewPanel)); assert!(!config.ui.features.is_enabled(FeatureFlags::PreviewPanel));
assert_eq!( assert_eq!(
config.ui.input_header, config.ui.input_bar.header,
Some(Template::parse("Input Header").unwrap()) Some(Template::parse("Input Header").unwrap())
); );
assert_eq!( assert_eq!(
@ -761,9 +772,12 @@ mod tests {
ui_scale: None, ui_scale: None,
features: None, features: None,
orientation: Some(Orientation::Portrait), orientation: Some(Orientation::Portrait),
input_bar_position: None, input_bar: Some(InputBarConfig {
input_header: Some(Template::parse("Original Header").unwrap()), position: InputPosition::default(),
input_prompt: None, header: Some(Template::parse("Original Header").unwrap()),
prompt: ">".to_string(),
border_type: BorderType::Rounded,
}),
preview_panel: Some(television::config::ui::PreviewPanelConfig { preview_panel: Some(television::config::ui::PreviewPanelConfig {
size: 50, size: 50,
header: Some( header: Some(
@ -773,7 +787,9 @@ mod tests {
Template::parse("Original Preview Footer").unwrap(), Template::parse("Original Preview Footer").unwrap(),
), ),
scrollbar: false, scrollbar: false,
border_type: BorderType::default(),
}), }),
results_panel: None,
status_bar: None, status_bar: None,
help_panel: None, help_panel: None,
remote_control: None, remote_control: None,
@ -800,7 +816,7 @@ mod tests {
let ui_spec = result_channel.ui.as_ref().unwrap(); let ui_spec = result_channel.ui.as_ref().unwrap();
assert_eq!( assert_eq!(
ui_spec.input_header, ui_spec.input_bar.as_ref().unwrap().header,
Some(Template::parse("CLI Input Header").unwrap()) Some(Template::parse("CLI Input Header").unwrap())
); );
assert_eq!(ui_spec.orientation, Some(Orientation::Landscape)); assert_eq!(ui_spec.orientation, Some(Orientation::Landscape));

View File

@ -1,5 +1,5 @@
use crate::{ use crate::{
channels::prototypes::Template, config::ui::InputBarConfig,
screen::{colors::Colorscheme, layout::InputPosition, spinner::Spinner}, screen::{colors::Colorscheme, layout::InputPosition, spinner::Spinner},
utils::input::Input, utils::input::Input,
}; };
@ -11,9 +11,7 @@ use ratatui::{
}, },
style::{Style, Stylize}, style::{Style, Stylize},
text::{Line, Span}, text::{Line, Span},
widgets::{ widgets::{Block, Borders, ListState, Paragraph, block::Position},
Block, BorderType, Borders, ListState, Paragraph, block::Position,
},
}; };
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
@ -28,19 +26,15 @@ pub fn draw_input_box(
channel_name: &str, channel_name: &str,
spinner: &Spinner, spinner: &Spinner,
colorscheme: &Colorscheme, colorscheme: &Colorscheme,
input_header: &Option<Template>, input_bar_config: &InputBarConfig,
input_prompt: &str,
input_bar_position: &InputPosition,
) -> Result<()> { ) -> Result<()> {
let header = input_header let header = input_bar_config
.header
.as_ref() .as_ref()
.and_then(|tpl| tpl.format(channel_name).ok()) .and_then(|tpl| tpl.format(channel_name).ok())
.unwrap_or_else(|| channel_name.to_string()); .unwrap_or_else(|| channel_name.to_string());
let input_block = Block::default() let mut input_block = Block::default()
.borders(Borders::ALL) .title_position(match input_bar_config.position {
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(colorscheme.general.border_fg))
.title_position(match input_bar_position {
InputPosition::Top => Position::Top, InputPosition::Top => Position::Top,
InputPosition::Bottom => Position::Bottom, InputPosition::Bottom => Position::Bottom,
}) })
@ -53,6 +47,14 @@ pub fn draw_input_box(
Style::default() Style::default()
.bg(colorscheme.general.background.unwrap_or_default()), .bg(colorscheme.general.background.unwrap_or_default()),
); );
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); let input_block_inner = input_block.inner(rect);
if input_block_inner.area() == 0 { if input_block_inner.area() == 0 {
@ -67,7 +69,8 @@ pub fn draw_input_box(
.constraints([ .constraints([
// prompt symbol + space // prompt symbol + space
Constraint::Length( 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 // input field
Constraint::Fill(1), Constraint::Fill(1),
@ -84,7 +87,7 @@ pub fn draw_input_box(
let arrow_block = Block::default(); let arrow_block = Block::default();
let arrow = Paragraph::new(Span::styled( let arrow = Paragraph::new(Span::styled(
format!("{} ", input_prompt), format!("{} ", input_bar_config.prompt),
Style::default().fg(colorscheme.input.input_fg).bold(), Style::default().fg(colorscheme.input.input_fg).bold(),
)) ))
.block(arrow_block); .block(arrow_block);

View File

@ -194,7 +194,7 @@ impl Layout {
// results percentage is whatever remains // results percentage is whatever remains
let results_percentage = 100u16.saturating_sub(preview_percentage); 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 // Preview is rendered on the right or bottom depending on orientation
(Orientation::Landscape, _) (Orientation::Landscape, _)
| (Orientation::Portrait, InputPosition::Top) => { | (Orientation::Portrait, InputPosition::Top) => {
@ -243,7 +243,7 @@ impl Layout {
// Now split the results window vertically into results list + input // Now split the results window vertically into results list + input
let result_chunks = layout::Layout::default() let result_chunks = layout::Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints(match ui_config.input_bar_position { .constraints(match ui_config.input_bar.position {
InputPosition::Top => results_constraints InputPosition::Top => results_constraints
.clone() .clone()
.into_iter() .into_iter()
@ -254,7 +254,8 @@ impl Layout {
.split(result_window); .split(result_window);
let (input_rect, results_rect) = match ui_config let (input_rect, results_rect) = match ui_config
.input_bar_position .input_bar
.position
{ {
InputPosition::Bottom => { InputPosition::Bottom => {
(result_chunks[1], result_chunks[0]) (result_chunks[1], result_chunks[0])
@ -278,7 +279,7 @@ impl Layout {
let mut portrait_constraints: Vec<Constraint> = Vec::new(); let mut portrait_constraints: Vec<Constraint> = Vec::new();
match ui_config.input_bar_position { match ui_config.input_bar.position {
InputPosition::Top => { InputPosition::Top => {
// Input bar is always the first chunk // Input bar is always the first chunk
portrait_constraints portrait_constraints

View File

@ -1,5 +1,6 @@
use crate::{ use crate::{
previewer::state::PreviewState, config::ui::{BorderType, PreviewPanelConfig},
previewer::{Preview, state::PreviewState},
screen::colors::Colorscheme, screen::colors::Colorscheme,
utils::strings::{ utils::strings::{
EMPTY_STRING, ReplaceNonPrintableConfig, replace_non_printable, EMPTY_STRING, ReplaceNonPrintableConfig, replace_non_printable,
@ -7,13 +8,12 @@ use crate::{
}, },
}; };
use anyhow::Result; use anyhow::Result;
use devicons::FileIcon;
use ratatui::{ use ratatui::{
Frame, Frame,
layout::{Alignment, Rect}, layout::{Alignment, Rect},
prelude::{Color, Line, Span, Style, Stylize, Text}, prelude::{Color, Line, Span, Style, Stylize, Text},
widgets::{ widgets::{
Block, BorderType, Borders, Clear, Padding, Paragraph, Scrollbar, Block, Borders, Clear, Padding, Paragraph, Scrollbar,
ScrollbarOrientation, ScrollbarState, StatefulWidget, ScrollbarOrientation, ScrollbarState, StatefulWidget,
}, },
}; };
@ -26,15 +26,14 @@ pub fn draw_preview_content_block(
preview_state: PreviewState, preview_state: PreviewState,
use_nerd_font_icons: bool, use_nerd_font_icons: bool,
colorscheme: &Colorscheme, colorscheme: &Colorscheme,
scrollbar_enabled: bool, preview_panel_config: &PreviewPanelConfig,
) -> Result<()> { ) -> Result<()> {
let inner = draw_content_outer_block( let inner = draw_content_outer_block(
f, f,
rect, rect,
colorscheme, colorscheme,
preview_state.preview.icon, &preview_panel_config.border_type,
&preview_state.preview.title, &preview_state.preview,
&preview_state.preview.footer,
use_nerd_font_icons, use_nerd_font_icons,
)?; )?;
let scroll = preview_state.scroll as usize; let scroll = preview_state.scroll as usize;
@ -50,7 +49,7 @@ pub fn draw_preview_content_block(
f.render_widget(rp, inner); f.render_widget(rp, inner);
// render scrollbar if enabled // render scrollbar if enabled
if scrollbar_enabled { if preview_panel_config.scrollbar {
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
.style(Style::default().fg(colorscheme.general.border_fg)); .style(Style::default().fg(colorscheme.general.border_fg));
@ -165,15 +164,14 @@ fn draw_content_outer_block(
f: &mut Frame, f: &mut Frame,
rect: Rect, rect: Rect,
colorscheme: &Colorscheme, colorscheme: &Colorscheme,
icon: Option<FileIcon>, border_type: &BorderType,
title: &str, preview: &Preview,
footer: &str,
use_nerd_font_icons: bool, use_nerd_font_icons: bool,
) -> Result<Rect> { ) -> Result<Rect> {
let mut preview_title_spans = vec![Span::from(" ")]; let mut preview_title_spans = vec![Span::from(" ")];
// optional icon // optional icon
if icon.is_some() && use_nerd_font_icons { if preview.icon.is_some() && use_nerd_font_icons {
let icon = icon.as_ref().unwrap(); let icon = preview.icon.as_ref().unwrap();
preview_title_spans.push(Span::styled( preview_title_spans.push(Span::styled(
{ {
let mut icon_str = String::from(icon.icon); let mut icon_str = String::from(icon.icon);
@ -187,7 +185,7 @@ fn draw_content_outer_block(
preview_title_spans.push(Span::styled( preview_title_spans.push(Span::styled(
shrink_with_ellipsis( shrink_with_ellipsis(
&replace_non_printable( &replace_non_printable(
title.as_bytes(), preview.title.as_bytes(),
&ReplaceNonPrintableConfig::default(), &ReplaceNonPrintableConfig::default(),
) )
.0, .0,
@ -205,10 +203,10 @@ fn draw_content_outer_block(
); );
// preview footer // preview footer
if !footer.is_empty() { if !preview.footer.is_empty() {
let footer_line = Line::from(vec![ let footer_line = Line::from(vec![
Span::from(" "), Span::from(" "),
Span::from(footer), Span::from(preview.footer.as_str()),
Span::from(" "), Span::from(" "),
]) ])
.alignment(Alignment::Center) .alignment(Alignment::Center)
@ -216,15 +214,18 @@ fn draw_content_outer_block(
block = block.title_bottom(footer_line); block = block.title_bottom(footer_line);
} }
let preview_outer_block = block let mut preview_outer_block = block
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(colorscheme.general.border_fg))
.style( .style(
Style::default() Style::default()
.bg(colorscheme.general.background.unwrap_or_default()), .bg(colorscheme.general.background.unwrap_or_default()),
) )
.padding(Padding::new(0, 1, 1, 0)); .padding(Padding::new(0, 1, 1, 0));
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); let inner = preview_outer_block.inner(rect);
f.render_widget(preview_outer_block, rect); f.render_widget(preview_outer_block, rect);

View File

@ -1,5 +1,6 @@
use crate::{ use crate::{
channels::entry::Entry, channels::entry::Entry,
config::ui::ResultsPanelConfig,
screen::{colors::Colorscheme, layout::InputPosition, result_item}, screen::{colors::Colorscheme, layout::InputPosition, result_item},
}; };
use anyhow::Result; use anyhow::Result;
@ -8,7 +9,7 @@ use ratatui::{
layout::{Alignment, Rect}, layout::{Alignment, Rect},
prelude::Style, prelude::Style,
text::Line, text::Line,
widgets::{Block, BorderType, Borders, ListState, Padding}, widgets::{Block, Borders, ListState, Padding},
}; };
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
@ -22,17 +23,23 @@ pub fn draw_results_list(
input_bar_position: InputPosition, input_bar_position: InputPosition,
use_nerd_font_icons: bool, use_nerd_font_icons: bool,
colorscheme: &Colorscheme, colorscheme: &Colorscheme,
results_panel_config: &ResultsPanelConfig,
) -> Result<()> { ) -> Result<()> {
let results_block = Block::default() let mut results_block = Block::default()
.title_top(Line::from(" Results ").alignment(Alignment::Center)) .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(
Style::default() Style::default()
.bg(colorscheme.general.background.unwrap_or_default()), .bg(colorscheme.general.background.unwrap_or_default()),
) )
.padding(Padding::right(1)); .padding(Padding::right(1));
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 { let list_direction = match input_bar_position {
InputPosition::Bottom => ratatui::widgets::ListDirection::BottomToTop, InputPosition::Bottom => ratatui::widgets::ListDirection::BottomToTop,

View File

@ -111,7 +111,7 @@ impl Television {
debug!("Merged config: {:?}", config); debug!("Merged config: {:?}", config);
let mut results_picker = Picker::new(input.clone()); 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(); results_picker = results_picker.inverted();
} }
@ -206,9 +206,11 @@ impl Television {
// ui // ui
if let Some(ui_spec) = &channel_prototype.ui { if let Some(ui_spec) = &channel_prototype.ui {
config.apply_prototype_ui_spec(ui_spec); config.apply_prototype_ui_spec(ui_spec);
if config.ui.input_header.is_none() { if config.ui.input_bar.header.is_none() {
if let Some(header_tpl) = &ui_spec.input_header { if let Some(input_bar) = &ui_spec.input_bar {
config.ui.input_header = Some(header_tpl.clone()); 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() { if config.ui.preview_panel.header.is_none() {