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
# 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

View File

@ -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!(
@ -734,9 +736,8 @@ 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!(ui.input_bar.is_none());
assert!(ui.preview_panel.is_some());
assert!(ui.input_header.is_none());
assert_eq!(
ui.preview_panel
.as_ref()

View File

@ -255,14 +255,14 @@ 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 {
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 {
self.ui.status_bar = value.clone();
}
@ -273,16 +273,6 @@ 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());
}
// Apply input_prompt
if let Some(value) = &ui_spec.input_prompt {
self.ui.input_prompt.clone_from(value);
}
// Handle preview_panel with field merging
if let Some(preview_panel) = &ui_spec.preview_panel {
self.ui.preview_panel.size = preview_panel.size;
@ -498,8 +488,8 @@ mod tests {
}
const USER_CONFIG_INPUT_PROMPT: &str = r#"
[ui]
input_prompt = ""
[ui.input_bar]
prompt = ""
"#;
#[test]
@ -518,7 +508,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]

View File

@ -232,7 +232,7 @@ struct Inner {
// input
input_text_fg: String,
result_count_fg: String,
//results
// results
result_name_fg: String,
result_line_number_fg: String,
result_value_fg: String,
@ -241,9 +241,9 @@ struct Inner {
// and falls back to match_fg
selection_fg: Option<String>,
match_fg: String,
//preview
// preview
preview_title_fg: String,
//modes
// modes
channel_mode_fg: String,
channel_mode_bg: Option<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_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)]
#[serde(default)]
pub struct StatusBarConfig {
@ -16,6 +36,12 @@ pub struct StatusBarConfig {
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)]
#[serde(default)]
pub struct PreviewPanelConfig {
@ -23,6 +49,7 @@ pub struct PreviewPanelConfig {
pub header: Option<Template>,
pub footer: Option<Template>,
pub scrollbar: bool,
pub border_type: BorderType,
}
impl Default for PreviewPanelConfig {
@ -32,6 +59,7 @@ impl Default for PreviewPanelConfig {
header: None,
footer: None,
scrollbar: true,
border_type: BorderType::default(),
}
}
}
@ -104,17 +132,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 +149,43 @@ 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, 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.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,
)?;
}

View File

@ -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) {
@ -330,7 +329,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 +391,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 +402,20 @@ 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;
}
@ -507,8 +514,12 @@ pub fn determine_channel(
#[cfg(test)]
mod tests {
use rustc_hash::FxHashMap;
use television::channels::prototypes::{
ChannelPrototype, CommandSpec, PreviewSpec, Template,
use television::{
channels::prototypes::{
ChannelPrototype, CommandSpec, PreviewSpec, Template,
},
config::ui::{BorderType, InputBarConfig},
screen::layout::InputPosition,
};
use super::*;
@ -716,7 +727,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())
);
}
@ -737,7 +748,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!(
@ -761,9 +772,12 @@ 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::Rounded,
}),
preview_panel: Some(television::config::ui::PreviewPanelConfig {
size: 50,
header: Some(
@ -773,7 +787,9 @@ mod tests {
Template::parse("Original Preview Footer").unwrap(),
),
scrollbar: false,
border_type: BorderType::default(),
}),
results_panel: None,
status_bar: None,
help_panel: None,
remote_control: None,
@ -800,7 +816,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));

View File

@ -1,5 +1,5 @@
use crate::{
channels::prototypes::Template,
config::ui::InputBarConfig,
screen::{colors::Colorscheme, layout::InputPosition, spinner::Spinner},
utils::input::Input,
};
@ -11,9 +11,7 @@ use ratatui::{
},
style::{Style, Stylize},
text::{Line, Span},
widgets::{
Block, BorderType, Borders, ListState, Paragraph, block::Position,
},
widgets::{Block, Borders, ListState, Paragraph, block::Position},
};
#[allow(clippy::too_many_arguments)]
@ -28,19 +26,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,
})
@ -53,6 +47,14 @@ pub fn draw_input_box(
Style::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);
if input_block_inner.area() == 0 {
@ -67,7 +69,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 +87,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);

View File

@ -194,7 +194,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 +243,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 +254,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 +279,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

View File

@ -1,5 +1,6 @@
use crate::{
previewer::state::PreviewState,
config::ui::{BorderType, PreviewPanelConfig},
previewer::{Preview, state::PreviewState},
screen::colors::Colorscheme,
utils::strings::{
EMPTY_STRING, ReplaceNonPrintableConfig, replace_non_printable,
@ -7,13 +8,12 @@ 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,
Block, Borders, Clear, Padding, Paragraph, Scrollbar,
ScrollbarOrientation, ScrollbarState, StatefulWidget,
},
};
@ -26,15 +26,14 @@ 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_state.preview,
use_nerd_font_icons,
)?;
let scroll = preview_state.scroll as usize;
@ -50,7 +49,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));
@ -165,15 +164,14 @@ fn draw_content_outer_block(
f: &mut Frame,
rect: Rect,
colorscheme: &Colorscheme,
icon: Option<FileIcon>,
title: &str,
footer: &str,
border_type: &BorderType,
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 +185,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 +203,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 +214,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));
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);

View File

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

View File

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