diff --git a/Cargo.lock b/Cargo.lock index c6e464f..2336448 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -280,7 +280,7 @@ dependencies = [ "blogr-themes", "chrono", "clap", - "crossterm", + "crossterm 0.27.0", "css-inline", "dotenvy", "git2", @@ -291,7 +291,7 @@ dependencies = [ "native-tls", "open", "pulldown-cmark", - "ratatui", + "ratatui 0.29.0", "reqwest", "rusqlite", "serde", @@ -318,7 +318,7 @@ version = "0.4.1" dependencies = [ "anyhow", "include_dir", - "ratatui", + "ratatui 0.24.0", "serde", "thiserror 2.0.16", "toml", @@ -364,6 +364,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.2.38" @@ -484,6 +493,20 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -559,6 +582,22 @@ dependencies = [ "winapi", ] +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.9.4", + "crossterm_winapi", + "mio 1.0.4", + "parking_lot", + "rustix 0.38.44", + "signal-hook", + "signal-hook-mio", + "winapi", +] + [[package]] name = "crossterm_winapi" version = "0.9.1" @@ -619,6 +658,41 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.106", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.106", +] + [[package]] name = "data-encoding" version = "2.9.0" @@ -893,7 +967,7 @@ version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" dependencies = [ - "unicode-width 0.2.1", + "unicode-width 0.2.0", ] [[package]] @@ -1336,6 +1410,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -1433,6 +1513,19 @@ version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" +[[package]] +name = "instability" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "io-uring" version = "0.7.10" @@ -1484,6 +1577,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -1623,6 +1725,12 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -1747,6 +1855,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", + "log", "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.59.0", ] @@ -2256,9 +2365,9 @@ checksum = "0ebc917cfb527a566c37ecb94c7e3fd098353516fb4eb6bea17015ade0182425" dependencies = [ "bitflags 2.9.4", "cassowary", - "crossterm", + "crossterm 0.27.0", "indoc", - "itertools", + "itertools 0.11.0", "lru", "paste", "strum 0.25.0", @@ -2266,6 +2375,27 @@ dependencies = [ "unicode-width 0.1.14", ] +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags 2.9.4", + "cassowary", + "compact_str", + "crossterm 0.28.1", + "indoc", + "instability", + "itertools 0.13.0", + "lru", + "paste", + "strum 0.26.3", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + [[package]] name = "rayon" version = "1.11.0" @@ -2419,6 +2549,19 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.4", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.1.2" @@ -2428,7 +2571,7 @@ dependencies = [ "bitflags 2.9.4", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.11.0", "windows-sys 0.61.0", ] @@ -2673,6 +2816,7 @@ checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", "mio 0.8.11", + "mio 1.0.4", "signal-hook", ] @@ -2810,6 +2954,15 @@ dependencies = [ "strum_macros 0.25.3", ] +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros 0.26.4", +] + [[package]] name = "strum" version = "0.27.2" @@ -2832,6 +2985,19 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.106", +] + [[package]] name = "strum_macros" version = "0.27.2" @@ -2941,7 +3107,7 @@ dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", - "rustix", + "rustix 1.1.2", "windows-sys 0.61.0", ] @@ -3324,6 +3490,17 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools 0.13.0", + "unicode-segmentation", + "unicode-width 0.1.14", +] + [[package]] name = "unicode-width" version = "0.1.14" @@ -3332,9 +3509,9 @@ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" -version = "0.2.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "unsafe-libyaml" diff --git a/blogr-cli/Cargo.toml b/blogr-cli/Cargo.toml index c6a774f..8faedfe 100644 --- a/blogr-cli/Cargo.toml +++ b/blogr-cli/Cargo.toml @@ -14,7 +14,7 @@ path = "src/main.rs" [dependencies] blogr-themes = { version = "0.4.1", path = "../blogr-themes" } clap = { version = "4.0", features = ["derive"] } -ratatui = "0.24" +ratatui = "0.29" crossterm = "0.27" pulldown-cmark = "0.9" syntect = "5.0" diff --git a/blogr-cli/src/commands/theme.rs b/blogr-cli/src/commands/theme.rs index b6ede7b..b020a14 100644 --- a/blogr-cli/src/commands/theme.rs +++ b/blogr-cli/src/commands/theme.rs @@ -25,15 +25,15 @@ pub async fn handle_list() -> Result<()> { if all_themes.is_empty() { println!(" 📦 No themes available"); } else { - for (name, theme) in all_themes { + for theme in all_themes { let info = theme.info(); - let is_active = current_theme.as_ref() == Some(&name); + let is_active = current_theme.as_ref() == Some(&info.name); let status_icon = if is_active { "✅" } else { "📦" }; let status_text = if is_active { " (active)" } else { "" }; println!( " {} {}{} - {}", - status_icon, name, status_text, info.description + status_icon, info.name, status_text, info.description ); println!( " 👤 Author: {} | 📦 Version: {}", @@ -66,7 +66,7 @@ pub async fn handle_info(name: String) -> Result<()> { for (option_name, config_option) in info.config_schema { println!( " - {}: {} ({})", - option_name, config_option.default, config_option.description + option_name, config_option.value, config_option.description ); } } else { @@ -119,20 +119,7 @@ pub async fn handle_set(name: String) -> Result<()> { for (option_name, config_option) in theme_info.config_schema.clone() { // Only set default if the option doesn't exist in current config if let Entry::Vacant(e) = config.theme.config.entry(option_name) { - let default_value = match config_option.option_type.as_str() { - "boolean" => toml::Value::Boolean(config_option.default.parse().unwrap_or(false)), - "number" => { - if let Ok(int_val) = config_option.default.parse::() { - toml::Value::Integer(int_val) - } else if let Ok(float_val) = config_option.default.parse::() { - toml::Value::Float(float_val) - } else { - toml::Value::String(config_option.default) - } - } - _ => toml::Value::String(config_option.default), - }; - e.insert(default_value); + e.insert(config_option.value); } } @@ -183,9 +170,11 @@ pub async fn handle_preview(name: String) -> Result<()> { for (option_name, config_option) in &theme_info.config_schema { println!( " • {} ({}): {}", - option_name, config_option.option_type, config_option.description + option_name, + config_option.value.type_str(), + config_option.description ); - println!(" Default: {}", config_option.default); + println!(" Default: {}", config_option.value); } } else { println!("⚙️ No configuration options available"); @@ -209,7 +198,7 @@ pub async fn handle_preview(name: String) -> Result<()> { println!( " • {}: {}", option_name.replace('_', " "), - config_option.default + config_option.value ); } } diff --git a/blogr-cli/src/newsletter/ui/approval.rs b/blogr-cli/src/newsletter/ui/approval.rs index 9cfe2d5..2a0d777 100644 --- a/blogr-cli/src/newsletter/ui/approval.rs +++ b/blogr-cli/src/newsletter/ui/approval.rs @@ -757,7 +757,7 @@ impl ModernApprovalApp { /// Render the modern approval interface pub fn render(&mut self, frame: &mut Frame) { - let area = frame.size(); + let area = frame.area(); match self.mode { AppMode::Loading => self.render_loading(frame), AppMode::List => self.render_list(frame), @@ -770,7 +770,7 @@ impl ModernApprovalApp { } fn render_loading(&mut self, frame: &mut Frame) { - let area = frame.size(); + let area = frame.area(); // Center the loading indicator let loading_area = centered_rect(40, 20, area); @@ -799,7 +799,7 @@ impl ModernApprovalApp { Constraint::Min(1), // Table Constraint::Length(4), // Status bar with performance info ]) - .split(frame.size()); + .split(frame.area()); self.render_header(frame, chunks[0]); self.render_table(frame, chunks[1]); @@ -994,8 +994,14 @@ impl ModernApprovalApp { self.current_page + 1, total_pages.max(1) ); - - let table = Table::new(rows) + let widths = [ + Constraint::Length(2), // Selection indicator + Constraint::Min(25), // Email + Constraint::Length(10), // Status + Constraint::Length(16), // Subscribed date + Constraint::Min(10), // Notes + ]; + let table = Table::new(rows, widths) .header(header) .block( Block::default() @@ -1008,14 +1014,7 @@ impl ModernApprovalApp { .add_modifier(Modifier::BOLD), ), ) - .widths(&[ - Constraint::Length(2), // Selection indicator - Constraint::Min(25), // Email - Constraint::Length(10), // Status - Constraint::Length(16), // Subscribed date - Constraint::Min(10), // Notes - ]) - .highlight_style( + .row_highlight_style( Style::default() .bg(self.theme.surface) .fg(self.theme.text) diff --git a/blogr-cli/src/tui/app.rs b/blogr-cli/src/tui/app.rs index 8adeeed..747a79b 100644 --- a/blogr-cli/src/tui/app.rs +++ b/blogr-cli/src/tui/app.rs @@ -228,7 +228,7 @@ impl App { Constraint::Min(0), // Main content Constraint::Length(3), // Status bar ]) - .split(frame.size()); + .split(frame.area()); // Render header self.render_header(frame, main_chunks[0]); @@ -332,7 +332,7 @@ impl App { } fn render_help_overlay(&self, frame: &mut Frame) { - let area = frame.size(); + let area = frame.area(); // Create a centered rectangle let popup_area = Layout::default() diff --git a/blogr-cli/src/tui/config_app.rs b/blogr-cli/src/tui/config_app.rs index 636bf28..36e0e6e 100644 --- a/blogr-cli/src/tui/config_app.rs +++ b/blogr-cli/src/tui/config_app.rs @@ -1,19 +1,27 @@ +use std::collections::HashMap; + use crate::config::Config; use crate::project::Project; use crate::tui::theme::TuiTheme; +use anyhow::Ok; +use blogr_themes::{get_all_themes, ThemeInfo}; use crossterm::event::{KeyCode, KeyEvent}; use ratatui::{ layout::{Constraint, Direction, Layout, Rect}, - style::{Modifier, Style}, - text::{Line, Span}, - widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap}, + style::{Modifier, Style, Stylize}, + text::{Line, Span, Text}, + widgets::{ + Block, Borders, Cell, Clear, HighlightSpacing, List, ListItem, ListState, Paragraph, Row, + Table, TableState, Wrap, + }, Frame, }; -use strum::{EnumIter, IntoEnumIterator, VariantArray}; +use serde::Deserialize; +use strum::{EnumIter, IntoEnumIterator}; pub type AppResult = anyhow::Result; -#[derive(PartialEq, Eq)] +#[derive(PartialEq)] enum HighLevelListItem { Field(ConfigField), Section(ConfigSection), @@ -22,9 +30,9 @@ enum HighLevelListItem { struct HighLevelConfigList(Vec); impl HighLevelConfigList { - fn new() -> Self { + fn new(config: &Config) -> Self { let inner = ConfigSection::iter() - .map(|section| (section, section.get_fields())) + .map(|section| (section, section.get_section(config))) .map(|(section, fields)| { let mut list_section = vec![ HighLevelListItem::BlankLine, @@ -33,7 +41,7 @@ impl HighLevelConfigList { list_section.append( &mut fields .iter() - .map(|field| HighLevelListItem::Field(*field)) + .map(|field| HighLevelListItem::Field(field.clone())) .collect(), ); list_section @@ -52,10 +60,91 @@ impl HighLevelConfigList { _ => false, }) } + + fn next(&self, index: usize) -> Option<(usize, &ConfigField)> { + self.0 + .iter() + .enumerate() + .filter_map(|(i, item)| match item { + HighLevelListItem::Field(field) => Some((i, field)), + _ => None, + }) + .find(|(i, _item)| *i > index) + } + + fn prev(&self, index: usize) -> Option<(usize, &ConfigField)> { + self.0 + .iter() + .enumerate() + .rev() + .filter_map(|(i, item)| match item { + HighLevelListItem::Field(field) => Some((i, field)), + _ => None, + }) + .find(|(i, _item)| *i < index) + } +} + +fn set_theme_option( + config: &mut Config, + option_name: String, + old_value: &toml::Value, + new_value: String, +) -> AppResult<()> { + // if the field type was last String, don't try parsing the new value into anything but that. + let new_value = match matches!(old_value, toml::Value::String(_)) { + true => toml::Value::String(new_value), + false => toml::Value::deserialize(toml::de::ValueDeserializer::new(&new_value))?, + }; + config + .theme + .config + .entry(option_name) + .insert_entry(new_value); + Ok(()) +} + +fn set_primary_domain(config: &mut Config, new_value: String) { + if config.blog.domains.is_none() { + config.blog.domains = Some(crate::config::DomainConfig { + primary: None, + aliases: Vec::new(), + subdomain: None, + enforce_https: true, + github_pages_domain: None, + }); + } + if let Some(domains) = &mut config.blog.domains { + domains.primary = match new_value.is_empty() { + true => None, + false => Some(new_value.clone()), + }; + domains.github_pages_domain = match new_value.is_empty() { + true => None, + false => Some(new_value), + }; + } +} + +fn set_domain_enforce_https(config: &mut Config, new_value: String) -> AppResult<()> { + let enforce_https = new_value.parse()?; + if config.blog.domains.is_none() { + config.blog.domains = Some(crate::config::DomainConfig { + primary: None, + aliases: Vec::new(), + subdomain: None, + enforce_https, + github_pages_domain: None, + }); + } + if let Some(domains) = &mut config.blog.domains { + domains.enforce_https = enforce_https; + } + Ok(()) } /// Configuration field types -#[derive(Debug, Clone, Copy, EnumIter, VariantArray, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq)] pub enum ConfigField { BlogTitle, BlogAuthor, @@ -64,6 +153,7 @@ pub enum ConfigField { BlogLanguage, BlogTimezone, ThemeName, + ThemeOption { name: String, value: toml::Value }, DomainPrimary, DomainEnforceHttps, BuildOutputDir, @@ -83,6 +173,7 @@ impl std::fmt::Display for ConfigField { Self::BlogLanguage => "Language", Self::BlogTimezone => "Timezone", Self::ThemeName => "Theme Name", + Self::ThemeOption { name, .. } => name, Self::DomainPrimary => "Primary Domain", Self::DomainEnforceHttps => "Enforce HTTPS", Self::BuildOutputDir => "Output Directory", @@ -96,23 +187,6 @@ impl std::fmt::Display for ConfigField { } impl ConfigField { - pub fn category(&self) -> ConfigSection { - match self { - Self::BlogTitle - | Self::BlogAuthor - | Self::BlogDescription - | Self::BlogBaseUrl - | Self::BlogLanguage - | Self::BlogTimezone => ConfigSection::Blog, - Self::ThemeName => ConfigSection::Theme, - Self::DomainPrimary | ConfigField::DomainEnforceHttps => ConfigSection::Domain, - Self::BuildOutputDir | Self::BuildDrafts | Self::BuildFuturePosts => { - ConfigSection::Build - } - Self::DevPort | ConfigField::DevAutoReload => ConfigSection::Development, - } - } - pub fn get_value(&self, config: &Config) -> String { match self { Self::BlogTitle => config.blog.title.clone(), @@ -122,6 +196,11 @@ impl ConfigField { Self::BlogLanguage => config.blog.language.as_deref().unwrap_or("").to_string(), Self::BlogTimezone => config.blog.timezone.as_deref().unwrap_or("").to_string(), Self::ThemeName => config.theme.name.clone(), + // don't render toml strings with added quotes + Self::ThemeOption { value, .. } => match value { + toml::Value::String(val) => val.clone(), + _ => value.to_string(), + }, Self::DomainPrimary => { if let Some(domains) = &config.blog.domains { domains.primary.as_deref().unwrap_or("").to_string() @@ -149,6 +228,35 @@ impl ConfigField { } } + fn set(&self, config: &mut Config, new_value: String) -> AppResult<()> { + match self { + Self::BlogTitle => config.blog.author = new_value, + Self::BlogAuthor => config.blog.author = new_value, + Self::BlogDescription => config.blog.description = new_value, + Self::BlogBaseUrl => config.blog.base_url = new_value, + Self::BlogLanguage => { + config.blog.language = (!new_value.is_empty()).then_some(new_value) + } + Self::BlogTimezone => { + config.blog.timezone = (!new_value.is_empty()).then_some(new_value) + } + Self::ThemeName => config.theme.name = new_value, + Self::ThemeOption { name, value } => { + set_theme_option(config, name.clone(), value, new_value)? + } + Self::DomainPrimary => set_primary_domain(config, new_value), + Self::DomainEnforceHttps => set_domain_enforce_https(config, new_value)?, + Self::BuildOutputDir => { + config.build.output_dir = (!new_value.is_empty()).then_some(new_value) + } + Self::BuildDrafts => config.build.drafts = new_value.parse()?, + Self::BuildFuturePosts => config.build.future_posts = new_value.parse()?, + Self::DevPort => config.dev.port = new_value.parse()?, + Self::DevAutoReload => config.dev.auto_reload = new_value.parse()?, + } + Ok(()) + } + pub fn is_boolean(&self) -> bool { matches!( self, @@ -156,14 +264,41 @@ impl ConfigField { | Self::BuildDrafts | Self::BuildFuturePosts | Self::DevAutoReload + | Self::ThemeOption { + value: toml::Value::Boolean(_), + .. + } ) } pub fn is_numeric(&self) -> bool { - matches!(self, Self::DevPort) + matches!( + self, + Self::DevPort + | Self::ThemeOption { + value: toml::Value::Integer(_), + .. + } + ) } } +fn get_theme_specific_config_fields(config: &Config) -> Vec { + config + .theme + .config + .clone() + .into_iter() + .map(|(name, value)| ConfigField::ThemeOption { name, value }) + .collect::>() +} + +fn get_all_theme_fields(config: &Config) -> Vec { + let mut fields = vec![ConfigField::ThemeName]; + fields.append(&mut get_theme_specific_config_fields(config)); + fields +} + #[derive(Debug, Clone, Copy, EnumIter, PartialEq, Eq, Hash)] pub enum ConfigSection { Blog, @@ -187,10 +322,25 @@ impl std::fmt::Display for ConfigSection { } impl ConfigSection { - fn get_fields(&self) -> Vec { - ConfigField::iter() - .filter(|field| field.category() == *self) - .collect() + fn get_section(&self, config: &Config) -> Vec { + match self { + Self::Blog => vec![ + ConfigField::BlogTitle, + ConfigField::BlogAuthor, + ConfigField::BlogDescription, + ConfigField::BlogBaseUrl, + ConfigField::BlogLanguage, + ConfigField::BlogTimezone, + ], + Self::Theme => get_all_theme_fields(config), + Self::Domain => vec![ConfigField::DomainPrimary, ConfigField::DomainEnforceHttps], + Self::Build => vec![ + ConfigField::BuildOutputDir, + ConfigField::BuildDrafts, + ConfigField::BuildFuturePosts, + ], + Self::Development => vec![ConfigField::DevPort, ConfigField::DevAutoReload], + } } } @@ -214,7 +364,7 @@ impl ConfigApp { Constraint::Min(0), // Main content Constraint::Length(3), // Status bar ]) - .split(frame.size()); + .split(frame.area()); self.state.render_header(frame, chunks[0], &self.theme); self.state @@ -240,6 +390,7 @@ impl ConfigApp { enum ConfigAppState { Browse(Box), Edit(Box), + EditTheme(Box), Help(Box), Shutdown(Shutdown), } @@ -253,6 +404,7 @@ impl ConfigAppState { match self { Self::Browse(app) => app.render_browse_mode(frame, area, theme), Self::Edit(app) => app.render_edit_mode(frame, area, theme), + Self::EditTheme(app) => app.render_table(frame, area, theme), Self::Help(app) => app.render_help_overlay(frame, theme), // Help is rendered as overlay Self::Shutdown(_) => {} } @@ -290,6 +442,7 @@ impl ConfigAppState { match self { Self::Browse(app) => Ok(app.handle_key_event(key)), Self::Edit(app) => app.handle_key_event(key), + Self::EditTheme(app) => app.handle_key_event(key), Self::Help(app) => Ok(app.handle_key_event(key)), Self::Shutdown(app) => Ok(app.into()), } @@ -299,6 +452,7 @@ impl ConfigAppState { match self { Self::Browse(app) => app.status_message.clone(), Self::Edit(app) => app.browse_data.status_message.clone(), + Self::EditTheme(app) => app.browse_data.status_message.clone(), Self::Help(app) => app.browse_data.status_message.clone(), Self::Shutdown(_) => "Shutting down".to_string(), } @@ -317,6 +471,12 @@ impl From for ConfigAppState { } } +impl From for ConfigAppState { + fn from(value: EditTheme) -> Self { + ConfigAppState::EditTheme(Box::new(value)) + } +} + impl From for ConfigAppState { fn from(value: Help) -> Self { ConfigAppState::Help(Box::new(value)) @@ -347,14 +507,15 @@ struct Browse { impl Browse { fn new(config: Config, project: Project) -> Self { let mut list_state = ListState::default(); - let list_layout = HighLevelConfigList::new(); + let list_layout = HighLevelConfigList::new(&config); let selected_field = ConfigField::BlogTitle; - list_state.select(list_layout.index_of(&selected_field)); + let config_index = list_layout.index_of(&selected_field).unwrap_or(2); + list_state.select(Some(config_index)); Self { config, project, selected_field, - config_index: 0, // must match the index of selected_field in ConfigField::VARIANTS + config_index, list_layout, list_state, status_message: "Navigate with ↑/↓, Enter to edit, 'q' to quit".to_string(), @@ -367,7 +528,10 @@ impl Browse { KeyCode::Char('h') | KeyCode::F(1) => self.enter_help_mode().into(), KeyCode::Up => self.key_up().into(), KeyCode::Down => self.key_down().into(), - KeyCode::Enter => self.enter_edit_mode().into(), + KeyCode::Enter => match self.selected_field { + ConfigField::ThemeName => self.enter_edit_theme_mode().into(), + _ => self.enter_edit_mode().into(), + }, _ => self.into(), } } @@ -376,22 +540,23 @@ impl Browse { if self.config_index == 0 { return self; } - let prev = ConfigField::VARIANTS.get(self.config_index - 1); - if let Some(prev) = prev { - self.selected_field = *prev; - self.config_index -= 1; - self.list_state.select(self.list_layout.index_of(prev)); + let prev: Option<(usize, &ConfigField)> = self.list_layout.prev(self.config_index); + if let Some((index, prev)) = prev { + self.selected_field = prev.clone(); + self.config_index = index; + self.list_state.select(Some(index)); } self } fn key_down(mut self) -> Self { - let next = ConfigField::VARIANTS.get(self.config_index + 1); - if let Some(next) = next { - self.selected_field = *next; - self.config_index += 1; - self.list_state.select(self.list_layout.index_of(next)); + let next = self.list_layout.next(self.config_index); + if let Some((index, next)) = next { + self.selected_field = next.clone(); + self.config_index = index; + self.list_state.select(Some(index)); } + self } @@ -449,6 +614,7 @@ impl Browse { .highlight_style( Style::default() .bg(theme.primary_color) + .fg(theme.background_color) .add_modifier(Modifier::BOLD), ); @@ -464,11 +630,8 @@ impl Browse { }; let content = format!( - "Field: {}\nCategory: {}\nCurrent Value: {}{}\n\nPress Enter to edit this field", - self.selected_field, - self.selected_field.category(), - value, - effective_url + "Field: {}\nCurrent Value: {}{}\n\nPress Enter to edit this field", + self.selected_field, value, effective_url ); let details = Paragraph::new(content) @@ -492,11 +655,16 @@ impl Browse { ); Edit { new_config: self.config.clone(), + target_field: self.selected_field.clone(), browse_data: self, edit_buffer, } } + fn enter_edit_theme_mode(self) -> EditTheme { + self.into() + } + fn enter_help_mode(self) -> Help { Help { browse_data: self } } @@ -508,6 +676,7 @@ impl Browse { struct Edit { browse_data: Browse, + target_field: ConfigField, edit_buffer: String, new_config: Config, } @@ -526,10 +695,7 @@ impl Edit { browse_data.status_message = "Edit cancelled".to_string(); Ok(browse_data.into()) } - KeyCode::Enter => { - let browse_data = self.apply_edit()?; - Ok(browse_data.into()) - } + KeyCode::Enter => self.apply(), KeyCode::Backspace => { self.edit_buffer.pop(); Ok(self.into()) @@ -591,103 +757,20 @@ impl Edit { frame.render_widget(help, edit_area[1]); } - fn apply_edit(mut self) -> AppResult { - let value = self.edit_buffer.trim().to_string(); - - // Apply the change to the configuration - match self.browse_data.selected_field { - ConfigField::BlogTitle => { - if !value.is_empty() { - self.new_config.blog.title = value; - } - } - ConfigField::BlogAuthor => { - if !value.is_empty() { - self.new_config.blog.author = value; - } - } - ConfigField::BlogDescription => { - if !value.is_empty() { - self.new_config.blog.description = value; - } - } - ConfigField::BlogBaseUrl => { - if !value.is_empty() { - self.new_config.blog.base_url = value; - } - } - ConfigField::BlogLanguage => { - self.new_config.blog.language = if value.is_empty() { None } else { Some(value) }; - } - ConfigField::BlogTimezone => { - self.new_config.blog.timezone = if value.is_empty() { None } else { Some(value) }; - } - ConfigField::ThemeName => { - if !value.is_empty() { - self.new_config.theme.name = value; - } - } - ConfigField::DomainPrimary => { - if self.new_config.blog.domains.is_none() { - self.new_config.blog.domains = Some(crate::config::DomainConfig { - primary: None, - aliases: Vec::new(), - subdomain: None, - enforce_https: true, - github_pages_domain: None, - }); - } - if let Some(domains) = &mut self.new_config.blog.domains { - domains.primary = if value.is_empty() { - None - } else { - Some(value.clone()) - }; - domains.github_pages_domain = if value.is_empty() { None } else { Some(value) }; - } - } - ConfigField::DomainEnforceHttps => { - let enforce_https = value.to_lowercase() == "true"; - if self.new_config.blog.domains.is_none() { - self.new_config.blog.domains = Some(crate::config::DomainConfig { - primary: None, - aliases: Vec::new(), - subdomain: None, - enforce_https, - github_pages_domain: None, - }); - } - if let Some(domains) = &mut self.new_config.blog.domains { - domains.enforce_https = enforce_https; - } - } - ConfigField::BuildOutputDir => { - self.new_config.build.output_dir = - if value.is_empty() { None } else { Some(value) }; - } - ConfigField::BuildDrafts => { - self.new_config.build.drafts = value.to_lowercase() == "true"; - } - ConfigField::BuildFuturePosts => { - self.new_config.build.future_posts = value.to_lowercase() == "true"; - } - ConfigField::DevPort => { - if let Ok(port) = value.parse::() { - if port > 0 { - self.new_config.dev.port = port; - } - } - } - ConfigField::DevAutoReload => { - self.new_config.dev.auto_reload = value.to_lowercase() == "true"; - } + fn apply(mut self) -> AppResult { + let new_value = self.edit_buffer.trim().to_string(); + if new_value.is_empty() { + self.browse_data.status_message = "Edit discarded".to_string(); + return Ok(self.browse_data.into()); } - let config_path = self.browse_data.project.root.join("blogr.toml"); - self.new_config.save_to_file(&config_path)?; - self.browse_data.config = self.new_config.clone(); - self.browse_data.status_message = "Configuration saved successfully!".to_string(); - Ok(self.enter_browse_mode()) + if let Err(e) = self.target_field.set(&mut self.new_config, new_value) { + self.browse_data.status_message = e.to_string(); + return Ok(self.into()); + }; + + self.browse_data = save_and_refresh(self.browse_data, self.new_config.clone())?; + Ok(self.enter_browse_mode().into()) } fn enter_browse_mode(self) -> Browse { @@ -704,7 +787,7 @@ impl Help { } fn render_help_overlay(&self, frame: &mut Frame, theme: &TuiTheme) { - let area = frame.size(); + let area = frame.area(); let popup_area = Layout::default() .direction(Direction::Vertical) .constraints([ @@ -763,3 +846,164 @@ impl Help { self.browse_data } } + +struct EditTheme { + browse_data: Browse, + options: Vec, + table_state: TableState, + row_index: usize, + new_config: Config, + current_theme_index: Option, +} + +impl From for EditTheme { + fn from(value: Browse) -> Self { + let options = get_all_themes() + .iter() + .map(|theme| theme.info()) + .collect::>(); + + let current_theme_index = options + .iter() + .position(|theme| theme.name == value.config.theme.name); + + let row_index = current_theme_index.unwrap_or(0); + let mut table_state = TableState::default(); + table_state.select(Some(row_index)); + + let new_config = value.config.clone(); + Self { + browse_data: value, + new_config, + options, + row_index, + table_state, + current_theme_index, + } + } +} + +impl EditTheme { + fn render_table(&mut self, frame: &mut Frame, area: Rect, theme: &TuiTheme) { + let header_style = Style::default() + .fg(theme.primary_color) + .add_modifier(Modifier::BOLD); + let selected_row_style = Style::default() + .add_modifier(Modifier::REVERSED) + .fg(theme.focused_border_color); + let selected_col_style = Style::default().fg(theme.cursor_color); + let selected_cell_style = Style::default() + .add_modifier(Modifier::REVERSED) + .fg(theme.background_color); + + let header = ["Name", "Version", "Author", "Description"] + .into_iter() + .map(Cell::from) + .collect::() + .style(header_style) + .height(1); + let rows = self.options.iter().enumerate().map(|(i, data)| { + let item = data.as_data_row(); + let style = match self.current_theme_index { + Some(j) if j == i => Style::new() + .fg(theme.text_color) + .bg(theme.background_color) + .italic(), + _ => Style::new().fg(theme.text_color), + }; + item.into_iter() + .map(|content| Cell::from(Text::from(format!("\n{content}\n")))) + .collect::() + .style(style) + .height(4) + }); + + let bar = " █ "; + let t = Table::new( + rows, + [ + // + 1 is for padding. + Constraint::Length(20 + 1), + Constraint::Length(10), + Constraint::Length(20), + Constraint::Min(40), + ], + ) + .block( + Block::new() + .borders(Borders::ALL) + .title("Themes") + .border_style(theme.focused_border_style()), + ) + .header(header) + .row_highlight_style(selected_row_style) + .column_highlight_style(selected_col_style) + .cell_highlight_style(selected_cell_style) + .highlight_symbol(Text::from(vec![ + "".into(), + bar.into(), + bar.into(), + "".into(), + ])) + .highlight_spacing(HighlightSpacing::Always); + frame.render_stateful_widget(t, area, &mut self.table_state); + } + + fn handle_key_event(self, key: KeyEvent) -> AppResult { + match key.code { + KeyCode::Esc => Ok(self.enter_browse_mode().into()), + KeyCode::Up => Ok(self.key_up().into()), + KeyCode::Down => Ok(self.key_down().into()), + KeyCode::Enter => Ok(self.set_theme()?.into()), + _ => Ok(self.into()), + } + } + + fn key_up(mut self) -> Self { + if self.row_index == 0 { + return self; + } + self.row_index -= 1; + self.table_state.select(Some(self.row_index)); + self + } + + fn key_down(mut self) -> Self { + if self.row_index >= self.options.len() - 1 { + return self; + } + self.row_index += 1; + self.table_state.select(Some(self.row_index)); + self + } + + fn set_theme(mut self) -> AppResult { + let theme = self + .options + .get(self.row_index) + .expect("Index out of bounds") + .clone(); + let default_theme_config = theme + .config_schema + .into_iter() + .map(|(field_name, config)| (field_name, config.value)) + .collect::>(); + self.new_config.set_theme(theme.name, default_theme_config); + //save + self.browse_data = save_and_refresh(self.browse_data, self.new_config.clone())?; + Ok(self.enter_browse_mode()) + } + + fn enter_browse_mode(self) -> Browse { + self.browse_data + } +} + +fn save_and_refresh(mut browse_data: Browse, new_config: Config) -> AppResult { + let config_path = browse_data.project.root.join("blogr.toml"); + browse_data.config = new_config; + browse_data.config.save_to_file(&config_path)?; + browse_data.list_layout = HighLevelConfigList::new(&browse_data.config); + browse_data.status_message = "Configuration saved successfully!".to_string(); + Ok(browse_data) +} diff --git a/blogr-themes/src/brutja/mod.rs b/blogr-themes/src/brutja/mod.rs index a98cc2d..1cc1e43 100644 --- a/blogr-themes/src/brutja/mod.rs +++ b/blogr-themes/src/brutja/mod.rs @@ -17,8 +17,7 @@ impl Theme for BrutjaTheme { schema.insert( "css".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "static/styles.css".to_string(), + value: toml::Value::String("static/styles.css".to_string()), description: "Path to user CSS (served from /static/)".to_string(), }, ); @@ -26,8 +25,7 @@ impl Theme for BrutjaTheme { schema.insert( "hero_title".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "Welcome".to_string(), + value: toml::Value::String("Welcome".to_string()), description: "Homepage hero title".to_string(), }, ); @@ -35,8 +33,7 @@ impl Theme for BrutjaTheme { schema.insert( "hero_subtitle".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "Customize your theme".to_string(), + value: toml::Value::String("Customize your theme".to_string()), description: "Homepage hero subtitle".to_string(), }, ); @@ -44,8 +41,7 @@ impl Theme for BrutjaTheme { schema.insert( "github_username".to_string(), ConfigOption { - option_type: "string".to_string(), - default: String::new(), + value: toml::Value::String(String::new()), description: "Your github username".to_string(), }, ); @@ -53,8 +49,7 @@ impl Theme for BrutjaTheme { schema.insert( "linkedin_username".to_string(), ConfigOption { - option_type: "string".to_string(), - default: String::new(), + value: toml::Value::String(String::new()), description: "The last segment of your linkedin profile URL. Do not include slashes." .to_string(), diff --git a/blogr-themes/src/dark_minimal/mod.rs b/blogr-themes/src/dark_minimal/mod.rs index d1adcee..53902cf 100644 --- a/blogr-themes/src/dark_minimal/mod.rs +++ b/blogr-themes/src/dark_minimal/mod.rs @@ -18,8 +18,7 @@ impl Theme for DarkMinimalTheme { config_schema.insert( "primary_color".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "#00ff88".to_string(), + value: toml::Value::String("#00ff88".to_string()), description: "Primary accent color (neon green)".to_string(), }, ); @@ -27,8 +26,7 @@ impl Theme for DarkMinimalTheme { config_schema.insert( "background_color".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "#0a0a0a".to_string(), + value: toml::Value::String("#0a0a0a".to_string()), description: "Background color (pure dark)".to_string(), }, ); @@ -36,8 +34,7 @@ impl Theme for DarkMinimalTheme { config_schema.insert( "text_color".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "#e0e0e0".to_string(), + value: toml::Value::String("#e0e0e0".to_string()), description: "Main text color (soft white)".to_string(), }, ); @@ -45,8 +42,7 @@ impl Theme for DarkMinimalTheme { config_schema.insert( "secondary_text_color".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "#888888".to_string(), + value: toml::Value::String("#888888".to_string()), description: "Secondary text color (gray)".to_string(), }, ); @@ -54,10 +50,10 @@ impl Theme for DarkMinimalTheme { config_schema.insert( "font_family".to_string(), ConfigOption { - option_type: "string".to_string(), - default: + value: toml::Value::String( "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" .to_string(), + ), description: "Font family".to_string(), }, ); @@ -65,8 +61,7 @@ impl Theme for DarkMinimalTheme { config_schema.insert( "enable_animations".to_string(), ConfigOption { - option_type: "boolean".to_string(), - default: "true".to_string(), + value: toml::Value::Boolean(true), description: "Enable smooth animations".to_string(), }, ); @@ -74,8 +69,7 @@ impl Theme for DarkMinimalTheme { config_schema.insert( "show_social_icons".to_string(), ConfigOption { - option_type: "boolean".to_string(), - default: "true".to_string(), + value: toml::Value::Boolean(true), description: "Show social media icons".to_string(), }, ); @@ -83,8 +77,7 @@ impl Theme for DarkMinimalTheme { config_schema.insert( "show_status_bar".to_string(), ConfigOption { - option_type: "boolean".to_string(), - default: "true".to_string(), + value: toml::Value::Boolean(true), description: "Show availability status bar".to_string(), }, ); @@ -92,8 +85,7 @@ impl Theme for DarkMinimalTheme { config_schema.insert( "status_text".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "Available for opportunities".to_string(), + value: toml::Value::String("Available for opportunities".to_string()), description: "Custom text for status bar".to_string(), }, ); @@ -101,8 +93,7 @@ impl Theme for DarkMinimalTheme { config_schema.insert( "status_color".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "#00ff88".to_string(), + value: toml::Value::String("#00ff88".to_string()), description: "Status dot color (hex code)".to_string(), }, ); diff --git a/blogr-themes/src/lib.rs b/blogr-themes/src/lib.rs index f5e3133..ba9d94c 100644 --- a/blogr-themes/src/lib.rs +++ b/blogr-themes/src/lib.rs @@ -28,10 +28,15 @@ pub struct ThemeInfo { pub config_schema: HashMap, } +impl ThemeInfo { + pub fn as_data_row(&self) -> [&String; 4] { + [&self.name, &self.version, &self.author, &self.description] + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConfigOption { - pub option_type: String, - pub default: String, + pub value: toml::Value, pub description: String, } @@ -70,52 +75,69 @@ impl IntoIterator for ThemeTemplates { } #[must_use] -pub fn get_all_themes() -> HashMap> { - let mut themes: HashMap> = HashMap::new(); - - let minimal_retro = MinimalRetroTheme::new(); - themes.insert("minimal-retro".to_string(), Box::new(minimal_retro)); - - let obsidian = ObsidianTheme::new(); - themes.insert("obsidian".to_string(), Box::new(obsidian)); - - let terminal_candy = TerminalCandyTheme::new(); - themes.insert("terminal-candy".to_string(), Box::new(terminal_candy)); - - let dark_minimal = DarkMinimalTheme::new(); - themes.insert("dark-minimal".to_string(), Box::new(dark_minimal)); - - let musashi = MusashiTheme::new(); - themes.insert("musashi".to_string(), Box::new(musashi)); - - let slate_portfolio = SlatePortfolioTheme::new(); - themes.insert("slate-portfolio".to_string(), Box::new(slate_portfolio)); - - let typewriter = TypewriterTheme::new(); - themes.insert("typewriter".to_string(), Box::new(typewriter)); - - let brutja = BrutjaTheme::new(); - themes.insert("brutja".to_string(), Box::new(brutja)); - - themes +pub fn get_all_themes() -> Vec> { + vec![ + Box::new(MinimalRetroTheme::new()), + Box::new(ObsidianTheme::new()), + Box::new(TerminalCandyTheme::new()), + Box::new(DarkMinimalTheme::new()), + Box::new(MusashiTheme::new()), + Box::new(SlatePortfolioTheme::new()), + Box::new(TypewriterTheme::new()), + Box::new(BrutjaTheme::new()), + ] } #[must_use] pub fn get_theme(name: &str) -> Option> { - match name { - "minimal-retro" => Some(Box::new(MinimalRetroTheme::new()) as Box), - "obsidian" => Some(Box::new(ObsidianTheme::new()) as Box), - "terminal-candy" => Some(Box::new(TerminalCandyTheme::new()) as Box), - "dark-minimal" => Some(Box::new(DarkMinimalTheme::new()) as Box), - "musashi" => Some(Box::new(MusashiTheme::new()) as Box), - "slate-portfolio" => Some(Box::new(SlatePortfolioTheme::new()) as Box), - "typewriter" => Some(Box::new(TypewriterTheme::new()) as Box), - "brutja" => Some(Box::new(BrutjaTheme::new()) as Box), - _ => None, - } + get_all_themes() + .into_iter() + .find(|theme| theme.info().name.to_lowercase() == name.to_lowercase()) } #[must_use] pub fn get_theme_by_name(name: &str) -> Option> { get_theme(name) } + +#[cfg(test)] +mod test { + use std::collections::{HashMap, HashSet}; + + use crate::get_all_themes; + + #[test] + fn themes_have_unique_names() { + let all_theme_names = get_all_themes() + .iter() + .map(|theme| theme.info().name) + .collect::>(); + + let unique_theme_names = all_theme_names + .clone() + .into_iter() + .collect::>(); + + // we could assert and end the test here but really we want to know which name has been duplicated. + if unique_theme_names.len() == all_theme_names.len() { + return; + } + + let duplicate_theme_name = all_theme_names + .iter() + .fold(HashMap::new(), |mut acc: HashMap, name| { + acc.entry(name.clone()) + .and_modify(|entry| *entry += 1) + .or_insert(1); + acc + }) + .into_iter() + .find(|(_name, count)| *count > 1); + + if let Some((duplicate, count)) = duplicate_theme_name { + panic!("Theme name {duplicate} occurs {count} times."); + } else { + panic!("Test working incorrectly. Unreachable statement reached."); + } + } +} diff --git a/blogr-themes/src/minimal_retro/mod.rs b/blogr-themes/src/minimal_retro/mod.rs index 2a35b0f..ec890dc 100644 --- a/blogr-themes/src/minimal_retro/mod.rs +++ b/blogr-themes/src/minimal_retro/mod.rs @@ -18,8 +18,7 @@ impl Theme for MinimalRetroTheme { config_schema.insert( "primary_color".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "#FF6B35".to_string(), + value: toml::Value::String("#FF6B35".to_string()), description: "Primary accent color (retro orange)".to_string(), }, ); @@ -27,8 +26,7 @@ impl Theme for MinimalRetroTheme { config_schema.insert( "secondary_color".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "#F7931E".to_string(), + value: toml::Value::String("#F7931E".to_string()), description: "Secondary accent color (warm amber)".to_string(), }, ); @@ -36,8 +34,7 @@ impl Theme for MinimalRetroTheme { config_schema.insert( "background_color".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "#2D1B0F".to_string(), + value: toml::Value::String("#2D1B0F".to_string()), description: "Background color (dark brown)".to_string(), }, ); @@ -45,8 +42,9 @@ impl Theme for MinimalRetroTheme { config_schema.insert( "font_family".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "'Crimson Text', 'Playfair Display', Georgia, serif".to_string(), + value: toml::Value::String( + "'Crimson Text', 'Playfair Display', Georgia, serif".to_string(), + ), description: "Artistic serif font family".to_string(), }, ); @@ -54,8 +52,7 @@ impl Theme for MinimalRetroTheme { config_schema.insert( "accent_font".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "'Space Mono', 'Courier Prime', monospace".to_string(), + value: toml::Value::String("'Space Mono', 'Courier Prime', monospace".to_string()), description: "Monospace accent font for tags and metadata".to_string(), }, ); @@ -63,8 +60,7 @@ impl Theme for MinimalRetroTheme { config_schema.insert( "show_reading_time".to_string(), ConfigOption { - option_type: "boolean".to_string(), - default: "true".to_string(), + value: toml::Value::Boolean(true), description: "Display estimated reading time".to_string(), }, ); @@ -72,8 +68,7 @@ impl Theme for MinimalRetroTheme { config_schema.insert( "show_author".to_string(), ConfigOption { - option_type: "boolean".to_string(), - default: "true".to_string(), + value: toml::Value::Boolean(true), description: "Display post author".to_string(), }, ); @@ -81,8 +76,7 @@ impl Theme for MinimalRetroTheme { config_schema.insert( "expandable_posts".to_string(), ConfigOption { - option_type: "boolean".to_string(), - default: "true".to_string(), + value: toml::Value::Boolean(true), description: "Enable expandable post previews on homepage".to_string(), }, ); diff --git a/blogr-themes/src/musashi/mod.rs b/blogr-themes/src/musashi/mod.rs index 1ecbf83..26bf395 100644 --- a/blogr-themes/src/musashi/mod.rs +++ b/blogr-themes/src/musashi/mod.rs @@ -18,8 +18,7 @@ impl Theme for MusashiTheme { config_schema.insert( "primary_color".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "#1a1a1a".to_string(), + value: toml::Value::String("#1a1a1a".to_string()), description: "Primary color (ink black)".to_string(), }, ); @@ -27,8 +26,7 @@ impl Theme for MusashiTheme { config_schema.insert( "background_color".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "#faf9f7".to_string(), + value: toml::Value::String("#faf9f7".to_string()), description: "Background color (warm paper)".to_string(), }, ); @@ -36,8 +34,7 @@ impl Theme for MusashiTheme { config_schema.insert( "text_color".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "#2d2d2d".to_string(), + value: toml::Value::String("#2d2d2d".to_string()), description: "Main text color (charcoal)".to_string(), }, ); @@ -45,8 +42,7 @@ impl Theme for MusashiTheme { config_schema.insert( "secondary_text_color".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "#6b6b6b".to_string(), + value: toml::Value::String("#6b6b6b".to_string()), description: "Secondary text color (warm gray)".to_string(), }, ); @@ -54,8 +50,7 @@ impl Theme for MusashiTheme { config_schema.insert( "accent_color".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "#4a4a4a".to_string(), + value: toml::Value::String("#4a4a4a".to_string()), description: "Accent color (slate)".to_string(), }, ); @@ -63,8 +58,7 @@ impl Theme for MusashiTheme { config_schema.insert( "font_family".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "'Noto Serif JP', 'Georgia', serif".to_string(), + value: toml::Value::String("'Noto Serif JP', 'Georgia', serif".to_string()), description: "Font family with Japanese serif style".to_string(), }, ); @@ -72,8 +66,7 @@ impl Theme for MusashiTheme { config_schema.insert( "enable_animations".to_string(), ConfigOption { - option_type: "boolean".to_string(), - default: "true".to_string(), + value: toml::Value::Boolean(true), description: "Enable subtle zen-like animations".to_string(), }, ); @@ -81,8 +74,7 @@ impl Theme for MusashiTheme { config_schema.insert( "show_brush_strokes".to_string(), ConfigOption { - option_type: "boolean".to_string(), - default: "true".to_string(), + value: toml::Value::Boolean(true), description: "Show ink brush stroke decorative elements".to_string(), }, ); @@ -90,8 +82,7 @@ impl Theme for MusashiTheme { config_schema.insert( "zen_mode".to_string(), ConfigOption { - option_type: "boolean".to_string(), - default: "false".to_string(), + value: toml::Value::Boolean(false), description: "Ultra-minimalist mode with maximum whitespace".to_string(), }, ); diff --git a/blogr-themes/src/obsidian/mod.rs b/blogr-themes/src/obsidian/mod.rs index 631d217..9ce92f5 100644 --- a/blogr-themes/src/obsidian/mod.rs +++ b/blogr-themes/src/obsidian/mod.rs @@ -18,8 +18,7 @@ impl Theme for ObsidianTheme { schema.insert( "obsidian_css".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "static/obsidian.css".to_string(), + value: toml::Value::String("static/obsidian.css".to_string()), description: "Path to Obsidian CSS (served from /static/)".to_string(), }, ); @@ -27,8 +26,7 @@ impl Theme for ObsidianTheme { schema.insert( "color_mode".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "auto".to_string(), + value: toml::Value::String("auto".to_string()), description: "Dark/light mode handling (auto | dark | light)".to_string(), }, ); diff --git a/blogr-themes/src/slate_portfolio/mod.rs b/blogr-themes/src/slate_portfolio/mod.rs index ac90a27..dcfecff 100644 --- a/blogr-themes/src/slate_portfolio/mod.rs +++ b/blogr-themes/src/slate_portfolio/mod.rs @@ -18,8 +18,7 @@ impl Theme for SlatePortfolioTheme { config_schema.insert( "accent_color".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "#10b981".to_string(), + value: toml::Value::String("#10b981".to_string()), description: "Accent color (emerald)".to_string(), }, ); @@ -27,8 +26,7 @@ impl Theme for SlatePortfolioTheme { config_schema.insert( "background_color".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "#0f172a".to_string(), + value: toml::Value::String("#0f172a".to_string()), description: "Background color (slate 900)".to_string(), }, ); @@ -36,8 +34,7 @@ impl Theme for SlatePortfolioTheme { config_schema.insert( "card_background".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "#1e293b".to_string(), + value: toml::Value::String("#1e293b".to_string()), description: "Card background color (slate 800)".to_string(), }, ); @@ -45,8 +42,7 @@ impl Theme for SlatePortfolioTheme { config_schema.insert( "text_color".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "#f1f5f9".to_string(), + value: toml::Value::String("#f1f5f9".to_string()), description: "Main text color (slate 100)".to_string(), }, ); @@ -54,8 +50,7 @@ impl Theme for SlatePortfolioTheme { config_schema.insert( "secondary_text_color".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "#94a3b8".to_string(), + value: toml::Value::String("#94a3b8".to_string()), description: "Secondary text color (slate 400)".to_string(), }, ); @@ -63,10 +58,10 @@ impl Theme for SlatePortfolioTheme { config_schema.insert( "font_family".to_string(), ConfigOption { - option_type: "string".to_string(), - default: + value: toml::Value::String( "'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" .to_string(), + ), description: "Font family".to_string(), }, ); @@ -74,8 +69,7 @@ impl Theme for SlatePortfolioTheme { config_schema.insert( "show_avatar".to_string(), ConfigOption { - option_type: "boolean".to_string(), - default: "false".to_string(), + value: toml::Value::Boolean(false), description: "Show avatar image in About section".to_string(), }, ); @@ -83,8 +77,7 @@ impl Theme for SlatePortfolioTheme { config_schema.insert( "avatar_url".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "".to_string(), + value: toml::Value::String(String::new()), description: "URL to avatar image".to_string(), }, ); @@ -92,8 +85,7 @@ impl Theme for SlatePortfolioTheme { config_schema.insert( "cta_text".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "View My Work".to_string(), + value: toml::Value::String("View My Work".to_string()), description: "Call-to-action button text".to_string(), }, ); diff --git a/blogr-themes/src/terminal_candy/mod.rs b/blogr-themes/src/terminal_candy/mod.rs index 8c8445e..0af211b 100644 --- a/blogr-themes/src/terminal_candy/mod.rs +++ b/blogr-themes/src/terminal_candy/mod.rs @@ -18,8 +18,7 @@ impl Theme for TerminalCandyTheme { config_schema.insert( "primary_color".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "#FFB3D9".to_string(), + value: toml::Value::String("#FFB3D9".to_string()), description: "Primary accent color (pastel pink)".to_string(), }, ); @@ -27,8 +26,7 @@ impl Theme for TerminalCandyTheme { config_schema.insert( "secondary_color".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "#B4F8C8".to_string(), + value: toml::Value::String("#B4F8C8".to_string()), description: "Secondary accent color (mint green)".to_string(), }, ); @@ -36,8 +34,7 @@ impl Theme for TerminalCandyTheme { config_schema.insert( "tertiary_color".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "#C7CEEA".to_string(), + value: toml::Value::String("#C7CEEA".to_string()), description: "Tertiary accent color (lavender)".to_string(), }, ); @@ -45,8 +42,7 @@ impl Theme for TerminalCandyTheme { config_schema.insert( "background_color".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "#0D1117".to_string(), + value: toml::Value::String("#0D1117".to_string()), description: "Background color (dark terminal)".to_string(), }, ); @@ -54,8 +50,7 @@ impl Theme for TerminalCandyTheme { config_schema.insert( "text_color".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "#E6EDF3".to_string(), + value: toml::Value::String("#E6EDF3".to_string()), description: "Main text color (light)".to_string(), }, ); @@ -63,8 +58,9 @@ impl Theme for TerminalCandyTheme { config_schema.insert( "font_family".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "'JetBrains Mono', 'Fira Code', 'Consolas', monospace".to_string(), + value: toml::Value::String( + "'JetBrains Mono', 'Fira Code', 'Consolas', monospace".to_string(), + ), description: "Monospace font family".to_string(), }, ); @@ -72,8 +68,7 @@ impl Theme for TerminalCandyTheme { config_schema.insert( "enable_glitch".to_string(), ConfigOption { - option_type: "boolean".to_string(), - default: "true".to_string(), + value: toml::Value::Boolean(true), description: "Enable glitch effects on hover".to_string(), }, ); @@ -81,8 +76,7 @@ impl Theme for TerminalCandyTheme { config_schema.insert( "enable_typewriter".to_string(), ConfigOption { - option_type: "boolean".to_string(), - default: "true".to_string(), + value: toml::Value::Boolean(true), description: "Enable typewriter animation for bio/description".to_string(), }, ); @@ -90,8 +84,7 @@ impl Theme for TerminalCandyTheme { config_schema.insert( "show_ascii_art".to_string(), ConfigOption { - option_type: "boolean".to_string(), - default: "true".to_string(), + value: toml::Value::Boolean(true), description: "Show ASCII art decorations".to_string(), }, ); diff --git a/blogr-themes/src/typewriter/mod.rs b/blogr-themes/src/typewriter/mod.rs index 0857aaf..c78bf3a 100644 --- a/blogr-themes/src/typewriter/mod.rs +++ b/blogr-themes/src/typewriter/mod.rs @@ -18,8 +18,7 @@ impl Theme for TypewriterTheme { config_schema.insert( "paper_color".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "#f4f1e8".to_string(), + value: toml::Value::String("#f4f1e8".to_string()), description: "Paper background color (vintage cream)".to_string(), }, ); @@ -27,8 +26,7 @@ impl Theme for TypewriterTheme { config_schema.insert( "ink_color".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "#2b2b2b".to_string(), + value: toml::Value::String("#2b2b2b".to_string()), description: "Text/ink color (dark charcoal)".to_string(), }, ); @@ -36,8 +34,7 @@ impl Theme for TypewriterTheme { config_schema.insert( "accent_color".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "#8b4513".to_string(), + value: toml::Value::String("#8b4513".to_string()), description: "Accent color (vintage brown)".to_string(), }, ); @@ -45,8 +42,7 @@ impl Theme for TypewriterTheme { config_schema.insert( "font_family".to_string(), ConfigOption { - option_type: "string".to_string(), - default: "'Courier Prime', 'Courier New', monospace".to_string(), + value: toml::Value::String("'Courier Prime', 'Courier New', monospace".to_string()), description: "Typewriter-style font family".to_string(), }, ); @@ -54,8 +50,7 @@ impl Theme for TypewriterTheme { config_schema.insert( "show_paper_texture".to_string(), ConfigOption { - option_type: "boolean".to_string(), - default: "true".to_string(), + value: toml::Value::Boolean(true), description: "Show subtle paper texture overlay".to_string(), }, ); @@ -63,8 +58,7 @@ impl Theme for TypewriterTheme { config_schema.insert( "typing_animation".to_string(), ConfigOption { - option_type: "boolean".to_string(), - default: "true".to_string(), + value: toml::Value::Boolean(true), description: "Enable typewriter typing animation for title".to_string(), }, ); @@ -72,8 +66,7 @@ impl Theme for TypewriterTheme { config_schema.insert( "show_date_stamp".to_string(), ConfigOption { - option_type: "boolean".to_string(), - default: "true".to_string(), + value: toml::Value::Boolean(true), description: "Show vintage date stamp in header".to_string(), }, ); @@ -81,8 +74,7 @@ impl Theme for TypewriterTheme { config_schema.insert( "cursor_blink".to_string(), ConfigOption { - option_type: "boolean".to_string(), - default: "true".to_string(), + value: toml::Value::Boolean(true), description: "Show blinking cursor effect".to_string(), }, );