tuika 0.6.0

The application framework for Rust terminal UIs — flexbox layout, overlays, focus, keymap, components, and safe ratatui interoperability.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
//! Bundled standard themes.
//!
//! [`Theme`] is a plain `Copy` struct of colors, so a palette is
//! just data — these presets are written out as full `const Theme` structures
//! rather than hidden behind a builder. Each is also reachable by a stable
//! `name` through [`by_name`] and the [`PRESETS`] catalog, so a host can offer a
//! `--theme` flag or a picker without hard-coding the list.
//!
//! ```
//! use tuika::themes;
//!
//! let t = themes::SOLARIZED_DARK;                    // the struct directly
//! let same = themes::by_name("solarized-dark").unwrap(); // …or by name
//! assert_eq!(t, same);
//! ```
//!
//! The palettes below use each project's published colors. tuika maps them onto
//! its own slots (`background`, `surface`, `accent`, the [`CodeTheme`] syntax
//! roles); it does not reproduce every upstream UI element.

use ratatui_core::style::Color;

use crate::style::{CodeTheme, Theme};

/// A bundled [`Theme`] paired with the identifiers a host uses to expose it.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct NamedTheme {
    /// Stable, lowercase, kebab-case id for config and `--theme` (`"gruvbox-dark"`).
    pub name: &'static str,
    /// Human-facing label for menus and pickers (`"Gruvbox Dark"`).
    pub label: &'static str,
    /// The palette itself.
    pub theme: Theme,
}

/// Every bundled theme, in menu order. Iterate this to build a picker or to
/// validate a `--theme` value.
pub const PRESETS: &[NamedTheme] = &[
    NamedTheme {
        name: "solarized-dark",
        label: "Solarized Dark",
        theme: SOLARIZED_DARK,
    },
    NamedTheme {
        name: "solarized-light",
        label: "Solarized Light",
        theme: SOLARIZED_LIGHT,
    },
    NamedTheme {
        name: "gruvbox-dark",
        label: "Gruvbox Dark",
        theme: GRUVBOX_DARK,
    },
    NamedTheme {
        name: "light",
        label: "Light",
        theme: LIGHT,
    },
    NamedTheme {
        name: "dracula",
        label: "Dracula",
        theme: DRACULA,
    },
    NamedTheme {
        name: "terminal",
        label: "Terminal",
        theme: TERMINAL,
    },
];

/// Look a bundled theme up by its [`NamedTheme::name`], case-insensitively.
/// Returns `None` for an unknown id; pair it with [`PRESETS`] to list the valid
/// choices in an error message.
pub fn by_name(name: &str) -> Option<Theme> {
    PRESETS
        .iter()
        .find(|p| p.name.eq_ignore_ascii_case(name))
        .map(|p| p.theme)
}

// --- Solarized (Ethan Schoonover) -----------------------------------------
// base03 #002b36, base02 #073642, base01 #586e75, base00 #657b83,
// base0 #839496, base1 #93a1a1, base2 #eee8d5, base3 #fdf6e3,
// yellow #b58900, orange #cb4b16, red #dc322f, magenta #d33682,
// blue #268bd2, cyan #2aa198, green #859900.

/// Solarized Dark — Ethan Schoonover's precision palette on the dark base tones.
pub const SOLARIZED_DARK: Theme = Theme {
    background: Color::Rgb(0, 43, 54),    // base03
    surface: Color::Rgb(7, 54, 66),       // base02
    text: Color::Rgb(131, 148, 150),      // base0
    muted: Color::Rgb(88, 110, 117),      // base01
    dim: Color::Rgb(58, 82, 92),          // between base01 and base02
    accent: Color::Rgb(38, 139, 210),     // blue
    accent_alt: Color::Rgb(42, 161, 152), // cyan
    border: Color::Rgb(58, 82, 92),
    border_focused: Color::Rgb(38, 139, 210),
    selection_bg: Color::Rgb(7, 54, 66),     // base02
    selection_fg: Color::Rgb(147, 161, 161), // base1
    code: CodeTheme {
        heading: Color::Rgb(147, 161, 161),     // base1
        link: Color::Rgb(38, 139, 210),         // blue
        background: Color::Rgb(7, 54, 66),      // base02
        text: Color::Rgb(131, 148, 150),        // base0
        label: Color::Rgb(88, 110, 117),        // base01
        keyword: Color::Rgb(133, 153, 0),       // green
        function: Color::Rgb(38, 139, 210),     // blue
        type_name: Color::Rgb(181, 137, 0),     // yellow
        constant: Color::Rgb(211, 54, 130),     // magenta
        string: Color::Rgb(42, 161, 152),       // cyan
        comment: Color::Rgb(88, 110, 117),      // base01
        punctuation: Color::Rgb(101, 123, 131), // base00
    },
};

