Remove uploading css functionality. Themes are hard-coded. Change the code

This commit is contained in:
Hugh Kaznowski
2026-09-07 01:02:41 +01:00
parent 901dcf0a3f
commit 82fa0dfeb4
24 changed files with 578 additions and 1702 deletions
+40 -288
View File
@@ -1,13 +1,9 @@
pub mod profile;
use std::collections::hash_map::DefaultHasher;
use std::fmt;
use std::fs;
use std::hash::{Hash, Hasher};
use std::io;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
@@ -17,39 +13,10 @@ use crate::config::profile::Profile;
pub type SettingsHandle = Arc<Mutex<Settings>>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum ColorScheme {
Light,
#[serde(alias = "grey")]
Dark,
Red,
Green,
Blue,
Yellow,
Pink,
Orange,
#[serde(rename = "light_green")]
LightGreen,
#[serde(rename = "light_red")]
LightRed,
#[serde(rename = "light_blue")]
LightBlue,
#[serde(rename = "light_yellow")]
LightYellow,
#[serde(rename = "light_pink")]
LightPink,
#[serde(rename = "light_orange")]
LightOrange,
#[default]
System,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum CssEntry {
Imported { id: String, filename: String },
Linked { path_or_url: String },
/// Name of the selected theme file in `themes/` (see `THEMES` in `src/api/mod.rs`), or
/// `"system"` to follow the OS light/dark preference between the `dark`/`light` themes.
fn default_theme() -> String {
"system".to_string()
}
#[derive(Debug, Clone, Deserialize, Default)]
@@ -76,11 +43,13 @@ impl Default for ProfilesOnDisk {
#[derive(Debug, Clone, Deserialize)]
struct SettingsOnDisk {
color_scheme: ColorScheme,
#[serde(default)]
css_entries: Vec<CssEntry>,
theme: Option<String>,
// Pre-theme-directory settings stored the selection as `color_scheme` (a fixed enum); a
// separate `custom_theme_id` pointed at an uploaded/linked CSS entry (both removed) and can't
// be mapped onto a theme file, so it's left out here and just ignored by serde on load.
#[serde(default)]
custom_theme_id: Option<String>,
color_scheme: Option<String>,
#[serde(default)]
profiles: ProfilesOnDisk,
#[serde(default)]
@@ -101,10 +70,15 @@ impl From<SettingsOnDisk> for Settings {
ProfilesOnDisk::Inline(profiles) => (profiles, None),
ProfilesOnDisk::Legacy(pointer) => (Vec::new(), pointer.active_profile),
};
let theme = on_disk.theme.unwrap_or_else(|| {
match on_disk.color_scheme.as_deref() {
Some("grey") => "dark".to_string(),
Some(scheme) => scheme.to_string(),
None => default_theme(),
}
});
Settings {
color_scheme: on_disk.color_scheme,
css_entries: on_disk.css_entries,
custom_theme_id: on_disk.custom_theme_id,
theme,
profiles,
active_profile: on_disk.active_profile.or(legacy_active_profile),
max_logs: on_disk.max_logs,
@@ -115,13 +89,11 @@ impl From<SettingsOnDisk> for Settings {
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(from = "SettingsOnDisk")]
pub struct Settings {
pub color_scheme: ColorScheme,
pub css_entries: Vec<CssEntry>,
#[serde(default)]
pub custom_theme_id: Option<String>,
#[serde(default = "default_theme")]
pub theme: String,
pub profiles: Vec<Profile>,
pub active_profile: Option<String>,
#[serde(default)]
@@ -134,6 +106,20 @@ pub struct Settings {
pub ntfy_server: Option<String>,
}
impl Default for Settings {
fn default() -> Self {
Settings {
theme: default_theme(),
profiles: Vec::new(),
active_profile: None,
max_logs: None,
alert_rules: Vec::new(),
ntfy_topic: None,
ntfy_server: None,
}
}
}
pub fn default_settings_path() -> Option<PathBuf> {
ProjectDirs::from(
crate::consts::APP_QUALIFIER,
@@ -231,119 +217,6 @@ impl Settings {
}
}
#[derive(Debug)]
pub enum CssError {
NotFound,
NotImported,
Io(io::Error),
}
impl fmt::Display for CssError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CssError::NotFound => write!(f, "no CSS entry with that id"),
CssError::NotImported => write!(f, "CSS entry is linked, not imported"),
CssError::Io(err) => write!(f, "I/O error: {err}"),
}
}
}
impl std::error::Error for CssError {}
impl From<io::Error> for CssError {
fn from(err: io::Error) -> Self {
CssError::Io(err)
}
}
pub fn css_entry_id(entry: &CssEntry) -> String {
match entry {
CssEntry::Imported { id, .. } => id.clone(),
CssEntry::Linked { path_or_url, .. } => linked_entry_id(path_or_url),
}
}
fn linked_entry_id(path_or_url: &str) -> String {
let mut hasher = DefaultHasher::new();
path_or_url.hash(&mut hasher);
format!("linked-{:016x}", hasher.finish())
}
fn generate_imported_css_id(filename: &str, content: &str) -> String {
let mut hasher = DefaultHasher::new();
filename.hash(&mut hasher);
content.hash(&mut hasher);
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
nanos.hash(&mut hasher);
format!("{:016x}", hasher.finish())
}
fn imported_css_file_path(css_dir: &Path, id: &str) -> PathBuf {
css_dir.join(format!("{id}.css"))
}
impl Settings {
pub fn import_css(
&mut self,
settings_path: &Path,
css_dir: &Path,
filename: String,
content: &str,
) -> Result<String, CssError> {
fs::create_dir_all(css_dir)?;
let id = generate_imported_css_id(&filename, content);
fs::write(imported_css_file_path(css_dir, &id), content)?;
self.css_entries.push(CssEntry::Imported {
id: id.clone(),
filename,
});
self.save(settings_path)?;
Ok(id)
}
pub fn link_css(&mut self, settings_path: &Path, path_or_url: String) -> Result<(), CssError> {
self.css_entries.push(CssEntry::Linked { path_or_url });
self.save(settings_path)?;
Ok(())
}
pub fn remove_css(
&mut self,
settings_path: &Path,
css_dir: &Path,
id: &str,
) -> Result<(), CssError> {
let index = self
.css_entries
.iter()
.position(|entry| css_entry_id(entry) == id);
match index.map(|i| &self.css_entries[i]) {
None => Err(CssError::NotFound),
Some(CssEntry::Linked { .. }) => Err(CssError::NotImported),
Some(CssEntry::Imported { .. }) => {
let idx = index.unwrap();
self.css_entries.remove(idx);
let _ = fs::remove_file(imported_css_file_path(css_dir, id));
self.save(settings_path)?;
Ok(())
}
}
}
pub fn css_content(&self, css_dir: &Path, id: &str) -> Result<String, CssError> {
match self.css_entries.iter().find(|entry| css_entry_id(entry) == id) {
None => Err(CssError::NotFound),
Some(CssEntry::Linked { .. }) => Err(CssError::NotImported),
Some(CssEntry::Imported { .. }) => {
Ok(fs::read_to_string(imported_css_file_path(css_dir, id))?)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -371,29 +244,12 @@ mod tests {
}
#[test]
fn light_variant_color_schemes_round_trip_through_serde() {
for (scheme, rename) in [
(ColorScheme::LightGreen, "light_green"),
(ColorScheme::LightRed, "light_red"),
(ColorScheme::LightBlue, "light_blue"),
(ColorScheme::LightYellow, "light_yellow"),
(ColorScheme::LightPink, "light_pink"),
(ColorScheme::LightOrange, "light_orange"),
] {
let json = serde_json::to_string(&scheme).unwrap();
assert_eq!(json, format!("\"{rename}\""));
let deserialized: ColorScheme = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, scheme);
}
}
#[test]
fn legacy_grey_color_scheme_deserializes_as_dark() {
fn legacy_grey_color_scheme_migrates_to_dark_theme() {
let settings: Settings = serde_json::from_str(
r#"{"color_scheme":"grey","css_entries":[],"custom_theme_id":null,"profiles":{"names":[],"active_profile":null,"profiles_dir":"profiles"},"max_logs":null,"alert_rules":[],"ntfy_topic":null,"ntfy_server":null}"#,
)
.unwrap();
assert_eq!(settings.color_scheme, ColorScheme::Dark);
assert_eq!(settings.theme, "dark");
}
#[test]
@@ -411,15 +267,10 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("settings.json");
let mut settings = Settings::default();
settings.color_scheme = ColorScheme::Dark;
settings.css_entries.push(CssEntry::Imported {
id: "abc123".to_string(),
filename: "theme.css".to_string(),
});
settings.css_entries.push(CssEntry::Linked {
path_or_url: "https://example.com/style.css".to_string(),
});
let mut settings = Settings {
theme: "dark".to_string(),
..Settings::default()
};
settings.profiles.push(crate::config::profile::Profile::new("default".to_string()));
settings.active_profile = Some("default".to_string());
settings.alert_rules.push(crate::engine::alerts::AlertRule {
@@ -449,105 +300,6 @@ mod tests {
assert!(path.exists());
}
#[test]
fn import_css_persists_file_and_entry_across_reload() {
let dir = tempfile::tempdir().unwrap();
let settings_path = dir.path().join("settings.json");
let css_dir = dir.path().to_path_buf();
let mut settings = Settings::load(&settings_path);
let id = settings
.import_css(
&settings_path,
&css_dir,
"theme.css".to_string(),
"body { color: red; }",
)
.unwrap();
let css_file = css_dir.join(format!("{id}.css"));
assert!(css_file.exists());
assert_eq!(fs::read_to_string(&css_file).unwrap(), "body { color: red; }");
drop(settings);
let reloaded = Settings::load(&settings_path);
assert!(reloaded.css_entries.iter().any(|entry| matches!(
entry,
CssEntry::Imported { id: entry_id, filename, .. } if entry_id == &id && filename == "theme.css"
)));
assert!(css_file.exists());
assert_eq!(
reloaded.css_content(&css_dir, &id).unwrap(),
"body { color: red; }"
);
}
#[test]
fn remove_css_deletes_file_and_entry() {
let dir = tempfile::tempdir().unwrap();
let settings_path = dir.path().join("settings.json");
let css_dir = dir.path().to_path_buf();
let mut settings = Settings::load(&settings_path);
let id = settings
.import_css(&settings_path, &css_dir, "a.css".to_string(), "a{}")
.unwrap();
let css_file = css_dir.join(format!("{id}.css"));
assert!(css_file.exists());
settings.remove_css(&settings_path, &css_dir, &id).unwrap();
assert!(!css_file.exists());
assert!(!settings.css_entries.iter().any(|e| css_entry_id(e) == id));
let reloaded = Settings::load(&settings_path);
assert!(!reloaded.css_entries.iter().any(|e| css_entry_id(e) == id));
}
#[test]
fn remove_css_rejects_linked_entry() {
let dir = tempfile::tempdir().unwrap();
let settings_path = dir.path().join("settings.json");
let css_dir = dir.path().to_path_buf();
let mut settings = Settings::load(&settings_path);
settings
.link_css(&settings_path, "https://example.com/style.css".to_string())
.unwrap();
let linked_id = css_entry_id(&settings.css_entries[0]);
let result = settings.remove_css(&settings_path, &css_dir, &linked_id);
assert!(matches!(result, Err(CssError::NotImported)));
assert_eq!(settings.css_entries.len(), 1);
}
#[test]
fn remove_css_rejects_unknown_id() {
let dir = tempfile::tempdir().unwrap();
let settings_path = dir.path().join("settings.json");
let css_dir = dir.path().to_path_buf();
let mut settings = Settings::load(&settings_path);
let result = settings.remove_css(&settings_path, &css_dir, "does-not-exist");
assert!(matches!(result, Err(CssError::NotFound)));
}
#[test]
fn css_content_rejects_linked_entry() {
let dir = tempfile::tempdir().unwrap();
let settings_path = dir.path().join("settings.json");
let css_dir = dir.path().to_path_buf();
let mut settings = Settings::load(&settings_path);
settings
.link_css(&settings_path, "https://example.com/style.css".to_string())
.unwrap();
let linked_id = css_entry_id(&settings.css_entries[0]);
let result = settings.css_content(&css_dir, &linked_id);
assert!(matches!(result, Err(CssError::NotImported)));
}
#[test]
fn load_migrates_legacy_profiles_file_into_inline_settings() {
let dir = tempfile::tempdir().unwrap();