Add built-in theme support with various pre-defined themes
Some checks failed
ci/someci/tag/woodpecker/5 Pipeline is pending
ci/someci/tag/woodpecker/6 Pipeline is pending
ci/someci/tag/woodpecker/7 Pipeline is pending
ci/someci/tag/woodpecker/1 Pipeline failed
ci/someci/tag/woodpecker/2 Pipeline failed
ci/someci/tag/woodpecker/3 Pipeline failed
ci/someci/tag/woodpecker/4 Pipeline failed

- Introduce multiple built-in themes (`default_dark`, `default_light`, `gruvbox`, `dracula`, `solarized`, `midnight-ocean`, `rose-pine`, `monokai`, `material-dark`, `material-light`).
- Implement theming system with customizable color schemes for all UI components in the TUI.
- Include documentation for themes in `themes/README.md`.
- Add fallback mechanisms for default themes in case of parsing errors.
- Support custom themes with overrides via configuration.
This commit is contained in:
2025-10-03 07:44:11 +02:00
parent 6a3f44f911
commit 96e2482782
24 changed files with 1740 additions and 247 deletions

View File

@@ -45,10 +45,7 @@ impl StorageManager {
// Ensure the directory exists
if !sessions_dir.exists() {
fs::create_dir_all(&sessions_dir).map_err(|e| {
Error::Storage(format!(
"Failed to create sessions directory: {}",
e
))
Error::Storage(format!("Failed to create sessions directory: {}", e))
})?;
}
@@ -66,7 +63,11 @@ impl StorageManager {
}
/// Save a conversation to disk
pub fn save_conversation(&self, conversation: &Conversation, name: Option<String>) -> Result<PathBuf> {
pub fn save_conversation(
&self,
conversation: &Conversation,
name: Option<String>,
) -> Result<PathBuf> {
self.save_conversation_with_description(conversation, name, None)
}
@@ -75,7 +76,7 @@ impl StorageManager {
&self,
conversation: &Conversation,
name: Option<String>,
description: Option<String>
description: Option<String>,
) -> Result<PathBuf> {
let filename = if let Some(ref session_name) = name {
// Use provided name, sanitized
@@ -101,26 +102,22 @@ impl StorageManager {
save_conv.description = description;
}
let json = serde_json::to_string_pretty(&save_conv).map_err(|e| {
Error::Storage(format!("Failed to serialize conversation: {}", e))
})?;
let json = serde_json::to_string_pretty(&save_conv)
.map_err(|e| Error::Storage(format!("Failed to serialize conversation: {}", e)))?;
fs::write(&path, json).map_err(|e| {
Error::Storage(format!("Failed to write session file: {}", e))
})?;
fs::write(&path, json)
.map_err(|e| Error::Storage(format!("Failed to write session file: {}", e)))?;
Ok(path)
}
/// Load a conversation from disk
pub fn load_conversation(&self, path: impl AsRef<Path>) -> Result<Conversation> {
let content = fs::read_to_string(path.as_ref()).map_err(|e| {
Error::Storage(format!("Failed to read session file: {}", e))
})?;
let content = fs::read_to_string(path.as_ref())
.map_err(|e| Error::Storage(format!("Failed to read session file: {}", e)))?;
let conversation: Conversation = serde_json::from_str(&content).map_err(|e| {
Error::Storage(format!("Failed to parse session file: {}", e))
})?;
let conversation: Conversation = serde_json::from_str(&content)
.map_err(|e| Error::Storage(format!("Failed to parse session file: {}", e)))?;
Ok(conversation)
}
@@ -129,14 +126,12 @@ impl StorageManager {
pub fn list_sessions(&self) -> Result<Vec<SessionMeta>> {
let mut sessions = Vec::new();
let entries = fs::read_dir(&self.sessions_dir).map_err(|e| {
Error::Storage(format!("Failed to read sessions directory: {}", e))
})?;
let entries = fs::read_dir(&self.sessions_dir)
.map_err(|e| Error::Storage(format!("Failed to read sessions directory: {}", e)))?;
for entry in entries {
let entry = entry.map_err(|e| {
Error::Storage(format!("Failed to read directory entry: {}", e))
})?;
let entry = entry
.map_err(|e| Error::Storage(format!("Failed to read directory entry: {}", e)))?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) != Some("json") {
@@ -172,9 +167,8 @@ impl StorageManager {
/// Delete a saved session
pub fn delete_session(&self, path: impl AsRef<Path>) -> Result<()> {
fs::remove_file(path.as_ref()).map_err(|e| {
Error::Storage(format!("Failed to delete session file: {}", e))
})
fs::remove_file(path.as_ref())
.map_err(|e| Error::Storage(format!("Failed to delete session file: {}", e)))
}
/// Get the sessions directory path
@@ -237,7 +231,9 @@ mod tests {
#[cfg(target_os = "macos")]
{
// macOS should use ~/Library/Application Support
assert!(path.to_string_lossy().contains("Library/Application Support"));
assert!(path
.to_string_lossy()
.contains("Library/Application Support"));
}
println!("Default sessions directory: {}", path.display());
@@ -257,10 +253,13 @@ mod tests {
let mut conv = Conversation::new("test-model".to_string());
conv.messages.push(Message::user("Hello".to_string()));
conv.messages.push(Message::assistant("Hi there!".to_string()));
conv.messages
.push(Message::assistant("Hi there!".to_string()));
// Save conversation
let path = storage.save_conversation(&conv, Some("test_session".to_string())).unwrap();
let path = storage
.save_conversation(&conv, Some("test_session".to_string()))
.unwrap();
assert!(path.exists());
// Load conversation
@@ -280,7 +279,9 @@ mod tests {
for i in 0..3 {
let mut conv = Conversation::new("test-model".to_string());
conv.messages.push(Message::user(format!("Message {}", i)));
storage.save_conversation(&conv, Some(format!("session_{}", i))).unwrap();
storage
.save_conversation(&conv, Some(format!("session_{}", i)))
.unwrap();
}
// List sessions