Add note export options: PDF, markdown and HTML. Also add more themes

This commit is contained in:
mrkmntal 2026-09-29 15:30:10 -04:00
commit 0e0dcdd2c0
8 changed files with 710 additions and 109 deletions

View file

@ -68,7 +68,7 @@ pub fn render_app(
script defer src=(base.url("/static/alpine.min.js")) {}
}
body x-data="app()" x-on:keydown="handleKeydown($event)" {
(render_menu_bar(base))
(render_menu_bar(base, current_note))
(render_toolbar(current_note, query, theme_name, base))
@if let Some(msg) = message {
div class="flash-message" { (msg) }
@ -130,7 +130,7 @@ fn render_tab_bar(default_tab: &str) -> Markup {
}
}
fn render_menu_bar(base: &Base<'_>) -> Markup {
fn render_menu_bar(base: &Base<'_>, current_note: Option<&Note>) -> Markup {
html! {
nav class="menu-bar" {
ul class="menu-list" {
@ -138,7 +138,7 @@ fn render_menu_bar(base: &Base<'_>) -> Markup {
x-on:click="toggleMenu('file')"
x-bind:class="{ 'open': openMenu === 'file' }" {
span { "File" }
ul class="menu-dropdown" {
ul class="menu-dropdown file-menu-dropdown" {
li {
form action=(base.url("/notes")) method="post" {
button type="submit" { "New Note" }
@ -148,6 +148,37 @@ fn render_menu_bar(base: &Base<'_>) -> Markup {
li {
a href=(base.url("/export")) { "Export All Notes" }
}
li class="menu-submenu" {
@if let Some(note) = current_note {
button type="button" class="menu-submenu-trigger"
x-on:click="toggleSubmenu('noteExport', $event)"
x-bind:aria-expanded="openSubmenu === 'noteExport'" {
"Export Note As…"
span class="submenu-chevron" aria-hidden="true" { "›" }
}
ul class="menu-submenu-list"
x-bind:class="{ 'open': openSubmenu === 'noteExport' }" {
li {
a href=(base.url(&format!("/notes/{}/export/pdf", note.id))) {
"Print/PDF"
}
}
li {
a href=(base.url(&format!("/notes/{}/export/markdown", note.id))) {
"Markdown"
}
}
li {
a href=(base.url(&format!("/notes/{}/export/html", note.id))) {
"HTML"
}
}
}
} @else {
button type="button" class="menu-submenu-trigger" disabled
aria-disabled="true" { "Export Note As…" }
}
}
}
}
li class="menu-item" tabindex="0"
@ -387,6 +418,67 @@ pub fn render_preview_fragment(html_content: &str) -> Markup {
}
}
/// Renders a self-contained, theme-aware note document for HTML download or
/// the browser's print-to-PDF flow.
pub fn render_note_export(
note: &Note,
theme: &crate::themes::Theme,
print_on_load: bool,
) -> Markup {
let rendered = render_markdown(&note.content, theme);
html! {
(DOCTYPE)
html lang="en" data-theme=(theme.name) {
head {
meta charset="utf-8";
meta name="viewport" content="width=device-width, initial-scale=1";
title { (note.title) }
style { (PreEscaped(theme.to_css_block())) }
style { (PreEscaped(include_str!("../static/style.css"))) }
style {
(PreEscaped(r#"
html, body { height: auto; min-height: 100%; }
body { display: block; height: auto; min-height: 100vh; overflow: auto; }
.note-export { max-width: 900px; margin: 0 auto; padding: 48px; }
.note-export-title { margin: 0 0 28px; overflow-wrap: anywhere; }
@media print {
@page { margin: 18mm; }
html, body {
height: auto;
min-height: 0;
background-color: var(--app-bg) !important;
color: var(--app-fg) !important;
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
}
body { display: block; overflow: visible; }
.note-export { max-width: none; margin: 0; padding: 0; }
.note-export-title { break-after: avoid-page; }
.markdown-body pre {
overflow: visible;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.markdown-body pre, .markdown-body blockquote,
.markdown-body table { break-inside: avoid; }
.markdown-body a { color: inherit; text-decoration: none; }
}
"#))
}
}
body class="note-export-page" {
main class="note-export" {
h1 class="note-export-title" { (note.title) }
div class="markdown-body" { (PreEscaped(rendered)) }
}
@if print_on_load {
script { "window.addEventListener('load', () => window.print());" }
}
}
}
}
}
/// Renders the `/login` page.
///
/// TOTP enrolment lives in the terminal (`setup::run_interactive_setup`) and
@ -462,7 +554,7 @@ pub fn render_about(theme_name: &str, base: &Base<'_>) -> Markup {
style { (PreEscaped(include_str!("../static/style.css"))) }
}
body class="about-page" {
(render_menu_bar(base))
(render_menu_bar(base, None))
main {
h1 { "EternaLibre Notes" }
p class="tagline" { "Free notes, forever." }
@ -480,11 +572,11 @@ pub fn render_about(theme_name: &str, base: &Base<'_>) -> Markup {
// Counted from the palette itself, and split by
// background luminance. This number used to be
// hardcoded and had drifted to "28" for what was
// really 25 themes.
// really 24 themes.
li {
(format!(
"{} built-in themes ({} dark, {} light) with a full \
ANSI palette — Solarized, Dracula, Nord, Monokai, and more",
ANSI palette — Solarized, Dracula, KDE Breeze, Lilac Paper, and more",
themes.len(),
dark_themes,
themes.len() - dark_themes,
@ -494,6 +586,7 @@ pub fn render_about(theme_name: &str, base: &Base<'_>) -> Markup {
li { "Notes stored as plain Markdown files on disk" }
li { "TOTP-locked, with no accounts or third-party services" }
li { "Note editing, search, and theming work without JavaScript" }
li { "Export saved notes as Markdown, themed HTML, or PDF via the browser print dialog" }
}
h2 { "Author" }
ul class="about-links" {

View file

@ -122,6 +122,12 @@ pub fn router(state: AppState) -> Router {
.route("/theme", post(theme_route))
.route("/about", get(about_route))
.route("/export", get(export_route))
.route(
"/notes/:id/export/markdown",
get(export_note_markdown_route),
)
.route("/notes/:id/export/html", get(export_note_html_route))
.route("/notes/:id/export/pdf", get(export_note_pdf_route))
.route_layer(middleware::from_fn_with_state(state.clone(), require_auth));
public.merge(protected).with_state(state)
@ -471,6 +477,92 @@ async fn export_route(_headers: HeaderMap) -> Result<impl IntoResponse, AppError
Ok(response)
}
async fn export_note_markdown_route(Path(id): Path<String>) -> Result<impl IntoResponse, AppError> {
let note = notes::load_note(&id)?;
let markdown = format!("# {}\n\n{}", note.title.replace('\n', " "), note.content);
let filename = note_export_filename(&note, "md");
Ok(note_export_response(
markdown.into_response(),
"text/markdown; charset=utf-8",
&filename,
"attachment",
))
}
async fn export_note_html_route(
Path(id): Path<String>,
headers: HeaderMap,
) -> Result<impl IntoResponse, AppError> {
let note = notes::load_note(&id)?;
let theme = get_theme(&get_theme_from_cookie(&headers));
let html = pages::render_note_export(&note, theme, false).into_string();
let filename = note_export_filename(&note, "html");
Ok(note_export_response(
Html(html).into_response(),
"text/html; charset=utf-8",
&filename,
"attachment",
))
}
async fn export_note_pdf_route(
Path(id): Path<String>,
headers: HeaderMap,
) -> Result<impl IntoResponse, AppError> {
let note = notes::load_note(&id)?;
let theme = get_theme(&get_theme_from_cookie(&headers));
// The browser's print dialog can save this styled document as PDF without
// a separate renderer, while preserving the selected theme and CSS.
let html = pages::render_note_export(&note, theme, true).into_string();
let filename = note_export_filename(&note, "html");
Ok(note_export_response(
Html(html).into_response(),
"text/html; charset=utf-8",
&filename,
"inline",
))
}
fn note_export_filename(note: &Note, extension: &str) -> String {
let mut slug = String::new();
let mut separator = false;
for ch in note.title.chars() {
if ch.is_ascii_alphanumeric() {
if separator && !slug.is_empty() {
slug.push('-');
}
slug.push(ch.to_ascii_lowercase());
separator = false;
} else {
separator = true;
}
if slug.len() >= 80 {
break;
}
}
let slug = slug.trim_end_matches('-');
let stem = if slug.is_empty() { "note" } else { slug };
format!("{stem}.{extension}")
}
fn note_export_response(
mut response: Response,
content_type: &str,
filename: &str,
disposition: &str,
) -> Response {
let headers = response.headers_mut();
headers.insert(header::CONTENT_TYPE, content_type.parse().unwrap());
headers.insert(
header::CONTENT_DISPOSITION,
format!("{disposition}; filename=\"{filename}\"")
.parse()
.unwrap(),
);
headers.insert(header::CACHE_CONTROL, "private, no-store".parse().unwrap());
response
}
#[derive(Debug)]
pub struct AppError(anyhow::Error);

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 24 themes, and adding a third implementation would cost more
/// on all 34 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
@ -100,6 +100,105 @@ fn relative_luminance(hex: &str) -> f64 {
}
pub static PREDEFINED_THEMES: &[Theme] = &[
Theme {
name: "Amber on Black",
fg: "#ffbf00",
bg: "#000000",
ansi_normal: [
"#000000", "#cc0000", "#00cc00", "#cccc00", "#0000cc", "#cc00cc", "#00cccc", "#cccccc",
],
ansi_bright: [
"#666666", "#ff0000", "#00ff00", "#ffff00", "#a9a9f9", "#ff00ff", "#00ffff", "#ffffff",
],
},
Theme {
name: "Autumn Forest",
fg: "#d4a373",
bg: "#3e2723",
ansi_normal: [
"#3e2723", "#bf360c", "#e65100", "#8d6e63", "#5d4037", "#33691e", "#9e9d24", "#a1887f",
],
ansi_bright: [
"#6d4c41", "#e64a19", "#f57c00", "#a1887f", "#795548", "#558b2f", "#c0ca33", "#d7ccc8",
],
},
Theme {
name: "Blueberry on Black",
fg: "#8ec5fc",
bg: "#000000",
ansi_normal: [
"#000000", "#0d47a1", "#1565c0", "#1976d2", "#1e88e5", "#42a5f5", "#64b5f6", "#90caf9",
],
ansi_bright: [
"#37474f", "#1e88e5", "#42a5f5", "#64b5f6", "#90caf9", "#bbdefb", "#e3f2fd", "#ffffff",
],
},
Theme {
name: "Breeze",
fg: "#232629",
bg: "#eff0f1",
ansi_normal: [
"#eff0f1", "#b00020", "#2e7d32", "#8a5a00", "#1d5f91", "#8e44ad", "#007f7f", "#232629",
],
ansi_bright: [
"#7f8c8d", "#a31515", "#256d2a", "#805400", "#0067c0", "#7b2e9a", "#006b6b", "#111111",
],
},
Theme {
name: "Breeze Dark",
fg: "#eff0f1",
bg: "#232629",
ansi_normal: [
"#232629", "#f07178", "#8bd49c", "#e5c07b", "#3daee9", "#c792ea", "#56b6c2", "#d8dee9",
],
ansi_bright: [
"#697078", "#ff7a85", "#a3e6b1", "#f2cc8f", "#61c5f0", "#d6a8f5", "#70d0db", "#ffffff",
],
},
Theme {
name: "Chartreuse on Black",
fg: "#7fff00",
bg: "#000000",
ansi_normal: [
"#000000", "#cc0000", "#00cc00", "#cccc00", "#0000cc", "#cc00cc", "#00cccc", "#cccccc",
],
ansi_bright: [
"#666666", "#ff0000", "#00ff00", "#ffff00", "#a9a9f9", "#ff00ff", "#00ffff", "#ffffff",
],
},
Theme {
name: "Classic Windows 95",
fg: "#111111",
bg: "#c0c0c0",
ansi_normal: [
"#c0c0c0", "#800000", "#006400", "#806000", "#000080", "#800080", "#008080", "#111111",
],
ansi_bright: [
"#808080", "#c00000", "#008000", "#9a6700", "#004e98", "#9a4e9a", "#008b8b", "#ffffff",
],
},
Theme {
name: "Cyan on Black",
fg: "#00ffff",
bg: "#000000",
ansi_normal: [
"#000000", "#cc0000", "#00cc00", "#cccc00", "#0000cc", "#cc00cc", "#00cccc", "#cccccc",
],
ansi_bright: [
"#666666", "#ff0000", "#00ff00", "#ffff00", "#a9a9f9", "#ff00ff", "#00ffff", "#ffffff",
],
},
Theme {
name: "Cyberpunk 2077",
fg: "#f9f227",
bg: "#0d0d0d",
ansi_normal: [
"#0d0d0d", "#ff0055", "#00ff41", "#f9f227", "#00b8ff", "#ff00cc", "#00ffff", "#f9f227",
],
ansi_bright: [
"#333333", "#ff3377", "#33ff66", "#ffff33", "#33ccff", "#ff33ff", "#33ffff", "#ffff66",
],
},
Theme {
name: "Dark",
fg: "#d0d0d0",
@ -111,39 +210,6 @@ pub static PREDEFINED_THEMES: &[Theme] = &[
"#666666", "#ff6666", "#66ff66", "#ffff66", "#6666ff", "#ff66ff", "#66ffff", "#ffffff",
],
},
Theme {
name: "Light",
fg: "#1c1c1c",
bg: "#d0d0d0",
ansi_normal: [
"#1c1c1c", "#c50f1f", "#23a523", "#b58900", "#268bd2", "#d33682", "#2aa198", "#d0d0d0",
],
ansi_bright: [
"#666666", "#ff6666", "#66ff66", "#ffff66", "#6666ff", "#ff66ff", "#66ffff", "#ffffff",
],
},
Theme {
name: "Solarized Dark",
fg: "#839496",
bg: "#002b36",
ansi_normal: [
"#073642", "#dc322f", "#859900", "#b58900", "#268bd2", "#d33682", "#2aa198", "#eee8d5",
],
ansi_bright: [
"#002b36", "#cb4b16", "#586e75", "#657b83", "#839496", "#6c71c4", "#93a1a1", "#fdf6e3",
],
},
Theme {
name: "Solarized Light",
fg: "#657b83",
bg: "#fdf6e3",
ansi_normal: [
"#073642", "#dc322f", "#859900", "#b58900", "#268bd2", "#d33682", "#2aa198", "#eee8d5",
],
ansi_bright: [
"#002b36", "#cb4b16", "#586e75", "#657b83", "#839496", "#6c71c4", "#93a1a1", "#fdf6e3",
],
},
Theme {
name: "Dracula",
fg: "#f8f8f2",
@ -155,6 +221,17 @@ pub static PREDEFINED_THEMES: &[Theme] = &[
"#6272a4", "#ff6e6e", "#69ff94", "#ffffa5", "#d6acff", "#ff92df", "#a4ffff", "#ffffff",
],
},
Theme {
name: "Foggy Morning",
fg: "#cfd8dc",
bg: "#455a64",
ansi_normal: [
"#455a64", "#546e7a", "#607d8b", "#78909c", "#90a4ae", "#b0bec5", "#cfd8dc", "#eceff1",
],
ansi_bright: [
"#607d8b", "#78909c", "#90a4ae", "#b0bec5", "#cfd8dc", "#eceff1", "#f5f5f5", "#ffffff",
],
},
Theme {
name: "Gruvbox Dark",
fg: "#ebdbb2",
@ -178,80 +255,25 @@ pub static PREDEFINED_THEMES: &[Theme] = &[
],
},
Theme {
name: "Nord",
fg: "#d8dee9",
bg: "#2e3440",
name: "Light",
fg: "#1c1c1c",
bg: "#d0d0d0",
ansi_normal: [
"#3b4252", "#bf616a", "#a3be8c", "#ebcb8b", "#81a1c1", "#b48ead", "#88c0d0", "#e5e9f0",
"#1c1c1c", "#c50f1f", "#23a523", "#b58900", "#268bd2", "#d33682", "#2aa198", "#d0d0d0",
],
ansi_bright: [
"#4c566a", "#bf616a", "#a3be8c", "#ebcb8b", "#81a1c1", "#b48ead", "#8fbcbb", "#eceff4",
"#666666", "#ff6666", "#66ff66", "#ffff66", "#6666ff", "#ff66ff", "#66ffff", "#ffffff",
],
},
Theme {
name: "Monokai",
fg: "#f8f8f2",
bg: "#272822",
name: "Lilac Paper",
fg: "#302b3d",
bg: "#f4f0fa",
ansi_normal: [
"#272822", "#f92672", "#a6e22e", "#f4bf75", "#66d9ef", "#ae81ff", "#a1efe4", "#f8f8f2",
"#f4f0fa", "#a33a58", "#3d7553", "#856000", "#584c9d", "#814f91", "#287781", "#302b3d",
],
ansi_bright: [
"#75715e", "#f92672", "#a6e22e", "#e6db74", "#66d9ef", "#ae81ff", "#a1efe4", "#f9f8f5",
],
},
Theme {
name: "Tokyo Night",
fg: "#c0caf5",
bg: "#1a1b26",
ansi_normal: [
"#15161e", "#f7768e", "#9ece6a", "#e0af68", "#7aa2f7", "#bb9af7", "#7dcfff", "#a9b1d6",
],
ansi_bright: [
"#414868", "#f7768e", "#9ece6a", "#e0af68", "#7aa2f7", "#bb9af7", "#7dcfff", "#c0caf5",
],
},
Theme {
name: "One Dark",
fg: "#abb2bf",
bg: "#282c34",
ansi_normal: [
"#282c34", "#e06c75", "#98c379", "#e5c07b", "#61afef", "#c678dd", "#56b6c2", "#abb2bf",
],
ansi_bright: [
"#5c6370", "#e06c75", "#98c379", "#e5c07b", "#61afef", "#c678dd", "#56b6c2", "#ffffff",
],
},
Theme {
name: "Cyberpunk 2077",
fg: "#f9f227",
bg: "#0d0d0d",
ansi_normal: [
"#0d0d0d", "#ff0055", "#00ff41", "#f9f227", "#00b8ff", "#ff00cc", "#00ffff", "#f9f227",
],
ansi_bright: [
"#333333", "#ff3377", "#33ff66", "#ffff33", "#33ccff", "#ff33ff", "#33ffff", "#ffff66",
],
},
Theme {
name: "Cyan on Black",
fg: "#00ffff",
bg: "#000000",
ansi_normal: [
"#000000", "#cc0000", "#00cc00", "#cccc00", "#0000cc", "#cc00cc", "#00cccc", "#cccccc",
],
ansi_bright: [
"#666666", "#ff0000", "#00ff00", "#ffff00", "#a9a9f9", "#ff00ff", "#00ffff", "#ffffff",
],
},
Theme {
name: "Red on Black",
fg: "#ff0000",
bg: "#000000",
ansi_normal: [
"#000000", "#cc0000", "#00cc00", "#cccc00", "#0000cc", "#cc00cc", "#00cccc", "#cccccc",
],
ansi_bright: [
"#666666", "#ff0000", "#00ff00", "#ffff00", "#a9a9f9", "#ff00ff", "#00ffff", "#ffffff",
"#89829a", "#96334f", "#2d704a", "#755500", "#5d468f", "#713f83", "#176c74", "#211a2a",
],
},
Theme {
@ -266,8 +288,63 @@ pub static PREDEFINED_THEMES: &[Theme] = &[
],
},
Theme {
name: "Chartreuse on Black",
fg: "#7fff00",
name: "Monokai",
fg: "#f8f8f2",
bg: "#272822",
ansi_normal: [
"#272822", "#f92672", "#a6e22e", "#f4bf75", "#66d9ef", "#ae81ff", "#a1efe4", "#f8f8f2",
],
ansi_bright: [
"#75715e", "#f92672", "#a6e22e", "#e6db74", "#66d9ef", "#ae81ff", "#a1efe4", "#f9f8f5",
],
},
Theme {
name: "Nord",
fg: "#d8dee9",
bg: "#2e3440",
ansi_normal: [
"#3b4252", "#bf616a", "#a3be8c", "#ebcb8b", "#81a1c1", "#b48ead", "#88c0d0", "#e5e9f0",
],
ansi_bright: [
"#4c566a", "#bf616a", "#a3be8c", "#ebcb8b", "#81a1c1", "#b48ead", "#8fbcbb", "#eceff4",
],
},
Theme {
name: "One Dark",
fg: "#abb2bf",
bg: "#282c34",
ansi_normal: [
"#282c34", "#e06c75", "#98c379", "#e5c07b", "#61afef", "#c678dd", "#56b6c2", "#abb2bf",
],
ansi_bright: [
"#5c6370", "#e06c75", "#98c379", "#e5c07b", "#61afef", "#c678dd", "#56b6c2", "#ffffff",
],
},
Theme {
name: "Orchid Noir",
fg: "#f3eaf8",
bg: "#110b18",
ansi_normal: [
"#110b18", "#ff6b9b", "#8fe0b0", "#f4cf78", "#b775f2", "#ff66b3", "#65d7dc", "#f3eaf8",
],
ansi_bright: [
"#685d70", "#ff8ab0", "#a7ecc1", "#ffdf96", "#c78df7", "#ff86c3", "#82e7eb", "#ffffff",
],
},
Theme {
name: "Platinum Desktop",
fg: "#242424",
bg: "#d6d6d6",
ansi_normal: [
"#d6d6d6", "#a33a3a", "#3d6b45", "#8a6200", "#336699", "#78508a", "#287d78", "#242424",
],
ansi_bright: [
"#777777", "#8f1d1d", "#2f6a38", "#765000", "#225a91", "#6b3f7e", "#176963", "#101010",
],
},
Theme {
name: "Red on Black",
fg: "#ff0000",
bg: "#000000",
ansi_normal: [
"#000000", "#cc0000", "#00cc00", "#cccc00", "#0000cc", "#cc00cc", "#00cccc", "#cccccc",
@ -277,25 +354,36 @@ pub static PREDEFINED_THEMES: &[Theme] = &[
],
},
Theme {
name: "Amber on Black",
fg: "#ffbf00",
bg: "#000000",
name: "Rose Quartz",
fg: "#3a2630",
bg: "#fff1f4",
ansi_normal: [
"#000000", "#cc0000", "#00cc00", "#cccc00", "#0000cc", "#cc00cc", "#00cccc", "#cccccc",
"#fff1f4", "#a52a4e", "#39724a", "#856000", "#b83f68", "#853f78", "#176f76", "#3a2630",
],
ansi_bright: [
"#666666", "#ff0000", "#00ff00", "#ffff00", "#a9a9f9", "#ff00ff", "#00ffff", "#ffffff",
"#8b7b84", "#922243", "#2e653e", "#795400", "#9b3157", "#713365", "#0d646b", "#25171d",
],
},
Theme {
name: "Blueberry on Black",
fg: "#8ec5fc",
bg: "#000000",
name: "Solarized Dark",
fg: "#839496",
bg: "#002b36",
ansi_normal: [
"#000000", "#0d47a1", "#1565c0", "#1976d2", "#1e88e5", "#42a5f5", "#64b5f6", "#90caf9",
"#073642", "#dc322f", "#859900", "#b58900", "#268bd2", "#d33682", "#2aa198", "#eee8d5",
],
ansi_bright: [
"#37474f", "#1e88e5", "#42a5f5", "#64b5f6", "#90caf9", "#bbdefb", "#e3f2fd", "#ffffff",
"#002b36", "#cb4b16", "#586e75", "#657b83", "#839496", "#6c71c4", "#93a1a1", "#fdf6e3",
],
},
Theme {
name: "Solarized Light",
fg: "#657b83",
bg: "#fdf6e3",
ansi_normal: [
"#073642", "#dc322f", "#859900", "#b58900", "#268bd2", "#d33682", "#2aa198", "#eee8d5",
],
ansi_bright: [
"#002b36", "#cb4b16", "#586e75", "#657b83", "#839496", "#6c71c4", "#93a1a1", "#fdf6e3",
],
},
Theme {
@ -309,6 +397,17 @@ pub static PREDEFINED_THEMES: &[Theme] = &[
"#558b2f", "#e91e63", "#8bc34a", "#ffeb3b", "#009688", "#ec407a", "#c5e1a5", "#fce4ec",
],
},
Theme {
name: "Stormy Night",
fg: "#b0bec5",
bg: "#263238",
ansi_normal: [
"#263238", "#37474f", "#455a64", "#546e7a", "#607d8b", "#78909c", "#90a4ae", "#b0bec5",
],
ansi_bright: [
"#455a64", "#607d8b", "#78909c", "#90a4ae", "#b0bec5", "#cfd8dc", "#eceff1", "#ffffff",
],
},
Theme {
name: "Summer Sunset",
fg: "#ff9a5a",
@ -321,14 +420,47 @@ pub static PREDEFINED_THEMES: &[Theme] = &[
],
},
Theme {
name: "Autumn Forest",
fg: "#d4a373",
bg: "#3e2723",
name: "Tokyo Night",
fg: "#c0caf5",
bg: "#1a1b26",
ansi_normal: [
"#3e2723", "#bf360c", "#e65100", "#8d6e63", "#5d4037", "#33691e", "#9e9d24", "#a1887f",
"#15161e", "#f7768e", "#9ece6a", "#e0af68", "#7aa2f7", "#bb9af7", "#7dcfff", "#a9b1d6",
],
ansi_bright: [
"#6d4c41", "#e64a19", "#f57c00", "#a1887f", "#795548", "#558b2f", "#c0ca33", "#d7ccc8",
"#414868", "#f7768e", "#9ece6a", "#e0af68", "#7aa2f7", "#bb9af7", "#7dcfff", "#c0caf5",
],
},
Theme {
name: "Velvet Dusk",
fg: "#eae1f2",
bg: "#1c1422",
ansi_normal: [
"#1c1422", "#f0718a", "#91d39b", "#e8c27a", "#ab82d3", "#d08acb", "#70c9c4", "#eae1f2",
],
ansi_bright: [
"#63586b", "#ff8299", "#a6e6af", "#f2d08c", "#c19ae8", "#e1a0dc", "#83ddd6", "#ffffff",
],
},
Theme {
name: "Vista Aero",
fg: "#eaf4fb",
bg: "#14263a",
ansi_normal: [
"#14263a", "#ff6b7a", "#8fe3b2", "#ffd27a", "#53b8f2", "#ce9eff", "#67dce3", "#eaf4fb",
],
ansi_bright: [
"#697f96", "#ff8793", "#a4eac1", "#ffdda0", "#62c8ff", "#d9b4ff", "#7ae8ed", "#ffffff",
],
},
Theme {
name: "Windows XP",
fg: "#1c2a3a",
bg: "#eaf2fa",
ansi_normal: [
"#eaf2fa", "#a52222", "#247a35", "#886000", "#245edc", "#873b80", "#007984", "#1c2a3a",
],
ansi_bright: [
"#7b8794", "#8f1d1d", "#216b2f", "#765000", "#245edc", "#71316b", "#006b74", "#111820",
],
},
Theme {
@ -342,35 +474,18 @@ pub static PREDEFINED_THEMES: &[Theme] = &[
"#283593", "#00897b", "#00acc1", "#26c6da", "#4dd0e1", "#80deea", "#b2ebf2", "#ffffff",
],
},
Theme {
name: "Stormy Night",
fg: "#b0bec5",
bg: "#263238",
ansi_normal: [
"#263238", "#37474f", "#455a64", "#546e7a", "#607d8b", "#78909c", "#90a4ae", "#b0bec5",
],
ansi_bright: [
"#455a64", "#607d8b", "#78909c", "#90a4ae", "#b0bec5", "#cfd8dc", "#eceff1", "#ffffff",
],
},
Theme {
name: "Foggy Morning",
fg: "#cfd8dc",
bg: "#455a64",
ansi_normal: [
"#455a64", "#546e7a", "#607d8b", "#78909c", "#90a4ae", "#b0bec5", "#cfd8dc", "#eceff1",
],
ansi_bright: [
"#607d8b", "#78909c", "#90a4ae", "#b0bec5", "#cfd8dc", "#eceff1", "#f5f5f5", "#ffffff",
],
},
];
pub fn get_theme(name: &str) -> &'static Theme {
PREDEFINED_THEMES
.iter()
.find(|t| t.name == name)
.unwrap_or(&PREDEFINED_THEMES[0])
.unwrap_or_else(|| {
PREDEFINED_THEMES
.iter()
.find(|theme| theme.name == "Dark")
.expect("the default Dark theme must be present")
})
}
#[cfg(test)]
@ -426,8 +541,41 @@ mod tests {
// 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 25.
assert_eq!(PREDEFINED_THEMES.len(), 24);
// for what was really 24.
assert_eq!(PREDEFINED_THEMES.len(), 34);
}
#[test]
fn breeze_variants_are_a_light_dark_pair() {
let light = get_theme("Breeze");
let dark = get_theme("Breeze Dark");
assert!(!light.is_dark());
assert!(dark.is_dark());
assert_eq!(light.ansi_bright[4], "#0067c0");
assert_eq!(dark.ansi_normal[4], "#3daee9");
}
#[test]
fn lavender_and_rose_themes_cover_light_and_dark_modes() {
assert!(!get_theme("Lilac Paper").is_dark());
assert!(!get_theme("Rose Quartz").is_dark());
assert!(get_theme("Velvet Dusk").is_dark());
assert!(get_theme("Orchid Noir").is_dark());
assert_eq!(get_theme("Rose Quartz").ansi_bright[4], "#9b3157");
assert_eq!(get_theme("Orchid Noir").ansi_bright[5], "#ff86c3");
}
#[test]
fn theme_names_are_alphabetical() {
let names: Vec<String> = PREDEFINED_THEMES
.iter()
.map(|theme| theme.name.to_lowercase())
.collect();
assert!(
names.windows(2).all(|pair| pair[0] <= pair[1]),
"theme names should be alphabetized"
);
}
#[test]