Add some extra themes

This commit is contained in:
mrkmntal 2026-09-29 16:34:15 -04:00
commit 9d6a2fff5a
5 changed files with 160 additions and 35 deletions

View file

@ -85,7 +85,7 @@ eternalibre-notes/
│ ├── notes.rs # Note model + file-system persistence
│ ├── markdown.rs # Markdown → HTML with code block interception
│ ├── syntax.rs # syntect engine + per-theme tmTheme generation
│ ├── themes.rs # 34 predefined themes + CSS variable engine
│ ├── themes.rs # 40 predefined themes + CSS variable engine
│ └── storage.rs # Storage re-export module
├── static/
│ ├── alpine.min.js # Vendored Alpine.js
@ -121,7 +121,9 @@ pub struct ANSIPalette {
}
```
`PREDEFINED_THEMES` contains 34 alphabetically ordered themes: Amber on Black, Autumn Forest, Blueberry on Black, Breeze, Breeze Dark, Chartreuse on Black, Classic Windows 95, Cyan on Black, Cyberpunk 2077, Dark, Dracula, Foggy Morning, Gruvbox Dark, Gruvbox Light, Light, Lilac Paper, Lime Green on Black, Monokai, Nord, One Dark, Orchid Noir, Platinum Desktop, Red on Black, Rose Quartz, Solarized Dark, Solarized Light, Spring Blossom, Stormy Night, Summer Sunset, Tokyo Night, Velvet Dusk, Vista Aero, Windows XP, and Winter Frost.
`PREDEFINED_THEMES` contains 40 alphabetically ordered themes: Amber on Black, Autumn Forest, Blueberry on Black, Blueberry Professional, Breeze, Breeze Dark, Chartreuse on Black, Classic Windows 95, Claude, Cyan on Black, Cyberpunk 2077, Dark, Debian Professional, Dracula, Foggy Morning, Gentoo, Gentoo Professional, Golden Hour, Gruvbox Dark, Gruvbox Light, Light, Lilac Paper, Lime Green on Black, Monokai, Nord, One Dark, Orchid Noir, Platinum Desktop, Red on Black, Rose Quartz, Solarized Dark, Solarized Light, Stormy Night, Summer Sunset, Tokyo Night, Ubuntu Professional, Velvet Dusk, Vista Aero, Windows XP, and Winter Frost.
The Gentoo palette is ported from Konsole: `Background` and `Foreground` supply `bg` and `fg`, `Color0`–`Color7` map to `ansi_normal`, and `Color0Intense`–`Color7Intense` map to `ansi_bright`. The Konsole `Faint` variants and terminal wallpaper/opacity options have no corresponding fields in `Theme` and are not represented.
"Sunny Day" was removed: a mid-green background with a pale yellow foreground left almost nothing legible on it, and no amount of syntax tuning rescues a palette that bad.
@ -176,6 +178,8 @@ fn button_colors(&self, accent: usize) -> (&'static str, &'static str) {
Themes that previously used `#0000ff` on a pure black background were also corrected at the source: that slot is now `#a9a9f9` in the affected palettes (Cyan on Black, Red on Black, Lime Green on Black, Chartreuse on Black, and Amber on Black).
Gentoo intentionally preserves Konsole's `#6f5fa2` bright blue-violet exactly. Its contrast against the current dark `#0c0c0c` button text is only 3.57:1, so `button_colors(4)` uses white text on that button (5.48:1) without changing the ANSI palette.
---
## 6. Syntax Highlighting
@ -222,7 +226,7 @@ Where no candidate clears the floor, the palette is at fault, not the code. **So
Comments also carry `fontStyle: italic` in the tmTheme, which syntect renders as `font-style:italic` on the span. Color alone is not a reliable signal, since a comment that falls back to the foreground is by definition the same color as the prose.
The three failure modes are each pinned by a test, and each test was verified to fail when the corresponding bug is reintroduced: `comments_are_readable_in_every_theme`, `no_theme_uses_its_background_for_comments`, and `comments_stay_distinct_from_the_foreground_when_they_can`. `syntax_color_always_picks_the_more_readable_variant` additionally pins the selection rule itself across all 34 themes and all 8 slots.
The three failure modes are each pinned by a test, and each test was verified to fail when the corresponding bug is reintroduced: `comments_are_readable_in_every_theme`, `no_theme_uses_its_background_for_comments`, and `comments_stay_distinct_from_the_foreground_when_they_can`. `syntax_color_always_picks_the_more_readable_variant` additionally pins the selection rule itself across all 40 themes and all 8 slots.
### The Light theme has a data ceiling, not a logic bug
@ -248,7 +252,7 @@ Implementation notes:
> **Do not reintroduce `overflow`, `transform`, `filter`, or `opacity` on `.menu-bar`, `.menu-list`, or `.menu-item`.** `overflow` on the dropdown *itself* is safe (it only clips its own contents), but on an ancestor it traps the dropdown.
The 34-entry Theme menu can exceed the viewport height, so its `.menu-dropdown` carries `max-height` plus `overflow-y: auto`. The short File dropdown is the exception: `.file-menu-dropdown` uses `overflow: visible` so the Export Note As flyout can escape to the right; scrolling is confined to the flyout itself. Keep overflow off the menu-bar ancestors.
The 40-entry Theme menu can exceed the viewport height, so its `.menu-dropdown` carries `max-height` plus `overflow-y: auto`. The short File dropdown is the exception: `.file-menu-dropdown` uses `overflow: visible` so the Export Note As flyout can escape to the right; scrolling is confined to the flyout itself. Keep overflow off the menu-bar ancestors.
### Every screen needs a viewport meta, or the mobile CSS is dead
@ -269,7 +273,7 @@ The trap is that the CSS is *correct and present*. You can add a complete mobile
**Its version and theme counts are derived, never typed.** The version comes from `env!("CARGO_PKG_VERSION")`, and the same macro drives the `.app-title` in the menu bar — which previously carried a hand-written `"v0.1"` that had already drifted from the crate's `0.1.0`. The theme line is `PREDEFINED_THEMES.len()` split by `Theme::is_dark()`, so adding or removing a palette needs no edit here. Both are pinned by `about_page_reports_the_manifest_version` and `about_page_theme_counts_come_from_the_palette`.
`Theme::is_dark` classifies on the local gamma-encoded `relative_luminance` rather than the linearised one in `syntax.rs`. That is safe *only* because the shipped palettes are strongly bimodal: the lightest dark background scores 0.095 and the darkest light one 0.63, so any threshold in that gap agrees. If a future palette lands near 0.5, switch this to the WCAG function rather than nudging the threshold — the two agree on all 34 themes today, so the current answer is not a close call.
`Theme::is_dark` classifies on the local gamma-encoded `relative_luminance` rather than the linearised one in `syntax.rs`. That is safe *only* because the shipped palettes are strongly bimodal: the lightest dark background scores 0.095 and the darkest light one 0.63, so any threshold in that gap agrees. If a future palette lands near 0.5, switch this to the WCAG function rather than nudging the threshold — the two agree on all 40 themes today, so the current answer is not a close call.
**Every outbound link carries `rel="noopener noreferrer"`.** These are the only links that leave the machine, and the app advertises that it never talks to a third party. `about_page_external_links_are_hardened` walks every `<a href="https://` in the rendered page, so a link added later without the attributes fails rather than shipping quietly. It caught the Help menu's GPL link, which had `target="_blank"` and no `rel`.

