Skip to main content

television/config/
ui.rs

1use crate::{
2    channels::prototypes::Template,
3    config::themes::DEFAULT_THEME,
4    screen::layout::{InputPosition, Orientation},
5};
6use serde::{Deserialize, Serialize};
7
8pub const DEFAULT_UI_SCALE: u16 = 100;
9pub const DEFAULT_PREVIEW_SIZE: u16 = 50;
10pub const DEFAULT_PROMPT: &str = ">";
11
12#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Hash)]
13#[serde(default)]
14pub struct InputBarConfig {
15    pub position: InputPosition,
16    pub header: Option<String>,
17    #[serde(default = "default_prompt")]
18    pub prompt: Option<String>,
19    pub border_type: BorderType,
20    pub padding: Padding,
21}
22
23impl Default for InputBarConfig {
24    fn default() -> Self {
25        Self {
26            position: InputPosition::default(),
27            header: None,
28            prompt: Some(String::from(DEFAULT_PROMPT)),
29            border_type: BorderType::default(),
30            padding: Padding::uniform(0),
31        }
32    }
33}
34
35#[allow(clippy::unnecessary_wraps)]
36fn default_prompt() -> Option<String> {
37    Some(String::from(DEFAULT_PROMPT))
38}
39
40#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Hash, Default)]
41#[serde(default)]
42pub struct StatusBarConfig {
43    pub separator_open: String,
44    pub separator_close: String,
45    pub hidden: bool,
46}
47
48#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Hash, Default)]
49#[serde(default)]
50pub struct ResultsPanelConfig {
51    pub border_type: BorderType,
52    pub padding: Padding,
53}
54
55#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Hash)]
56#[serde(default)]
57pub struct PreviewPanelConfig {
58    pub size: u16,
59    pub header: Option<Template>,
60    pub footer: Option<Template>,
61    pub scrollbar: bool,
62    pub border_type: BorderType,
63    pub padding: Padding,
64    pub word_wrap: bool,
65    pub hidden: bool,
66}
67
68impl Default for PreviewPanelConfig {
69    fn default() -> Self {
70        Self {
71            size: DEFAULT_PREVIEW_SIZE,
72            header: None,
73            footer: None,
74            scrollbar: true,
75            border_type: BorderType::default(),
76            padding: Padding::uniform(0),
77            hidden: false,
78            word_wrap: false,
79        }
80    }
81}
82
83#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Hash)]
84#[serde(default)]
85pub struct HelpPanelConfig {
86    pub show_categories: bool,
87    #[serde(default = "default_help_hidden")]
88    pub hidden: bool,
89    pub disabled: bool,
90}
91
92fn default_help_hidden() -> bool {
93    true
94}
95
96impl Default for HelpPanelConfig {
97    fn default() -> Self {
98        Self {
99            show_categories: true,
100            hidden: true,
101            disabled: false,
102        }
103    }
104}
105
106#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Hash)]
107#[serde(default)]
108pub struct RemoteControlConfig {
109    pub show_channel_descriptions: bool,
110    pub sort_alphabetically: bool,
111    pub disabled: bool,
112}
113
114impl Default for RemoteControlConfig {
115    fn default() -> Self {
116        Self {
117            show_channel_descriptions: true,
118            sort_alphabetically: true,
119            disabled: false,
120        }
121    }
122}
123
124/// Theme color overrides that can be specified in the configuration file
125/// to customize the appearance of the selected theme
126#[derive(
127    Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash, Default,
128)]
129#[serde(default)]
130pub struct ThemeOverrides {
131    // General colors
132    pub background: Option<String>,
133    pub border_fg: Option<String>,
134    pub text_fg: Option<String>,
135    pub dimmed_text_fg: Option<String>,
136
137    // Input colors
138    pub input_text_fg: Option<String>,
139    pub result_count_fg: Option<String>,
140
141    // Result colors
142    pub result_name_fg: Option<String>,
143    pub result_line_number_fg: Option<String>,
144    pub result_value_fg: Option<String>,
145    pub selection_bg: Option<String>,
146    pub selection_fg: Option<String>,
147    pub match_fg: Option<String>,
148
149    // Preview colors
150    pub preview_title_fg: Option<String>,
151
152    // Mode colors
153    pub channel_mode_fg: Option<String>,
154    pub channel_mode_bg: Option<String>,
155    pub remote_control_mode_fg: Option<String>,
156    pub remote_control_mode_bg: Option<String>,
157    pub action_picker_mode_fg: Option<String>,
158    pub action_picker_mode_bg: Option<String>,
159}
160
161impl ThemeOverrides {
162    pub fn merge(self, other: Self) -> Self {
163        Self {
164            background: self.background.or(other.background),
165            border_fg: self.border_fg.or(other.border_fg),
166            text_fg: self.text_fg.or(other.text_fg),
167            dimmed_text_fg: self.dimmed_text_fg.or(other.dimmed_text_fg),
168            input_text_fg: self.input_text_fg.or(other.input_text_fg),
169            result_count_fg: self.result_count_fg.or(other.result_count_fg),
170            result_name_fg: self.result_name_fg.or(other.result_name_fg),
171            result_line_number_fg: self
172                .result_line_number_fg
173                .or(other.result_line_number_fg),
174            result_value_fg: self.result_value_fg.or(other.result_value_fg),
175            selection_bg: self.selection_bg.or(other.selection_bg),
176            selection_fg: self.selection_fg.or(other.selection_fg),
177            match_fg: self.match_fg.or(other.match_fg),
178            preview_title_fg: self.preview_title_fg.or(other.preview_title_fg),
179            channel_mode_fg: self.channel_mode_fg.or(other.channel_mode_fg),
180            channel_mode_bg: self.channel_mode_bg.or(other.channel_mode_bg),
181            remote_control_mode_fg: self
182                .remote_control_mode_fg
183                .or(other.remote_control_mode_fg),
184            remote_control_mode_bg: self
185                .remote_control_mode_bg
186                .or(other.remote_control_mode_bg),
187            action_picker_mode_fg: self
188                .action_picker_mode_fg
189                .or(other.action_picker_mode_fg),
190            action_picker_mode_bg: self
191                .action_picker_mode_bg
192                .or(other.action_picker_mode_bg),
193        }
194    }
195}
196
197#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Hash)]
198#[serde(default)]
199pub struct UiConfig {
200    pub ui_scale: u16,
201    pub orientation: Orientation,
202    pub theme: String,
203
204    // Feature-specific configurations
205    pub input_bar: InputBarConfig,
206    pub status_bar: StatusBarConfig,
207    pub preview_panel: PreviewPanelConfig,
208    pub results_panel: ResultsPanelConfig,
209    pub help_panel: HelpPanelConfig,
210    pub remote_control: RemoteControlConfig,
211
212    // Theme color overrides
213    #[serde(default)]
214    pub theme_overrides: ThemeOverrides,
215}
216
217impl Default for UiConfig {
218    fn default() -> Self {
219        Self {
220            ui_scale: DEFAULT_UI_SCALE,
221            orientation: Orientation::Landscape,
222            theme: String::from(DEFAULT_THEME),
223            input_bar: InputBarConfig::default(),
224            status_bar: StatusBarConfig::default(),
225            preview_panel: PreviewPanelConfig::default(),
226            results_panel: ResultsPanelConfig::default(),
227            help_panel: HelpPanelConfig::default(),
228            remote_control: RemoteControlConfig::default(),
229            theme_overrides: ThemeOverrides::default(),
230        }
231    }
232}
233
234#[derive(
235    Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Hash, Default, Eq,
236)]
237#[serde(rename_all = "snake_case")]
238pub enum BorderType {
239    None,
240    Plain,
241    #[default]
242    Rounded,
243    Thick,
244}
245
246impl BorderType {
247    pub fn to_ratatui_border_type(
248        &self,
249    ) -> Option<ratatui::widgets::BorderType> {
250        match self {
251            BorderType::None => None,
252            BorderType::Plain => Some(ratatui::widgets::BorderType::Plain),
253            BorderType::Rounded => Some(ratatui::widgets::BorderType::Rounded),
254            BorderType::Thick => Some(ratatui::widgets::BorderType::Thick),
255        }
256    }
257}
258
259impl From<crate::cli::args::BorderType> for BorderType {
260    fn from(border_type: crate::cli::args::BorderType) -> Self {
261        match border_type {
262            crate::cli::args::BorderType::None => BorderType::None,
263            crate::cli::args::BorderType::Plain => BorderType::Plain,
264            crate::cli::args::BorderType::Rounded => BorderType::Rounded,
265            crate::cli::args::BorderType::Thick => BorderType::Thick,
266        }
267    }
268}
269
270#[derive(
271    Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Hash, Default, Eq,
272)]
273#[serde(default)]
274pub struct Padding {
275    pub top: u16,
276    pub bottom: u16,
277    pub left: u16,
278    pub right: u16,
279}
280
281impl Padding {
282    pub fn new(top: u16, bottom: u16, left: u16, right: u16) -> Self {
283        Self {
284            top,
285            bottom,
286            left,
287            right,
288        }
289    }
290
291    pub fn uniform(padding: u16) -> Self {
292        Self {
293            top: padding,
294            bottom: padding,
295            left: padding,
296            right: padding,
297        }
298    }
299
300    pub fn horizontal(padding: u16) -> Self {
301        Self {
302            top: 0,
303            bottom: 0,
304            left: padding,
305            right: padding,
306        }
307    }
308
309    pub fn vertical(padding: u16) -> Self {
310        Self {
311            top: padding,
312            bottom: padding,
313            left: 0,
314            right: 0,
315        }
316    }
317}
318
319impl From<Padding> for ratatui::widgets::Padding {
320    fn from(padding: Padding) -> Self {
321        ratatui::widgets::Padding {
322            top: padding.top,
323            bottom: padding.bottom,
324            left: padding.left,
325            right: padding.right,
326        }
327    }
328}