1use std::collections::HashMap;
2use std::fs;
3use std::path::PathBuf;
4use std::sync::OnceLock;
5use cross_xdg::BaseDirs;
6use ratatui::style::Color;
7
8#[derive(Debug, Clone)]
9pub struct Colors {
10 pub editor_title_focused: Color,
11 pub editor_title_unfocused: Color,
12 pub gutter_text: Color,
13
14 pub output_title_focused: Color,
15 pub output_title_unfocused: Color,
16
17 pub tape_border_focused: Color,
18 pub tape_border_unfocused: Color,
19 pub tape_cell_empty: Color,
20 pub tape_cell_nonzero: Color,
21 pub tape_cell_pointer: Color,
22
23 pub status_text: Color,
24 pub dialog_title: Color,
25 pub dialog_bg: Color,
26 pub dialog_error: Color,
27 pub dialog_text: Color,
28 pub help_hint: Color,
29
30 pub editor_op_right: Color, pub editor_op_left: Color, pub editor_op_inc: Color, pub editor_op_dec: Color, pub editor_op_output: Color, pub editor_op_input: Color, pub editor_op_bracket: Color, pub editor_non_bf: Color,
38}
39
40impl Default for Colors {
41 fn default() -> Self {
42 Self {
44 editor_title_focused: Color::Cyan,
45 editor_title_unfocused: Color::Gray,
46 gutter_text: Color::DarkGray,
47
48 output_title_focused: Color::Cyan,
49 output_title_unfocused: Color::Gray,
50
51 tape_border_focused: Color::Cyan,
52 tape_border_unfocused: Color::Gray,
53 tape_cell_empty: Color::DarkGray,
54 tape_cell_nonzero: Color::White,
55 tape_cell_pointer: Color::Yellow,
56
57 status_text: Color::White,
58 dialog_title: Color::White,
59 dialog_bg: Color::Black,
60 dialog_error: Color::Red,
61 dialog_text: Color::White,
62 help_hint: Color::Gray,
63
64 editor_op_right: Color::Cyan,
65 editor_op_left: Color::Green,
66 editor_op_inc: Color::LightGreen,
67 editor_op_dec: Color::Red,
68 editor_op_output: Color::Yellow,
69 editor_op_input: Color::Magenta,
70 editor_op_bracket: Color::LightMagenta,
71 editor_non_bf: Color::Gray,
72 }
73 }
74}
75
76static COLORS: OnceLock<Colors> = OnceLock::new();
77
78pub fn colors() -> &'static Colors {
79 COLORS.get_or_init(|| load_from_toml().unwrap_or_default())
80}
81
82fn parse_color(value: &str) -> Option<Color> {
83 let s = value.trim();
84 if let Some(hex) = s.strip_prefix('#') {
85 if hex.len() == 6 {
86 if let (Ok(r), Ok(g), Ok(b)) = (
87 u8::from_str_radix(&hex[0..2], 16),
88 u8::from_str_radix(&hex[2..4], 16),
89 u8::from_str_radix(&hex[4..6], 16),
90 ) {
91 return Some(Color::Rgb(r, g, b));
92 }
93 }
94 } else {
95 let name = s.to_ascii_lowercase();
97 return Some(match name.as_str() {
98 "black" => Color::Black,
99 "red" => Color::Red,
100 "green" => Color::Green,
101 "yellow" => Color::Yellow,
102 "blue" => Color::Blue,
103 "magenta" => Color::Magenta,
104 "cyan" => Color::Cyan,
105 "gray" | "grey" => Color::Gray,
106 "darkgray" | "dark_grey" | "darkgrey" | "dark_gray" => Color::DarkGray,
107 "lightred" | "light_red" => Color::LightRed,
108 "lightgreen" | "light_green" => Color::LightGreen,
109 "lightblue" | "light_blue" => Color::LightBlue,
110 "lightmagenta" | "light_magenta" => Color::LightMagenta,
111 "lightcyan" | "light_cyan" => Color::LightCyan,
112 "white" => Color::White,
113 _ => return None,
114 });
115 }
116 None
117}
118
119fn load_from_toml() -> Option<Colors> {
120 let base_dirs = BaseDirs::new().unwrap();
122
123 let config_home = base_dirs.config_home();
127
128 let mut path = PathBuf::from(config_home);
129 path.push("bf.toml");
130
131 let content = fs::read_to_string(path).ok()?;
132 let mut in_colors = false;
135 let mut map: HashMap<String, String> = HashMap::new();
136 for line in content.lines() {
137 let line = line.trim();
138 if line.is_empty() || line.starts_with('#') { continue; }
139 if line.starts_with('[') && line.ends_with(']') {
140 in_colors = &line[1..line.len()-1] == "colors";
141 continue;
142 }
143 if !in_colors { continue; }
144 if let Some(eq) = line.find('=') {
145 let key = line[..eq].trim().to_string();
146 let val_raw = line[eq+1..].trim();
147 let val = if val_raw.starts_with('"') && val_raw.ends_with('"') && val_raw.len() >= 2 {
149 val_raw[1..val_raw.len()-1].to_string()
150 } else { val_raw.to_string() };
151 map.insert(key, val);
152 }
153 }
154
155 let mut cfg = Colors::default();
156
157 macro_rules! set {
158 ($field:ident, $key:literal) => {
159 if let Some(v) = map.get($key).and_then(|s| parse_color(s)) { cfg.$field = v; }
160 };
161 }
162
163 set!(editor_title_focused, "editor_title_focused");
164 set!(editor_title_unfocused, "editor_title_unfocused");
165 set!(gutter_text, "gutter_text");
166
167 set!(output_title_focused, "output_title_focused");
168 set!(output_title_unfocused, "output_title_unfocused");
169
170 set!(tape_border_focused, "tape_border_focused");
171 set!(tape_border_unfocused, "tape_border_unfocused");
172 set!(tape_cell_empty, "tape_cell_empty");
173 set!(tape_cell_nonzero, "tape_cell_nonzero");
174 set!(tape_cell_pointer, "tape_cell_pointer");
175
176 set!(status_text, "status_text");
177 set!(dialog_title, "dialog_title");
178 set!(dialog_bg, "dialog_bg");
179 set!(dialog_error, "dialog_error");
180 set!(dialog_text, "dialog_text");
181 set!(help_hint, "help_hint");
182
183 set!(editor_op_right, "editor_op_right");
184 set!(editor_op_left, "editor_op_left");
185 set!(editor_op_inc, "editor_op_inc");
186 set!(editor_op_dec, "editor_op_dec");
187 set!(editor_op_output, "editor_op_output");
188 set!(editor_op_input, "editor_op_input");
189 set!(editor_op_bracket, "editor_op_bracket");
190 set!(editor_non_bf, "editor_non_bf");
191
192 Some(cfg)
193}