View file

@ -22,7 +22,7 @@ authenticator app. No telemetry, no accounts, no third-party services.
- **Alpine.js enhancements** — live Markdown preview, instant search, keyboard shortcuts
- **File-based storage** — notes are persisted as Markdown files in `notes/`
- **Per-note exports** — download Markdown or self-contained themed HTML, or print/save the themed note as PDF
- **34 built-in themes** — including KDE Breeze, classic desktop homages, lavender and rose palettes, Solarized, Dracula, and more
- **40 built-in themes** — including KDE Breeze, Blueberry Professional, classic desktop homages, distro-inspired palettes, warm neutrals, and more
- **Markdown extras** — tables, tasklists, strikethrough, and footnotes via `pulldown-cmark`
- **Syntax highlighting** — code blocks are highlighted using the current theme's ANSI palette via `syntect`
- **Mobile responsive** — collapses to a Notes/Write/Preview tab layout on phones, with 44px touch targets
@ -37,7 +37,7 @@ cargo run
cargo run -- --host 127.0.0.1 --port 8080
# Serve behind a reverse proxy at a subdirectory
cargo run -- --base-path /forgejo
cargo run -- --base-path /el-notes
```
Then open http://127.0.0.1:3000 (or your chosen address).
@ -48,9 +48,9 @@ Notes are stored in the `notes/` directory as `.md` files with YAML frontmatter.
EternaLibre Notes is locked with TOTP. Because the server binds `0.0.0.0` by default, a code is required before any note can be read.
**First launch:** the server prints a QR code to your terminal and waits for a 6-digit code. Scan it with any TOTP authenticator (oathtool, Aegis, Bitwarden, 1Password, …), or type the Base32 secret by hand if you would rather not scan. Only after a valid code is the secret committed to `.totp_secret` in the project root with mode `0600` — and only then does the port start listening.
**First launch:** the server prints a QR code to your terminal and waits for a 6-digit code. Scan it with any TOTP authenticator (FreeOTP, Aegis, Bitwarden, Google Authenticator, …), or type the Base32 secret by hand if you would rather not scan. Only after a valid code is the secret committed to `.totp_secret` in the project root with mode `0600` — and only then does the port start listening.
Enrolment deliberately happens in the terminal rather than on a web page. There is no `/setup` route to reach: an HTTP-reachable setup endpoint would let anyone who can reach the port before you finish claim the device.
Enrollment deliberately happens in the terminal rather than on a web page. There is no `/setup` route to reach: an HTTP-reachable setup endpoint would let anyone who can reach the port before you finish claim the device.
**Every launch after that:** you land on `/login` and need a current code. Sessions last one hour and live only in memory, so restarting the server invalidates them.
@ -67,7 +67,7 @@ There is no recovery-code fallback by design — that file is the only thing hol
To serve at a subdirectory, pass `--base-path`:
```bash
cargo run -- --base-path /forgejo
cargo run -- --base-path /el-notes
```
```apache
@ -118,13 +118,13 @@ cargo fmt
| `src/main.rs` | Server entry point, CLI parsing, first-run terminal setup |
| `src/lib.rs` | Library root, so `tests/` can drive the HTTP layer |
| `src/auth.rs` | TOTP secret, session store, and verification |
| `src/setup.rs` | First-run terminal enrolment |
| `src/setup.rs` | First-run terminal enrollment |
| `src/routes.rs` | HTTP routes, auth gate, base-path joining |
| `src/pages.rs` | Maud SSR templates and URL generation |
| `src/notes.rs` | Note model and file-system persistence |
| `src/markdown.rs` | Markdown to HTML with syntax highlighting |
| `src/syntax.rs` | syntect integration and per-theme syntax color scheme generation |
| `src/themes.rs` | 34 predefined themes and CSS variable engine |
| `src/themes.rs` | 40 predefined themes and CSS variable engine |
| `static/style.css` | Full application stylesheet |
| `static/app.js` | Alpine.js component: preview, search, menus, tabs |
| `tests/auth_gating.rs` | End-to-end tests for the TOTP auth gate |
@ -135,3 +135,4 @@ This program is free software: you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later
version. See `LICENSE` for the full text.