/// Solarized Light — the same accents on the light base tones.
pub const SOLARIZED_LIGHT: Theme = Theme {
    background: Color::Rgb(253, 246, 227), // base3
    surface: Color::Rgb(238, 232, 213),    // base2
    text: Color::Rgb(101, 123, 131),       // base00
    muted: Color::Rgb(147, 161, 161),      // base1
    dim: Color::Rgb(203, 198, 182),        // faint over the light base
    accent: Color::Rgb(38, 139, 210),      // blue
    accent_alt: Color::Rgb(42, 161, 152),  // cyan
    border: Color::Rgb(147, 161, 161),     // base1
    border_focused: Color::Rgb(38, 139, 210),
    selection_bg: Color::Rgb(238, 232, 213), // base2
    selection_fg: Color::Rgb(88, 110, 117),  // base01
    code: CodeTheme {
        heading: Color::Rgb(88, 110, 117),      // base01
        link: Color::Rgb(38, 139, 210),         // blue
        background: Color::Rgb(238, 232, 213),  // base2
        text: Color::Rgb(101, 123, 131),        // base00
        label: Color::Rgb(147, 161, 161),       // base1
        keyword: Color::Rgb(133, 153, 0),       // green
        function: Color::Rgb(38, 139, 210),     // blue
        type_name: Color::Rgb(181, 137, 0),     // yellow
        constant: Color::Rgb(211, 54, 130),     // magenta
        string: Color::Rgb(42, 161, 152),       // cyan
        comment: Color::Rgb(147, 161, 161),     // base1
        punctuation: Color::Rgb(101, 123, 131), // base00
    },
};

// --- Gruvbox (morhetz), dark medium ---------------------------------------
// bg0 #282828, bg1 #3c3836, bg2 #504945, bg3 #665c54, fg0 #fbf1c7,
// fg1 #ebdbb2, fg4 #a89984, gray #928374, bright red #fb4934,
// bright green #b8bb26, bright yellow #fabd2f, bright purple #d3869b,
// bright aqua #8ec07c, bright orange #fe8019, neutral blue #83a598.

/// Gruvbox Dark (medium) — morhetz's warm retro palette.
pub const GRUVBOX_DARK: Theme = Theme {
    background: Color::Rgb(40, 40, 40),    // bg0
    surface: Color::Rgb(60, 56, 54),       // bg1
    text: Color::Rgb(235, 219, 178),       // fg1
    muted: Color::Rgb(168, 153, 132),      // fg4
    dim: Color::Rgb(102, 92, 84),          // bg3
    accent: Color::Rgb(254, 128, 25),      // bright orange
    accent_alt: Color::Rgb(142, 192, 124), // bright aqua
    border: Color::Rgb(102, 92, 84),       // bg3
    border_focused: Color::Rgb(254, 128, 25),
    selection_bg: Color::Rgb(80, 73, 69),    // bg2
    selection_fg: Color::Rgb(251, 241, 199), // fg0
    code: CodeTheme {
        heading: Color::Rgb(235, 219, 178),     // fg1
        link: Color::Rgb(131, 165, 152),        // neutral blue
        background: Color::Rgb(29, 32, 33),     // bg0_hard
        text: Color::Rgb(235, 219, 178),        // fg1
        label: Color::Rgb(168, 153, 132),       // fg4
        keyword: Color::Rgb(251, 73, 52),       // bright red
        function: Color::Rgb(142, 192, 124),    // bright aqua
        type_name: Color::Rgb(250, 189, 47),    // bright yellow
        constant: Color::Rgb(211, 134, 155),    // bright purple
        string: Color::Rgb(184, 187, 38),       // bright green
        comment: Color::Rgb(146, 131, 116),     // gray
        punctuation: Color::Rgb(168, 153, 132), // fg4
    },
};

// --- Plain Light -----------------------------------------------------------
// A neutral, near-monochrome light theme for terminals with a light background.
// Deliberately understated so it reads as "the light default", not a brand.

