max / makenotwork
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
19 files changed,
+843 insertions,
-126 deletions
| @@ -1530,6 +1530,25 @@ | |||
| 1530 | 1530 | "icu_properties", | |
| 1531 | 1531 | ] | |
| 1532 | 1532 | ||
| 1533 | + | [[package]] | |
| 1534 | + | name = "include_dir" | |
| 1535 | + | version = "0.7.4" | |
| 1536 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1537 | + | checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" | |
| 1538 | + | dependencies = [ | |
| 1539 | + | "include_dir_macros", | |
| 1540 | + | ] | |
| 1541 | + | ||
| 1542 | + | [[package]] | |
| 1543 | + | name = "include_dir_macros" | |
| 1544 | + | version = "0.7.4" | |
| 1545 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1546 | + | checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" | |
| 1547 | + | dependencies = [ | |
| 1548 | + | "proc-macro2", | |
| 1549 | + | "quote", | |
| 1550 | + | ] | |
| 1551 | + | ||
| 1533 | 1552 | [[package]] | |
| 1534 | 1553 | name = "indexmap" | |
| 1535 | 1554 | version = "2.13.0" | |
| @@ -1794,6 +1813,34 @@ | |||
| 1794 | 1813 | "winapi", | |
| 1795 | 1814 | ] | |
| 1796 | 1815 | ||
| 1816 | + | [[package]] | |
| 1817 | + | name = "makeover" | |
| 1818 | + | version = "2.4.1" | |
| 1819 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1820 | + | checksum = "b0a796c931419e54dd71a8aaa7d62230730e958f3101d37d4c1d3ab7b541b38e" | |
| 1821 | + | dependencies = [ | |
| 1822 | + | "include_dir", | |
| 1823 | + | "serde", | |
| 1824 | + | "toml", | |
| 1825 | + | ] | |
| 1826 | + | ||
| 1827 | + | [[package]] | |
| 1828 | + | name = "makeover-layout" | |
| 1829 | + | version = "0.12.0" | |
| 1830 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1831 | + | checksum = "58edd16523115ed4c9ca6de016693300ac95cf1bb0bd8ccf7fd246213102a7ff" | |
| 1832 | + | ||
| 1833 | + | [[package]] | |
| 1834 | + | name = "makeover-tui" | |
| 1835 | + | version = "0.12.0" | |
| 1836 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1837 | + | checksum = "88982f3e29d40336e9748e9adafa88278d123c42875aad178e0cea640a128d57" | |
| 1838 | + | dependencies = [ | |
| 1839 | + | "makeover", | |
| 1840 | + | "makeover-layout", | |
| 1841 | + | "ratatui", | |
| 1842 | + | ] | |
| 1843 | + | ||
| 1797 | 1844 | [[package]] | |
| 1798 | 1845 | name = "matchers" | |
| 1799 | 1846 | version = "0.2.0" | |
| @@ -1887,6 +1934,8 @@ | |||
| 1887 | 1934 | "crossterm", | |
| 1888 | 1935 | "hex", | |
| 1889 | 1936 | "hmac", | |
| 1937 | + | "makeover", | |
| 1938 | + | "makeover-tui", | |
| 1890 | 1939 | "rand 0.10.2", | |
| 1891 | 1940 | "ratatui", | |
| 1892 | 1941 | "reqwest", | |
| @@ -3062,6 +3111,15 @@ | |||
| 3062 | 3111 | "zmij", | |
| 3063 | 3112 | ] | |
| 3064 | 3113 | ||
| 3114 | + | [[package]] | |
| 3115 | + | name = "serde_spanned" | |
| 3116 | + | version = "1.1.1" | |
| 3117 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3118 | + | checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" | |
| 3119 | + | dependencies = [ | |
| 3120 | + | "serde_core", | |
| 3121 | + | ] | |
| 3122 | + | ||
| 3065 | 3123 | [[package]] | |
| 3066 | 3124 | name = "serde_urlencoded" | |
| 3067 | 3125 | version = "0.7.1" | |
| @@ -3696,6 +3754,45 @@ | |||
| 3696 | 3754 | "tokio", | |
| 3697 | 3755 | ] | |
| 3698 | 3756 | ||
| 3757 | + | [[package]] | |
| 3758 | + | name = "toml" | |
| 3759 | + | version = "1.1.4+spec-1.1.0" | |
| 3760 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3761 | + | checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" | |
| 3762 | + | dependencies = [ | |
| 3763 | + | "indexmap", | |
| 3764 | + | "serde_core", | |
| 3765 | + | "serde_spanned", | |
| 3766 | + | "toml_datetime", | |
| 3767 | + | "toml_parser", | |
| 3768 | + | "toml_writer", | |
| 3769 | + | "winnow", | |
| 3770 | + | ] | |
| 3771 | + | ||
| 3772 | + | [[package]] | |
| 3773 | + | name = "toml_datetime" | |
| 3774 | + | version = "1.1.1+spec-1.1.0" | |
| 3775 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3776 | + | checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" | |
| 3777 | + | dependencies = [ | |
| 3778 | + | "serde_core", | |
| 3779 | + | ] | |
| 3780 | + | ||
| 3781 | + | [[package]] | |
| 3782 | + | name = "toml_parser" | |
| 3783 | + | version = "1.1.3+spec-1.1.0" | |
| 3784 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3785 | + | checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" | |
| 3786 | + | dependencies = [ | |
| 3787 | + | "winnow", | |
| 3788 | + | ] | |
| 3789 | + | ||
| 3790 | + | [[package]] | |
| 3791 | + | name = "toml_writer" | |
| 3792 | + | version = "1.1.2+spec-1.1.0" | |
| 3793 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3794 | + | checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" | |
| 3795 | + | ||
| 3699 | 3796 | [[package]] | |
| 3700 | 3797 | name = "tower" | |
| 3701 | 3798 | version = "0.5.3" | |
| @@ -4414,6 +4511,12 @@ | |||
| 4414 | 4511 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4415 | 4512 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" | |
| 4416 | 4513 | ||
| 4514 | + | [[package]] | |
| 4515 | + | name = "winnow" | |
| 4516 | + | version = "1.0.4" | |
| 4517 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4518 | + | checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" | |
| 4519 | + | ||
| 4417 | 4520 | [[package]] | |
| 4418 | 4521 | name = "wit-bindgen" | |
| 4419 | 4522 | version = "0.51.0" | |
| @@ -4653,6 +4756,34 @@ | |||
| 4653 | 4756 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4654 | 4757 | checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" | |
| 4655 | 4758 | ||
| 4759 | + | [[patch.unused]] | |
| 4760 | + | name = "quasi-axum" | |
| 4761 | + | version = "0.1.0" | |
| 4762 | + | ||
| 4763 | + | [[patch.unused]] | |
| 4764 | + | name = "quasi-http" | |
| 4765 | + | version = "0.1.0" | |
| 4766 | + | ||
| 4767 | + | [[patch.unused]] | |
| 4768 | + | name = "quasi-router" | |
| 4769 | + | version = "0.1.0" | |
| 4770 | + | ||
| 4771 | + | [[patch.unused]] | |
| 4772 | + | name = "quasi-store" | |
| 4773 | + | version = "0.1.0" | |
| 4774 | + | ||
| 4775 | + | [[patch.unused]] | |
| 4776 | + | name = "quasi-tauri" | |
| 4777 | + | version = "0.1.0" | |
| 4778 | + | ||
| 4779 | + | [[patch.unused]] | |
| 4780 | + | name = "quasi-webview" | |
| 4781 | + | version = "0.1.0" | |
| 4782 | + | ||
| 4783 | + | [[patch.unused]] | |
| 4784 | + | name = "synckit-config" | |
| 4785 | + | version = "0.2.0" | |
| 4786 | + | ||
| 4656 | 4787 | [[patch.unused]] | |
| 4657 | 4788 | name = "docengine" | |
| 4658 | 4789 | version = "0.4.0" | |
| @@ -4668,7 +4799,3 @@ | |||
| 4668 | 4799 | [[patch.unused]] | |
| 4669 | 4800 | name = "tagtree" | |
| 4670 | 4801 | version = "0.4.0" | |
| 4671 | - | ||
| 4672 | - | [[patch.unused]] | |
| 4673 | - | name = "synckit-config" | |
| 4674 | - | version = "0.2.0" |
| @@ -11,6 +11,11 @@ | |||
| 11 | 11 | russh-sftp = "2.1" | |
| 12 | 12 | ratatui = "0.30" | |
| 13 | 13 | crossterm = "0.29" | |
| 14 | + | # The family's design system. `makeover` holds the themes and resolves a | |
| 15 | + | # selection; `makeover-tui` is the terminal renderer, and its `theme` feature is | |
| 16 | + | # the intents-to-ratatui-colours mapping. No colour is named in this crate. | |
| 17 | + | makeover = "2.4.1" | |
| 18 | + | makeover-tui = { version = "0.12.0", features = ["theme"] } | |
| 14 | 19 | tokio = { version = "1", features = ["full"] } | |
| 15 | 20 | # `rustls-no-provider` rather than `rustls`: the latter is an alias for | |
| 16 | 21 | # `__rustls-aws-lc-rs`, which links a C crypto backend. The provider is ring |
| @@ -53,6 +53,15 @@ | |||
| 53 | 53 | /// window where a new CLI talks to a server that predates the field. | |
| 54 | 54 | #[serde(default)] | |
| 55 | 55 | pub settlement_currency: Currency, | |
| 56 | + | /// The theme this creator picked, as `makeover::ThemeSelection` encodes it: | |
| 57 | + | /// a theme id, or `"system"` to follow the terminal. | |
| 58 | + | /// | |
| 59 | + | /// `None` where the server did not send the field, which is every server | |
| 60 | + | /// today — it is not stored yet, and adding it is filed as its own task. | |
| 61 | + | /// Absent and unset are the same thing until then, and both follow the | |
| 62 | + | /// terminal, so this reads correctly on both sides of that change. | |
| 63 | + | #[serde(default)] | |
| 64 | + | pub theme_id: Option<String>, | |
| 56 | 65 | } | |
| 57 | 66 | ||
| 58 | 67 | /// A creator's project with item count and revenue. |
| @@ -32,6 +32,13 @@ | |||
| 32 | 32 | /// Whether the client requested a PTY. The TUI needs one; without it a | |
| 33 | 33 | /// shell session gets the help text instead of hanging on a dead terminal. | |
| 34 | 34 | pty_requested: bool, | |
| 35 | + | /// What the client's terminal said about itself, for the theme. | |
| 36 | + | /// | |
| 37 | + | /// Collected here rather than read from this process's environment because | |
| 38 | + | /// the terminal being drawn on is at the other end of the connection; see | |
| 39 | + | /// `tui::theme`. `TERM` arrives on the PTY request, the rest on `env` | |
| 40 | + | /// requests if the client was configured to forward them. | |
| 41 | + | client_terminal: tui::theme::ClientTerminal, | |
| 35 | 42 | /// TUI application handle for forwarding keypresses. | |
| 36 | 43 | app: Option<tui::AppHandle>, | |
| 37 | 44 | /// Channels stored between open and shell/subsystem request. Only SFTP | |
| @@ -71,6 +78,7 @@ | |||
| 71 | 78 | user: None, | |
| 72 | 79 | term_size: (80, 24), | |
| 73 | 80 | pty_requested: false, | |
| 81 | + | client_terminal: tui::theme::ClientTerminal::default(), | |
| 74 | 82 | app: None, | |
| 75 | 83 | channels: Arc::new(Mutex::new(HashMap::new())), | |
| 76 | 84 | git_processes: HashMap::new(), | |
| @@ -193,7 +201,7 @@ | |||
| 193 | 201 | async fn pty_request( | |
| 194 | 202 | &mut self, | |
| 195 | 203 | _channel: ChannelId, | |
| 196 | - | _term: &str, | |
| 204 | + | term: &str, | |
| 197 | 205 | col_width: u32, | |
| 198 | 206 | row_height: u32, | |
| 199 | 207 | _pix_width: u32, | |
| @@ -203,7 +211,46 @@ | |||
| 203 | 211 | ) -> Result<(), Self::Error> { | |
| 204 | 212 | self.term_size = (col_width as u16, row_height as u16); | |
| 205 | 213 | self.pty_requested = true; | |
| 206 | - | tracing::debug!(cols = col_width, rows = row_height, "PTY requested"); | |
| 214 | + | // The one capability hint every client sends. `COLORTERM` would be the | |
| 215 | + | // better answer and openssh strips it, so on most sessions this is the | |
| 216 | + | // whole basis for the fidelity decision. | |
| 217 | + | self.client_terminal.term = term.to_string(); | |
| 218 | + | tracing::debug!(cols = col_width, rows = row_height, term, "PTY requested"); | |
| 219 | + | Ok(()) | |
| 220 | + | } | |
| 221 | + | ||
| 222 | + | /// Record the environment variables that decide how the TUI is painted. | |
| 223 | + | /// | |
| 224 | + | /// Only the two that do, and only when the client sends them at all — most | |
| 225 | + | /// do not, since forwarding either takes a `SendEnv` line the creator wrote. | |
| 226 | + | /// Everything else is ignored rather than accumulated: this is not a shell, | |
| 227 | + | /// nothing here spawns a process, and an environment nobody reads is a place | |
| 228 | + | /// for a client to park unbounded data. | |
| 229 | + | /// | |
| 230 | + | /// Answered with `channel_success` either way. A client that asked politely | |
| 231 | + | /// and was refused has no way to tell whether the request was unsupported or | |
| 232 | + | /// the session is broken, and openssh's `-o SendEnv` warns on failure. | |
| 233 | + | async fn env_request( | |
| 234 | + | &mut self, | |
| 235 | + | channel: ChannelId, | |
| 236 | + | variable_name: &str, | |
| 237 | + | variable_value: &str, | |
| 238 | + | session: &mut Session, | |
| 239 | + | ) -> Result<(), Self::Error> { | |
| 240 | + | // Bounded before it is stored. Both values are short by their own | |
| 241 | + | // specifications (`truecolor`, `15;0`), and neither is worth a | |
| 242 | + | // per-session allocation a client chooses the size of. | |
| 243 | + | const MAX: usize = 64; | |
| 244 | + | match variable_name { | |
| 245 | + | "COLORTERM" if variable_value.len() <= MAX => { | |
| 246 | + | self.client_terminal.colorterm = variable_value.to_string(); | |
| 247 | + | } | |
| 248 | + | "COLORFGBG" if variable_value.len() <= MAX => { | |
| 249 | + | self.client_terminal.colorfgbg = Some(variable_value.to_string()); | |
| 250 | + | } | |
| 251 | + | _ => {} | |
| 252 | + | } | |
| 253 | + | session.channel_success(channel)?; | |
| 207 | 254 | Ok(()) | |
| 208 | 255 | } | |
| 209 | 256 | ||
| @@ -236,6 +283,28 @@ | |||
| 236 | 283 | return Ok(()); | |
| 237 | 284 | } | |
| 238 | 285 | ||
| 286 | + | // Resolved before the terminal is put into its alternate screen, so a | |
| 287 | + | // theme that will not load reports itself on the creator's own prompt | |
| 288 | + | // rather than on a screen that is then torn down. | |
| 289 | + | let selection = tui::theme::selection(user.theme_id.as_deref()); | |
| 290 | + | let theme = match tui::theme::load(&selection, &self.client_terminal) { | |
| 291 | + | Ok(theme) => theme, | |
| 292 | + | Err(e) => { | |
| 293 | + | // Every id this binary can resolve to is embedded in it and | |
| 294 | + | // covered by a test, so this is a packaging fault rather than | |
| 295 | + | // anything the creator chose wrongly. Say so instead of leaving | |
| 296 | + | // them looking for a setting to change. | |
| 297 | + | tracing::error!(user = %user.username, error = ?e, "theme failed to resolve"); | |
| 298 | + | let handle = session.handle(); | |
| 299 | + | let message = format!("mnw: cannot resolve a theme to render in ({e}).\r\n"); | |
| 300 | + | let _ = handle.data(channel, bytes::Bytes::from(message)).await; | |
| 301 | + | let _ = handle.exit_status_request(channel, 1).await; | |
| 302 | + | let _ = handle.eof(channel).await; | |
| 303 | + | let _ = handle.close(channel).await; | |
| 304 | + | return Ok(()); | |
| 305 | + | } | |
| 306 | + | }; | |
| 307 | + | ||
| 239 | 308 | tracing::info!(user = %user.username, "launching TUI"); | |
| 240 | 309 | ||
| 241 | 310 | let handle = session.handle(); | |
| @@ -248,6 +317,7 @@ | |||
| 248 | 317 | match tui::launch( | |
| 249 | 318 | terminal_handle, | |
| 250 | 319 | user_clone, | |
| 320 | + | theme, | |
| 251 | 321 | cols, | |
| 252 | 322 | rows, | |
| 253 | 323 | handle, |
| @@ -2,7 +2,7 @@ | |||
| 2 | 2 | ||
| 3 | 3 | use ratatui::Frame; | |
| 4 | 4 | use ratatui::layout::{Constraint, Layout}; | |
| 5 | - | use ratatui::style::{Color, Modifier, Style}; | |
| 5 | + | use ratatui::style::{Modifier, Style}; | |
| 6 | 6 | use ratatui::text::{Line, Span}; | |
| 7 | 7 | use ratatui::widgets::{Bar, BarChart, BarGroup, Block, Borders, Paragraph, Row}; | |
| 8 | 8 | ||
| @@ -35,7 +35,7 @@ | |||
| 35 | 35 | let block = Block::default() | |
| 36 | 36 | .title(title) | |
| 37 | 37 | .borders(Borders::ALL) | |
| 38 | - | .border_style(Style::default().fg(Color::Gray)); | |
| 38 | + | .border_style(Style::default().fg(app.theme.line_border)); | |
| 39 | 39 | ||
| 40 | 40 | let inner = block.inner(area); | |
| 41 | 41 | frame.render_widget(block, area); | |
| @@ -66,9 +66,9 @@ | |||
| 66 | 66 | // Status line | |
| 67 | 67 | if let Some(ref status) = app.analytics_status { | |
| 68 | 68 | let style = if status.starts_with("Error") { | |
| 69 | - | Style::default().fg(Color::Red) | |
| 69 | + | Style::default().fg(app.theme.status_danger) | |
| 70 | 70 | } else { | |
| 71 | - | Style::default().fg(Color::Green) | |
| 71 | + | Style::default().fg(app.theme.status_success) | |
| 72 | 72 | }; | |
| 73 | 73 | let status_line = Paragraph::new(Line::from(vec![ | |
| 74 | 74 | Span::raw(" "), | |
| @@ -92,7 +92,8 @@ | |||
| 92 | 92 | Span::raw(" Back"), | |
| 93 | 93 | ]; | |
| 94 | 94 | ||
| 95 | - | let keys = Paragraph::new(Line::from(key_spans)).style(Style::default().fg(Color::DarkGray)); | |
| 95 | + | let keys = | |
| 96 | + | Paragraph::new(Line::from(key_spans)).style(Style::default().fg(app.theme.content_muted)); | |
| 96 | 97 | frame.render_widget(keys, chunks[5]); | |
| 97 | 98 | } | |
| 98 | 99 | ||
| @@ -135,7 +136,7 @@ | |||
| 135 | 136 | for (i, (label, value, change)) in stats.iter().enumerate() { | |
| 136 | 137 | let block = Block::default() | |
| 137 | 138 | .borders(Borders::ALL) | |
| 138 | - | .border_style(Style::default().fg(Color::DarkGray)); | |
| 139 | + | .border_style(Style::default().fg(app.theme.content_muted)); | |
| 139 | 140 | let inner = block.inner(stat_chunks[i]); | |
| 140 | 141 | frame.render_widget(block, stat_chunks[i]); | |
| 141 | 142 | ||
| @@ -144,11 +145,18 @@ | |||
| 144 | 145 | format!(" {value}"), | |
| 145 | 146 | Style::default().add_modifier(Modifier::BOLD), | |
| 146 | 147 | ), | |
| 147 | - | Span::styled(format!(" {label}"), Style::default().fg(Color::DarkGray)), | |
| 148 | + | Span::styled( | |
| 149 | + | format!(" {label}"), | |
| 150 | + | Style::default().fg(app.theme.content_muted), | |
| 151 | + | ), | |
| 148 | 152 | ]; | |
| 149 | 153 | ||
| 150 | 154 | if let Some(Some((text, positive))) = change { | |
| 151 | - | let color = if *positive { Color::Green } else { Color::Red }; | |
| 155 | + | let color = if *positive { | |
| 156 | + | app.theme.status_success | |
| 157 | + | } else { | |
| 158 | + | app.theme.status_danger | |
| 159 | + | }; | |
| 152 | 160 | spans.push(Span::styled(format!(" {text}"), Style::default().fg(color))); | |
| 153 | 161 | } | |
| 154 | 162 | ||
| @@ -200,7 +208,7 @@ | |||
| 200 | 208 | } else { | |
| 201 | 209 | String::new() | |
| 202 | 210 | }) | |
| 203 | - | .style(Style::default().fg(Color::Cyan)) | |
| 211 | + | .style(Style::default().fg(app.theme.action_primary)) | |
| 204 | 212 | }) | |
| 205 | 213 | .collect(); | |
| 206 | 214 | ||
| @@ -251,7 +259,14 @@ | |||
| 251 | 259 | ||
| 252 | 260 | // See the home-screen revenue column on why 15 rather than 12. | |
| 253 | 261 | let widths = [Constraint::Min(20), Constraint::Length(15)]; | |
| 254 | - | widgets::render_table(frame, chunks[4], &[" Project", "Revenue"], &widths, rows); | |
| 262 | + | widgets::render_table( | |
| 263 | + | frame, | |
| 264 | + | &app.theme, | |
| 265 | + | chunks[4], | |
| 266 | + | &[" Project", "Revenue"], | |
| 267 | + | &widths, | |
| 268 | + | rows, | |
| 269 | + | ); | |
| 255 | 270 | } | |
| 256 | 271 | } | |
| 257 | 272 | } | |
| @@ -297,7 +312,11 @@ | |||
| 297 | 312 | tx.status.clone(), | |
| 298 | 313 | date.to_string(), | |
| 299 | 314 | ]) | |
| 300 | - | .style(widgets::selected_style(i, Some(app.selected_index))) | |
| 315 | + | .style(widgets::selected_style( | |
| 316 | + | &app.theme, | |
| 317 | + | i, | |
| 318 | + | Some(app.selected_index), | |
| 319 | + | )) | |
| 301 | 320 | }) | |
| 302 | 321 | .collect(); | |
| 303 | 322 | ||
| @@ -310,6 +329,7 @@ | |||
| 310 | 329 | ||
| 311 | 330 | widgets::render_table( | |
| 312 | 331 | frame, | |
| 332 | + | &app.theme, | |
| 313 | 333 | chunks[1], | |
| 314 | 334 | &[" Item", "Amount", "Status", "Date"], | |
| 315 | 335 | &widths, |
| @@ -2,7 +2,7 @@ | |||
| 2 | 2 | ||
| 3 | 3 | use ratatui::Frame; | |
| 4 | 4 | use ratatui::layout::{Constraint, Layout}; | |
| 5 | - | use ratatui::style::{Color, Modifier, Style}; | |
| 5 | + | use ratatui::style::{Modifier, Style}; | |
| 6 | 6 | use ratatui::text::{Line, Span}; | |
| 7 | 7 | use ratatui::widgets::{Block, Borders, Paragraph, Row}; | |
| 8 | 8 | ||
| @@ -27,7 +27,7 @@ | |||
| 27 | 27 | let block = Block::default() | |
| 28 | 28 | .title(title) | |
| 29 | 29 | .borders(Borders::ALL) | |
| 30 | - | .border_style(Style::default().fg(Color::Gray)); | |
| 30 | + | .border_style(Style::default().fg(app.theme.line_border)); | |
| 31 | 31 | ||
| 32 | 32 | let inner = block.inner(area); | |
| 33 | 33 | frame.render_widget(block, area); | |
| @@ -68,9 +68,9 @@ | |||
| 68 | 68 | // Status line | |
| 69 | 69 | if let Some(ref status) = app.blog_status { | |
| 70 | 70 | let style = if status.starts_with("Error") { | |
| 71 | - | Style::default().fg(Color::Red) | |
| 71 | + | Style::default().fg(app.theme.status_danger) | |
| 72 | 72 | } else { | |
| 73 | - | Style::default().fg(Color::Green) | |
| 73 | + | Style::default().fg(app.theme.status_success) | |
| 74 | 74 | }; | |
| 75 | 75 | let status_line = Paragraph::new(Line::from(vec![ | |
| 76 | 76 | Span::raw(" "), | |
| @@ -102,7 +102,8 @@ | |||
| 102 | 102 | Span::raw(" Back"), | |
| 103 | 103 | ]); | |
| 104 | 104 | ||
| 105 | - | let keys = Paragraph::new(Line::from(key_spans)).style(Style::default().fg(Color::DarkGray)); | |
| 105 | + | let keys = | |
| 106 | + | Paragraph::new(Line::from(key_spans)).style(Style::default().fg(app.theme.content_muted)); | |
| 106 | 107 | frame.render_widget(keys, chunks[4]); | |
| 107 | 108 | } | |
| 108 | 109 | ||
| @@ -128,7 +129,11 @@ | |||
| 128 | 129 | status, | |
| 129 | 130 | date.to_string(), | |
| 130 | 131 | ]) | |
| 131 | - | .style(widgets::selected_style(i, Some(app.selected_index))) | |
| 132 | + | .style(widgets::selected_style( | |
| 133 | + | &app.theme, | |
| 134 | + | i, | |
| 135 | + | Some(app.selected_index), | |
| 136 | + | )) | |
| 132 | 137 | }) | |
| 133 | 138 | .collect(); | |
| 134 | 139 | ||
| @@ -141,6 +146,7 @@ | |||
| 141 | 146 | ||
| 142 | 147 | widgets::render_table( | |
| 143 | 148 | frame, | |
| 149 | + | &app.theme, | |
| 144 | 150 | area, | |
| 145 | 151 | &[" Title", "Slug", "Status", "Created"], | |
| 146 | 152 | &widths, |
| @@ -2,7 +2,7 @@ | |||
| 2 | 2 | ||
| 3 | 3 | use ratatui::Frame; | |
| 4 | 4 | use ratatui::layout::{Constraint, Layout}; | |
| 5 | - | use ratatui::style::{Color, Modifier, Style}; | |
| 5 | + | use ratatui::style::{Modifier, Style}; | |
| 6 | 6 | use ratatui::text::{Line, Span}; | |
| 7 | 7 | use ratatui::widgets::{Block, Borders, Paragraph, Row}; | |
| 8 | 8 | ||
| @@ -25,7 +25,7 @@ | |||
| 25 | 25 | let block = Block::default() | |
| 26 | 26 | .title(title) | |
| 27 | 27 | .borders(Borders::ALL) | |
| 28 | - | .border_style(Style::default().fg(Color::Gray)); | |
| 28 | + | .border_style(Style::default().fg(app.theme.line_border)); | |
| 29 | 29 | ||
| 30 | 30 | let inner = block.inner(area); | |
| 31 | 31 | frame.render_widget(block, area); | |
| @@ -71,7 +71,11 @@ | |||
| 71 | 71 | status.to_string(), | |
| 72 | 72 | c.item_count.to_string(), | |
| 73 | 73 | ]) | |
| 74 | - | .style(widgets::selected_style(i, Some(app.selected_index))) | |
| 74 | + | .style(widgets::selected_style( | |
| 75 | + | &app.theme, | |
| 76 | + | i, | |
| 77 | + | Some(app.selected_index), | |
| 78 | + | )) | |
| 75 | 79 | }) | |
| 76 | 80 | .collect(); | |
| 77 | 81 | ||
| @@ -84,6 +88,7 @@ | |||
| 84 | 88 | ||
| 85 | 89 | widgets::render_table( | |
| 86 | 90 | frame, | |
| 91 | + | &app.theme, | |
| 87 | 92 | chunks[2], | |
| 88 | 93 | &[" Title", "Slug", "Status", "Items"], | |
| 89 | 94 | &widths, | |
| @@ -93,9 +98,9 @@ | |||
| 93 | 98 | ||
| 94 | 99 | if let Some(ref status) = app.collections_status { | |
| 95 | 100 | let style = if status.starts_with("Error") { | |
| 96 | - | Style::default().fg(Color::Red) | |
| 101 | + | Style::default().fg(app.theme.status_danger) | |
| 97 | 102 | } else { | |
| 98 | - | Style::default().fg(Color::Green) | |
| 103 | + | Style::default().fg(app.theme.status_success) | |
| 99 | 104 | }; | |
| 100 | 105 | let status_line = Paragraph::new(format!(" {status}")).style(style); | |
| 101 | 106 | frame.render_widget(status_line, chunks[3]); | |
| @@ -111,6 +116,7 @@ | |||
| 111 | 116 | Span::raw(" Back"), | |
| 112 | 117 | ]; | |
| 113 | 118 | ||
| 114 | - | let keys = Paragraph::new(Line::from(key_spans)).style(Style::default().fg(Color::DarkGray)); | |
| 119 | + | let keys = | |
| 120 | + | Paragraph::new(Line::from(key_spans)).style(Style::default().fg(app.theme.content_muted)); | |
| 115 | 121 | frame.render_widget(keys, chunks[4]); | |
| 116 | 122 | } |
| @@ -2,7 +2,7 @@ | |||
| 2 | 2 | ||
| 3 | 3 | use ratatui::Frame; | |
| 4 | 4 | use ratatui::layout::{Constraint, Layout}; | |
| 5 | - | use ratatui::style::{Color, Modifier, Style}; | |
| 5 | + | use ratatui::style::{Modifier, Style}; | |
| 6 | 6 | use ratatui::text::{Line, Span}; | |
| 7 | 7 | use ratatui::widgets::{Block, Borders, Paragraph, Row}; | |
| 8 | 8 | ||
| @@ -38,7 +38,7 @@ | |||
| 38 | 38 | let block = Block::default() | |
| 39 | 39 | .title(title) | |
| 40 | 40 | .borders(Borders::ALL) | |
| 41 | - | .border_style(Style::default().fg(Color::Gray)); | |
| 41 | + | .border_style(Style::default().fg(app.theme.line_border)); | |
| 42 | 42 | ||
| 43 | 43 | let inner = block.inner(area); | |
| 44 | 44 | frame.render_widget(block, area); | |
| @@ -99,7 +99,7 @@ | |||
| 99 | 99 | Span::styled("[q]", Style::default().add_modifier(Modifier::BOLD)), | |
| 100 | 100 | Span::raw(" Quit"), | |
| 101 | 101 | ])) | |
| 102 | - | .style(Style::default().fg(Color::DarkGray)); | |
| 102 | + | .style(Style::default().fg(app.theme.content_muted)); | |
| 103 | 103 | frame.render_widget(keys, chunks[5]); | |
| 104 | 104 | } | |
| 105 | 105 | ||
| @@ -133,7 +133,7 @@ | |||
| 133 | 133 | for (i, (label, value)) in stat_items.iter().enumerate() { | |
| 134 | 134 | let block = Block::default() | |
| 135 | 135 | .borders(Borders::ALL) | |
| 136 | - | .border_style(Style::default().fg(Color::DarkGray)); | |
| 136 | + | .border_style(Style::default().fg(app.theme.content_muted)); | |
| 137 | 137 | let inner = block.inner(stats_chunks[i]); | |
| 138 | 138 | frame.render_widget(block, stats_chunks[i]); | |
| 139 | 139 | ||
| @@ -142,7 +142,10 @@ | |||
| 142 | 142 | format!(" {value}"), | |
| 143 | 143 | Style::default().add_modifier(Modifier::BOLD), | |
| 144 | 144 | ), | |
| 145 | - | Span::styled(format!(" {label}"), Style::default().fg(Color::DarkGray)), | |
| 145 | + | Span::styled( | |
| 146 | + | format!(" {label}"), | |
| 147 | + | Style::default().fg(app.theme.content_muted), | |
| 148 | + | ), | |
| 146 | 149 | ])); | |
| 147 | 150 | frame.render_widget(text, inner); | |
| 148 | 151 | } | |
| @@ -163,7 +166,11 @@ | |||
| 163 | 166 | p.item_count.to_string(), | |
| 164 | 167 | p.revenue().display_compact(app.currency()), | |
| 165 | 168 | ]) | |
| 166 | - | .style(widgets::selected_style(i, Some(app.selected_index))) | |
| 169 | + | .style(widgets::selected_style( | |
| 170 | + | &app.theme, | |
| 171 | + | i, | |
| 172 | + | Some(app.selected_index), | |
| 173 | + | )) | |
| 167 | 174 | }) | |
| 168 | 175 | .collect(); | |
| 169 | 176 | ||
| @@ -179,6 +186,7 @@ | |||
| 179 | 186 | ||
| 180 | 187 | widgets::render_table( | |
| 181 | 188 | frame, | |
| 189 | + | &app.theme, | |
| 182 | 190 | area, | |
| 183 | 191 | &[" Title", "Type", "Status", "Items", "Revenue"], | |
| 184 | 192 | &widths, |
| @@ -2,7 +2,7 @@ | |||
| 2 | 2 | ||
| 3 | 3 | use ratatui::Frame; | |
| 4 | 4 | use ratatui::layout::{Constraint, Layout}; | |
| 5 | - | use ratatui::style::{Color, Modifier, Style}; | |
| 5 | + | use ratatui::style::{Modifier, Style}; | |
| 6 | 6 | use ratatui::text::{Line, Span}; | |
| 7 | 7 | use ratatui::widgets::{Block, Borders, Paragraph, Row}; | |
| 8 | 8 | ||
| @@ -41,7 +41,7 @@ | |||
| 41 | 41 | let block = Block::default() | |
| 42 | 42 | .title(title) | |
| 43 | 43 | .borders(Borders::ALL) | |
| 44 | - | .border_style(Style::default().fg(Color::Gray)); | |
| 44 | + | .border_style(Style::default().fg(app.theme.line_border)); | |
| 45 | 45 | ||
| 46 | 46 | let inner = block.inner(area); | |
| 47 | 47 | frame.render_widget(block, area); | |
| @@ -87,9 +87,9 @@ | |||
| 87 | 87 | // Status line | |
| 88 | 88 | if let Some(ref status) = app.item_status { | |
| 89 | 89 | let style = if status.starts_with("Error") { | |
| 90 | - | Style::default().fg(Color::Red) | |
| 90 | + | Style::default().fg(app.theme.status_danger) | |
| 91 | 91 | } else { | |
| 92 | - | Style::default().fg(Color::Green) | |
| 92 | + | Style::default().fg(app.theme.status_success) | |
| 93 | 93 | }; | |
| 94 | 94 | let status_line = Paragraph::new(Line::from(vec![ | |
| 95 | 95 | Span::raw(" "), | |
| @@ -138,7 +138,8 @@ | |||
| 138 | 138 | ]); | |
| 139 | 139 | } | |
| 140 | 140 | ||
| 141 | - | let keys = Paragraph::new(Line::from(key_spans)).style(Style::default().fg(Color::DarkGray)); | |
| 141 | + | let keys = | |
| 142 | + | Paragraph::new(Line::from(key_spans)).style(Style::default().fg(app.theme.content_muted)); | |
| 142 | 143 | frame.render_widget(keys, chunks[6]); | |
| 143 | 144 | } | |
| 144 | 145 | ||
| @@ -160,7 +161,7 @@ | |||
| 160 | 161 | let lines = vec![ | |
| 161 | 162 | Line::from(vec![ | |
| 162 | 163 | Span::raw(edit_marker(ItemEditField::Title)), | |
| 163 | - | Span::styled("Title: ", Style::default().fg(Color::DarkGray)), | |
| 164 | + | Span::styled("Title: ", Style::default().fg(app.theme.content_muted)), | |
| 164 | 165 | if editing == Some(ItemEditField::Title) { | |
| 165 | 166 | Span::styled( | |
| 166 | 167 | format!("{}_", app.edit_buffer), | |
| @@ -172,7 +173,10 @@ | |||
| 172 | 173 | ]), | |
| 173 | 174 | Line::from(vec![ | |
| 174 | 175 | Span::raw(edit_marker(ItemEditField::Description)), | |
| 175 | - | Span::styled("Description: ", Style::default().fg(Color::DarkGray)), | |
| 176 | + | Span::styled( | |
| 177 | + | "Description: ", | |
| 178 | + | Style::default().fg(app.theme.content_muted), | |
| 179 | + | ), | |
| 176 | 180 | if editing == Some(ItemEditField::Description) { | |
| 177 | 181 | Span::styled( | |
| 178 | 182 | format!("{}_", app.edit_buffer), | |
| @@ -184,7 +188,7 @@ | |||
| 184 | 188 | ]), | |
| 185 | 189 | Line::from(vec![ | |
| 186 | 190 | Span::raw(edit_marker(ItemEditField::Price)), | |
| 187 | - | Span::styled("Price: ", Style::default().fg(Color::DarkGray)), | |
| 191 | + | Span::styled("Price: ", Style::default().fg(app.theme.content_muted)), | |
| 188 | 192 | if editing == Some(ItemEditField::Price) { | |
| 189 | 193 | Span::styled( | |
| 190 | 194 | // The symbol on the edit prompt is the creator's own: they are | |
| @@ -198,18 +202,18 @@ | |||
| 198 | 202 | ]), | |
| 199 | 203 | Line::from(vec![ | |
| 200 | 204 | Span::raw(" "), | |
| 201 | - | Span::styled("Type: ", Style::default().fg(Color::DarkGray)), | |
| 205 | + | Span::styled("Type: ", Style::default().fg(app.theme.content_muted)), | |
| 202 | 206 | Span::raw(format::format_item_type(&item.item_type)), | |
| 203 | 207 | Span::raw(" "), | |
| 204 | - | Span::styled("Slug: ", Style::default().fg(Color::DarkGray)), | |
| 208 | + | Span::styled("Slug: ", Style::default().fg(app.theme.content_muted)), | |
| 205 | 209 | Span::raw(item.slug.clone()), | |
| 206 | 210 | ]), | |
| 207 | 211 | Line::from(vec![ | |
| 208 | 212 | Span::raw(" "), | |
| 209 | - | Span::styled("Sales: ", Style::default().fg(Color::DarkGray)), | |
| 213 | + | Span::styled("Sales: ", Style::default().fg(app.theme.content_muted)), | |
| 210 | 214 | Span::raw(item.sales_count.to_string()), | |
| 211 | 215 | Span::raw(" "), | |
| 212 | - | Span::styled("Downloads: ", Style::default().fg(Color::DarkGray)), | |
| 216 | + | Span::styled("Downloads: ", Style::default().fg(app.theme.content_muted)), | |
| 213 | 217 | Span::raw(item.download_count.to_string()), | |
| 214 | 218 | if item.play_count > 0 { | |
| 215 | 219 | Span::raw(format!(" Plays: {}", item.play_count)) | |
| @@ -219,19 +223,22 @@ | |||
| 219 | 223 | ]), | |
| 220 | 224 | Line::from(vec![ | |
| 221 | 225 | Span::raw(" "), | |
| 222 | - | Span::styled("Audio: ", Style::default().fg(Color::DarkGray)), | |
| 226 | + | Span::styled("Audio: ", Style::default().fg(app.theme.content_muted)), | |
| 223 | 227 | Span::raw(if item.has_audio { "yes" } else { "no" }), | |
| 224 | 228 | Span::raw(" "), | |
| 225 | - | Span::styled("Cover: ", Style::default().fg(Color::DarkGray)), | |
| 229 | + | Span::styled("Cover: ", Style::default().fg(app.theme.content_muted)), | |
| 226 | 230 | Span::raw(if item.has_cover { "yes" } else { "no" }), | |
| 227 | 231 | ]), | |
| 228 | 232 | Line::from({ | |
| 229 | 233 | let mut spans = vec![ | |
| 230 | 234 | Span::raw(" "), | |
| 231 | - | Span::styled("Tags: ", Style::default().fg(Color::DarkGray)), | |
| 235 | + | Span::styled("Tags: ", Style::default().fg(app.theme.content_muted)), | |
| 232 | 236 | ]; | |
| 233 | 237 | if app.item_tags.is_empty() { | |
| 234 | - | spans.push(Span::styled("(none)", Style::default().fg(Color::DarkGray))); | |
| 238 | + | spans.push(Span::styled( | |
| 239 | + | "(none)", | |
| 240 | + | Style::default().fg(app.theme.content_muted), | |
| 241 | + | )); | |
| 235 | 242 | } else { | |
| 236 | 243 | for (i, tag) in app.item_tags.iter().enumerate() { | |
| 237 | 244 | if i > 0 { | |
| @@ -287,6 +294,7 @@ | |||
| 287 | 294 | ||
| 288 | 295 | widgets::render_table( | |
| 289 | 296 | frame, | |
| 297 | + | &app.theme, | |
| 290 | 298 | area, | |
| 291 | 299 | &[" Version", "File", "Size", "Downloads", "Date"], | |
| 292 | 300 | &widths, |
| @@ -2,7 +2,7 @@ | |||
| 2 | 2 | ||
| 3 | 3 | use ratatui::Frame; | |
| 4 | 4 | use ratatui::layout::{Constraint, Layout}; | |
| 5 | - | use ratatui::style::{Color, Modifier, Style}; | |
| 5 | + | use ratatui::style::{Modifier, Style}; | |
| 6 | 6 | use ratatui::text::{Line, Span}; | |
| 7 | 7 | use ratatui::widgets::{Block, Borders, Paragraph, Row}; | |
| 8 | 8 | ||
| @@ -27,7 +27,7 @@ | |||
| 27 | 27 | let block = Block::default() | |
| 28 | 28 | .title(title) | |
| 29 | 29 | .borders(Borders::ALL) | |
| 30 | - | .border_style(Style::default().fg(Color::Gray)); | |
| 30 | + | .border_style(Style::default().fg(app.theme.line_border)); | |
| 31 | 31 | ||
| 32 | 32 | let inner = block.inner(area); | |
| 33 | 33 | frame.render_widget(block, area); | |
| @@ -68,9 +68,9 @@ | |||
| 68 | 68 | // Status line | |
| 69 | 69 | if let Some(ref status) = app.keys_status { | |
| 70 | 70 | let style = if status.starts_with("Error") { | |
| 71 | - | Style::default().fg(Color::Red) | |
| 71 | + | Style::default().fg(app.theme.status_danger) | |
| 72 | 72 | } else { | |
| 73 | - | Style::default().fg(Color::Green) | |
| 73 | + | Style::default().fg(app.theme.status_success) | |
| 74 | 74 | }; | |
| 75 | 75 | let status_line = Paragraph::new(Line::from(vec![ | |
| 76 | 76 | Span::raw(" "), | |
| @@ -102,7 +102,8 @@ | |||
| 102 | 102 | Span::raw(" Back"), | |
| 103 | 103 | ]); | |
| 104 | 104 | ||
| 105 | - | let keys = Paragraph::new(Line::from(key_spans)).style(Style::default().fg(Color::DarkGray)); | |
| 105 | + | let keys = | |
| 106 | + | Paragraph::new(Line::from(key_spans)).style(Style::default().fg(app.theme.content_muted)); | |
| 106 | 107 | frame.render_widget(keys, chunks[4]); | |
| 107 | 108 | } | |
| 108 | 109 | ||
| @@ -125,7 +126,11 @@ | |||
| 125 | 126 | activations, | |
| 126 | 127 | date.to_string(), | |
| 127 | 128 | ]) | |
| 128 | - | .style(widgets::selected_style(i, Some(app.selected_index))) | |
| 129 | + | .style(widgets::selected_style( | |
| 130 | + | &app.theme, | |
| 131 | + | i, | |
| 132 | + | Some(app.selected_index), | |
| 133 | + | )) | |
| 129 | 134 | }) | |
| 130 | 135 | .collect(); | |
| 131 | 136 | ||
| @@ -138,6 +143,7 @@ | |||
| 138 | 143 | ||
| 139 | 144 | widgets::render_table( | |
| 140 | 145 | frame, | |
| 146 | + | &app.theme, | |
| 141 | 147 | area, | |
| 142 | 148 | &[" Key", "Status", "Activations", "Created"], | |
| 143 | 149 | &widths, |
| @@ -11,6 +11,7 @@ | |||
| 11 | 11 | pub(crate) mod project; | |
| 12 | 12 | pub(crate) mod promo; | |
| 13 | 13 | pub(crate) mod settings; | |
| 14 | + | pub(crate) mod theme; | |
| 14 | 15 | pub(crate) mod tiers; | |
| 15 | 16 | pub(crate) mod upload; | |
| 16 | 17 | pub(crate) mod widgets; | |
| @@ -224,6 +225,13 @@ | |||
| 224 | 225 | /// Application state shared across screens. | |
| 225 | 226 | pub(crate) struct App { | |
| 226 | 227 | pub user: UserInfo, | |
| 228 | + | /// The colours every screen draws in, resolved once per session from the | |
| 229 | + | /// creator's selection and their terminal's capabilities. | |
| 230 | + | /// | |
| 231 | + | /// Held for the life of the session rather than re-resolved per frame, and | |
| 232 | + | /// `Copy`, so a screen reads it without a clone. Re-theming mid-session | |
| 233 | + | /// would mean rebuilding this and is not something any surface offers yet. | |
| 234 | + | pub theme: makeover_tui::Theme, | |
| 227 | 235 | pub projects: Vec<Project>, | |
| 228 | 236 | pub stats: Option<CreatorStats>, | |
| 229 | 237 | pub items: Vec<Item>, | |
| @@ -294,9 +302,10 @@ | |||
| 294 | 302 | self.user.settlement_currency | |
| 295 | 303 | } | |
| 296 | 304 | ||
| 297 | - | fn new(user: UserInfo) -> Self { | |
| 305 | + | fn new(user: UserInfo, theme: makeover_tui::Theme) -> Self { | |
| 298 | 306 | Self { | |
| 299 | 307 | user, | |
| 308 | + | theme, | |
| 300 | 309 | projects: Vec::new(), | |
| 301 | 310 | stats: None, | |
| 302 | 311 | items: Vec::new(), | |
| @@ -469,6 +478,108 @@ | |||
| 469 | 478 | mod tests { | |
| 470 | 479 | use super::*; | |
| 471 | 480 | ||
| 481 | + | /// An app with no data loaded, on a known theme. | |
| 482 | + | fn app_on_a_fixed_theme() -> App { | |
| 483 | + | App::new( | |
| 484 | + | UserInfo { | |
| 485 | + | user_id: "u1".to_string(), | |
| 486 | + | username: "creator".to_string(), | |
| 487 | + | display_name: None, | |
| 488 | + | creator_tier: Some("small_files".to_string()), | |
| 489 | + | can_create_projects: true, | |
| 490 | + | suspended: false, | |
| 491 | + | actor_token: String::new(), | |
| 492 | + | settlement_currency: Currency::default(), | |
| 493 | + | theme_id: None, | |
| 494 | + | }, | |
| 495 | + | theme::tests::fixed(), | |
| 496 | + | ) | |
| 497 | + | } | |
| 498 | + | ||
| 499 | + | /// Every colour a rendered frame can put on screen, from one theme. | |
| 500 | + | /// | |
| 501 | + | /// `Color::Reset` is in the set because that is what an unstyled cell holds, | |
| 502 | + | /// and `paint_page` has already given the whole viewport the theme's own | |
| 503 | + | /// page colours underneath it. | |
| 504 | + | fn colors_of(theme: &makeover_tui::Theme) -> Vec<ratatui::style::Color> { | |
| 505 | + | let mut colors = vec![ | |
| 506 | + | ratatui::style::Color::Reset, | |
| 507 | + | theme.surface_page, | |
| 508 | + | theme.surface_raised, | |
| 509 | + | theme.surface_sunken, | |
| 510 | + | theme.surface_overlay, | |
| 511 | + | theme.content_primary, | |
| 512 | + | theme.content_secondary, | |
| 513 | + | theme.content_muted, | |
| 514 | + | theme.action_primary, | |
| 515 | + | theme.status_danger, | |
| 516 | + | theme.status_success, | |
| 517 | + | theme.status_warning, | |
| 518 | + | theme.status_info, | |
| 519 | + | theme.line_border, | |
| 520 | + | theme.border_strong, | |
| 521 | + | theme.bevel_light, | |
| 522 | + | theme.bevel_dark, | |
| 523 | + | ]; | |
| 524 | + | colors.extend(theme.surface_well); | |
| 525 | + | colors.extend(theme.category); | |
| 526 | + | colors | |
| 527 | + | } | |
| 528 | + | ||
| 529 | + | /// No screen may paint a colour that is not in the theme. | |
| 530 | + | /// | |
| 531 | + | /// This is the guard the port exists to make possible, and the terminal's | |
| 532 | + | /// equivalent of the frontend lint the web surfaces carry: a `Color::Red` | |
| 533 | + | /// added back to a screen in six months fails here rather than being noticed | |
| 534 | + | /// by a creator whose theme has no red in it. Asserted over the rendered | |
| 535 | + | /// buffer rather than by grepping the source, so it also catches a literal | |
| 536 | + | /// arriving through a widget's own default. | |
| 537 | + | #[test] | |
| 538 | + | fn every_rendered_colour_comes_from_the_theme() { | |
| 539 | + | let app = app_on_a_fixed_theme(); | |
| 540 | + | let permitted = colors_of(&app.theme); | |
| 541 | + | ||
| 542 | + | let backend = ratatui::backend::TestBackend::new(120, 40); | |
| 543 | + | let mut terminal = Terminal::new(backend).expect("test backend"); | |
| 544 | + | terminal | |
| 545 | + | .draw(|frame| { | |
| 546 | + | run::paint_page(frame, &app); | |
| 547 | + | home::render(frame, &app); | |
| 548 | + | }) | |
| 549 | + | .expect("home renders"); | |
| 550 | + | ||
| 551 | + | for cell in terminal.backend().buffer().content() { | |
| 552 | + | assert!( | |
| 553 | + | permitted.contains(&cell.fg), | |
| 554 | + | "foreground {:?} is in no theme intent", | |
| 555 | + | cell.fg, | |
| 556 | + | ); | |
| 557 | + | assert!( | |
| 558 | + | permitted.contains(&cell.bg), | |
| 559 | + | "background {:?} is in no theme intent", | |
| 560 | + | cell.bg, | |
| 561 | + | ); | |
| 562 | + | } | |
| 563 | + | } | |
| 564 | + | ||
| 565 | + | // The page is painted, not left to the terminal's default. A transparent | |
| 566 | + | // background is what the port set out to remove, and it looks identical to | |
| 567 | + | // a correct one on a terminal whose own colours happen to match. | |
| 568 | + | #[test] | |
| 569 | + | fn the_page_surface_is_painted_under_every_screen() { | |
| 570 | + | let app = app_on_a_fixed_theme(); | |
| 571 | + | ||
| 572 | + | let backend = ratatui::backend::TestBackend::new(40, 10); | |
| 573 | + | let mut terminal = Terminal::new(backend).expect("test backend"); | |
| 574 | + | terminal | |
| 575 | + | .draw(|frame| run::paint_page(frame, &app)) | |
| 576 | + | .expect("the page paints"); | |
| 577 | + | ||
| 578 | + | for cell in terminal.backend().buffer().content() { | |
| 579 | + | assert_eq!(cell.bg, app.theme.surface_page); | |
| 580 | + | } | |
| 581 | + | } | |
| 582 | + | ||
| 472 | 583 | #[test] | |
| 473 | 584 | fn parse_price_whole_dollars() { | |
| 474 | 585 | assert_eq!(parse_price("5"), 500); |
| @@ -2,7 +2,7 @@ | |||
| 2 | 2 | ||
| 3 | 3 | use ratatui::Frame; | |
| 4 | 4 | use ratatui::layout::{Constraint, Layout}; | |
| 5 | - | use ratatui::style::{Color, Modifier, Style}; | |
| 5 | + | use ratatui::style::{Modifier, Style}; | |
| 6 | 6 | use ratatui::text::{Line, Span}; | |
| 7 | 7 | use ratatui::widgets::{Block, Borders, Paragraph, Row}; | |
| 8 | 8 | ||
| @@ -31,7 +31,7 @@ | |||
| 31 | 31 | let block = Block::default() | |
| 32 | 32 | .title(title) | |
| 33 | 33 | .borders(Borders::ALL) | |
| 34 | - | .border_style(Style::default().fg(Color::Gray)); | |
| 34 | + | .border_style(Style::default().fg(app.theme.line_border)); | |
| 35 | 35 | ||
| 36 | 36 | let inner = block.inner(area); | |
| 37 | 37 | frame.render_widget(block, area); | |
| @@ -54,7 +54,7 @@ | |||
| 54 | 54 | let visibility = if project.is_public { "public" } else { "draft" }; | |
| 55 | 55 | let info = Paragraph::new(Line::from(vec![ | |
| 56 | 56 | Span::raw(" "), | |
| 57 | - | Span::styled(&project.slug, Style::default().fg(Color::DarkGray)), | |
| 57 | + | Span::styled(&project.slug, Style::default().fg(app.theme.content_muted)), | |
| 58 | 58 | Span::raw(" "), | |
| 59 | 59 | Span::raw(format::format_project_type(&project.project_type)), | |
| 60 | 60 | Span::raw(" "), | |
| @@ -63,7 +63,7 @@ | |||
| 63 | 63 | // The detail line has the room for the full breakdown, so a project | |
| 64 | 64 | // spanning two currencies shows both here rather than a `+1` marker. | |
| 65 | 65 | Span::raw(project.revenue().display(app.currency())), | |
| 66 | - | Span::styled(" revenue", Style::default().fg(Color::DarkGray)), | |
| 66 | + | Span::styled(" revenue", Style::default().fg(app.theme.content_muted)), | |
| 67 | 67 | ])); | |
| 68 | 68 | frame.render_widget(info, chunks[1]); | |
| 69 | 69 | ||
| @@ -104,13 +104,13 @@ | |||
| 104 | 104 | } | |
| 105 | 105 | _ => String::new(), | |
| 106 | 106 | }; | |
| 107 | - | let confirm = Paragraph::new(msg).style(Style::default().fg(Color::Yellow)); | |
| 107 | + | let confirm = Paragraph::new(msg).style(Style::default().fg(app.theme.status_warning)); | |
| 108 | 108 | frame.render_widget(confirm, chunks[5]); | |
| 109 | 109 | } else if let Some(ref status) = app.item_status { | |
| 110 | 110 | let style = if status.starts_with("Error") { | |
| 111 | - | Style::default().fg(Color::Red) | |
| 111 | + | Style::default().fg(app.theme.status_danger) | |
| 112 | 112 | } else { | |
| 113 | - | Style::default().fg(Color::Green) | |
| 113 | + | Style::default().fg(app.theme.status_success) | |
| 114 | 114 | }; | |
| 115 | 115 | let status_line = Paragraph::new(format!(" {status}")).style(style); | |
| 116 | 116 | frame.render_widget(status_line, chunks[5]); | |
| @@ -142,7 +142,7 @@ | |||
| 142 | 142 | Span::styled( | |
| 143 | 143 | format!("{sel_count} selected"), | |
| 144 | 144 | Style::default() | |
| 145 | - | .fg(Color::Cyan) | |
| 145 | + | .fg(app.theme.action_primary) | |
| 146 | 146 | .add_modifier(Modifier::BOLD), | |
| 147 | 147 | ), | |
| 148 | 148 | Span::raw(" "), | |
| @@ -160,7 +160,8 @@ | |||
| 160 | 160 | Span::raw(" Back"), | |
| 161 | 161 | ]); | |
| 162 | 162 | ||
| 163 | - | let keys = Paragraph::new(Line::from(key_spans)).style(Style::default().fg(Color::DarkGray)); | |
| 163 | + | let keys = | |
| 164 | + | Paragraph::new(Line::from(key_spans)).style(Style::default().fg(app.theme.content_muted)); | |
| 164 | 165 | frame.render_widget(keys, chunks[6]); | |
| 165 | 166 | } | |
| 166 | 167 | ||
| @@ -187,7 +188,11 @@ | |||
| 187 | 188 | format::format_price(item.price_cents, app.currency()), | |
| 188 | 189 | visibility.to_string(), | |
| 189 | 190 | ]) | |
| 190 | - | .style(widgets::selected_style(i, Some(app.selected_index))) | |
| 191 | + | .style(widgets::selected_style( | |
| 192 | + | &app.theme, | |
| 193 | + | i, | |
| 194 | + | Some(app.selected_index), | |
| 195 | + | )) | |
| 191 | 196 | }) | |
| 192 | 197 | .collect(); | |
| 193 | 198 | ||
| @@ -200,6 +205,7 @@ | |||
| 200 | 205 | ||
| 201 | 206 | widgets::render_table( | |
| 202 | 207 | frame, | |
| 208 | + | &app.theme, | |
| 203 | 209 | area, | |
| 204 | 210 | &[" Title", "Type", "Price", "Status"], | |
| 205 | 211 | &widths, |
| @@ -2,7 +2,7 @@ | |||
| 2 | 2 | ||
| 3 | 3 | use ratatui::Frame; | |
| 4 | 4 | use ratatui::layout::{Constraint, Layout}; | |
| 5 | - | use ratatui::style::{Color, Modifier, Style}; | |
| 5 | + | use ratatui::style::{Modifier, Style}; | |
| 6 | 6 | use ratatui::text::{Line, Span}; | |
| 7 | 7 | use ratatui::widgets::{Block, Borders, Paragraph, Row}; | |
| 8 | 8 | ||
| @@ -27,7 +27,7 @@ | |||
| 27 | 27 | let block = Block::default() | |
| 28 | 28 | .title(title) | |
| 29 | 29 | .borders(Borders::ALL) | |
| 30 | - | .border_style(Style::default().fg(Color::Gray)); | |
| 30 | + | .border_style(Style::default().fg(app.theme.line_border)); | |
| 31 | 31 | ||
| 32 | 32 | let inner = block.inner(area); | |
| 33 | 33 | frame.render_widget(block, area); | |
| @@ -68,9 +68,9 @@ | |||
| 68 | 68 | // Status line | |
| 69 | 69 | if let Some(ref status) = app.promo_status { | |
| 70 | 70 | let style = if status.starts_with("Error") { | |
| 71 | - | Style::default().fg(Color::Red) | |
| 71 | + | Style::default().fg(app.theme.status_danger) | |
| 72 | 72 | } else { | |
| 73 | - | Style::default().fg(Color::Green) | |
| 73 | + | Style::default().fg(app.theme.status_success) | |
| 74 | 74 | }; | |
| 75 | 75 | let status_line = Paragraph::new(Line::from(vec![ | |
| 76 | 76 | Span::raw(" "), | |
| @@ -111,7 +111,8 @@ | |||
| 111 | 111 | ]); | |
| 112 | 112 | } | |
| 113 | 113 | ||
| 114 | - | let keys = Paragraph::new(Line::from(key_spans)).style(Style::default().fg(Color::DarkGray)); | |
| 114 | + | let keys = | |
| 115 | + | Paragraph::new(Line::from(key_spans)).style(Style::default().fg(app.theme.content_muted)); | |
| 115 | 116 | frame.render_widget(keys, chunks[4]); | |
| 116 | 117 | } | |
| 117 | 118 | ||
| @@ -142,7 +143,11 @@ | |||
| 142 | 143 | scope.to_string(), | |
| 143 | 144 | uses, | |
| 144 | 145 | ]) | |
| 145 | - | .style(widgets::selected_style(i, Some(app.selected_index))) | |
| 146 | + | .style(widgets::selected_style( | |
| 147 | + | &app.theme, | |
| 148 | + | i, | |
| 149 | + | Some(app.selected_index), | |
| 150 | + | )) | |
| 146 | 151 | }) | |
| 147 | 152 | .collect(); | |
| 148 | 153 | ||
| @@ -155,6 +160,7 @@ | |||
| 155 | 160 | ||
| 156 | 161 | widgets::render_table( | |
| 157 | 162 | frame, | |
| 163 | + | &app.theme, | |
| 158 | 164 | area, | |
| 159 | 165 | &[" Code", "Discount", "Scope", "Uses"], | |
| 160 | 166 | &widths, |
| @@ -11,11 +11,34 @@ | |||
| 11 | 11 | project, promo, settings, tiers, upload, | |
| 12 | 12 | }; | |
| 13 | 13 | ||
| 14 | + | /// Fill the whole viewport with the theme's page surface and body text colour. | |
| 15 | + | /// | |
| 16 | + | /// Painted under every screen rather than left to the terminal's own default. | |
| 17 | + | /// Left transparent, a light theme lands its parchment borders and muted labels | |
| 18 | + | /// on whatever background the creator's terminal happens to have, which on a | |
| 19 | + | /// dark one is the pairing the theme was authored to avoid. It also gives every | |
| 20 | + | /// unstyled `Span` in the screens below a colour that came from the theme, so a | |
| 21 | + | /// span with no `Style` is correct rather than merely inherited. | |
| 22 | + | pub(super) fn paint_page(frame: &mut ratatui::Frame, app: &App) { | |
| 23 | + | use ratatui::style::Style; | |
| 24 | + | use ratatui::widgets::Block; | |
| 25 | + | ||
| 26 | + | frame.render_widget( | |
| 27 | + | Block::default().style( | |
| 28 | + | Style::default() | |
| 29 | + | .bg(app.theme.surface_page) | |
| 30 | + | .fg(app.theme.content_primary), | |
| 31 | + | ), | |
| 32 | + | frame.area(), | |
| 33 | + | ); | |
| 34 | + | } | |
| 35 | + | ||
| 14 | 36 | /// Launch the TUI event loop in a background task. | |
| 15 | 37 | #[allow(clippy::too_many_arguments)] | |
| 16 | 38 | pub(crate) fn launch( | |
| 17 | 39 | writer: TerminalHandle, | |
| 18 | 40 | user: UserInfo, | |
| 41 | + | theme: makeover_tui::Theme, | |
| 19 | 42 | cols: u16, | |
| 20 | 43 | rows: u16, | |
| 21 | 44 | session_handle: russh::server::Handle, | |
| @@ -46,7 +69,7 @@ | |||
| 46 | 69 | }); | |
| 47 | 70 | ||
| 48 | 71 | tokio::spawn(async move { | |
| 49 | - | let mut app = App::new(user); | |
| 72 | + | let mut app = App::new(user, theme); | |
| 50 | 73 | let mut screen = Screen::Home; | |
| 51 | 74 | let staging_dir = staging_dir; | |
| 52 | 75 | ||
| @@ -59,7 +82,10 @@ | |||
| 59 | 82 | } | |
| 60 | 83 | ||
| 61 | 84 | // Initial render (loading state) | |
| 62 | - | if let Err(e) = terminal.draw(|frame| home::render(frame, &app)) { | |
| 85 | + | if let Err(e) = terminal.draw(|frame| { | |
| 86 | + | paint_page(frame, &app); | |
| 87 | + | home::render(frame, &app); | |
| 88 | + | }) { | |
| 63 | 89 | tracing::error!(error = ?e, "TUI: initial render failed"); | |
| 64 | 90 | cleanup(&mut terminal); | |
| 65 | 91 | return; | |
| @@ -355,24 +381,27 @@ | |||
| 355 | 381 | } | |
| 356 | 382 | ||
| 357 | 383 | // Re-render after every event | |
| 358 | - | if let Err(e) = terminal.draw(|frame| match &screen { | |
| 359 | - | Screen::Home => home::render(frame, &app), | |
| 360 | - | Screen::Project(idx) => { | |
| 361 | - | if let Some(p) = app.projects.get(*idx) { | |
| 362 | - | project::render(frame, &app, p); | |
| 363 | - | } else { | |
| 364 | - | home::render(frame, &app); | |
| 384 | + | if let Err(e) = terminal.draw(|frame| { | |
| 385 | + | paint_page(frame, &app); | |
| 386 | + | match &screen { | |
| 387 | + | Screen::Home => home::render(frame, &app), | |
| 388 | + | Screen::Project(idx) => { | |
| 389 | + | if let Some(p) = app.projects.get(*idx) { | |
| 390 | + | project::render(frame, &app, p); | |
| 391 | + | } else { | |
| 392 | + | home::render(frame, &app); | |
| 393 | + | } | |
| 365 | 394 | } | |
| 395 | + | Screen::Upload => upload::render(frame, &app), | |
| 396 | + | Screen::Item(..) => item::render(frame, &app), | |
| 397 | + | Screen::Blog(..) => blog::render(frame, &app), | |
| 398 | + | Screen::Promo => promo::render(frame, &app), | |
| 399 | + | Screen::Keys(..) => keys::render(frame, &app), | |
| 400 | + | Screen::Analytics => analytics::render(frame, &app), | |
| 401 | + | Screen::Settings => settings::render(frame, &app), | |
| 402 | + | Screen::Collections => collections::render(frame, &app), | |
| 403 | + | Screen::Tiers(..) => tiers::render(frame, &app), | |
| 366 | 404 | } | |
| 367 | - | Screen::Upload => upload::render(frame, &app), | |
| 368 | - | Screen::Item(..) => item::render(frame, &app), | |
| 369 | - | Screen::Blog(..) => blog::render(frame, &app), | |
| 370 | - | Screen::Promo => promo::render(frame, &app), | |
| 371 | - | Screen::Keys(..) => keys::render(frame, &app), | |
| 372 | - | Screen::Analytics => analytics::render(frame, &app), | |
| 373 | - | Screen::Settings => settings::render(frame, &app), | |
| 374 | - | Screen::Collections => collections::render(frame, &app), | |
| 375 | - | Screen::Tiers(..) => tiers::render(frame, &app), | |
| 376 | 405 | }) { | |
| 377 | 406 | tracing::error!(error = ?e, "render failed"); | |
| 378 | 407 | cleanup(&mut terminal); |
| @@ -2,7 +2,7 @@ | |||
| 2 | 2 | ||
| 3 | 3 | use ratatui::Frame; | |
| 4 | 4 | use ratatui::layout::{Constraint, Layout}; | |
| 5 | - | use ratatui::style::{Color, Modifier, Style}; | |
| 5 | + | use ratatui::style::{Modifier, Style}; | |
| 6 | 6 | use ratatui::text::{Line, Span}; | |
| 7 | 7 | use ratatui::widgets::{Block, Borders, Gauge, Paragraph, Row}; | |
| 8 | 8 | ||
| @@ -28,7 +28,7 @@ | |||
| 28 | 28 | let block = Block::default() | |
| 29 | 29 | .title(title) | |
| 30 | 30 | .borders(Borders::ALL) | |
| 31 | - | .border_style(Style::default().fg(Color::Gray)); | |
| 31 | + | .border_style(Style::default().fg(app.theme.line_border)); | |
| 32 | 32 | ||
| 33 | 33 | let inner = block.inner(area); | |
| 34 | 34 | frame.render_widget(block, area); | |
| @@ -75,7 +75,7 @@ | |||
| 75 | 75 | Line::from(vec![Span::raw(" Display: "), Span::raw(display_name)]), | |
| 76 | 76 | Line::from(vec![ | |
| 77 | 77 | Span::raw(" Tier: "), | |
| 78 | - | Span::styled(tier, Style::default().fg(Color::Cyan)), | |
| 78 | + | Span::styled(tier, Style::default().fg(app.theme.action_primary)), | |
| 79 | 79 | ]), | |
| 80 | 80 | ]; | |
| 81 | 81 | let profile = Paragraph::new(profile_lines); | |
| @@ -103,11 +103,11 @@ | |||
| 103 | 103 | ); | |
| 104 | 104 | ||
| 105 | 105 | let color = if pct > 90 { | |
| 106 | - | Color::Red | |
| 106 | + | app.theme.status_danger | |
| 107 | 107 | } else if pct > 70 { | |
| 108 | - | Color::Yellow | |
| 108 | + | app.theme.status_warning | |
| 109 | 109 | } else { | |
| 110 | - | Color::Green | |
| 110 | + | app.theme.status_success | |
| 111 | 111 | }; | |
| 112 | 112 | ||
| 113 | 113 | let gauge = Gauge::default() | |
| @@ -154,7 +154,11 @@ | |||
| 154 | 154 | format!("{}...", fp), | |
| 155 | 155 | date.to_string(), | |
| 156 | 156 | ]) | |
| 157 | - | .style(widgets::selected_style(i, Some(app.selected_index))) | |
| 157 | + | .style(widgets::selected_style( | |
| 158 | + | &app.theme, | |
| 159 | + | i, | |
| 160 | + | Some(app.selected_index), | |
| 161 | + | )) | |
| 158 | 162 | }) | |
| 159 | 163 | .collect(); | |
| 160 | 164 | ||
| @@ -166,6 +170,7 @@ | |||
| 166 | 170 | ||
| 167 | 171 | widgets::render_table( | |
| 168 | 172 | frame, | |
| 173 | + | &app.theme, | |
| 169 | 174 | chunks[8], | |
| 170 | 175 | &[" Label", "Fingerprint", "Added"], | |
| 171 | 176 | &widths, | |
| @@ -176,9 +181,9 @@ | |||
| 176 | 181 | // Status line | |
| 177 | 182 | if let Some(ref status) = app.settings_status { | |
| 178 | 183 | let style = if status.starts_with("Error") { | |
| 179 | - | Style::default().fg(Color::Red) | |
| 184 | + | Style::default().fg(app.theme.status_danger) | |
| 180 | 185 | } else { | |
| 181 | - | Style::default().fg(Color::Green) | |
| 186 | + | Style::default().fg(app.theme.status_success) | |
| 182 | 187 | }; | |
| 183 | 188 | let status_line = Paragraph::new(Line::from(vec![ | |
| 184 | 189 | Span::raw(" "), | |
| @@ -199,6 +204,6 @@ | |||
| 199 | 204 | ]; | |
| 200 | 205 | ||
| 201 | 206 | let keys_bar = | |
| 202 | - | Paragraph::new(Line::from(key_spans)).style(Style::default().fg(Color::DarkGray)); | |
| 207 | + | Paragraph::new(Line::from(key_spans)).style(Style::default().fg(app.theme.content_muted)); | |
| 203 | 208 | frame.render_widget(keys_bar, chunks[10]); | |
| 204 | 209 | } |
| @@ -2,7 +2,7 @@ | |||
| 2 | 2 | ||
| 3 | 3 | use ratatui::Frame; | |
| 4 | 4 | use ratatui::layout::{Constraint, Layout}; | |
| 5 | - | use ratatui::style::{Color, Modifier, Style}; | |
| 5 | + | use ratatui::style::{Modifier, Style}; | |
| 6 | 6 | use ratatui::text::{Line, Span}; | |
| 7 | 7 | use ratatui::widgets::{Block, Borders, Paragraph, Row}; | |
| 8 | 8 | ||
| @@ -34,7 +34,7 @@ | |||
| 34 | 34 | let block = Block::default() | |
| 35 | 35 | .title(title) | |
| 36 | 36 | .borders(Borders::ALL) | |
| 37 | - | .border_style(Style::default().fg(Color::Gray)); | |
| 37 | + | .border_style(Style::default().fg(app.theme.line_border)); | |
| 38 | 38 | ||
| 39 | 39 | let inner = block.inner(area); | |
| 40 | 40 | frame.render_widget(block, area); | |
| @@ -84,7 +84,11 @@ | |||
| 84 | 84 | status.to_string(), | |
| 85 | 85 | desc, | |
| 86 | 86 | ]) | |
| 87 | - | .style(widgets::selected_style(i, Some(app.selected_index))) | |
| 87 | + | .style(widgets::selected_style( | |
| 88 | + | &app.theme, | |
| 89 | + | i, | |
| 90 | + | Some(app.selected_index), | |
| 91 | + | )) | |
| 88 | 92 | }) | |
| 89 | 93 | .collect(); | |
| 90 | 94 | ||
| @@ -97,6 +101,7 @@ | |||
| 97 | 101 | ||
| 98 | 102 | widgets::render_table( | |
| 99 | 103 | frame, | |
| 104 | + | &app.theme, | |
| 100 | 105 | chunks[2], | |
| 101 | 106 | &[" Name", "Price", "Status", "Description"], | |
| 102 | 107 | &widths, | |
| @@ -106,9 +111,9 @@ | |||
| 106 | 111 | ||
| 107 | 112 | if let Some(ref status) = app.tiers_status { | |
| 108 | 113 | let style = if status.starts_with("Error") { | |
| 109 | - | Style::default().fg(Color::Red) | |
| 114 | + | Style::default().fg(app.theme.status_danger) | |
| 110 | 115 | } else { | |
| 111 | - | Style::default().fg(Color::Green) | |
| 116 | + | Style::default().fg(app.theme.status_success) | |
| 112 | 117 | }; | |
| 113 | 118 | let status_line = Paragraph::new(format!(" {status}")).style(style); | |
| 114 | 119 | frame.render_widget(status_line, chunks[3]); | |
| @@ -122,6 +127,7 @@ | |||
| 122 | 127 | Span::raw(" Back"), | |
| 123 | 128 | ]; | |
| 124 | 129 | ||
| 125 | - | let keys = Paragraph::new(Line::from(key_spans)).style(Style::default().fg(Color::DarkGray)); | |
| 130 | + | let keys = | |
| 131 | + | Paragraph::new(Line::from(key_spans)).style(Style::default().fg(app.theme.content_muted)); | |
| 126 | 132 | frame.render_widget(keys, chunks[4]); | |
| 127 | 133 | } |