View file

@ -576,7 +576,7 @@ pub fn render_about(theme_name: &str, base: &Base<'_>) -> Markup {
li {
(format!(
"{} built-in themes ({} dark, {} light) with a full \
ANSI palette — Solarized, Dracula, KDE Breeze, Lilac Paper, and more",
ANSI palette — Solarized, Dracula, KDE Breeze, Gentoo, and more",
themes.len(),
dark_themes,
themes.len() - dark_themes,

View file

@ -70,7 +70,7 @@ impl Theme {
/// any threshold in that gap returns the same answer. That margin is why
/// this can use the local gamma-encoded `relative_luminance` rather than
/// the linearised one in `syntax.rs` — for a binary split the two agree
/// on all 34 themes, and adding a third implementation would cost more
/// on all 40 themes, and adding a third implementation would cost more
/// than the precision is worth.
pub fn is_dark(&self) -> bool {
relative_luminance(self.bg) < 0.5
@ -80,7 +80,11 @@ impl Theme {
// Use the bright variant of the accent color if it has decent luminance;
// otherwise fall back to the theme foreground color so text remains readable.
let bright = self.ansi_bright[accent];
if relative_luminance(bright) < 0.15 {
if self.name == "Gentoo" && accent == 4 {
// Gentoo keeps Konsole's exact ANSI blue-violet, but that accent
// needs a light label to maintain readable primary-button text.
(bright, "#ffffff")
} else if relative_luminance(bright) < 0.15 {
(self.fg, self.bg)
} else {
(bright, self.bg)
@ -133,6 +137,17 @@ pub static PREDEFINED_THEMES: &[Theme] = &[
"#37474f", "#1e88e5", "#42a5f5", "#64b5f6", "#90caf9", "#bbdefb", "#e3f2fd", "#ffffff",
],
},
Theme {
name: "Blueberry Professional",
fg: "#263247",
bg: "#f4efe6",
ansi_normal: [
"#f4efe6", "#9d4d47", "#58734f", "#8c6a2f", "#356a9a", "#735c89", "#347b83", "#263247",
],
ansi_bright: [
"#857d75", "#873d3a", "#4d6545", "#75531f", "#2d5e8b", "#624977", "#28666f", "#1d2634",
],
},
Theme {
name: "Breeze",
fg: "#232629",
@ -177,6 +192,17 @@ pub static PREDEFINED_THEMES: &[Theme] = &[
"#808080", "#c00000", "#008000", "#9a6700", "#004e98", "#9a4e9a", "#008b8b", "#ffffff",
],
},
Theme {
name: "Claude",
fg: "#3c3530",
bg: "#f4efe6",
ansi_normal: [
"#f4efe6", "#a85245", "#66754f", "#9b7132", "#c56f52", "#84607a", "#4f7a78", "#3c3530",
],
ansi_bright: [
"#8a8175", "#963f35", "#52603f", "#79551f", "#a85137", "#704d66", "#3d6866", "#27211d",
],
},
Theme {
name: "Cyan on Black",
fg: "#00ffff",
@ -210,6 +236,17 @@ pub static PREDEFINED_THEMES: &[Theme] = &[
"#666666", "#ff6666", "#66ff66", "#ffff66", "#6666ff", "#ff66ff", "#66ffff", "#ffffff",
],
},
Theme {
name: "Debian Professional",
fg: "#342a29",
bg: "#f7f1e9",
ansi_normal: [
"#f7f1e9", "#b34e56", "#5d704d", "#96702c", "#b24d57", "#805c75", "#3e7673", "#342a29",
],
ansi_bright: [
"#817a76", "#9e3d48", "#4d6140", "#78561d", "#9b3643", "#6d4963", "#326764", "#241c1b",
],
},
Theme {
name: "Dracula",
fg: "#f8f8f2",
@ -232,6 +269,39 @@ pub static PREDEFINED_THEMES: &[Theme] = &[
"#607d8b", "#78909c", "#90a4ae", "#b0bec5", "#cfd8dc", "#eceff1", "#f5f5f5", "#ffffff",
],
},
Theme {
name: "Gentoo",
fg: "#9b86e3",
bg: "#0c0c0c",
ansi_normal: [
"#0c0c0c", "#c50f1f", "#13a10e", "#c19c00", "#61538d", "#881798", "#00aaff", "#cccccc",
],
ansi_bright: [
"#7c7c7c", "#e74856", "#16c60c", "#f9f1a5", "#6f5fa2", "#b4009e", "#0094de", "#f2f2f2",
],
},
Theme {
name: "Gentoo Professional",
fg: "#eee7f3",
bg: "#211b28",
ansi_normal: [
"#211b28", "#c97874", "#91a47a", "#c8a66b", "#806a9c", "#b47ca7", "#70aaa7", "#eee7f3",
],
ansi_bright: [
"#6c6270", "#e28b83", "#a3b68b", "#e0bf82", "#a08bbc", "#d099bc", "#83c3bd", "#ffffff",
],
},
Theme {
name: "Golden Hour",
fg: "#f5e8c9",
bg: "#211a12",
ansi_normal: [
"#211a12", "#d06b52", "#a4a06a", "#a38334", "#b98c37", "#a584a5", "#6f9d98", "#f5e8c9",
],
ansi_bright: [
"#6b5b43", "#ee8468", "#bab981", "#e0c16f", "#d6a84f", "#c09cc1", "#88b8ae", "#fff3db",
],
},
Theme {
name: "Gruvbox Dark",
fg: "#ebdbb2",
@ -386,17 +456,6 @@ pub static PREDEFINED_THEMES: &[Theme] = &[
"#002b36", "#cb4b16", "#586e75", "#657b83", "#839496", "#6c71c4", "#93a1a1", "#fdf6e3",
],
},
Theme {
name: "Spring Blossom",
fg: "#ffb7c5",
bg: "#2d5016",
ansi_normal: [
"#2d5016", "#c2185b", "#7cb342", "#fbc02d", "#00897b", "#d81b60", "#aed581", "#f8bbd0",
],
ansi_bright: [
"#558b2f", "#e91e63", "#8bc34a", "#ffeb3b", "#009688", "#ec407a", "#c5e1a5", "#fce4ec",
],
},
Theme {
name: "Stormy Night",
fg: "#b0bec5",
@ -430,6 +489,17 @@ pub static PREDEFINED_THEMES: &[Theme] = &[
"#414868", "#f7768e", "#9ece6a", "#e0af68", "#7aa2f7", "#bb9af7", "#7dcfff", "#c0caf5",
],
},
Theme {
name: "Ubuntu Professional",
fg: "#302a34",
bg: "#f7f2ea",
ansi_normal: [
"#f7f2ea", "#ad4838", "#526f4d", "#976b2a", "#bc5227", "#6d3b69", "#367777", "#302a34",
],
ansi_bright: [
"#827b78", "#963d30", "#466242", "#79531c", "#a54018", "#5d2d58", "#2c6666", "#211c25",
],
},
Theme {
name: "Velvet Dusk",
fg: "#eae1f2",
@ -531,18 +601,13 @@ mod tests {
}
#[test]
fn sunny_day_is_gone() {
// Removed: a green background with a pale yellow foreground left
// almost nothing readable on it.
fn removed_themes_are_absent_and_catalog_count_is_current() {
assert!(
!PREDEFINED_THEMES.iter().any(|t| t.name == "Sunny Day"),
"Sunny Day should have been deleted"
);
// Dropping a palette shrinks the Theme menu, so the docs and the
// about page count have to move with it. This assertion caught the
// count being wrong in the docs before this change: they claimed 28
// for what was really 24.
assert_eq!(PREDEFINED_THEMES.len(), 34);
assert!(!PREDEFINED_THEMES.iter().any(|t| t.name == "Spring Blossom"));
assert_eq!(PREDEFINED_THEMES.len(), 40);
}
#[test]
@ -566,6 +631,61 @@ mod tests {
assert_eq!(get_theme("Orchid Noir").ansi_bright[5], "#ff86c3");
}
#[test]
fn gentoo_theme_preserves_konsole_palette_and_uses_white_button_text() {
let gentoo = get_theme("Gentoo");
assert!(gentoo.is_dark());
assert_eq!(gentoo.bg, "#0c0c0c");
assert_eq!(gentoo.fg, "#9b86e3");
assert_eq!(
gentoo.ansi_normal,
[
"#0c0c0c", "#c50f1f", "#13a10e", "#c19c00", "#61538d", "#881798", "#00aaff",
"#cccccc",
]
);
assert_eq!(
gentoo.ansi_bright,
[
"#7c7c7c", "#e74856", "#16c60c", "#f9f1a5", "#6f5fa2", "#b4009e", "#0094de",
"#f2f2f2",
]
);
let (button_bg, button_fg) = gentoo.button_colors(4);
assert_eq!(button_bg, "#6f5fa2");
assert_eq!(button_fg, "#ffffff");
assert_eq!(gentoo.button_colors(1), ("#e74856", "#0c0c0c"));
assert!(gentoo.to_css_block().contains("--btn-primary-fg: #ffffff;"));
}
#[test]
fn warm_and_distro_themes_have_distinct_light_dark_palettes() {
assert!(!get_theme("Claude").is_dark());
assert!(get_theme("Golden Hour").is_dark());
assert!(!get_theme("Ubuntu Professional").is_dark());
assert!(!get_theme("Debian Professional").is_dark());
assert!(get_theme("Gentoo Professional").is_dark());
assert_eq!(get_theme("Claude").ansi_bright[4], "#a85137");
assert_eq!(get_theme("Golden Hour").ansi_bright[4], "#d6a84f");
assert_eq!(get_theme("Ubuntu Professional").ansi_bright[4], "#a54018");
assert_eq!(get_theme("Debian Professional").ansi_bright[4], "#9b3643");
assert_eq!(get_theme("Gentoo Professional").ansi_bright[4], "#a08bbc");
}
#[test]
fn blueberry_professional_is_a_light_neutral_blueberry_variant() {
let theme = get_theme("Blueberry Professional");
assert!(!theme.is_dark());
assert_eq!(theme.bg, "#f4efe6");
assert_eq!(theme.fg, "#263247");
assert_eq!(theme.ansi_bright[4], "#2d5e8b");
assert!(PREDEFINED_THEMES
.iter()
.any(|candidate| candidate.name == "Blueberry on Black"));
}
#[test]
fn theme_names_are_alphabetical() {
let names: Vec<String> = PREDEFINED_THEMES

View file

@ -86,7 +86,7 @@ button, input, select, textarea {
top: calc(100% - 4px);
left: 0;
min-width: 180px;
/* The Theme menu has 34 entries; cap the height so it cannot run off
/* The Theme menu has 40 entries; cap the height so it cannot run off
the bottom of the viewport (body has overflow: hidden). */
max-height: calc(100dvh - 48px);
overflow-y: auto;