use ratatui::style::Color;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SingleColor {
Hex(String), Ansi(u8), Named(String), }
impl<'de> serde::Deserialize<'de> for SingleColor {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct SingleColorVisitor;
impl<'de> serde::de::Visitor<'de> for SingleColorVisitor {
type Value = SingleColor;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a hex color string starting with '#', a color name string, or an ANSI u8 integer")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
if value.starts_with('#') {
Ok(SingleColor::Hex(value.to_string()))
} else {
Ok(SingleColor::Named(value.to_string()))
}
}
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
if value <= 255 {
Ok(SingleColor::Ansi(value as u8))
} else {
Err(serde::de::Error::invalid_value(
serde::de::Unexpected::Unsigned(value),
&"an ANSI color code between 0 and 255",
))
}
}
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
if (0..=255).contains(&value) {
Ok(SingleColor::Ansi(value as u8))
} else {
Err(serde::de::Error::invalid_value(
serde::de::Unexpected::Signed(value),
&"an ANSI color code between 0 and 255",
))
}
}
}
deserializer.deserialize_any(SingleColorVisitor)
}
}
impl serde::Serialize for SingleColor {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
SingleColor::Hex(hex) => serializer.serialize_str(hex),
SingleColor::Ansi(code) => serializer.serialize_u8(*code),
SingleColor::Named(name) => serializer.serialize_str(name),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
#[serde(untagged)]
pub enum ColorValue {
Single(SingleColor),
DarkLight {
dark: SingleColor,
light: SingleColor,
},
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ThemeColors {
pub primary: ColorValue,
pub secondary: ColorValue,
pub accent: ColorValue,
pub error: ColorValue,
pub warning: ColorValue,
pub success: ColorValue,
pub info: ColorValue,
pub text: ColorValue,
pub text_muted: ColorValue,
pub background: ColorValue,
#[serde(default)]
pub background_panel: Option<ColorValue>,
#[serde(default)]
pub background_element: Option<ColorValue>,
#[serde(default)]
pub border: Option<ColorValue>,
#[serde(default)]
pub border_active: Option<ColorValue>,
#[serde(default)]
pub border_subtle: Option<ColorValue>,
#[serde(default)]
pub diff_added: Option<ColorValue>,
#[serde(default)]
pub diff_removed: Option<ColorValue>,
#[serde(default)]
pub diff_context: Option<ColorValue>,
#[serde(default)]
pub diff_hunk_header: Option<ColorValue>,
#[serde(default)]
pub diff_highlight_added: Option<ColorValue>,
#[serde(default)]
pub diff_highlight_removed: Option<ColorValue>,
#[serde(default)]
pub diff_added_bg: Option<ColorValue>,
#[serde(default)]
pub diff_removed_bg: Option<ColorValue>,
#[serde(default)]
pub diff_context_bg: Option<ColorValue>,
#[serde(default)]
pub diff_line_number: Option<ColorValue>,
#[serde(default)]
pub diff_added_line_number_bg: Option<ColorValue>,
#[serde(default)]
pub diff_removed_line_number_bg: Option<ColorValue>,
#[serde(default)]
pub markdown_text: Option<ColorValue>,
#[serde(default)]
pub markdown_heading: Option<ColorValue>,
#[serde(default)]
pub markdown_link: Option<ColorValue>,
#[serde(default)]
pub markdown_link_text: Option<ColorValue>,
#[serde(default)]
pub markdown_code: Option<ColorValue>,
#[serde(default)]
pub markdown_block_quote: Option<ColorValue>,
#[serde(default)]
pub markdown_emph: Option<ColorValue>,
#[serde(default)]
pub markdown_strong: Option<ColorValue>,
#[serde(default)]
pub markdown_horizontal_rule: Option<ColorValue>,
#[serde(default)]
pub markdown_list_item: Option<ColorValue>,
#[serde(default)]
pub markdown_list_enumeration: Option<ColorValue>,
#[serde(default)]
pub markdown_image: Option<ColorValue>,
#[serde(default)]
pub markdown_image_text: Option<ColorValue>,
#[serde(default)]
pub markdown_code_block: Option<ColorValue>,
#[serde(default)]
pub syntax_comment: Option<ColorValue>,
#[serde(default)]
pub syntax_keyword: Option<ColorValue>,
#[serde(default)]
pub syntax_function: Option<ColorValue>,
#[serde(default)]
pub syntax_variable: Option<ColorValue>,
#[serde(default)]
pub syntax_string: Option<ColorValue>,
#[serde(default)]
pub syntax_number: Option<ColorValue>,
#[serde(default)]
pub syntax_type: Option<ColorValue>,
#[serde(default)]
pub syntax_operator: Option<ColorValue>,
#[serde(default)]
pub syntax_punctuation: Option<ColorValue>,
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub struct ThemeConfig {
#[serde(rename = "$schema", default)]
pub schema: Option<String>,
#[serde(default)]
pub defs: HashMap<String, ColorValue>,
pub theme: ThemeColors,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ThemeMode {
Dark,
Light,
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub struct ActiveTheme {
pub is_light: bool,
pub primary: Color,
pub secondary: Color,
pub accent: Color,
pub error: Color,
pub warning: Color,
pub success: Color,
pub info: Color,
pub text: Color,
pub text_muted: Color,
pub background: Option<Color>,
pub background_panel: Option<Color>,
pub background_element: Option<Color>,
pub border: Color,
pub border_active: Color,
pub border_subtle: Color,
pub diff_added: Color,
pub diff_removed: Color,
pub diff_context: Color,
pub diff_hunk_header: Color,
pub diff_highlight_added: Color,
pub diff_highlight_removed: Color,
pub diff_added_bg: Option<Color>,
pub diff_removed_bg: Option<Color>,
pub diff_context_bg: Option<Color>,
pub diff_line_number: Color,
pub diff_added_line_number_bg: Option<Color>,
pub diff_removed_line_number_bg: Option<Color>,
pub markdown_text: Color,
pub markdown_heading: Color,
pub markdown_link: Color,
pub markdown_link_text: Color,
pub markdown_code: Color,
pub markdown_block_quote: Color,
pub markdown_emph: Color,
pub markdown_strong: Color,
pub markdown_horizontal_rule: Color,
pub markdown_list_item: Color,
pub markdown_list_enumeration: Color,
pub markdown_image: Color,
pub markdown_image_text: Color,
pub markdown_code_block: Option<Color>,
pub syntax_comment: Color,
pub syntax_keyword: Color,
pub syntax_function: Color,
pub syntax_variable: Color,
pub syntax_string: Color,
pub syntax_number: Color,
pub syntax_type: Color,
pub syntax_operator: Color,
pub syntax_punctuation: Color,
}
impl Default for ActiveTheme {
fn default() -> Self {
Self {
is_light: false,
primary: Color::Rgb(255, 255, 255),
secondary: Color::Rgb(110, 110, 110),
accent: Color::Rgb(134, 194, 172),
error: Color::Rgb(220, 60, 60),
warning: Color::Rgb(220, 180, 60),
success: Color::Rgb(60, 200, 60),
info: Color::Rgb(60, 180, 220),
text: Color::Rgb(255, 255, 255),
text_muted: Color::Rgb(160, 160, 160),
background: Some(Color::Rgb(15, 15, 15)),
background_panel: Some(Color::Rgb(24, 24, 24)),
background_element: Some(Color::Rgb(24, 24, 24)),
border: Color::Rgb(60, 60, 60),
border_active: Color::Rgb(134, 194, 172),
border_subtle: Color::Rgb(40, 40, 40),
diff_added: Color::Rgb(60, 200, 60),
diff_removed: Color::Rgb(220, 60, 60),
diff_context: Color::Rgb(160, 160, 160),
diff_hunk_header: Color::Rgb(60, 180, 220),
diff_highlight_added: Color::Rgb(60, 200, 60),
diff_highlight_removed: Color::Rgb(220, 60, 60),
diff_added_bg: None,
diff_removed_bg: None,
diff_context_bg: None,
diff_line_number: Color::Rgb(110, 110, 110),
diff_added_line_number_bg: None,
diff_removed_line_number_bg: None,
markdown_text: Color::Rgb(255, 255, 255),
markdown_heading: Color::Rgb(255, 255, 255),
markdown_link: Color::Rgb(134, 194, 172),
markdown_link_text: Color::Rgb(134, 194, 172),
markdown_code: Color::Rgb(134, 194, 172),
markdown_block_quote: Color::Rgb(160, 160, 160),
markdown_emph: Color::Rgb(134, 194, 172),
markdown_strong: Color::Rgb(255, 255, 255),
markdown_horizontal_rule: Color::Rgb(60, 60, 60),
markdown_list_item: Color::Rgb(134, 194, 172),
markdown_list_enumeration: Color::Rgb(134, 194, 172),
markdown_image: Color::Rgb(134, 194, 172),
markdown_image_text: Color::Rgb(134, 194, 172),
markdown_code_block: Some(Color::Rgb(24, 24, 24)),
syntax_comment: Color::DarkGray,
syntax_keyword: Color::Magenta,
syntax_function: Color::Blue,
syntax_variable: Color::Cyan,
syntax_string: Color::Green,
syntax_number: Color::Yellow,
syntax_type: Color::Blue,
syntax_operator: Color::Cyan,
syntax_punctuation: Color::Cyan,
}
}
}
impl ActiveTheme {
pub fn light_default() -> Self {
Self {
is_light: true,
primary: Color::Rgb(190, 60, 100),
secondary: Color::Rgb(140, 140, 140),
accent: Color::Rgb(190, 60, 100),
error: Color::Rgb(200, 40, 40),
warning: Color::Rgb(180, 140, 40),
success: Color::Rgb(40, 160, 40),
info: Color::Rgb(40, 120, 180),
text: Color::Rgb(30, 30, 30),
text_muted: Color::Rgb(140, 140, 140),
background: Some(Color::Rgb(248, 248, 248)),
background_panel: Some(Color::Rgb(240, 240, 240)),
background_element: Some(Color::Rgb(240, 240, 240)),
border: Color::Rgb(200, 200, 205),
border_active: Color::Rgb(190, 60, 100),
border_subtle: Color::Rgb(220, 220, 225),
diff_added: Color::Rgb(40, 160, 40),
diff_removed: Color::Rgb(200, 40, 40),
diff_context: Color::Rgb(140, 140, 140),
diff_hunk_header: Color::Rgb(40, 120, 180),
diff_highlight_added: Color::Rgb(40, 160, 40),
diff_highlight_removed: Color::Rgb(200, 40, 40),
diff_added_bg: None,
diff_removed_bg: None,
diff_context_bg: None,
diff_line_number: Color::Rgb(150, 150, 150),
diff_added_line_number_bg: None,
diff_removed_line_number_bg: None,
markdown_text: Color::Rgb(30, 30, 30),
markdown_heading: Color::Rgb(30, 30, 30),
markdown_link: Color::Rgb(190, 60, 100),
markdown_link_text: Color::Rgb(190, 60, 100),
markdown_code: Color::Rgb(190, 60, 100),
markdown_block_quote: Color::Rgb(140, 140, 140),
markdown_emph: Color::Rgb(190, 60, 100),
markdown_strong: Color::Rgb(30, 30, 30),
markdown_horizontal_rule: Color::Rgb(200, 200, 205),
markdown_list_item: Color::Rgb(190, 60, 100),
markdown_list_enumeration: Color::Rgb(190, 60, 100),
markdown_image: Color::Rgb(190, 60, 100),
markdown_image_text: Color::Rgb(190, 60, 100),
markdown_code_block: Some(Color::Rgb(240, 240, 240)),
syntax_comment: Color::DarkGray,
syntax_keyword: Color::Magenta,
syntax_function: Color::Blue,
syntax_variable: Color::Cyan,
syntax_string: Color::Green,
syntax_number: Color::Yellow,
syntax_type: Color::Blue,
syntax_operator: Color::Cyan,
syntax_punctuation: Color::Cyan,
}
}
}
pub fn parse_hex(hex: &str) -> Option<Color> {
let hex = hex.strip_prefix('#')?;
if hex.len() != 6 {
return None;
}
let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
Some(Color::Rgb(r, g, b))
}
fn resolve_single_color(
sc: &SingleColor,
mode: &ThemeMode,
defs: &HashMap<String, ColorValue>,
depth: usize,
) -> Option<Color> {
if depth > 5 {
return None; }
match sc {
SingleColor::Ansi(code) => Some(Color::Indexed(*code)),
SingleColor::Hex(hex_str) => parse_hex(hex_str),
SingleColor::Named(name) => {
if name == "none" {
None
} else if let Some(cv) = defs.get(name) {
resolve_color_value(cv, mode, defs, depth + 1)
} else {
None
}
}
}
}
fn resolve_color_value(
cv: &ColorValue,
mode: &ThemeMode,
defs: &HashMap<String, ColorValue>,
depth: usize,
) -> Option<Color> {
match cv {
ColorValue::Single(sc) => resolve_single_color(sc, mode, defs, depth),
ColorValue::DarkLight { dark, light } => match mode {
ThemeMode::Dark => resolve_single_color(dark, mode, defs, depth),
ThemeMode::Light => resolve_single_color(light, mode, defs, depth),
},
}
}
impl ThemeConfig {
pub fn resolve(&self, mode: &ThemeMode) -> ActiveTheme {
let defs = &self.defs;
let resolve = |cv: &ColorValue| resolve_color_value(cv, mode, defs, 0);
let resolve_opt = |opt: &Option<ColorValue>| opt.as_ref().and_then(resolve);
let base_default = match mode {
ThemeMode::Dark => ActiveTheme::default(),
ThemeMode::Light => ActiveTheme::light_default(),
};
let primary = resolve(&self.theme.primary).unwrap_or(base_default.primary);
let secondary = resolve(&self.theme.secondary).unwrap_or(base_default.secondary);
let accent = resolve(&self.theme.accent).unwrap_or(base_default.accent);
let error = resolve(&self.theme.error).unwrap_or(base_default.error);
let warning = resolve(&self.theme.warning).unwrap_or(base_default.warning);
let success = resolve(&self.theme.success).unwrap_or(base_default.success);
let info = resolve(&self.theme.info).unwrap_or(base_default.info);
let text = resolve(&self.theme.text).unwrap_or(base_default.text);
let text_muted = resolve(&self.theme.text_muted).unwrap_or(base_default.text_muted);
let background = resolve_color_value(&self.theme.background, mode, defs, 0);
let is_light = match mode {
ThemeMode::Dark => false,
ThemeMode::Light => true,
};
ActiveTheme {
is_light,
primary,
secondary,
accent,
error,
warning,
success,
info,
text,
text_muted,
background,
background_panel: resolve_opt(&self.theme.background_panel).or(background),
background_element: resolve_opt(&self.theme.background_element).or(background),
border: resolve_opt(&self.theme.border).unwrap_or(text_muted),
border_active: resolve_opt(&self.theme.border_active).unwrap_or(accent),
border_subtle: resolve_opt(&self.theme.border_subtle)
.unwrap_or(base_default.border_subtle),
diff_added: resolve_opt(&self.theme.diff_added).unwrap_or(success),
diff_removed: resolve_opt(&self.theme.diff_removed).unwrap_or(error),
diff_context: resolve_opt(&self.theme.diff_context).unwrap_or(text_muted),
diff_hunk_header: resolve_opt(&self.theme.diff_hunk_header).unwrap_or(info),
diff_highlight_added: resolve_opt(&self.theme.diff_highlight_added).unwrap_or(success),
diff_highlight_removed: resolve_opt(&self.theme.diff_highlight_removed)
.unwrap_or(error),
diff_added_bg: resolve_opt(&self.theme.diff_added_bg),
diff_removed_bg: resolve_opt(&self.theme.diff_removed_bg),
diff_context_bg: resolve_opt(&self.theme.diff_context_bg),
diff_line_number: resolve_opt(&self.theme.diff_line_number).unwrap_or(text_muted),
diff_added_line_number_bg: resolve_opt(&self.theme.diff_added_line_number_bg),
diff_removed_line_number_bg: resolve_opt(&self.theme.diff_removed_line_number_bg),
markdown_text: resolve_opt(&self.theme.markdown_text).unwrap_or(text),
markdown_heading: resolve_opt(&self.theme.markdown_heading).unwrap_or(primary),
markdown_link: resolve_opt(&self.theme.markdown_link).unwrap_or(accent),
markdown_link_text: resolve_opt(&self.theme.markdown_link_text).unwrap_or(accent),
markdown_code: resolve_opt(&self.theme.markdown_code).unwrap_or(accent),
markdown_block_quote: resolve_opt(&self.theme.markdown_block_quote)
.unwrap_or(text_muted),
markdown_emph: resolve_opt(&self.theme.markdown_emph).unwrap_or(accent),
markdown_strong: resolve_opt(&self.theme.markdown_strong).unwrap_or(primary),
markdown_horizontal_rule: resolve_opt(&self.theme.markdown_horizontal_rule)
.unwrap_or(base_default.markdown_horizontal_rule),
markdown_list_item: resolve_opt(&self.theme.markdown_list_item).unwrap_or(accent),
markdown_list_enumeration: resolve_opt(&self.theme.markdown_list_enumeration)
.unwrap_or(accent),
markdown_image: resolve_opt(&self.theme.markdown_image).unwrap_or(accent),
markdown_image_text: resolve_opt(&self.theme.markdown_image_text).unwrap_or(accent),
markdown_code_block: resolve_opt(&self.theme.markdown_code_block).or(background),
syntax_comment: resolve_opt(&self.theme.syntax_comment)
.unwrap_or(base_default.syntax_comment),
syntax_keyword: resolve_opt(&self.theme.syntax_keyword)
.unwrap_or(base_default.syntax_keyword),
syntax_function: resolve_opt(&self.theme.syntax_function)
.unwrap_or(base_default.syntax_function),
syntax_variable: resolve_opt(&self.theme.syntax_variable)
.unwrap_or(base_default.syntax_variable),
syntax_string: resolve_opt(&self.theme.syntax_string)
.unwrap_or(base_default.syntax_string),
syntax_number: resolve_opt(&self.theme.syntax_number)
.unwrap_or(base_default.syntax_number),
syntax_type: resolve_opt(&self.theme.syntax_type).unwrap_or(base_default.syntax_type),
syntax_operator: resolve_opt(&self.theme.syntax_operator)
.unwrap_or(base_default.syntax_operator),
syntax_punctuation: resolve_opt(&self.theme.syntax_punctuation)
.unwrap_or(base_default.syntax_punctuation),
}
}
}
pub fn init_project_dir() {
if let Some(proj_root) = find_project_root() {
let dc_dir = proj_root.join(".darwincode");
let is_fresh_init = !dc_dir.exists();
let themes_dir = dc_dir.join("themes");
let commands_dir = dc_dir.join("commands");
let agents_dir = dc_dir.join("agents");
let _ = std::fs::create_dir_all(&themes_dir);
let _ = std::fs::create_dir_all(&commands_dir);
let _ = std::fs::create_dir_all(&agents_dir);
let readme_path = dc_dir.join("README.md");
if !readme_path.exists() {
let readme_content = r##"# Darwincode Project Configuration
This directory contains configuration files for `darwincode` that are specific to this project repository.
## Directory Structure
* `config.json` (created from `config.json.template`) — Project-level overrides for global configuration.
* `ignore` — Specific files and directories to ignore during workspace scanning (combines with `.gitignore`).
* `instructions.md` — Custom project-specific guidelines, coding standards, and style rules appended automatically to the AI prompt.
* `.env` — Key-value secrets (API keys, tokens) loaded for custom tools/commands.
* `themes/` — Custom theme JSON configuration files.
* `commands/` — Custom slash-commands configured using TOML (copy `commands/schema.toml.template` to define new commands).
* `agents/` — Custom specialized agent definitions configured using TOML (copy `agents/schema.toml.template` to define new agents).
To define new custom commands or agents, copy the respective `schema.toml.template` to a new `.toml` file in the same directory (e.g. `commands/build.toml` or `agents/reviewer.toml`) and edit its properties.
"##;
let _ = std::fs::write(&readme_path, readme_content);
}
let config_template_path = dc_dir.join("config.json.template");
if !config_template_path.exists() {
let config_template = r##"{
"model": "gemini-2.0-flash",
"base_url": "https://generativelanguage.googleapis.com/v1beta",
"enable_codebase_tools": true,
"enable_bash_tools": true,
"show_thoughts": true,
"permission_level": "Guardian",
"theme": "Auto",
"respect_ignore_rules": true
}
"##;
let _ = std::fs::write(&config_template_path, config_template);
}
let ignore_path = dc_dir.join("ignore");
if !ignore_path.exists() {
let ignore_content = r##"# Darwincode workspace ignores
# Add files or directories to exclude from the workspace scanner.
target/
node_modules/
.git/
.darwincode/
dist/
build/
*.log
"##;
let _ = std::fs::write(&ignore_path, ignore_content);
}
let inst_path = dc_dir.join("instructions.md");
if !inst_path.exists() {
let _ = std::fs::write(&inst_path, "");
}
let env_path = dc_dir.join(".env");
if !env_path.exists() {
let _ = std::fs::write(&env_path, "");
}
if is_fresh_init {
let gitignore_path = proj_root.join(".gitignore");
if gitignore_path.exists()
&& let Ok(content) = std::fs::read_to_string(&gitignore_path)
{
let mut new_content = content.clone();
let mut modified = false;
let rules_to_add = vec![".darwincode/config.json", ".darwincode/.env"];
for rule in rules_to_add {
if !content.lines().any(|l| l.trim() == rule) {
if !new_content.ends_with('\n') && !new_content.is_empty() {
new_content.push('\n');
}
new_content.push_str(rule);
new_content.push('\n');
modified = true;
}
}
if modified {
let _ = std::fs::write(&gitignore_path, new_content);
eprintln!(
"[darwincode] Added .darwincode/config.json and .darwincode/.env to .gitignore"
);
}
}
}
let cmd_template_path = commands_dir.join("schema.toml.template");
if !cmd_template_path.exists() {
let cmd_template = r##"# Custom command configuration schema.
# Copy and rename this file (removing .template) to activate it (e.g., commit.toml).
description = "Generate a commit message based on git changes"
model = "gemini-2.0-flash" # (Optional) Override model to use for this command
# Local shell commands executed before calling the AI to gather context
[context]
git_status = "git status --short"
git_diff = "git diff"
# Prompt template. Variables are substituted using the {{variable_name}} syntax
[prompt]
template = """
Write a clear, Conventional Commits-compliant commit message based on the changes.
Git Status:
{{git_status}}
Changes:
{{git_diff}}
"""
"##;
let _ = std::fs::write(&cmd_template_path, cmd_template);
}
let agent_template_path = agents_dir.join("schema.toml.template");
if !agent_template_path.exists() {
let agent_template = r##"# Custom agent configuration schema.
# Copy and rename this file (removing .template) to activate it (e.g., reviewer.toml).
#
# allowed_tools: list of tool names the agent is allowed to invoke.
# Tool names are case-insensitive and the runtime accepts a handful of
# common aliases (read_file → read, grep_search → grep, list_dir → glob,
# bash → sh, web_search → websearch, ...). The canonical names that the
# model actually sees are:
#
# read grep glob edit write sh websearch
# ask todo patch ps kill logs
#
# Unlisted tools return an error from the model.
name = "Code Reviewer"
model = "claude-3-5-sonnet" # (Optional) Default model for this agent
# List of allowed tools to limit access and enforce security boundaries
allowed_tools = ["read", "grep", "glob"]
system_prompt = """
You are a strict code reviewer. Your only task is to analyze project files and find bugs/defects.
You are forbidden from writing files or executing arbitrary bash commands.
"""
"##;
let _ = std::fs::write(&agent_template_path, agent_template);
}
let template_path = themes_dir.join("custom_template.json");
if !template_path.exists() {
let template_content = r##"{
"$schema": "https://raw.githubusercontent.com/femboypig/darwincode/main/theme.json",
"defs": {
"bg": "#1a1b26",
"panel_bg": "#16161e",
"fg": "#c0caf5",
"fg_muted": "#565f89",
"red": "#f7768e",
"orange": "#ff9e64",
"yellow": "#e0af68",
"green": "#9ece6a",
"teal": "#73daca",
"blue": "#7aa2f7",
"magenta": "#bb9af7",
"border": "#3b4261"
},
"theme": {
"primary": "blue",
"secondary": "fg_muted",
"accent": "teal",
"error": "red",
"warning": "orange",
"success": "green",
"info": "teal",
"text": "fg",
"textMuted": "fg_muted",
"background": "bg",
"backgroundPanel": "panel_bg",
"backgroundElement": "panel_bg",
"border": "border",
"borderActive": "blue",
"borderSubtle": "border"
}
}
"##;
let _ = std::fs::write(&template_path, template_content);
}
}
}
pub fn find_project_root() -> Option<PathBuf> {
crate::config::find_project_root()
}
pub fn custom_themes() -> &'static HashMap<String, ThemeConfig> {
static CUSTOM_THEMES: std::sync::OnceLock<HashMap<String, ThemeConfig>> =
std::sync::OnceLock::new();
CUSTOM_THEMES.get_or_init(|| {
let mut themes = HashMap::new();
let builtins = vec![
("tokyonight", include_str!("themes/tokyonight.json")),
("nord", include_str!("themes/nord.json")),
("gruvbox", include_str!("themes/gruvbox.json")),
("ayu", include_str!("themes/ayu.json")),
("everforest", include_str!("themes/everforest.json")),
("github", include_str!("themes/github.json")),
("dracula", include_str!("themes/dracula.json")),
("solarized", include_str!("themes/solarized.json")),
("catppuccin", include_str!("themes/catppuccin.json")),
("onedark", include_str!("themes/onedark.json")),
("botanical", include_str!("themes/botanical.json")),
("latte", include_str!("themes/latte.json")),
("midnight", include_str!("themes/midnight.json")),
("monochrome", include_str!("themes/monochrome.json")),
("ocean", include_str!("themes/ocean.json")),
("paper", include_str!("themes/paper.json")),
("phosphor", include_str!("themes/phosphor.json")),
("synthwave", include_str!("themes/synthwave.json")),
];
for (name, content) in builtins {
if let Ok(tc) = serde_json::from_str::<ThemeConfig>(content) {
themes.insert(name.to_string(), tc);
}
}
if let Some(mut user_dir) = dirs::config_dir() {
user_dir.push("darwincode");
user_dir.push("themes");
load_themes_from_dir(&user_dir, &mut themes);
}
if let Some(proj_root) = find_project_root() {
let proj_dir = proj_root.join(".darwincode").join("themes");
load_themes_from_dir(&proj_dir, &mut themes);
}
let cwd_dir = PathBuf::from(".").join(".darwincode").join("themes");
load_themes_from_dir(&cwd_dir, &mut themes);
themes
})
}
fn load_themes_from_dir(dir: &std::path::Path, map: &mut HashMap<String, ThemeConfig>) {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file()
&& path.extension().is_some_and(|ext| ext == "json")
&& let Some(stem) = path.file_stem().and_then(|s| s.to_str())
&& let Ok(content) = std::fs::read_to_string(&path)
&& let Ok(tc) = serde_json::from_str::<ThemeConfig>(&content)
{
map.insert(stem.to_string(), tc);
}
}
}
}
mod dirs {
use std::path::PathBuf;
pub fn config_dir() -> Option<PathBuf> {
std::env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.or_else(|| std::env::var_os("APPDATA").map(PathBuf::from))
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")))
.or_else(|| std::env::var_os("USERPROFILE").map(|h| PathBuf::from(h).join(".config")))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builtin_themes_parse() {
let builtins = vec![
("tokyonight", include_str!("themes/tokyonight.json")),
("nord", include_str!("themes/nord.json")),
("gruvbox", include_str!("themes/gruvbox.json")),
("ayu", include_str!("themes/ayu.json")),
("everforest", include_str!("themes/everforest.json")),
("github", include_str!("themes/github.json")),
("dracula", include_str!("themes/dracula.json")),
("solarized", include_str!("themes/solarized.json")),
("catppuccin", include_str!("themes/catppuccin.json")),
("onedark", include_str!("themes/onedark.json")),
("botanical", include_str!("themes/botanical.json")),
("latte", include_str!("themes/latte.json")),
("midnight", include_str!("themes/midnight.json")),
("monochrome", include_str!("themes/monochrome.json")),
("ocean", include_str!("themes/ocean.json")),
("paper", include_str!("themes/paper.json")),
("phosphor", include_str!("themes/phosphor.json")),
("synthwave", include_str!("themes/synthwave.json")),
];
for (name, content) in builtins {
let res = serde_json::from_str::<ThemeConfig>(content);
assert!(
res.is_ok(),
"Failed to parse built-in theme '{}': {:?}",
name,
res.err()
);
}
}
#[test]
fn test_builtin_themes_resolve() {
let builtins = vec![
("tokyonight", include_str!("themes/tokyonight.json")),
("nord", include_str!("themes/nord.json")),
("gruvbox", include_str!("themes/gruvbox.json")),
("ayu", include_str!("themes/ayu.json")),
("everforest", include_str!("themes/everforest.json")),
("github", include_str!("themes/github.json")),
("dracula", include_str!("themes/dracula.json")),
("solarized", include_str!("themes/solarized.json")),
("catppuccin", include_str!("themes/catppuccin.json")),
("onedark", include_str!("themes/onedark.json")),
("botanical", include_str!("themes/botanical.json")),
("latte", include_str!("themes/latte.json")),
("midnight", include_str!("themes/midnight.json")),
("monochrome", include_str!("themes/monochrome.json")),
("ocean", include_str!("themes/ocean.json")),
("paper", include_str!("themes/paper.json")),
("phosphor", include_str!("themes/phosphor.json")),
("synthwave", include_str!("themes/synthwave.json")),
];
for (name, content) in builtins {
let tc = serde_json::from_str::<ThemeConfig>(content)
.unwrap_or_else(|e| panic!("Failed to parse builtin '{}': {:?}", name, e));
let dark_resolved = tc.resolve(&ThemeMode::Dark);
if name == "tokyonight" {
assert_eq!(dark_resolved.primary, Color::Rgb(122, 162, 247));
assert!(!dark_resolved.is_light);
}
let light_resolved = tc.resolve(&ThemeMode::Light);
if name == "tokyonight" {
assert_eq!(light_resolved.primary, Color::Rgb(52, 84, 140));
assert!(light_resolved.is_light);
}
}
}
}