/// Light — a neutral, near-monochrome palette for light terminals.
pub const LIGHT: Theme = Theme {
    background: Color::Rgb(250, 250, 250),
    surface: Color::Rgb(240, 240, 240),
    text: Color::Rgb(30, 30, 30),
    muted: Color::Rgb(110, 110, 110),
    dim: Color::Rgb(190, 190, 190),
    accent: Color::Rgb(30, 102, 204),    // a calm blue
    accent_alt: Color::Rgb(0, 135, 120), // teal
    border: Color::Rgb(200, 200, 200),
    border_focused: Color::Rgb(30, 102, 204),
    selection_bg: Color::Rgb(210, 226, 250),
    selection_fg: Color::Rgb(20, 40, 70),
    code: CodeTheme {
        heading: Color::Rgb(20, 20, 20),
        link: Color::Rgb(30, 102, 204),
        background: Color::Rgb(240, 240, 240),
        text: Color::Rgb(40, 40, 40),
        label: Color::Rgb(110, 110, 110),
        keyword: Color::Rgb(170, 13, 145),  // magenta
        function: Color::Rgb(30, 102, 204), // blue
        type_name: Color::Rgb(0, 92, 197),
        constant: Color::Rgb(155, 76, 0), // brown/orange
        string: Color::Rgb(24, 128, 56),  // green
        comment: Color::Rgb(120, 130, 120),
        punctuation: Color::Rgb(90, 90, 90),
    },
};

// --- Dracula (Zeno Rocha et al.) ------------------------------------------
// bg #282a36, current-line #44475a, fg #f8f8f2, comment #6272a4,
// cyan #8be9fd, green #50fa7b, orange #ffb86c, pink #ff79c6,
// purple #bd93f9, red #ff5555, yellow #f1fa8c.

/// Dracula — the widely-ported dark theme.
pub const DRACULA: Theme = Theme {
    background: Color::Rgb(40, 42, 54),
    surface: Color::Rgb(68, 71, 90), // current line
    text: Color::Rgb(248, 248, 242),
    muted: Color::Rgb(98, 114, 164), // comment
    dim: Color::Rgb(73, 76, 98),
    accent: Color::Rgb(189, 147, 249),     // purple
    accent_alt: Color::Rgb(255, 121, 198), // pink
    border: Color::Rgb(98, 114, 164),
    border_focused: Color::Rgb(189, 147, 249),
    selection_bg: Color::Rgb(68, 71, 90),
    selection_fg: Color::Rgb(248, 248, 242),
    code: CodeTheme {
        heading: Color::Rgb(248, 248, 242),
        link: Color::Rgb(139, 233, 253), // cyan
        background: Color::Rgb(33, 34, 44),
        text: Color::Rgb(248, 248, 242),
        label: Color::Rgb(98, 114, 164),
        keyword: Color::Rgb(255, 121, 198),   // pink
        function: Color::Rgb(80, 250, 123),   // green
        type_name: Color::Rgb(139, 233, 253), // cyan
        constant: Color::Rgb(189, 147, 249),  // purple
        string: Color::Rgb(241, 250, 140),    // yellow
        comment: Color::Rgb(98, 114, 164),
        punctuation: Color::Rgb(248, 248, 242),
    },
};

// --- Terminal (inherited) --------------------------------------------------
// Not a palette at all: a mapping onto the slots the *terminal* resolves. Every
// other preset names 24-bit colors; this one names ANSI indices and `Reset`, so
// what the user sees is whatever their terminal was configured with.

/// Terminal — inherit the user's own terminal colors, with no probe.
///
/// Every slot is either [`Color::Reset`] (the terminal's default foreground or
/// background) or a [`Color::Indexed`] ANSI slot, so the terminal resolves the
/// palette itself. That makes this the one theme that cannot be wrong about the
/// user's setup and cannot fail: it needs no query, no timeout, and no
/// capability — it works on a terminal that answers nothing.
///
/// The trade is that ANSI has no tone *between* two slots, so tuika's raised and
/// faint roles collapse: `surface` and the code background are the plain
/// background, and `dim`, `border`, and `muted` all land on bright black. For
/// panels and rules that read as distinct, ask the terminal for its actual
/// colors instead and derive them — see [`Theme::from_terminal`] and
/// [`crate::term::palette`], which falls back to exactly this theme when the terminal
/// stays silent.
///
/// [`Theme::from_terminal`]: crate::Theme::from_terminal
pub const TERMINAL: Theme = Theme {
    background: Color::Reset,
    // ANSI cannot express a raised fill; a distinct one here would have to be a
    // real color, which is precisely what this theme refuses to guess.
    surface: Color::Reset,
    text: Color::Reset,
    muted: Color::Indexed(8),
    dim: Color::Indexed(8),
    accent: Color::Indexed(4),
    accent_alt: Color::Indexed(6),
    border: Color::Indexed(8),
    border_focused: Color::Indexed(4),
    // A selection has to paint a real background, so it uses the one pairing
    // that reads on a light and a dark terminal alike.
    selection_bg: Color::Indexed(4),
    selection_fg: Color::Indexed(15),
    code: CodeTheme {
        heading: Color::Reset,
        link: Color::Indexed(4),
        background: Color::Reset,
        text: Color::Reset,
        label: Color::Indexed(8),
        keyword: Color::Indexed(5),
        function: Color::Indexed(4),
        type_name: Color::Indexed(6),
        constant: Color::Indexed(3),
        string: Color::Indexed(2),
        comment: Color::Indexed(8),
        punctuation: Color::Indexed(8),
    },
};

