1use regex::Regex;
6use std::collections::HashMap;
7
8#[derive(Debug, Clone)]
10pub struct StylePattern {
11 pub pattern: String,
12 pub weight: u64,
13 pub values: Vec<String>,
14 pub eval: bool,
15}
16
17impl StylePattern {
18 pub fn new(pattern: &str, values: Vec<String>, eval: bool) -> Self {
19 let weight = Self::calculate_weight(pattern);
20 Self {
21 pattern: pattern.to_string(),
22 weight,
23 values,
24 eval,
25 }
26 }
27
28 fn calculate_weight(pattern: &str) -> u64 {
29 let mut weight: u64 = 0;
30 let mut tmp = 2u64;
31 let mut first = true;
32
33 for ch in pattern.chars() {
34 if first && ch == '*' {
35 tmp = 0;
36 continue;
37 }
38 first = false;
39
40 if ch == '('
41 || ch == '|'
42 || ch == '*'
43 || ch == '['
44 || ch == '<'
45 || ch == '?'
46 || ch == '#'
47 || ch == '^'
48 {
49 tmp = 1;
50 }
51
52 if ch == ':' {
53 weight += 1 << 32;
54 first = true;
55 weight += tmp;
56 tmp = 2;
57 }
58 }
59 weight + tmp
60 }
61
62 pub fn matches(&self, context: &str) -> bool {
63 if self.pattern == "*" {
64 return true;
65 }
66
67 let regex_pattern = glob_to_regex(&self.pattern);
68 if let Ok(re) = Regex::new(®ex_pattern) {
69 re.is_match(context)
70 } else {
71 self.pattern == context
72 }
73 }
74}
75
76fn glob_to_regex(pattern: &str) -> String {
77 let mut result = String::from("^");
78 for ch in pattern.chars() {
79 match ch {
80 '*' => result.push_str(".*"),
81 '?' => result.push('.'),
82 '.' | '+' | '(' | ')' | '[' | ']' | '{' | '}' | '^' | '$' | '|' | '\\' => {
83 result.push('\\');
84 result.push(ch);
85 }
86 _ => result.push(ch),
87 }
88 }
89 result.push('$');
90 result
91}
92
93#[derive(Debug, Default)]
95pub struct StyleTable {
96 styles: HashMap<String, Vec<StylePattern>>,
97}
98
99impl StyleTable {
100 pub fn new() -> Self {
101 Self::default()
102 }
103
104 pub fn set(&mut self, pattern: &str, style: &str, values: Vec<String>, eval: bool) {
105 let style_patterns = self.styles.entry(style.to_string()).or_default();
106
107 if let Some(existing) = style_patterns.iter_mut().find(|p| p.pattern == pattern) {
108 existing.values = values;
109 existing.eval = eval;
110 } else {
111 let sp = StylePattern::new(pattern, values, eval);
112 let weight = sp.weight;
113 let pos = style_patterns
114 .iter()
115 .position(|p| p.weight < weight)
116 .unwrap_or(style_patterns.len());
117 style_patterns.insert(pos, sp);
118 }
119 }
120
121 pub fn get(&self, context: &str, style: &str) -> Option<&[String]> {
122 self.styles.get(style).and_then(|patterns| {
123 patterns
124 .iter()
125 .find(|p| p.matches(context))
126 .map(|p| p.values.as_slice())
127 })
128 }
129
130 pub fn delete(&mut self, pattern: Option<&str>, style: Option<&str>) {
131 match (pattern, style) {
132 (None, None) => self.styles.clear(),
133 (Some(pat), None) => {
134 for patterns in self.styles.values_mut() {
135 patterns.retain(|p| p.pattern != pat);
136 }
137 self.styles.retain(|_, v| !v.is_empty());
138 }
139 (Some(pat), Some(sty)) => {
140 if let Some(patterns) = self.styles.get_mut(sty) {
141 patterns.retain(|p| p.pattern != pat);
142 if patterns.is_empty() {
143 self.styles.remove(sty);
144 }
145 }
146 }
147 (None, Some(sty)) => {
148 self.styles.remove(sty);
149 }
150 }
151 }
152
153 pub fn list(&self, context: Option<&str>) -> Vec<(String, String, Vec<String>)> {
154 let mut result = Vec::new();
155 for (style, patterns) in &self.styles {
156 for pat in patterns {
157 if let Some(ctx) = context {
158 if !pat.matches(ctx) {
159 continue;
160 }
161 }
162 result.push((style.clone(), pat.pattern.clone(), pat.values.clone()));
163 }
164 }
165 result
166 }
167
168 pub fn list_styles(&self) -> Vec<&str> {
169 self.styles.keys().map(|s| s.as_str()).collect()
170 }
171
172 pub fn list_patterns(&self) -> Vec<&str> {
173 let mut patterns = Vec::new();
174 for pats in self.styles.values() {
175 for pat in pats {
176 if !patterns.contains(&pat.pattern.as_str()) {
177 patterns.push(pat.pattern.as_str());
178 }
179 }
180 }
181 patterns
182 }
183
184 pub fn test(&self, context: &str, style: &str, values: Option<&[&str]>) -> bool {
185 if let Some(found) = self.get(context, style) {
186 if let Some(test_vals) = values {
187 test_vals.iter().any(|v| found.contains(&v.to_string()))
188 } else {
189 matches!(
190 found.first().map(|s| s.as_str()),
191 Some("true" | "yes" | "on" | "1")
192 )
193 }
194 } else {
195 false
196 }
197 }
198
199 pub fn test_bool(&self, context: &str, style: &str) -> Option<bool> {
200 self.get(context, style).and_then(|vals| {
201 if vals.len() == 1 {
202 match vals[0].as_str() {
203 "yes" | "true" | "on" | "1" => Some(true),
204 "no" | "false" | "off" | "0" => Some(false),
205 _ => None,
206 }
207 } else {
208 None
209 }
210 })
211 }
212}
213
214pub fn zformat(format: &str, specs: &HashMap<char, String>, _presence: bool) -> String {
216 let mut result = String::new();
217 let mut chars = format.chars().peekable();
218
219 while let Some(ch) = chars.next() {
220 if ch == '%' {
221 let mut right = false;
222 let mut min: Option<usize> = None;
223 let mut max: Option<usize> = None;
224
225 if chars.peek() == Some(&'-') {
226 right = true;
227 chars.next();
228 }
229
230 let mut num_str = String::new();
231 while let Some(&c) = chars.peek() {
232 if c.is_ascii_digit() {
233 num_str.push(c);
234 chars.next();
235 } else {
236 break;
237 }
238 }
239 if !num_str.is_empty() {
240 min = num_str.parse().ok();
241 }
242
243 if chars.peek() == Some(&'.') || chars.peek() == Some(&'(') {
244 let is_ternary = chars.peek() == Some(&'(');
245 if !is_ternary {
246 chars.next();
247 }
248
249 let mut max_str = String::new();
250 while let Some(&c) = chars.peek() {
251 if c.is_ascii_digit() {
252 max_str.push(c);
253 chars.next();
254 } else {
255 break;
256 }
257 }
258 if !max_str.is_empty() {
259 max = max_str.parse().ok();
260 }
261 }
262
263 if let Some(&spec_char) = chars.peek() {
264 chars.next();
265
266 if spec_char == '(' {
267 continue;
268 }
269
270 if let Some(spec_val) = specs.get(&spec_char) {
271 let mut val = spec_val.clone();
272
273 if let Some(m) = max {
274 if val.len() > m {
275 val.truncate(m);
276 }
277 }
278
279 let out_len = min.map(|m| m.max(val.len())).unwrap_or(val.len());
280
281 if val.len() >= out_len {
282 result.push_str(&val[..out_len]);
283 } else {
284 let padding = out_len - val.len();
285 if right {
286 result.push_str(&" ".repeat(padding));
287 result.push_str(&val);
288 } else {
289 result.push_str(&val);
290 result.push_str(&" ".repeat(padding));
291 }
292 }
293 } else if spec_char == '%' {
294 result.push('%');
295 }
296 }
297 } else {
298 result.push(ch);
299 }
300 }
301
302 result
303}
304
305#[derive(Debug, Clone)]
307pub struct OptDesc {
308 pub name: String,
309 pub takes_arg: bool,
310 pub optional_arg: bool,
311 pub multiple: bool,
312 pub array_name: Option<String>,
313}
314
315impl OptDesc {
316 pub fn parse(spec: &str) -> Option<Self> {
317 if spec.is_empty() {
318 return None;
319 }
320
321 let mut name = String::new();
322 let mut takes_arg = false;
323 let mut optional_arg = false;
324 let mut multiple = false;
325 let mut array_name = None;
326 let mut chars = spec.chars().peekable();
327
328 while let Some(&ch) = chars.peek() {
329 if ch == '+' {
330 multiple = true;
331 chars.next();
332 break;
333 } else if ch == ':' || ch == '=' {
334 break;
335 } else if ch == '\\' {
336 chars.next();
337 if let Some(c) = chars.next() {
338 name.push(c);
339 }
340 } else {
341 name.push(ch);
342 chars.next();
343 }
344 }
345
346 if name.is_empty() {
347 return None;
348 }
349
350 if chars.peek() == Some(&':') {
351 takes_arg = true;
352 chars.next();
353 if chars.peek() == Some(&':') {
354 optional_arg = true;
355 chars.next();
356 }
357 }
358
359 if chars.peek() == Some(&'=') {
360 chars.next();
361 array_name = Some(chars.collect());
362 }
363
364 Some(Self {
365 name,
366 takes_arg,
367 optional_arg,
368 multiple,
369 array_name,
370 })
371 }
372}
373
374pub fn zparseopts(
376 args: &[String],
377 specs: &[OptDesc],
378 delete: bool,
379 extract: bool,
380) -> Result<(HashMap<String, Vec<String>>, Vec<String>), String> {
381 let mut results: HashMap<String, Vec<String>> = HashMap::new();
382 let mut remaining = Vec::new();
383 let mut i = 0;
384
385 let short_opts: HashMap<char, &OptDesc> = specs
386 .iter()
387 .filter(|s| s.name.len() == 1)
388 .map(|s| (s.name.chars().next().unwrap(), s))
389 .collect();
390
391 let long_opts: HashMap<&str, &OptDesc> = specs
392 .iter()
393 .filter(|s| s.name.len() > 1)
394 .map(|s| (s.name.as_str(), s))
395 .collect();
396
397 while i < args.len() {
398 let arg = &args[i];
399
400 if !arg.starts_with('-') || arg == "-" {
401 if extract {
402 if !delete {
403 remaining.push(arg.clone());
404 }
405 i += 1;
406 continue;
407 } else {
408 remaining.extend(args[i..].iter().cloned());
409 break;
410 }
411 }
412
413 if arg == "--" {
414 i += 1;
415 remaining.extend(args[i..].iter().cloned());
416 break;
417 }
418
419 let opt_str = &arg[1..];
420
421 if let Some(desc) = long_opts.get(opt_str) {
422 let key = format!("-{}", desc.name);
423 let entry = results.entry(key).or_default();
424
425 if desc.takes_arg {
426 if i + 1 < args.len() && !desc.optional_arg {
427 i += 1;
428 entry.push(args[i].clone());
429 } else if desc.optional_arg {
430 entry.push(String::new());
431 } else {
432 return Err(format!("missing argument for option: -{}", desc.name));
433 }
434 } else {
435 entry.push(String::new());
436 }
437 } else if opt_str.starts_with('-') {
438 let long_name = &opt_str[1..];
439 if let Some((name, value)) = long_name.split_once('=') {
440 if let Some(desc) = long_opts.get(name) {
441 let key = format!("-{}", desc.name);
442 results.entry(key).or_default().push(value.to_string());
443 } else {
444 if !extract {
445 remaining.extend(args[i..].iter().cloned());
446 break;
447 }
448 remaining.push(arg.clone());
449 }
450 } else if let Some(desc) = long_opts.get(long_name) {
451 let key = format!("-{}", desc.name);
452 let entry = results.entry(key).or_default();
453
454 if desc.takes_arg {
455 if i + 1 < args.len() && !desc.optional_arg {
456 i += 1;
457 entry.push(args[i].clone());
458 } else if desc.optional_arg {
459 entry.push(String::new());
460 } else {
461 return Err(format!("missing argument for option: --{}", desc.name));
462 }
463 } else {
464 entry.push(String::new());
465 }
466 } else {
467 if !extract {
468 remaining.extend(args[i..].iter().cloned());
469 break;
470 }
471 remaining.push(arg.clone());
472 }
473 } else {
474 let mut j = 0;
475 let chars: Vec<char> = opt_str.chars().collect();
476
477 while j < chars.len() {
478 let ch = chars[j];
479 if let Some(desc) = short_opts.get(&ch) {
480 let key = format!("-{}", desc.name);
481 let entry = results.entry(key).or_default();
482
483 if desc.takes_arg {
484 if j + 1 < chars.len() {
485 entry.push(chars[j + 1..].iter().collect());
486 break;
487 } else if i + 1 < args.len() && !desc.optional_arg {
488 i += 1;
489 entry.push(args[i].clone());
490 } else if desc.optional_arg {
491 entry.push(String::new());
492 } else {
493 return Err(format!("missing argument for option: -{}", desc.name));
494 }
495 } else {
496 entry.push(String::new());
497 }
498 } else {
499 if !extract {
500 remaining.push(arg.clone());
501 remaining.extend(args[i + 1..].iter().cloned());
502 return Ok((results, remaining));
503 }
504 break;
505 }
506 j += 1;
507 }
508 }
509 i += 1;
510 }
511
512 if !delete && !extract {
513 remaining = args[i..].to_vec();
514 }
515
516 Ok((results, remaining))
517}
518
519pub fn zformat_align(sep: &str, values: &[&str]) -> Vec<String> {
521 let mut max_pre = 0;
522
523 for value in values {
524 if let Some(pos) = value.find(':') {
525 let pre_len = value[..pos].chars().filter(|c| *c != '\\').count();
526 if pre_len > max_pre {
527 max_pre = pre_len;
528 }
529 }
530 }
531
532 let mut result = Vec::new();
533 for value in values {
534 if let Some(pos) = value.find(':') {
535 let pre = &value[..pos];
536 let post = &value[pos + 1..];
537 let pre_len = pre.chars().filter(|c| *c != '\\').count();
538 let padding = max_pre - pre_len;
539
540 let clean_pre: String = pre.chars().filter(|c| *c != '\\').collect();
541
542 result.push(format!(
543 "{}{}{}{}",
544 clean_pre,
545 " ".repeat(padding),
546 sep,
547 post
548 ));
549 } else {
550 result.push(value.to_string());
551 }
552 }
553
554 result
555}
556
557#[cfg(test)]
558mod tests {
559 use super::*;
560
561 #[test]
562 fn test_style_pattern_weight() {
563 let p1 = StylePattern::new("*", vec![], false);
564 let p2 = StylePattern::new(":completion:*", vec![], false);
565 let p3 = StylePattern::new(":completion:zsh:*", vec![], false);
566
567 assert!(p3.weight > p2.weight);
568 assert!(p2.weight > p1.weight);
569 }
570
571 #[test]
572 fn test_style_pattern_matches() {
573 let p = StylePattern::new(":completion:*", vec![], false);
574 assert!(p.matches(":completion:zsh:complete"));
575 assert!(!p.matches(":other:zsh"));
576
577 let p2 = StylePattern::new("*", vec![], false);
578 assert!(p2.matches("anything"));
579 }
580
581 #[test]
582 fn test_style_table_set_get() {
583 let mut table = StyleTable::new();
584 table.set(":completion:*", "verbose", vec!["yes".to_string()], false);
585
586 let result = table.get(":completion:zsh", "verbose");
587 assert_eq!(result, Some(&["yes".to_string()][..]));
588
589 let result = table.get(":other", "verbose");
590 assert!(result.is_none());
591 }
592
593 #[test]
594 fn test_style_table_priority() {
595 let mut table = StyleTable::new();
596 table.set("*", "menu", vec!["no".to_string()], false);
597 table.set(":completion:*", "menu", vec!["yes".to_string()], false);
598
599 let result = table.get(":completion:zsh", "menu");
600 assert_eq!(result, Some(&["yes".to_string()][..]));
601 }
602
603 #[test]
604 fn test_style_table_delete() {
605 let mut table = StyleTable::new();
606 table.set("*", "style1", vec!["val".to_string()], false);
607 table.set("*", "style2", vec!["val".to_string()], false);
608
609 table.delete(None, Some("style1"));
610 assert!(table.get("test", "style1").is_none());
611 assert!(table.get("test", "style2").is_some());
612 }
613
614 #[test]
615 fn test_style_test_bool() {
616 let mut table = StyleTable::new();
617 table.set("*", "enabled", vec!["yes".to_string()], false);
618 table.set("*", "disabled", vec!["no".to_string()], false);
619 table.set(
620 "*",
621 "multiple",
622 vec!["a".to_string(), "b".to_string()],
623 false,
624 );
625
626 assert_eq!(table.test_bool("ctx", "enabled"), Some(true));
627 assert_eq!(table.test_bool("ctx", "disabled"), Some(false));
628 assert_eq!(table.test_bool("ctx", "multiple"), None);
629 }
630
631 #[test]
632 fn test_zformat_basic() {
633 let mut specs = HashMap::new();
634 specs.insert('n', "test".to_string());
635 specs.insert('v', "42".to_string());
636
637 let result = zformat("Name: %n, Value: %v", &specs, false);
638 assert_eq!(result, "Name: test, Value: 42");
639 }
640
641 #[test]
642 fn test_zformat_padding() {
643 let mut specs = HashMap::new();
644 specs.insert('n', "hi".to_string());
645
646 let result = zformat("[%10n]", &specs, false);
647 assert_eq!(result, "[hi ]");
648
649 let result = zformat("[%-10n]", &specs, false);
650 assert_eq!(result, "[ hi]");
651 }
652
653 #[test]
654 fn test_zformat_truncate() {
655 let mut specs = HashMap::new();
656 specs.insert('n', "hello world".to_string());
657
658 let result = zformat("[%.5n]", &specs, false);
659 assert_eq!(result, "[hello]");
660 }
661
662 #[test]
663 fn test_zformat_escape() {
664 let specs = HashMap::new();
665 let result = zformat("100%%", &specs, false);
666 assert_eq!(result, "100%");
667 }
668
669 #[test]
670 fn test_opt_desc_parse() {
671 let desc = OptDesc::parse("v").unwrap();
672 assert_eq!(desc.name, "v");
673 assert!(!desc.takes_arg);
674
675 let desc = OptDesc::parse("o:").unwrap();
676 assert_eq!(desc.name, "o");
677 assert!(desc.takes_arg);
678 assert!(!desc.optional_arg);
679
680 let desc = OptDesc::parse("o::").unwrap();
681 assert!(desc.optional_arg);
682
683 let desc = OptDesc::parse("v+").unwrap();
684 assert!(desc.multiple);
685
686 let desc = OptDesc::parse("a:=myarray").unwrap();
687 assert_eq!(desc.array_name, Some("myarray".to_string()));
688 }
689
690 #[test]
691 fn test_zparseopts_basic() {
692 let specs = vec![OptDesc::parse("v").unwrap(), OptDesc::parse("o:").unwrap()];
693
694 let args: Vec<String> = vec!["-v", "-o", "value", "rest"]
695 .into_iter()
696 .map(String::from)
697 .collect();
698
699 let (opts, remaining) = zparseopts(&args, &specs, false, false).unwrap();
700
701 assert!(opts.contains_key("-v"));
702 assert_eq!(opts.get("-o"), Some(&vec!["value".to_string()]));
703 assert_eq!(remaining, vec!["rest"]);
704 }
705
706 #[test]
707 fn test_zparseopts_combined() {
708 let specs = vec![
709 OptDesc::parse("a").unwrap(),
710 OptDesc::parse("b").unwrap(),
711 OptDesc::parse("c:").unwrap(),
712 ];
713
714 let args: Vec<String> = vec!["-abc", "val"].into_iter().map(String::from).collect();
715
716 let (opts, _) = zparseopts(&args, &specs, false, false).unwrap();
717
718 assert!(opts.contains_key("-a"));
719 assert!(opts.contains_key("-b"));
720 assert_eq!(opts.get("-c"), Some(&vec!["val".to_string()]));
721 }
722
723 #[test]
724 fn test_zparseopts_long() {
725 let specs = vec![
726 OptDesc::parse("verbose").unwrap(),
727 OptDesc::parse("output:").unwrap(),
728 ];
729
730 let args: Vec<String> = vec!["--verbose", "--output", "file.txt"]
731 .into_iter()
732 .map(String::from)
733 .collect();
734
735 let (opts, _) = zparseopts(&args, &specs, false, false).unwrap();
736
737 assert!(opts.contains_key("-verbose"));
738 assert_eq!(opts.get("-output"), Some(&vec!["file.txt".to_string()]));
739 }
740
741 #[test]
742 fn test_zformat_align() {
743 let values = vec!["short:desc1", "verylongname:desc2", "med:desc3"];
744 let result = zformat_align(" -- ", &values);
745
746 assert_eq!(result[0], "short -- desc1");
747 assert_eq!(result[1], "verylongname -- desc2");
748 assert_eq!(result[2], "med -- desc3");
749 }
750}