1#![doc(
2 html_favicon_url = "https://raw.githubusercontent.com/hyprutils/hyprparser/refs/heads/main/hyprparser.png"
3)]
4#![doc(
5 html_logo_url = "https://raw.githubusercontent.com/hyprutils/hyprparser/refs/heads/main/hyprparser.png"
6)]
7
8use std::collections::HashMap;
33use std::{env, fmt, fs};
34
35#[derive(Debug, Default)]
37pub struct HyprlandConfig {
38 pub content: Vec<String>,
39 pub sections: HashMap<String, (usize, usize)>,
40 pub sourced_content: Vec<Vec<String>>,
41 pub sourced_sections: HashMap<String, (usize, usize)>,
42 pub sourced_paths: Vec<String>,
43}
44
45impl HyprlandConfig {
46 pub fn new() -> Self {
47 Self::default()
48 }
49
50 pub fn parse(&mut self, config_str: &str, sourced: bool) {
52 let mut section_stack = Vec::new();
53 let mut sourced_content: Vec<String> = Vec::new();
54 let source_index = if sourced {
55 self.sourced_content.len()
56 } else {
57 0
58 };
59
60 let mut env_vars = HashMap::new();
61 let home = env::var("HOME").unwrap_or_default();
62 env_vars.insert("HOME".to_string(), home.clone());
63
64 println!("Parsing env vars from config:");
65 for line in config_str.lines() {
66 let trimmed = line.trim();
67 if let Some((var, val)) = trimmed
68 .split_once('=')
69 .map(|(v, p)| (v.trim(), p.split('#').next().unwrap_or(p).trim()))
70 {
71 if let Some(stripped) = var.strip_prefix('$') {
72 println!("Found env var: {} = {}", var, val);
73 let mut expanded_val = val.to_string();
74 for (existing_var, existing_val) in &env_vars {
75 expanded_val =
76 expanded_val.replace(&format!("${}", existing_var), existing_val);
77 }
78 env_vars.insert(stripped.to_string(), expanded_val);
79 continue;
80 }
81 }
82 }
83 println!("Collected env vars: {:?}", env_vars);
84
85 for (i, line) in config_str.lines().enumerate() {
86 let trimmed = line.trim();
87
88 if trimmed.starts_with("source") && !sourced {
89 if let Some(path) = trimmed
90 .split_once('=')
91 .map(|(_, p)| p.split('#').next().unwrap_or(p).trim())
92 {
93 println!("Processing source path: {}", path);
94 let mut expanded_path = path.to_string();
95
96 for (var, val) in &env_vars {
97 let var_pattern = format!("${}", var);
98 println!("Replacing {} with {}", var_pattern, val);
99 expanded_path = expanded_path.replace(&var_pattern, val);
100 }
101 println!("After env var expansion: {}", expanded_path);
102
103 if !expanded_path.starts_with('/') && !expanded_path.starts_with('~') {
104 expanded_path = format!("{}/.config/hypr/{}", home, expanded_path);
105 } else {
106 expanded_path = expanded_path.replacen("~", &home, 1);
107 }
108 println!("Final expanded path: {}", expanded_path);
109
110 match fs::read_to_string(&expanded_path) {
111 Ok(content) => {
112 println!("Successfully read sourced file");
113 self.parse(&content, true);
114 self.sourced_paths.push(expanded_path);
115 }
116 Err(e) => println!("Failed to read file: {}", e),
117 }
118 }
119 } else if trimmed.ends_with('{') {
120 let section_name = trimmed.trim_end_matches('{').trim().to_string();
121 section_stack.push((section_name, i));
122 } else if trimmed == "}" && !section_stack.is_empty() {
123 let (name, start) = section_stack.pop().unwrap();
124 let full_name = section_stack
125 .iter()
126 .map(|(n, _)| n.as_str())
127 .chain(std::iter::once(name.as_str()))
128 .collect::<Vec<_>>()
129 .join(".");
130 if sourced {
131 self.sourced_sections
132 .insert(format!("{}_{}", full_name, source_index), (start, i));
133 } else {
134 self.sections.insert(full_name, (start, i));
135 }
136 }
137 if sourced {
138 sourced_content.push(line.to_string());
139 } else {
140 self.content.push(line.to_string());
141 }
142 }
143 if sourced {
144 self.sourced_content.push(sourced_content);
145 }
146 }
147
148 pub fn add_entry(&mut self, category: &str, entry: &str) {
150 let parts: Vec<&str> = category.split('.').collect();
151 let parent_category = if parts.len() > 1 {
152 parts[..parts.len() - 1].join(".")
153 } else {
154 category.to_string()
155 };
156
157 if let Some((source_index, _)) = self.find_sourced_section(&parent_category) {
158 let section_key = format!("{}_{}", parent_category, source_index);
159 let (start, mut end) = *self.sourced_sections.get(§ion_key).unwrap();
160 let depth = parent_category.matches('.').count();
161 let key = entry.split('=').next().unwrap().trim();
162
163 let mut should_update_sections = false;
164 let mut content_updated = String::new();
165
166 if let Some(sourced_content) = self.sourced_content.get_mut(source_index) {
167 let subcategory_key = format!("{}_{}", category, source_index);
168
169 if parts.len() > 1 && !self.sourced_sections.contains_key(&subcategory_key) {
170 let last_part = parts.last().unwrap();
171 let section_start = format!("{}{} {{", " ".repeat(depth + 1), last_part);
172 let section_end = format!("{}}}", " ".repeat(depth + 1));
173
174 if end > 0
175 && end <= sourced_content.len()
176 && !sourced_content[end - 1].trim().is_empty()
177 {
178 sourced_content.insert(end, String::new());
179 end += 1;
180 }
181
182 sourced_content.insert(end, section_start);
183 sourced_content
184 .insert(end + 1, format!("{}{}", " ".repeat(depth + 2), entry));
185 sourced_content.insert(end + 2, section_end);
186
187 self.sourced_sections
188 .insert(subcategory_key, (end + 1, end + 1));
189 should_update_sections = true;
190 } else if let Some(&(sub_start, sub_end)) =
191 self.sourced_sections.get(&subcategory_key)
192 {
193 let parent_category = if parts.len() > 1 {
194 parts[..parts.len()].join(".")
195 } else {
196 category.to_string()
197 };
198 let depth = parent_category.matches('.').count();
199
200 let formatted_entry = format!("{}{}", " ".repeat(depth + 1), entry);
201 let existing_line = sourced_content[sub_start..=sub_end]
202 .iter()
203 .position(|line| line.trim().starts_with(key));
204
205 match existing_line {
206 Some(line_num) => {
207 sourced_content[sub_start + line_num] = formatted_entry;
208 }
209 None => {
210 sourced_content.insert(sub_end, formatted_entry);
211 should_update_sections = true;
212 }
213 }
214 } else {
215 let formatted_entry = format!("{}{}", " ".repeat(depth + 1), entry);
216 let existing_line = sourced_content[start..=end]
217 .iter()
218 .position(|line| line.trim().starts_with(key));
219
220 match existing_line {
221 Some(line_num) => {
222 sourced_content[start + line_num] = formatted_entry;
223 }
224 None => {
225 sourced_content.insert(end, formatted_entry);
226 should_update_sections = true;
227 }
228 }
229 }
230
231 content_updated = sourced_content.join("\n");
232 }
233
234 if should_update_sections {
235 self.update_sourced_sections(source_index, end, 1);
236 }
237
238 if let Some(sourced_path) = self.sourced_paths.get(source_index) {
239 if !sourced_path.is_empty() {
240 if let Err(e) = fs::write(sourced_path, content_updated) {
241 eprintln!("Failed to write to sourced file {}: {}", sourced_path, e);
242 }
243 }
244 }
245 return;
246 }
247
248 let parts: Vec<&str> = category.split('.').collect();
249 let mut current_section = String::new();
250 let mut insert_pos = self.content.len();
251
252 for (depth, (i, part)) in parts.iter().enumerate().enumerate() {
253 if i > 0 {
254 current_section.push('.');
255 }
256 current_section.push_str(part);
257
258 if !self.sections.contains_key(¤t_section) {
259 self.create_category(¤t_section, depth, &mut insert_pos);
260 }
261
262 let &(start, end) = self.sections.get(¤t_section).unwrap();
263 insert_pos = end;
264
265 if i == parts.len() - 1 {
266 let key = entry.split('=').next().unwrap().trim();
267 let existing_line = self.content[start..=end]
268 .iter()
269 .position(|line| line.trim().starts_with(key))
270 .map(|pos| start + pos);
271
272 let formatted_entry = format!("{}{}", " ".repeat(depth + 1), entry);
273
274 match existing_line {
275 Some(line_num) => {
276 self.content[line_num] = formatted_entry;
277 }
278 None => {
279 self.content.insert(end, formatted_entry);
280 self.update_sections(end, 1);
281 }
282 }
283 return;
284 }
285 }
286 }
287
288 pub fn add_entry_headless(&mut self, key: &str, value: &str) {
295 if key.is_empty() && value.is_empty() {
296 self.content.push(String::new());
297 } else {
298 let entry = format!("{} = {}", key, value);
299 if !self.content.iter().any(|line| line.trim() == entry.trim()) {
300 self.content.push(entry);
301 }
302 }
303 }
304
305 pub fn add_sourced(&mut self, config: Vec<String>) {
307 self.sourced_content.push(config);
308 self.sourced_paths.push(String::new());
309 }
310
311 fn update_sections(&mut self, pos: usize, offset: usize) {
312 for (start, end) in self.sections.values_mut() {
313 if *start >= pos {
314 *start += offset;
315 *end += offset;
316 } else if *end >= pos {
317 *end += offset;
318 }
319 }
320 }
321
322 fn update_sourced_sections(&mut self, source_index: usize, pos: usize, offset: usize) {
323 for ((_, (start, end)), sourced_path) in self
324 .sourced_sections
325 .iter_mut()
326 .filter(|(_, (start, _))| *start >= pos)
327 .zip(self.sourced_paths.iter().skip(source_index))
328 {
329 if !sourced_path.is_empty() {
330 if *start >= pos {
331 *start += offset;
332 *end += offset;
333 } else if *end >= pos {
334 *end += offset;
335 }
336 }
337 }
338 }
339
340 pub fn parse_color(&self, color_str: &str) -> Option<(f32, f32, f32, f32)> {
357 if color_str.starts_with("rgba(") {
358 let rgba = color_str.trim_start_matches("rgba(").trim_end_matches(')');
359 let rgba = u32::from_str_radix(rgba, 16).ok()?;
360 Some((
361 ((rgba >> 24) & 0xFF) as f32 / 255.0,
362 ((rgba >> 16) & 0xFF) as f32 / 255.0,
363 ((rgba >> 8) & 0xFF) as f32 / 255.0,
364 (rgba & 0xFF) as f32 / 255.0,
365 ))
366 } else if color_str.starts_with("rgb(") {
367 let rgb = color_str.trim_start_matches("rgb(").trim_end_matches(')');
368 let rgb = u32::from_str_radix(rgb, 16).ok()?;
369 Some((
370 ((rgb >> 16) & 0xFF) as f32 / 255.0,
371 ((rgb >> 8) & 0xFF) as f32 / 255.0,
372 (rgb & 0xFF) as f32 / 255.0,
373 1.0,
374 ))
375 } else if let Some(stripped) = color_str.strip_prefix("0x") {
376 let argb = u32::from_str_radix(stripped, 16).ok()?;
377 Some((
378 ((argb >> 16) & 0xFF) as f32 / 255.0,
379 ((argb >> 8) & 0xFF) as f32 / 255.0,
380 (argb & 0xFF) as f32 / 255.0,
381 ((argb >> 24) & 0xFF) as f32 / 255.0,
382 ))
383 } else {
384 None
385 }
386 }
387
388 pub fn format_color(&self, red: f32, green: f32, blue: f32, alpha: f32) -> String {
390 format!(
391 "rgba({:02x}{:02x}{:02x}{:02x})",
392 (red * 255.0) as u8,
393 (green * 255.0) as u8,
394 (blue * 255.0) as u8,
395 (alpha * 255.0) as u8
396 )
397 }
398
399 fn create_category(&mut self, category: &str, depth: usize, insert_pos: &mut usize) {
400 let part = category.split('.').last().unwrap();
401 let new_section = format!("{}{} {{", " ".repeat(depth), part);
402
403 let mut lines_added = 0;
404 if *insert_pos > 0 && !self.content[*insert_pos - 1].trim().is_empty() {
405 self.content.insert(*insert_pos, String::new());
406 *insert_pos += 1;
407 lines_added += 1;
408 }
409
410 self.content.insert(*insert_pos, new_section);
411 *insert_pos += 1;
412 self.content
413 .insert(*insert_pos, format!("{}}}", " ".repeat(depth)));
414 *insert_pos += 1;
415 self.content.insert(*insert_pos, String::new());
416 *insert_pos += 1;
417
418 self.update_sections(*insert_pos - 3 - lines_added, 3 + lines_added);
419 self.sections.insert(
420 category.to_string(),
421 (*insert_pos - 3 - lines_added, *insert_pos - 2),
422 );
423 }
424
425 fn find_sourced_section(&self, category: &str) -> Option<(usize, (usize, usize))> {
426 for (idx, _) in self.sourced_content.iter().enumerate() {
427 let section_key = format!("{}_{}", category, idx);
428 if let Some(§ion) = self.sourced_sections.get(§ion_key) {
429 if self.sourced_paths.get(idx).map_or(false, |p| !p.is_empty()) {
430 return Some((idx, section));
431 }
432 }
433 }
434 None
435 }
436}
437
438pub fn parse_config(config_str: &str) -> HyprlandConfig {
440 let mut config = HyprlandConfig::new();
441 config.parse(config_str, false);
442 config
443}
444
445impl fmt::Display for HyprlandConfig {
446 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
447 for (i, line) in self.content.iter().enumerate() {
448 if i == self.content.len() - 1 {
449 write!(f, "{}", line)?;
450 } else {
451 writeln!(f, "{}", line)?;
452 }
453 }
454 Ok(())
455 }
456}
457
458impl PartialEq for HyprlandConfig {
459 fn eq(&self, other: &Self) -> bool {
460 self.content == other.content
461 }
462}