impl Theme {
    /// The [`SOLARIZED_DARK`] preset.
    pub const fn solarized_dark() -> Theme {
        SOLARIZED_DARK
    }

    /// The [`SOLARIZED_LIGHT`] preset.
    pub const fn solarized_light() -> Theme {
        SOLARIZED_LIGHT
    }

    /// The [`GRUVBOX_DARK`] preset.
    pub const fn gruvbox_dark() -> Theme {
        GRUVBOX_DARK
    }

    /// The [`LIGHT`] preset.
    pub const fn light() -> Theme {
        LIGHT
    }

    /// The [`DRACULA`] preset.
    pub const fn dracula() -> Theme {
        DRACULA
    }

    /// The [`TERMINAL`] preset — inherit the terminal's own colors.
    pub const fn terminal() -> Theme {
        TERMINAL
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn by_name_matches_presets_case_insensitively() {
        for p in PRESETS {
            assert_eq!(by_name(p.name), Some(p.theme));
            assert_eq!(by_name(&p.name.to_uppercase()), Some(p.theme));
        }
        assert_eq!(by_name("no-such-theme"), None);
    }

    #[test]
    fn constructors_match_catalog() {
        assert_eq!(Theme::solarized_dark(), by_name("solarized-dark").unwrap());
        assert_eq!(
            Theme::solarized_light(),
            by_name("solarized-light").unwrap()
        );
        assert_eq!(Theme::gruvbox_dark(), by_name("gruvbox-dark").unwrap());
        assert_eq!(Theme::light(), by_name("light").unwrap());
        assert_eq!(Theme::dracula(), by_name("dracula").unwrap());
        assert_eq!(Theme::terminal(), by_name("terminal").unwrap());
    }

    /// Every other preset is a fixed palette; this one must resolve entirely at
    /// the terminal, so a literal color anywhere in it would be a bug.
    #[test]
    fn the_terminal_preset_names_no_color_of_its_own() {
        let t = TERMINAL;
        let slots = [
            t.background,
            t.surface,
            t.text,
            t.muted,
            t.dim,
            t.accent,
            t.accent_alt,
            t.border,
            t.border_focused,
            t.selection_bg,
            t.selection_fg,
            t.code.heading,
            t.code.link,
            t.code.background,
            t.code.text,
            t.code.label,
            t.code.keyword,
            t.code.function,
            t.code.type_name,
            t.code.constant,
            t.code.string,
            t.code.comment,
            t.code.punctuation,
        ];
        for slot in slots {
            assert!(
                matches!(slot, Color::Reset | Color::Indexed(0..=15)),
                "{slot:?} is not something the terminal resolves"
            );
        }
    }

    #[test]
    fn preset_names_are_unique_and_kebab_case() {
        let mut seen = std::collections::HashSet::new();
        for p in PRESETS {
            assert!(seen.insert(p.name), "duplicate preset name {}", p.name);
            assert!(
                p.name
                    .chars()
                    .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-'),
                "preset name {} is not kebab-case",
                p.name
            );
            assert!(!p.label.is_empty());
        }
    }

    #[test]
    fn every_preset_has_semantic_status_colors() {
        use crate::SemanticRole;

        for preset in PRESETS {
            let theme = preset.theme;
            assert_eq!(
                theme.semantic_color(SemanticRole::Success),
                theme.code.string,
                "{} success",
                preset.name
            );
            assert_eq!(
                theme.semantic_color(SemanticRole::Warning),
                theme.code.constant,
                "{} warning",
                preset.name
            );
            assert_eq!(
                theme.semantic_color(SemanticRole::Danger),
                theme.code.keyword,
                "{} danger",
                preset.name
            );
            assert_eq!(
                theme.semantic_color(SemanticRole::Info),
                theme.code.link,
                "{} info",
                preset.name
            );
        }
    }
}