| 1 | //! All the palettes available in Codewhale's terminal theme picker. |
| 2 | |
| 3 | use ratatui::{ |
| 4 | buffer::Buffer, |
| 5 | layout::Rect, |
| 6 | style::{Modifier, Style}, |
| 7 | text::Line, |
| 8 | }; |
| 9 | |
| 10 | use super::Entry; |
| 11 | use crate::{Caps, Paint, Role, Theme, TuiInk, TuiPalette, WorkbarPanel, detect::Appearance, text}; |
| 12 | |
| 13 | /// Known terminal grounds use the preset's appearance; an unknown ground |
| 14 | /// continues to use the host's conservative capability fallback. |
| 15 | pub(crate) fn theme_for(palette: TuiPalette, input: &Theme) -> Theme { |
| 16 | let caps = input.caps(); |
| 17 | Theme::new(Caps { |
| 18 | appearance: if caps.appearance == Appearance::Unknown { |
| 19 | Appearance::Unknown |
| 20 | } else if palette.light() { |
| 21 | Appearance::Light |
| 22 | } else { |
| 23 | Appearance::Dark |
| 24 | }, |
| 25 | ..caps |
| 26 | }) |
| 27 | .tui_palette(palette) |
| 28 | } |
| 29 | |
| 30 | pub(crate) fn palette_for_name(name: &str) -> Option<TuiPalette> { |
| 31 | let name = name.strip_prefix("tui-theme-")?; |
| 32 | TuiPalette::ALL |
| 33 | .into_iter() |
| 34 | .find(|palette| palette.name() == name) |
| 35 | } |
| 36 | |
| 37 | fn band(area: Rect, top: u16, height: u16) -> Rect { |
| 38 | let top = top.min(area.height); |
| 39 | Rect::new( |
| 40 | area.x, |
| 41 | area.y.saturating_add(top), |
| 42 | area.width, |
| 43 | height.min(area.height.saturating_sub(top)), |
| 44 | ) |
| 45 | } |
| 46 | |
| 47 | fn row(area: Rect, buf: &mut Buffer, label: &str, style: Style, theme: &Theme) { |
| 48 | let area = area.intersection(buf.area); |
| 49 | if area.is_empty() { |
| 50 | return; |
| 51 | } |
| 52 | let label = text::display_safe(label); |
| 53 | let label = text::truncate_words(&label, usize::from(area.width), theme.ascii()); |
| 54 | buf.set_line(area.x, area.y, &Line::styled(label, style), area.width); |
| 55 | } |
| 56 | |
| 57 | fn clear(area: Rect, buf: &mut Buffer, ground: Role, theme: &Theme) { |
| 58 | for y in area.y..area.bottom() { |
| 59 | row( |
| 60 | Rect::new(area.x, y, area.width, 1), |
| 61 | buf, |
| 62 | &" ".repeat(usize::from(area.width)), |
| 63 | theme.fg(Role::Foreground).patch(theme.bg(ground)), |
| 64 | theme, |
| 65 | ); |
| 66 | } |
| 67 | } |
| 68 | |
| 69 | fn columns(area: Rect, count: usize) -> Vec<Rect> { |
| 70 | (0..count) |
| 71 | .map(|index| { |
| 72 | let left = u32::from(area.width) * index as u32 / count as u32; |
| 73 | let right = u32::from(area.width) * (index as u32 + 1) / count as u32; |
| 74 | Rect::new( |
| 75 | area.x.saturating_add(left as u16), |
| 76 | area.y, |
| 77 | (right - left) as u16, |
| 78 | area.height, |
| 79 | ) |
| 80 | }) |
| 81 | .collect() |
| 82 | } |
| 83 | |
| 84 | fn inks(area: Rect, buf: &mut Buffer, labels: &[(&str, Style)], theme: &Theme) { |
| 85 | for (column, (label, style)) in columns(area, labels.len()).into_iter().zip(labels) { |
| 86 | let inset = u16::from(column.width > 2); |
| 87 | row( |
| 88 | Rect { |
| 89 | x: column.x.saturating_add(inset), |
| 90 | width: column.width.saturating_sub(inset * 2), |
| 91 | ..column |
| 92 | }, |
| 93 | buf, |
| 94 | label, |
| 95 | *style, |
| 96 | theme, |
| 97 | ); |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | pub(crate) fn sample(palette: TuiPalette, area: Rect, buf: &mut Buffer, input: &Theme) { |
| 102 | let area = area.intersection(buf.area); |
| 103 | if area.is_empty() { |
| 104 | return; |
| 105 | } |
| 106 | let theme = theme_for(palette, input); |
| 107 | clear(area, buf, Role::Background, &theme); |
| 108 | clear(band(area, 0, 2), buf, Role::Sidebar, &theme); |
| 109 | row( |
| 110 | band(area, 0, 1), |
| 111 | buf, |
| 112 | &format!("Codewhale / {}", palette.name()), |
| 113 | theme.fg(Role::Foreground).add_modifier(Modifier::BOLD), |
| 114 | &theme, |
| 115 | ); |
| 116 | row( |
| 117 | band(area, 1, 1), |
| 118 | buf, |
| 119 | if palette.light() { |
| 120 | "Light theme" |
| 121 | } else { |
| 122 | "Dark theme" |
| 123 | }, |
| 124 | theme.fg(Role::Muted), |
| 125 | &theme, |
| 126 | ); |
| 127 | |
| 128 | row( |
| 129 | band(area, 3, 1), |
| 130 | buf, |
| 131 | "Build with the colors already in Codewhale.", |
| 132 | theme.fg(Role::Foreground), |
| 133 | &theme, |
| 134 | ); |
| 135 | for (column, (label, ground)) in columns(band(area, 5, 3), 5).into_iter().zip([ |
| 136 | ("Stage", Role::Background), |
| 137 | ("Panel", Role::Surface), |
| 138 | ("Raised", Role::Hover), |
| 139 | ("Chrome", Role::Sidebar), |
| 140 | ("Selected", Role::Selected), |
| 141 | ]) { |
| 142 | clear(column, buf, ground, &theme); |
| 143 | let inset = u16::from(column.width > 2); |
| 144 | row( |
| 145 | Rect { |
| 146 | x: column.x.saturating_add(inset), |
| 147 | y: column.y.saturating_add(u16::from(column.height > 1)), |
| 148 | width: column.width.saturating_sub(inset * 2), |
| 149 | height: u16::from(column.height > 0), |
| 150 | }, |
| 151 | buf, |
| 152 | label, |
| 153 | theme.fg(Role::Foreground).patch(theme.bg(ground)), |
| 154 | &theme, |
| 155 | ); |
| 156 | } |
| 157 | inks( |
| 158 | band(area, 10, 1), |
| 159 | buf, |
| 160 | &[ |
| 161 | ("Working", theme.tui_ink(TuiInk::Working)), |
| 162 | ("Completed", theme.tui_ink(TuiInk::Success)), |
| 163 | ("Needs you", theme.tui_ink(TuiInk::Warning)), |
| 164 | ("Failed", theme.fg(Role::Danger)), |
| 165 | ], |
| 166 | &theme, |
| 167 | ); |
| 168 | inks( |
| 169 | band(area, 12, 1), |
| 170 | buf, |
| 171 | &[ |
| 172 | ("Work", theme.tui_ink(TuiInk::ModeWork)), |
| 173 | ("Plan", theme.tui_ink(TuiInk::ModePlan)), |
| 174 | ("Operate", theme.tui_ink(TuiInk::ModeOperate)), |
| 175 | ], |
| 176 | &theme, |
| 177 | ); |
| 178 | inks( |
| 179 | band(area, 14, 1), |
| 180 | buf, |
| 181 | &[ |
| 182 | ("Ask", theme.tui_ink(TuiInk::PermissionAsk)), |
| 183 | ("Auto review", theme.tui_ink(TuiInk::PermissionAutoReview)), |
| 184 | ("Full access", theme.tui_ink(TuiInk::PermissionFullAccess)), |
| 185 | ], |
| 186 | &theme, |
| 187 | ); |
| 188 | row( |
| 189 | band(area, 16, 1), |
| 190 | buf, |
| 191 | "Keep the work visible. Stay in control.", |
| 192 | theme.tui_ink(TuiInk::Soft), |
| 193 | &theme, |
| 194 | ); |
| 195 | if area.height >= 22 { |
| 196 | super::workbar::sample(WorkbarPanel::Tasks) |
| 197 | .offset(1) |
| 198 | .selected("graph:extract") |
| 199 | .paint(band(area, area.height.saturating_sub(5), 5), buf, &theme); |
| 200 | } |
| 201 | } |
| 202 | |
| 203 | macro_rules! palette_draw { |
| 204 | ($name:ident, $palette:ident) => { |
| 205 | fn $name(area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 206 | sample(TuiPalette::$palette, area, buf, theme); |
| 207 | } |
| 208 | }; |
| 209 | } |
| 210 | palette_draw!(underwater, Underwater); |
| 211 | palette_draw!(underwater_retro, UnderwaterRetro); |
| 212 | palette_draw!(shoreline, Shoreline); |
| 213 | palette_draw!(shoreline_light, ShorelineLight); |
| 214 | palette_draw!(whale, Whale); |
| 215 | palette_draw!(whale_light, WhaleLight); |
| 216 | palette_draw!(terminal, Terminal); |
| 217 | palette_draw!(grayscale, Grayscale); |
| 218 | palette_draw!(catppuccin_mocha, CatppuccinMocha); |
| 219 | palette_draw!(tokyo_night, TokyoNight); |
| 220 | palette_draw!(dracula, Dracula); |
| 221 | palette_draw!(gruvbox_dark, GruvboxDark); |
| 222 | palette_draw!(claude, Claude); |
| 223 | palette_draw!(matrix, Matrix); |
| 224 | palette_draw!(solarized_light, SolarizedLight); |
| 225 | palette_draw!(uwu, Uwu); |
| 226 | |
| 227 | pub(crate) fn entries() -> Vec<Entry> { |
| 228 | [ |
| 229 | ( |
| 230 | "tui-theme-underwater", |
| 231 | underwater as fn(Rect, &mut Buffer, &Theme), |
| 232 | ), |
| 233 | ("tui-theme-underwater-retro", underwater_retro), |
| 234 | ("tui-theme-shoreline", shoreline), |
| 235 | ("tui-theme-shoreline-light", shoreline_light), |
| 236 | ("tui-theme-whale", whale), |
| 237 | ("tui-theme-whale-light", whale_light), |
| 238 | ("tui-theme-terminal", terminal), |
| 239 | ("tui-theme-grayscale", grayscale), |
| 240 | ("tui-theme-catppuccin-mocha", catppuccin_mocha), |
| 241 | ("tui-theme-tokyo-night", tokyo_night), |
| 242 | ("tui-theme-dracula", dracula), |
| 243 | ("tui-theme-gruvbox-dark", gruvbox_dark), |
| 244 | ("tui-theme-claude", claude), |
| 245 | ("tui-theme-matrix", matrix), |
| 246 | ("tui-theme-solarized-light", solarized_light), |
| 247 | ("tui-theme-uwu", uwu), |
| 248 | ] |
| 249 | .into_iter() |
| 250 | .map(|(name, draw)| Entry { |
| 251 | name, |
| 252 | width: 104, |
| 253 | height: 24, |
| 254 | draw, |
| 255 | }) |
| 256 | .collect() |
| 257 | } |
| 258 |