use serde::{ de::{Error, Visitor}, Deserialize, Deserializer, Serialize, Serializer, }; use std::{ collections::{BTreeMap, HashMap}, fmt, }; use crate::data::Styling; /// How a status bar action button is decorated (OSC 5577). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)] pub enum StatusBarButtonStyle { /// `[Approve]` -- bracketed, no index. Boxed, /// `1:Approve` -- the button's 1-based position, then its label. Useful when /// something else binds those numbers to the same actions, so the bar shows /// which key does what. Indexed, } impl Default for StatusBarButtonStyle { fn default() -> Self { StatusBarButtonStyle::Boxed } } impl StatusBarButtonStyle { pub fn from_str(name: &str) -> Option { match name.trim().to_lowercase().as_str() { "boxed" => Some(StatusBarButtonStyle::Boxed), "indexed" => Some(StatusBarButtonStyle::Indexed), _ => None, } } pub fn possible_values() -> &'static str { "\"boxed\", \"indexed\"" } /// How this style renders a button. `index` is 1-based. pub fn render(&self, index: usize, label: &str) -> String { match self { StatusBarButtonStyle::Boxed => format!("[{}]", label), StatusBarButtonStyle::Indexed => format!("{}:{}", index, label), } } } /// Visual treatment of the stacked-pane picker's entries. #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] pub enum StackListStyle { /// Each entry bracketed in vertical bars, with a "> " marker on the active /// one. Boxed, /// No brackets and no marker; the active entry is shown by inverting its /// foreground and background instead. Minimal, } impl Default for StackListStyle { fn default() -> Self { StackListStyle::Boxed } } impl StackListStyle { pub fn from_str(name: &str) -> Option { match name.trim().to_lowercase().as_str() { "boxed" => Some(StackListStyle::Boxed), "minimal" => Some(StackListStyle::Minimal), _ => None, } } pub fn possible_values() -> &'static str { "\"boxed\", \"minimal\"" } /// Columns an entry spends on decoration. The picker's column arithmetic /// lives in Tab and must agree with the renderer, so both read this. pub fn decoration_cols(&self) -> usize { match self { // "> " marker, plus a border and a pad on each side StackListStyle::Boxed => 6, // one pad each side, so the inverted block has some air around it StackListStyle::Minimal => 2, } } } #[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] pub struct UiConfig { pub pane_frames: FrameConfig, } impl UiConfig { pub fn merge(&self, other: UiConfig) -> Self { let mut merged = self.clone(); merged.pane_frames = merged.pane_frames.merge(other.pane_frames); merged } } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct FrameConfig { pub rounded_corners: bool, pub hide_session_name: bool, pub title_alignment: crate::data::PaneTitleAlignment, /// outer gap between panes, expressed in ROWS (see Style::pane_gap_y). /// Columns are scaled by `gap_aspect` so the gap looks square. pub gap: usize, /// inner padding between a pane's frame and its content, per side. /// Already resolved at parse time (including any aspect correction). pub padding: crate::data::PanePadding, /// Columns per row. Terminal cells are roughly twice as tall as they are /// wide, so 1 row of gap needs ~2 columns to look like the same distance. /// 0 is treated as 1 (no correction). pub gap_aspect: usize, /// How strongly to desaturate the CONTENT of unfocused panes, 0-100. /// 0 disables it entirely (the default). pub inactive_grayscale: u8, /// Omit the auto-generated "Pane #N" placeholder title, so a pane shows no /// name until the user sets one or the running program sets a title. pub hide_default_pane_names: bool, /// Collapse to the swap layout named `responsive_layout` when the terminal /// is narrower than this many columns. 0 disables the whole feature. pub responsive_min_cols: usize, /// Name of the swap tiled layout to use when below the threshold. pub responsive_layout: String, /// Cap on how many entries the stacked-pane picker shows at once. The list /// windows around the focused entry and reports the rest as "+N". 0 = show /// every member, however many rows that takes. pub stack_list_max_rows: usize, /// Pack picker entries into a grid instead of one per row. The row cap then /// becomes a space budget rather than a count. pub stack_list_grid: bool, /// Visual treatment of picker entries. pub stack_list_style: StackListStyle, /// How status bar action buttons are decorated. pub status_bar_button_style: StatusBarButtonStyle, } /// Hand-written rather than derived: `gap_aspect` must default to 2 to match /// the KDL parser's default. A derived Default would give 0, which makes a /// parsed config disagree with a default-constructed one and shows up as /// config round-trip diffs. impl Default for FrameConfig { fn default() -> Self { FrameConfig { rounded_corners: false, hide_session_name: false, title_alignment: Default::default(), gap: 0, padding: Default::default(), gap_aspect: 2, inactive_grayscale: 0, hide_default_pane_names: false, responsive_min_cols: 0, responsive_layout: String::from("stacked"), stack_list_max_rows: 0, stack_list_grid: false, stack_list_style: StackListStyle::Boxed, status_bar_button_style: StatusBarButtonStyle::Boxed, } } } impl FrameConfig { fn aspect(&self) -> usize { self.gap_aspect.max(1) } /// (columns, rows) of outer gap pub fn resolved_gap(&self) -> (usize, usize) { (self.gap * self.aspect(), self.gap) } pub fn resolved_padding(&self) -> crate::data::PanePadding { self.padding } } impl FrameConfig { pub fn merge(&self, other: FrameConfig) -> Self { let mut merged = self.clone(); merged.rounded_corners = other.rounded_corners; merged.hide_session_name = other.hide_session_name; merged.title_alignment = other.title_alignment; merged.gap = other.gap; merged.padding = other.padding; merged.gap_aspect = other.gap_aspect; merged.inactive_grayscale = other.inactive_grayscale; merged.hide_default_pane_names = other.hide_default_pane_names; merged.responsive_min_cols = other.responsive_min_cols; merged.responsive_layout = other.responsive_layout.clone(); merged.stack_list_max_rows = other.stack_list_max_rows; merged.stack_list_grid = other.stack_list_grid; merged.stack_list_style = other.stack_list_style; merged.status_bar_button_style = other.status_bar_button_style; merged } } #[derive(Clone, PartialEq, Default, Serialize, Deserialize)] pub struct Themes(HashMap); impl fmt::Debug for Themes { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut stable_sorted = BTreeMap::new(); for (theme_name, theme) in self.0.iter() { stable_sorted.insert(theme_name, theme); } write!(f, "{:#?}", stable_sorted) } } impl Themes { pub fn from_data(theme_data: HashMap) -> Self { Themes(theme_data) } pub fn insert(&mut self, theme_name: String, theme: Theme) { self.0.insert(theme_name, theme); } pub fn merge(&self, mut other: Themes) -> Self { let mut merged = self.clone(); for (name, theme) in other.0.drain() { merged.0.insert(name, theme); } merged } pub fn get_theme(&self, theme_name: &str) -> Option<&Theme> { self.0.get(theme_name) } pub fn inner(&self) -> &HashMap { &self.0 } } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct Theme { pub sourced_from_external_file: bool, #[serde(flatten)] pub palette: Styling, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct HexColor(u8, u8, u8); impl From for (u8, u8, u8) { fn from(e: HexColor) -> (u8, u8, u8) { let HexColor(r, g, b) = e; (r, g, b) } } pub struct HexColorVisitor(); impl<'de> Visitor<'de> for HexColorVisitor { type Value = HexColor; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { write!(formatter, "a hex color in the format #RGB or #RRGGBB") } fn visit_str(self, s: &str) -> Result where E: Error, { if let Some(stripped) = s.strip_prefix('#') { return self.visit_str(stripped); } if s.len() == 3 { Ok(HexColor( u8::from_str_radix(&s[0..1], 16).map_err(E::custom)? * 0x11, u8::from_str_radix(&s[1..2], 16).map_err(E::custom)? * 0x11, u8::from_str_radix(&s[2..3], 16).map_err(E::custom)? * 0x11, )) } else if s.len() == 6 { Ok(HexColor( u8::from_str_radix(&s[0..2], 16).map_err(E::custom)?, u8::from_str_radix(&s[2..4], 16).map_err(E::custom)?, u8::from_str_radix(&s[4..6], 16).map_err(E::custom)?, )) } else { Err(Error::custom( "Hex color must be of form \"#RGB\" or \"#RRGGBB\"", )) } } } impl<'de> Deserialize<'de> for HexColor { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { deserializer.deserialize_str(HexColorVisitor()) } } impl Serialize for HexColor { fn serialize(&self, serializer: S) -> Result where S: Serializer, { serializer.serialize_str(format!("{:02X}{:02X}{:02X}", self.0, self.1, self.2).as_str()) } } #[cfg(test)] #[path = "./unit/theme_test.rs"] mod theme_test;