diff --git a/Cargo.lock b/Cargo.lock index 895ee653..402d3213 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -907,6 +907,7 @@ dependencies = [ "itoa", "rustversion", "ryu", + "serde", "static_assertions", ] @@ -4364,6 +4365,7 @@ dependencies = [ "itertools 0.13.0", "lru", "paste", + "serde", "strum", "unicode-segmentation", "unicode-truncate", diff --git a/Cargo.toml b/Cargo.toml index ff5ec858..50d0e25d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,7 @@ mime_guess = "^2.0.4" nom = "7.0.0" open = "3.2.0" rand = "0.8.5" -ratatui = "0.29.0" +ratatui = { version = "0.29.0", features = ["serde"] } ratatui-image = { version = "~8.0.1", features = ["serde"] } regex = "^1.5" rpassword = "^7.2" diff --git a/src/config.rs b/src/config.rs index 635a2959..5babad57 100644 --- a/src/config.rs +++ b/src/config.rs @@ -526,6 +526,71 @@ pub struct ImagePreviewProtocolValues { pub font_size: Option<(u16, u16)>, } +#[derive(Clone, Deserialize, Default)] +pub struct Colorscheme { + pub border: Option, + pub border_unfocused: Option, + pub window_title: Option, + pub tab_title: Option, + pub tab_title_unfocused: Option, + pub room_list: Option, + pub room_list_unread: Option, +} + +#[derive(Clone, Deserialize)] +pub struct ColorschemeValues { + pub border: Style, + pub border_unfocused: Style, + pub window_title: Style, + pub tab_title: Style, + pub tab_title_unfocused: Style, + pub room_list: Style, + pub room_list_unread: Style, +} + +fn merge_colorscheme( + profile: Option, + global: Option, +) -> Option { + match (profile, global) { + (None, None) => None, + (Some(c), None) | (None, Some(c)) => Some(c), + (Some(profile), Some(global)) => { + Some(Colorscheme { + border: profile.border.or(global.border), + border_unfocused: profile.border_unfocused.or(global.border_unfocused), + window_title: profile.window_title.or(global.window_title), + tab_title: profile.tab_title.or(global.tab_title), + tab_title_unfocused: profile.tab_title_unfocused.or(global.tab_title_unfocused), + room_list: profile.room_list.or(global.room_list), + room_list_unread: profile.room_list_unread.or(global.room_list_unread), + }) + }, + } +} + +impl Colorscheme { + pub fn values(self) -> ColorschemeValues { + let border = self.border.map(Into::into).unwrap_or_default(); + let border_unfocused = self.border_unfocused.map(Into::into).unwrap_or(border); + let window_title = self.window_title.map(Into::into).unwrap_or_default(); + let tab_title = self.tab_title.map(Into::into).unwrap_or_default(); + let tab_title_unfocused = self.tab_title_unfocused.map(Into::into).unwrap_or(tab_title); + let room_list = self.room_list.map(Into::into).unwrap_or_default(); + let room_list_unread = self.room_list_unread.map(Into::into).unwrap_or(room_list); + + ColorschemeValues { + border, + border_unfocused, + window_title, + tab_title, + tab_title_unfocused, + room_list, + room_list_unread, + } + } +} + #[derive(Clone)] pub struct SortValues { pub chats: Vec>, @@ -581,6 +646,7 @@ pub struct TunableValues { pub user_gutter_width: usize, pub external_edit_file_suffix: String, pub tabstop: usize, + pub colors: ColorschemeValues, } #[derive(Clone, Default, Deserialize)] @@ -609,6 +675,7 @@ pub struct Tunables { pub user_gutter_width: Option, pub external_edit_file_suffix: Option, pub tabstop: Option, + pub colors: Option, } impl Tunables { @@ -643,6 +710,7 @@ impl Tunables { .external_edit_file_suffix .or(other.external_edit_file_suffix), tabstop: self.tabstop.or(other.tabstop), + colors: merge_colorscheme(self.colors, other.colors), } } @@ -673,6 +741,7 @@ impl Tunables { .external_edit_file_suffix .unwrap_or_else(|| ".md".to_string()), tabstop: self.tabstop.unwrap_or(4), + colors: self.colors.unwrap_or_default().values(), } } } diff --git a/src/main.rs b/src/main.rs index cee2247d..f6a7aab4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -304,6 +304,7 @@ impl Application { let focused = self.focused; let sstate = &mut self.screen; let term = &mut self.terminal; + let colors = store.application.settings.tunables.colors.clone(); if store.application.ring_bell { store.application.ring_bell = term.backend_mut().write_all(&[7]).is_err(); @@ -328,9 +329,10 @@ impl Application { .show_dialog(dialogstr) .show_mode(modestr) .borders(true) - .border_style(Style::default().add_modifier(Modifier::DIM)) - .tab_style(Style::default().add_modifier(Modifier::DIM)) - .tab_style_focused(Style::default().remove_modifier(Modifier::DIM)) + .border_style(colors.border_unfocused.add_modifier(Modifier::DIM)) + .border_style_focused(colors.border.remove_modifier(Modifier::DIM)) + .tab_style(colors.tab_title_unfocused.add_modifier(Modifier::DIM)) + .tab_style_focused(colors.tab_title.remove_modifier(Modifier::DIM)) .focus(focused); f.render_stateful_widget(screen, area, sstate); diff --git a/src/tests.rs b/src/tests.rs index 4a31d649..a678e1cc 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -26,6 +26,7 @@ use crate::{ user_color, user_style_from_color, ApplicationSettings, + Colorscheme, DirectoryValues, Notifications, NotifyVia, @@ -201,6 +202,7 @@ pub fn mock_tunables() -> TunableValues { image_preview: None, user_gutter_width: 30, tabstop: 4, + colors: Colorscheme::default().values(), } } diff --git a/src/windows/mod.rs b/src/windows/mod.rs index 690399b3..17ff3f92 100644 --- a/src/windows/mod.rs +++ b/src/windows/mod.rs @@ -92,37 +92,37 @@ type MatrixRoomInfo = Arc<(MatrixRoom, Option)>; const MEMBER_FETCH_DEBOUNCE: Duration = Duration::from_secs(5); #[inline] -fn bold_style() -> Style { - Style::default().add_modifier(StyleModifier::BOLD) +fn bold_style(style: Style) -> Style { + style.add_modifier(StyleModifier::BOLD) } #[inline] -fn bold_span(s: &str) -> Span<'_> { - Span::styled(s, bold_style()) +fn bold_span(s: &str, style: Style) -> Span<'_> { + Span::styled(s, bold_style(style)) } #[inline] -fn bold_spans(s: &str) -> Line<'_> { - bold_span(s).into() +fn bold_spans(s: &str, style: Style) -> Line<'_> { + bold_span(s, style).into() } #[inline] -fn selected_style(selected: bool) -> Style { +fn selected_style(selected: bool, style: Style) -> Style { if selected { - Style::default().add_modifier(StyleModifier::REVERSED) + style.add_modifier(StyleModifier::REVERSED) } else { - Style::default() + style } } #[inline] -fn selected_span(s: &str, selected: bool) -> Span<'_> { - Span::styled(s, selected_style(selected)) +fn selected_span(s: &str, selected: bool, style: Style) -> Span<'_> { + Span::styled(s, selected_style(selected, style)) } #[inline] -fn selected_text(s: &str, selected: bool) -> Text<'_> { - Text::from(selected_span(s, selected)) +fn selected_text(s: &str, selected: bool, style: Style) -> Text<'_> { + Text::from(selected_span(s, selected, style)) } fn name_and_labels(name: &str, unread: bool, style: Style) -> (Span<'_>, Vec>>) { @@ -744,14 +744,15 @@ impl Window for IambWindow { } fn get_tab_title(&self, store: &mut ProgramStore) -> Line<'_> { + let style = Default::default(); match self { - IambWindow::DirectList(_) => bold_spans("Direct Messages"), - IambWindow::RoomList(_) => bold_spans("Rooms"), - IambWindow::SpaceList(_) => bold_spans("Spaces"), - IambWindow::VerifyList(_) => bold_spans("Verifications"), - IambWindow::Welcome(_) => bold_spans("Welcome to iamb"), - IambWindow::ChatList(_) => bold_spans("DMs & Rooms"), - IambWindow::UnreadList(_) => bold_spans("Unread Messages"), + IambWindow::DirectList(_) => bold_spans("Direct Messages", style), + IambWindow::RoomList(_) => bold_spans("Rooms", style), + IambWindow::SpaceList(_) => bold_spans("Spaces", style), + IambWindow::VerifyList(_) => bold_spans("Verifications", style), + IambWindow::Welcome(_) => bold_spans("Welcome to iamb", style), + IambWindow::ChatList(_) => bold_spans("DMs & Rooms", style), + IambWindow::UnreadList(_) => bold_spans("Unread Messages", style), IambWindow::Room(w) => { let title = store.application.get_room_title(w.id()); @@ -762,8 +763,8 @@ impl Window for IambWindow { let title = store.application.get_room_title(room_id.as_ref()); let n = state.len(); let v = vec![ - bold_span("Room Members "), - Span::styled(format!("({n}): "), bold_style()), + bold_span("Room Members ", style), + Span::styled(format!("({n}): "), bold_style(style)), title.into(), ]; Line::from(v) @@ -772,22 +773,23 @@ impl Window for IambWindow { } fn get_win_title(&self, store: &mut ProgramStore) -> Line<'_> { + let style = store.application.settings.tunables.colors.window_title; match self { - IambWindow::DirectList(_) => bold_spans("Direct Messages"), - IambWindow::RoomList(_) => bold_spans("Rooms"), - IambWindow::SpaceList(_) => bold_spans("Spaces"), - IambWindow::VerifyList(_) => bold_spans("Verifications"), - IambWindow::Welcome(_) => bold_spans("Welcome to iamb"), - IambWindow::ChatList(_) => bold_spans("DMs & Rooms"), - IambWindow::UnreadList(_) => bold_spans("Unread Messages"), + IambWindow::DirectList(_) => bold_spans("Direct Messages", style), + IambWindow::RoomList(_) => bold_spans("Rooms", style), + IambWindow::SpaceList(_) => bold_spans("Spaces", style), + IambWindow::VerifyList(_) => bold_spans("Verifications", style), + IambWindow::Welcome(_) => bold_spans("Welcome to iamb", style), + IambWindow::ChatList(_) => bold_spans("DMs & Rooms", style), + IambWindow::UnreadList(_) => bold_spans("Unread Messages", style), IambWindow::Room(w) => w.get_title(store), IambWindow::MemberList(state, room_id, _) => { let title = store.application.get_room_title(room_id.as_ref()); let n = state.len(); let v = vec![ - bold_span("Room Members "), - Span::styled(format!("({n}): "), bold_style()), + bold_span("Room Members ", style), + Span::styled(format!("({n}): "), bold_style(style)), title.into(), ]; Line::from(v) @@ -963,10 +965,15 @@ impl ListItem for GenericChatItem { &self, selected: bool, _: &ViewportContext, - _: &mut ProgramStore, + store: &mut ProgramStore, ) -> Text<'_> { let unread = self.unread.is_unread(); - let style = selected_style(selected); + let style = if unread { + store.application.settings.tunables.colors.room_list_unread + } else { + store.application.settings.tunables.colors.room_list + }; + let style = selected_style(selected, style); let (name, mut labels) = name_and_labels(&self.name, unread, style); let mut spans = vec![name]; @@ -1082,10 +1089,15 @@ impl ListItem for RoomItem { &self, selected: bool, _: &ViewportContext, - _: &mut ProgramStore, + store: &mut ProgramStore, ) -> Text<'_> { let unread = self.unread.is_unread(); - let style = selected_style(selected); + let style = if unread { + store.application.settings.tunables.colors.room_list_unread + } else { + store.application.settings.tunables.colors.room_list + }; + let style = selected_style(selected, style); let (name, mut labels) = name_and_labels(&self.name, unread, style); let mut spans = vec![name]; @@ -1191,10 +1203,15 @@ impl ListItem for DirectItem { &self, selected: bool, _: &ViewportContext, - _: &mut ProgramStore, + store: &mut ProgramStore, ) -> Text<'_> { let unread = self.unread.is_unread(); - let style = selected_style(selected); + let style = if unread { + store.application.settings.tunables.colors.room_list_unread + } else { + store.application.settings.tunables.colors.room_list + }; + let style = selected_style(selected, style); let (name, mut labels) = name_and_labels(&self.name, unread, style); let mut spans = vec![name]; @@ -1299,9 +1316,13 @@ impl ListItem for SpaceItem { &self, selected: bool, _: &ViewportContext, - _: &mut ProgramStore, + store: &mut ProgramStore, ) -> Text<'_> { - selected_text(self.name.as_str(), selected) + selected_text( + self.name.as_str(), + selected, + store.application.settings.tunables.colors.room_list, + ) } fn get_word(&self) -> Option { @@ -1440,7 +1461,7 @@ impl ListItem for VerifyItem { let mut lines = vec![]; let bold = Style::default().add_modifier(StyleModifier::BOLD); - let item = Span::styled(self.show_item(), selected_style(selected)); + let item = Span::styled(self.show_item(), selected_style(selected, Default::default())); lines.push(Line::from(item)); if self.sasv1.is_done() {