1use std::env;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum CmdState {
30 For,
31 While,
32 Repeat,
33 Select,
34 Until,
35 If,
36 Then,
37 Else,
38 Elif,
39 Math,
40 Cond,
41 CmdOr,
42 CmdAnd,
43 Pipe,
44 ErrPipe,
45 Foreach,
46 Case,
47 Function,
48 Subsh,
49 Cursh,
50 Array,
51 Quote,
52 DQuote,
53 BQuote,
54 CmdSubst,
55 MathSubst,
56 ElifThen,
57 Heredoc,
58 HeredocD,
59 Brace,
60 BraceParam,
61 Always,
62}
63
64impl CmdState {
65 pub fn name(&self) -> &'static str {
66 match self {
67 CmdState::For => "for",
68 CmdState::While => "while",
69 CmdState::Repeat => "repeat",
70 CmdState::Select => "select",
71 CmdState::Until => "until",
72 CmdState::If => "if",
73 CmdState::Then => "then",
74 CmdState::Else => "else",
75 CmdState::Elif => "elif",
76 CmdState::Math => "math",
77 CmdState::Cond => "cond",
78 CmdState::CmdOr => "cmdor",
79 CmdState::CmdAnd => "cmdand",
80 CmdState::Pipe => "pipe",
81 CmdState::ErrPipe => "errpipe",
82 CmdState::Foreach => "foreach",
83 CmdState::Case => "case",
84 CmdState::Function => "function",
85 CmdState::Subsh => "subsh",
86 CmdState::Cursh => "cursh",
87 CmdState::Array => "array",
88 CmdState::Quote => "quote",
89 CmdState::DQuote => "dquote",
90 CmdState::BQuote => "bquote",
91 CmdState::CmdSubst => "cmdsubst",
92 CmdState::MathSubst => "mathsubst",
93 CmdState::ElifThen => "elif-then",
94 CmdState::Heredoc => "heredoc",
95 CmdState::HeredocD => "heredocd",
96 CmdState::Brace => "brace",
97 CmdState::BraceParam => "braceparam",
98 CmdState::Always => "always",
99 }
100 }
101}
102
103#[derive(Debug, Clone, Copy, Default)]
105pub struct TextAttrs {
106 pub bold: bool,
107 pub underline: bool,
108 pub standout: bool,
109 pub fg_color: Option<Color>,
110 pub bg_color: Option<Color>,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub enum Color {
116 Black,
117 Red,
118 Green,
119 Yellow,
120 Blue,
121 Magenta,
122 Cyan,
123 White,
124 Default,
125 Numbered(u8),
126 Rgb(u8, u8, u8),
127}
128
129impl Color {
130 pub fn from_name(name: &str) -> Option<Color> {
131 match name.to_lowercase().as_str() {
132 "black" => Some(Color::Black),
133 "red" => Some(Color::Red),
134 "green" => Some(Color::Green),
135 "yellow" => Some(Color::Yellow),
136 "blue" => Some(Color::Blue),
137 "magenta" => Some(Color::Magenta),
138 "cyan" => Some(Color::Cyan),
139 "white" => Some(Color::White),
140 "default" => Some(Color::Default),
141 _ => {
142 if let Ok(n) = name.parse::<u8>() {
143 Some(Color::Numbered(n))
144 } else {
145 None
146 }
147 }
148 }
149 }
150
151 pub fn to_ansi_fg(&self) -> String {
152 match self {
153 Color::Black => "\x1b[30m".to_string(),
154 Color::Red => "\x1b[31m".to_string(),
155 Color::Green => "\x1b[32m".to_string(),
156 Color::Yellow => "\x1b[33m".to_string(),
157 Color::Blue => "\x1b[34m".to_string(),
158 Color::Magenta => "\x1b[35m".to_string(),
159 Color::Cyan => "\x1b[36m".to_string(),
160 Color::White => "\x1b[37m".to_string(),
161 Color::Default => "\x1b[39m".to_string(),
162 Color::Numbered(n) => format!("\x1b[38;5;{}m", n),
163 Color::Rgb(r, g, b) => format!("\x1b[38;2;{};{};{}m", r, g, b),
164 }
165 }
166
167 pub fn to_ansi_bg(&self) -> String {
168 match self {
169 Color::Black => "\x1b[40m".to_string(),
170 Color::Red => "\x1b[41m".to_string(),
171 Color::Green => "\x1b[42m".to_string(),
172 Color::Yellow => "\x1b[43m".to_string(),
173 Color::Blue => "\x1b[44m".to_string(),
174 Color::Magenta => "\x1b[45m".to_string(),
175 Color::Cyan => "\x1b[46m".to_string(),
176 Color::White => "\x1b[47m".to_string(),
177 Color::Default => "\x1b[49m".to_string(),
178 Color::Numbered(n) => format!("\x1b[48;5;{}m", n),
179 Color::Rgb(r, g, b) => format!("\x1b[48;2;{};{};{}m", r, g, b),
180 }
181 }
182}
183
184pub struct PromptContext {
186 pub pwd: String,
187 pub home: String,
188 pub user: String,
189 pub host: String,
190 pub host_short: String,
191 pub tty: String,
192 pub lastval: i32,
193 pub histnum: i64,
194 pub shlvl: i32,
195 pub num_jobs: i32,
196 pub is_root: bool,
197 pub cmd_stack: Vec<CmdState>,
198 pub psvar: Vec<String>,
199 pub term_width: usize,
200 pub lineno: i64,
201}
202
203impl Default for PromptContext {
204 fn default() -> Self {
205 let home = env::var("HOME").unwrap_or_default();
206 let pwd = env::current_dir()
207 .map(|p| p.to_string_lossy().to_string())
208 .unwrap_or_else(|_| "/".to_string());
209
210 let user = env::var("USER")
211 .or_else(|_| env::var("LOGNAME"))
212 .unwrap_or_else(|_| "user".to_string());
213
214 let host = hostname::get()
215 .map(|h| h.to_string_lossy().to_string())
216 .unwrap_or_else(|_| "localhost".to_string());
217
218 let host_short = host.split('.').next().unwrap_or(&host).to_string();
219
220 let tty = std::fs::read_link("/proc/self/fd/0")
221 .map(|p| p.to_string_lossy().to_string())
222 .unwrap_or_else(|_| String::new());
223
224 let shlvl = env::var("SHLVL")
225 .ok()
226 .and_then(|s| s.parse().ok())
227 .unwrap_or(1);
228
229 PromptContext {
230 pwd,
231 home,
232 user,
233 host,
234 host_short,
235 tty,
236 lastval: 0,
237 histnum: 1,
238 shlvl,
239 num_jobs: 0,
240 is_root: unsafe { libc::geteuid() } == 0,
241 cmd_stack: Vec::new(),
242 psvar: Vec::new(),
243 term_width: 80,
244 lineno: 1,
245 }
246 }
247}
248
249pub struct PromptExpander<'a> {
251 ctx: &'a PromptContext,
252 input: &'a str,
253 pos: usize,
254 output: String,
255 attrs: TextAttrs,
256 in_escape: bool,
257 prompt_percent: bool,
258 prompt_bang: bool,
259}
260
261impl<'a> PromptExpander<'a> {
262 pub fn new(input: &'a str, ctx: &'a PromptContext) -> Self {
263 PromptExpander {
264 ctx,
265 input,
266 pos: 0,
267 output: String::with_capacity(input.len() * 2),
268 attrs: TextAttrs::default(),
269 in_escape: false,
270 prompt_percent: true,
271 prompt_bang: true,
272 }
273 }
274
275 pub fn with_prompt_percent(mut self, enable: bool) -> Self {
276 self.prompt_percent = enable;
277 self
278 }
279
280 pub fn with_prompt_bang(mut self, enable: bool) -> Self {
281 self.prompt_bang = enable;
282 self
283 }
284
285 fn peek(&self) -> Option<char> {
286 self.input[self.pos..].chars().next()
287 }
288
289 fn advance(&mut self) -> Option<char> {
290 let c = self.peek()?;
291 self.pos += c.len_utf8();
292 Some(c)
293 }
294
295 fn parse_number(&mut self) -> Option<i32> {
296 let start = self.pos;
297 let mut negative = false;
298
299 if self.peek() == Some('-') {
300 negative = true;
301 self.advance();
302 }
303
304 while let Some(c) = self.peek() {
305 if c.is_ascii_digit() {
306 self.advance();
307 } else {
308 break;
309 }
310 }
311
312 if self.pos == start || (negative && self.pos == start + 1) {
313 if negative {
314 self.pos = start;
315 }
316 return None;
317 }
318
319 let num_str = &self.input[if negative { start + 1 } else { start }..self.pos];
320 let num: i32 = num_str.parse().ok()?;
321 Some(if negative { -num } else { num })
322 }
323
324 fn parse_braced_arg(&mut self) -> Option<String> {
325 if self.peek() != Some('{') {
326 return None;
327 }
328 self.advance(); let start = self.pos;
331 let mut depth = 1;
332
333 while let Some(c) = self.advance() {
334 match c {
335 '{' => depth += 1,
336 '}' => {
337 depth -= 1;
338 if depth == 0 {
339 return Some(self.input[start..self.pos - 1].to_string());
340 }
341 }
342 '\\' => {
343 self.advance(); }
345 _ => {}
346 }
347 }
348
349 None
350 }
351
352 fn path_with_tilde(&self, path: &str) -> String {
354 if !self.ctx.home.is_empty() && path.starts_with(&self.ctx.home) {
355 format!("~{}", &path[self.ctx.home.len()..])
356 } else {
357 path.to_string()
358 }
359 }
360
361 fn trailing_path(&self, path: &str, n: usize, with_tilde: bool) -> String {
363 let path = if with_tilde {
364 self.path_with_tilde(path)
365 } else {
366 path.to_string()
367 };
368
369 if n == 0 {
370 return path;
371 }
372
373 let components: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
374 if components.len() <= n {
375 return path;
376 }
377
378 components[components.len() - n..].join("/")
379 }
380
381 fn leading_path(&self, path: &str, n: usize) -> String {
383 if n == 0 {
384 return path.to_string();
385 }
386
387 let components: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
388 if components.len() <= n {
389 return path.to_string();
390 }
391
392 let result = components[..n].join("/");
393 if path.starts_with('/') {
394 format!("/{}", result)
395 } else {
396 result
397 }
398 }
399
400 fn start_escape(&mut self) {
402 if !self.in_escape {
403 self.output.push('\x01'); self.in_escape = true;
405 }
406 }
407
408 fn end_escape(&mut self) {
410 if self.in_escape {
411 self.output.push('\x02'); self.in_escape = false;
413 }
414 }
415
416 fn apply_attrs(&mut self) {
418 self.start_escape();
419
420 self.output.push_str("\x1b[0m");
422
423 if self.attrs.bold {
424 self.output.push_str("\x1b[1m");
425 }
426 if self.attrs.underline {
427 self.output.push_str("\x1b[4m");
428 }
429 if self.attrs.standout {
430 self.output.push_str("\x1b[7m");
431 }
432 if let Some(ref color) = self.attrs.fg_color {
433 self.output.push_str(&color.to_ansi_fg());
434 }
435 if let Some(ref color) = self.attrs.bg_color {
436 self.output.push_str(&color.to_ansi_bg());
437 }
438
439 self.end_escape();
440 }
441
442 fn parse_conditional(&mut self, arg: i32) -> bool {
444 if self.peek() != Some('(') {
445 return false;
446 }
447 self.advance(); let cond_char = match self.advance() {
451 Some(c) => c,
452 None => return false,
453 };
454
455 let test = match cond_char {
457 '/' | 'c' | '.' | '~' | 'C' => {
458 let path = self.path_with_tilde(&self.ctx.pwd);
460 let depth = path.matches('/').count() as i32;
461 if arg == 0 {
462 depth > 0
463 } else {
464 depth >= arg
465 }
466 }
467 '?' => self.ctx.lastval == arg,
468 '#' => {
469 let euid = unsafe { libc::geteuid() };
470 euid == arg as u32
471 }
472 'L' => self.ctx.shlvl >= arg,
473 'j' => self.ctx.num_jobs >= arg,
474 'v' => (arg as usize) <= self.ctx.psvar.len(),
475 'V' => {
476 if arg <= 0 || (arg as usize) > self.ctx.psvar.len() {
477 false
478 } else {
479 !self.ctx.psvar[arg as usize - 1].is_empty()
480 }
481 }
482 '_' => self.ctx.cmd_stack.len() >= arg as usize,
483 't' | 'T' | 'd' | 'D' | 'w' => {
484 let now = chrono::Local::now();
485 match cond_char {
486 't' => now.format("%M").to_string().parse::<i32>().unwrap_or(0) == arg,
487 'T' => now.format("%H").to_string().parse::<i32>().unwrap_or(0) == arg,
488 'd' => now.format("%d").to_string().parse::<i32>().unwrap_or(0) == arg,
489 'D' => now.format("%m").to_string().parse::<i32>().unwrap_or(0) == arg - 1,
490 'w' => now.format("%w").to_string().parse::<i32>().unwrap_or(0) == arg,
491 _ => false,
492 }
493 }
494 '!' => self.ctx.is_root,
495 _ => false,
496 };
497
498 let sep = match self.advance() {
500 Some(c) => c,
501 None => return false,
502 };
503
504 let true_start = self.pos;
506 let mut depth = 1;
507 while let Some(c) = self.peek() {
508 if c == '(' {
509 depth += 1;
510 } else if c == ')' {
511 depth -= 1;
512 if depth == 0 {
513 break;
514 }
515 } else if c == sep && depth == 1 {
516 break;
517 }
518 self.advance();
519 }
520 let true_branch = &self.input[true_start..self.pos].to_string();
521
522 if self.peek() != Some(sep) {
523 return false;
524 }
525 self.advance(); let false_start = self.pos;
529 depth = 1;
530 while let Some(c) = self.peek() {
531 if c == '(' {
532 depth += 1;
533 } else if c == ')' {
534 depth -= 1;
535 if depth == 0 {
536 break;
537 }
538 }
539 self.advance();
540 }
541 let false_branch = &self.input[false_start..self.pos].to_string();
542
543 if self.peek() != Some(')') {
544 return false;
545 }
546 self.advance(); let branch = if test { true_branch } else { false_branch };
550 let expanded = expand_prompt(branch, self.ctx);
551 self.output.push_str(&expanded);
552
553 true
554 }
555
556 fn process_percent(&mut self) {
558 let arg = self.parse_number().unwrap_or(0);
559
560 if self.peek() == Some('(') {
562 self.parse_conditional(arg);
563 return;
564 }
565
566 let c = match self.advance() {
567 Some(c) => c,
568 None => return,
569 };
570
571 match c {
572 '~' => {
574 let path = if arg == 0 {
575 self.path_with_tilde(&self.ctx.pwd)
576 } else if arg > 0 {
577 self.trailing_path(&self.ctx.pwd, arg as usize, true)
578 } else {
579 self.leading_path(&self.path_with_tilde(&self.ctx.pwd), (-arg) as usize)
580 };
581 self.output.push_str(&path);
582 }
583 'd' | '/' => {
584 let path = if arg == 0 {
585 self.ctx.pwd.clone()
586 } else if arg > 0 {
587 self.trailing_path(&self.ctx.pwd, arg as usize, false)
588 } else {
589 self.leading_path(&self.ctx.pwd, (-arg) as usize)
590 };
591 self.output.push_str(&path);
592 }
593 'c' | '.' => {
594 let n = if arg == 0 {
595 1
596 } else {
597 arg.unsigned_abs() as usize
598 };
599 let path = self.trailing_path(&self.ctx.pwd, n, true);
600 self.output.push_str(&path);
601 }
602 'C' => {
603 let n = if arg == 0 {
604 1
605 } else {
606 arg.unsigned_abs() as usize
607 };
608 let path = self.trailing_path(&self.ctx.pwd, n, false);
609 self.output.push_str(&path);
610 }
611
612 'n' => self.output.push_str(&self.ctx.user),
614 'M' => self.output.push_str(&self.ctx.host),
615 'm' => {
616 let n = if arg == 0 { 1 } else { arg };
617 if n > 0 {
618 let parts: Vec<&str> = self.ctx.host.split('.').collect();
619 let take = (n as usize).min(parts.len());
620 self.output.push_str(&parts[..take].join("."));
621 } else {
622 let parts: Vec<&str> = self.ctx.host.split('.').collect();
623 let skip = ((-n) as usize).min(parts.len());
624 self.output.push_str(&parts[skip..].join("."));
625 }
626 }
627
628 'l' => {
630 let tty = if self.ctx.tty.starts_with("/dev/tty") {
631 &self.ctx.tty[8..]
632 } else if self.ctx.tty.starts_with("/dev/") {
633 &self.ctx.tty[5..]
634 } else {
635 "()"
636 };
637 self.output.push_str(tty);
638 }
639 'y' => {
640 let tty = if self.ctx.tty.starts_with("/dev/") {
641 &self.ctx.tty[5..]
642 } else {
643 &self.ctx.tty
644 };
645 self.output.push_str(tty);
646 }
647
648 '?' => self.output.push_str(&self.ctx.lastval.to_string()),
650 '#' => self.output.push(if self.ctx.is_root { '#' } else { '%' }),
651
652 'h' | '!' => self.output.push_str(&self.ctx.histnum.to_string()),
654
655 'j' => self.output.push_str(&self.ctx.num_jobs.to_string()),
657
658 'L' => self.output.push_str(&self.ctx.shlvl.to_string()),
660
661 'i' => self.output.push_str(&self.ctx.lineno.to_string()),
663
664 'D' => {
666 let now = chrono::Local::now();
667 if let Some(fmt) = self.parse_braced_arg() {
668 let zsh_fmt = convert_zsh_time_format(&fmt);
669 self.output.push_str(&now.format(&zsh_fmt).to_string());
670 } else {
671 self.output.push_str(&now.format("%y-%m-%d").to_string());
672 }
673 }
674 'T' => {
675 let now = chrono::Local::now();
676 self.output.push_str(&now.format("%H:%M").to_string());
677 }
678 '*' => {
679 let now = chrono::Local::now();
680 self.output.push_str(&now.format("%H:%M:%S").to_string());
681 }
682 't' | '@' => {
683 let now = chrono::Local::now();
684 self.output.push_str(&now.format("%l:%M%p").to_string());
685 }
686 'w' => {
687 let now = chrono::Local::now();
688 self.output.push_str(&now.format("%a %e").to_string());
689 }
690 'W' => {
691 let now = chrono::Local::now();
692 self.output.push_str(&now.format("%m/%d/%y").to_string());
693 }
694
695 'B' => {
697 self.attrs.bold = true;
698 self.apply_attrs();
699 }
700 'b' => {
701 self.attrs.bold = false;
702 self.apply_attrs();
703 }
704 'U' => {
705 self.attrs.underline = true;
706 self.apply_attrs();
707 }
708 'u' => {
709 self.attrs.underline = false;
710 self.apply_attrs();
711 }
712 'S' => {
713 self.attrs.standout = true;
714 self.apply_attrs();
715 }
716 's' => {
717 self.attrs.standout = false;
718 self.apply_attrs();
719 }
720
721 'F' => {
723 let color = if let Some(name) = self.parse_braced_arg() {
724 Color::from_name(&name)
725 } else if arg > 0 {
726 Some(Color::Numbered(arg as u8))
727 } else {
728 None
729 };
730 if let Some(c) = color {
731 self.attrs.fg_color = Some(c);
732 self.apply_attrs();
733 }
734 }
735 'f' => {
736 self.attrs.fg_color = None;
737 self.apply_attrs();
738 }
739 'K' => {
740 let color = if let Some(name) = self.parse_braced_arg() {
741 Color::from_name(&name)
742 } else if arg > 0 {
743 Some(Color::Numbered(arg as u8))
744 } else {
745 None
746 };
747 if let Some(c) = color {
748 self.attrs.bg_color = Some(c);
749 self.apply_attrs();
750 }
751 }
752 'k' => {
753 self.attrs.bg_color = None;
754 self.apply_attrs();
755 }
756
757 '{' => self.start_escape(),
759 '}' => self.end_escape(),
760
761 'G' => {
763 let n = if arg > 0 { arg as usize } else { 1 };
764 for _ in 0..n {
765 self.output.push(' ');
766 }
767 }
768
769 'v' => {
771 let idx = if arg == 0 { 1 } else { arg };
772 if idx > 0 && (idx as usize) <= self.ctx.psvar.len() {
773 self.output.push_str(&self.ctx.psvar[idx as usize - 1]);
774 }
775 }
776
777 '_' => {
779 if !self.ctx.cmd_stack.is_empty() {
780 let n = if arg == 0 {
781 self.ctx.cmd_stack.len()
782 } else if arg > 0 {
783 (arg as usize).min(self.ctx.cmd_stack.len())
784 } else {
785 ((-arg) as usize).min(self.ctx.cmd_stack.len())
786 };
787
788 let names: Vec<&str> = if arg >= 0 {
789 self.ctx
790 .cmd_stack
791 .iter()
792 .rev()
793 .take(n)
794 .map(|s| s.name())
795 .collect()
796 } else {
797 self.ctx
798 .cmd_stack
799 .iter()
800 .take(n)
801 .map(|s| s.name())
802 .collect()
803 };
804 self.output.push_str(&names.join(" "));
805 }
806 }
807
808 'E' => {
810 self.start_escape();
811 self.output.push_str("\x1b[K");
812 self.end_escape();
813 }
814
815 '%' => self.output.push('%'),
817 ')' => self.output.push(')'),
818 '\0' => {}
819
820 _ => {
822 self.output.push('%');
823 self.output.push(c);
824 }
825 }
826 }
827
828 pub fn expand(mut self) -> String {
830 while let Some(c) = self.advance() {
831 if c == '%' && self.prompt_percent {
832 self.process_percent();
833 } else if c == '!' && self.prompt_bang {
834 if self.peek() == Some('!') {
835 self.advance();
836 self.output.push('!');
837 } else {
838 self.output.push_str(&self.ctx.histnum.to_string());
839 }
840 } else {
841 self.output.push(c);
842 }
843 }
844
845 if self.attrs.bold
847 || self.attrs.underline
848 || self.attrs.standout
849 || self.attrs.fg_color.is_some()
850 || self.attrs.bg_color.is_some()
851 {
852 self.start_escape();
853 self.output.push_str("\x1b[0m");
854 self.end_escape();
855 }
856
857 self.output
858 }
859}
860
861fn convert_zsh_time_format(fmt: &str) -> String {
863 let mut result = String::new();
864 let mut chars = fmt.chars().peekable();
865
866 while let Some(c) = chars.next() {
867 if c == '%' {
868 match chars.next() {
869 Some('a') => result.push_str("%a"), Some('A') => result.push_str("%A"), Some('b') | Some('h') => result.push_str("%b"), Some('B') => result.push_str("%B"), Some('c') => result.push_str("%c"), Some('C') => result.push_str("%y"), Some('d') => result.push_str("%d"), Some('D') => result.push_str("%m/%d/%y"), Some('e') => result.push_str("%e"), Some('f') => result.push_str("%e"), Some('F') => result.push_str("%Y-%m-%d"), Some('H') => result.push_str("%H"), Some('I') => result.push_str("%I"), Some('j') => result.push_str("%j"), Some('k') => result.push_str("%k"), Some('K') => result.push_str("%H"), Some('l') => result.push_str("%l"), Some('L') => result.push_str("%3f"), Some('m') => result.push_str("%m"), Some('M') => result.push_str("%M"), Some('n') => result.push('\n'),
890 Some('N') => result.push_str("%9f"), Some('p') => result.push_str("%p"), Some('P') => result.push_str("%P"), Some('r') => result.push_str("%r"), Some('R') => result.push_str("%R"), Some('s') => result.push_str("%s"), Some('S') => result.push_str("%S"), Some('t') => result.push('\t'),
898 Some('T') => result.push_str("%T"), Some('u') => result.push_str("%u"), Some('U') => result.push_str("%U"), Some('V') => result.push_str("%V"), Some('w') => result.push_str("%w"), Some('W') => result.push_str("%W"), Some('x') => result.push_str("%x"), Some('X') => result.push_str("%X"), Some('y') => result.push_str("%y"), Some('Y') => result.push_str("%Y"), Some('z') => result.push_str("%z"), Some('Z') => result.push_str("%Z"), Some('%') => result.push('%'),
911 Some(other) => {
912 result.push('%');
913 result.push(other);
914 }
915 None => result.push('%'),
916 }
917 } else {
918 result.push(c);
919 }
920 }
921
922 result
923}
924
925pub fn expand_prompt(s: &str, ctx: &PromptContext) -> String {
927 PromptExpander::new(s, ctx).expand()
928}
929
930pub fn expand_prompt_default(s: &str) -> String {
932 let ctx = PromptContext::default();
933 expand_prompt(s, &ctx)
934}
935
936pub fn prompt_width(s: &str) -> usize {
938 let mut width = 0;
939 let mut in_escape = false;
940 let mut chars = s.chars().peekable();
941
942 while let Some(c) = chars.next() {
943 match c {
944 '\x01' => in_escape = true, '\x02' => in_escape = false, '\x1b' => {
947 while let Some(&next) = chars.peek() {
949 chars.next();
950 if next == 'm' {
951 break;
952 }
953 }
954 }
955 _ if !in_escape => {
956 width += unicode_width::UnicodeWidthChar::width(c).unwrap_or(1);
957 }
958 _ => {}
959 }
960 }
961
962 width
963}
964
965pub fn prompt_truncate(s: &str, max_width: usize, from_right: bool, indicator: &str) -> String {
974 let visible_len = prompt_width(s);
975 if visible_len <= max_width {
976 return s.to_string();
977 }
978
979 let ind_len = indicator.len();
980 if max_width <= ind_len {
981 return indicator[..max_width.min(ind_len)].to_string();
982 }
983
984 let keep = max_width - ind_len;
985
986 if from_right {
987 let mut result = String::new();
989 let mut width = 0;
990 for c in s.chars() {
991 let cw = unicode_width::UnicodeWidthChar::width(c).unwrap_or(1);
992 if width + cw > keep {
993 break;
994 }
995 result.push(c);
996 width += cw;
997 }
998 result.push_str(indicator);
999 result
1000 } else {
1001 let chars: Vec<char> = s.chars().collect();
1003 let total_chars = chars.len();
1004 let mut width = 0;
1005 let mut start = total_chars;
1006 for i in (0..total_chars).rev() {
1007 let cw = unicode_width::UnicodeWidthChar::width(chars[i]).unwrap_or(1);
1008 if width + cw > keep {
1009 break;
1010 }
1011 width += cw;
1012 start = i;
1013 }
1014 let mut result = indicator.to_string();
1015 for &c in &chars[start..] {
1016 result.push(c);
1017 }
1018 result
1019 }
1020}
1021
1022pub fn countprompt(s: &str) -> (usize, usize) {
1025 let width = prompt_width(s);
1026 let lines = s.chars().filter(|&c| c == '\n').count();
1027 (width, lines)
1028}
1029
1030pub struct CmdStack {
1032 stack: Vec<CmdState>,
1033}
1034
1035impl CmdStack {
1036 pub fn new() -> Self {
1037 CmdStack { stack: Vec::new() }
1038 }
1039
1040 pub fn push(&mut self, state: CmdState) {
1041 self.stack.push(state);
1042 }
1043
1044 pub fn pop(&mut self) -> Option<CmdState> {
1045 self.stack.pop()
1046 }
1047
1048 pub fn top(&self) -> Option<&CmdState> {
1049 self.stack.last()
1050 }
1051
1052 pub fn depth(&self) -> usize {
1053 self.stack.len()
1054 }
1055
1056 pub fn as_slice(&self) -> &[CmdState] {
1057 &self.stack
1058 }
1059}
1060
1061impl Default for CmdStack {
1062 fn default() -> Self {
1063 Self::new()
1064 }
1065}
1066
1067pub fn match_named_colour(name: &str) -> Option<u8> {
1069 match name.to_lowercase().as_str() {
1070 "black" => Some(0),
1071 "red" => Some(1),
1072 "green" => Some(2),
1073 "yellow" => Some(3),
1074 "blue" => Some(4),
1075 "magenta" => Some(5),
1076 "cyan" => Some(6),
1077 "white" => Some(7),
1078 "default" => Some(9),
1079 _ => name.parse::<u8>().ok(),
1080 }
1081}
1082
1083pub fn output_colour(colour: u8, is_fg: bool) -> String {
1085 let base = if is_fg { 30 } else { 40 };
1086 if colour < 8 {
1087 format!("\x1b[{}m", base + colour)
1088 } else if colour < 16 {
1089 format!("\x1b[{};1m", base + colour - 8)
1090 } else {
1091 let mode = if is_fg { 38 } else { 48 };
1092 format!("\x1b[{};5;{}m", mode, colour)
1093 }
1094}
1095
1096pub fn output_truecolor(r: u8, g: u8, b: u8, is_fg: bool) -> String {
1098 let mode = if is_fg { 38 } else { 48 };
1099 format!("\x1b[{};2;{};{};{}m", mode, r, g, b)
1100}
1101
1102pub fn parsehighlight(spec: &str) -> TextAttrs {
1104 let mut attrs = TextAttrs::default();
1105 for part in spec.split(',') {
1106 let part = part.trim();
1107 match part {
1108 "bold" => attrs.bold = true,
1109 "underline" => attrs.underline = true,
1110 "standout" => attrs.standout = true,
1111 "none" => {
1112 attrs = TextAttrs::default();
1113 }
1114 s if s.starts_with("fg=") => {
1115 let color_name = &s[3..];
1116 if let Some(code) = match_named_colour(color_name) {
1117 attrs.fg_color = Some(Color::Numbered(code));
1118 }
1119 }
1120 s if s.starts_with("bg=") => {
1121 let color_name = &s[3..];
1122 if let Some(code) = match_named_colour(color_name) {
1123 attrs.bg_color = Some(Color::Numbered(code));
1124 }
1125 }
1126 _ => {}
1127 }
1128 }
1129 attrs
1130}
1131
1132pub fn apply_text_attributes(attrs: &TextAttrs) -> String {
1134 let mut codes = Vec::new();
1135 if attrs.bold {
1136 codes.push("1");
1137 }
1138 if attrs.underline {
1139 codes.push("4");
1140 }
1141 if attrs.standout {
1142 codes.push("7");
1143 }
1144 let fg_code;
1145 if let Some(ref color) = attrs.fg_color {
1146 fg_code = color.to_ansi_fg();
1147 codes.push(&fg_code);
1148 }
1149 let bg_code;
1150 if let Some(ref color) = attrs.bg_color {
1151 bg_code = color.to_ansi_bg();
1152 codes.push(&bg_code);
1153 }
1154 if codes.is_empty() {
1155 String::new()
1156 } else {
1157 format!("\x1b[{}m", codes.join(";"))
1158 }
1159}
1160
1161pub fn reset_text_attributes() -> &'static str {
1163 "\x1b[0m"
1164}
1165
1166pub fn set_default_colour_sequences() -> (String, String) {
1168 ("\x1b[0m".to_string(), "\x1b[0m".to_string())
1170}
1171
1172pub fn right_prompt_padding(
1174 left_width: usize,
1175 right_prompt: &str,
1176 term_width: usize,
1177 indent: usize,
1178) -> Option<String> {
1179 let right_width = prompt_width(right_prompt);
1180 let total = left_width + right_width + indent;
1181 if total >= term_width {
1182 return None; }
1184 let padding = term_width - total;
1185 Some(" ".repeat(padding))
1186}
1187
1188pub fn transient_prompt(_original: &str) -> String {
1190 String::new()
1191}
1192
1193pub fn promptpath(path: &str, npath: usize, tilde: bool, home: &str) -> String {
1199 let display = if tilde && !home.is_empty() && path.starts_with(home) {
1200 let rest = &path[home.len()..];
1201 if rest.is_empty() || rest.starts_with('/') {
1202 format!("~{}", rest)
1203 } else {
1204 path.to_string()
1205 }
1206 } else {
1207 path.to_string()
1208 };
1209
1210 if npath == 0 {
1211 return display;
1212 }
1213
1214 let components: Vec<&str> = display.split('/').filter(|s| !s.is_empty()).collect();
1216 if components.len() <= npath {
1217 return display;
1218 }
1219 components[components.len() - npath..].join("/")
1220}
1221
1222pub fn promptexpand(s: &str, ctx: &PromptContext) -> String {
1224 expand_prompt(s, ctx)
1225}
1226
1227pub fn zattrescape(attrs: &TextAttrs) -> String {
1229 let mut result = String::new();
1230 if attrs.bold {
1231 result.push_str("%B");
1232 }
1233 if attrs.underline {
1234 result.push_str("%U");
1235 }
1236 if attrs.standout {
1237 result.push_str("%S");
1238 }
1239 if let Some(ref color) = attrs.fg_color {
1240 result.push_str(&format!("%F{{{}}}", color_name(color)));
1241 }
1242 if let Some(ref color) = attrs.bg_color {
1243 result.push_str(&format!("%K{{{}}}", color_name(color)));
1244 }
1245 result
1246}
1247
1248fn color_name(c: &Color) -> String {
1249 match c {
1250 Color::Black => "black".to_string(),
1251 Color::Red => "red".to_string(),
1252 Color::Green => "green".to_string(),
1253 Color::Yellow => "yellow".to_string(),
1254 Color::Blue => "blue".to_string(),
1255 Color::Magenta => "magenta".to_string(),
1256 Color::Cyan => "cyan".to_string(),
1257 Color::White => "white".to_string(),
1258 Color::Default => "default".to_string(),
1259 Color::Numbered(n) => n.to_string(),
1260 Color::Rgb(r, g, b) => format!("#{:02x}{:02x}{:02x}", r, g, b),
1261 }
1262}
1263
1264pub fn parsecolorchar(arg: &str, is_fg: bool) -> Option<(Color, String)> {
1266 let color = Color::from_name(arg)?;
1267 let ansi = if is_fg {
1268 color.to_ansi_fg()
1269 } else {
1270 color.to_ansi_bg()
1271 };
1272 Some((color, ansi))
1273}
1274
1275pub fn pputc(buf: &mut String, c: char) {
1278 buf.push(c);
1279}
1280
1281pub fn addbufspc(_buf: &mut String, _need: usize) {
1284 }
1286
1287pub fn stradd(buf: &mut String, s: &str) {
1289 buf.push_str(s);
1290}
1291
1292pub fn tsetcap(cap: &str) -> String {
1294 match cap {
1296 "md" | "bold" => "\x1b[1m".to_string(),
1297 "me" | "sgr0" => "\x1b[0m".to_string(),
1298 "so" | "smso" => "\x1b[7m".to_string(),
1299 "se" | "rmso" => "\x1b[27m".to_string(),
1300 "us" | "smul" => "\x1b[4m".to_string(),
1301 "ue" | "rmul" => "\x1b[24m".to_string(),
1302 _ => String::new(),
1303 }
1304}
1305
1306pub fn putstr(cap: &str) -> String {
1308 tsetcap(cap)
1309}
1310
1311pub fn treplaceattrs(old: &TextAttrs, new: &TextAttrs) -> String {
1313 let mut result = String::new();
1314
1315 let need_reset = (old.bold && !new.bold)
1317 || (old.underline && !new.underline)
1318 || (old.standout && !new.standout);
1319
1320 if need_reset {
1321 result.push_str("\x1b[0m");
1322 if new.bold {
1324 result.push_str("\x1b[1m");
1325 }
1326 if new.underline {
1327 result.push_str("\x1b[4m");
1328 }
1329 if new.standout {
1330 result.push_str("\x1b[7m");
1331 }
1332 } else {
1333 if !old.bold && new.bold {
1335 result.push_str("\x1b[1m");
1336 }
1337 if !old.underline && new.underline {
1338 result.push_str("\x1b[4m");
1339 }
1340 if !old.standout && new.standout {
1341 result.push_str("\x1b[7m");
1342 }
1343 }
1344
1345 if old.fg_color != new.fg_color {
1347 if let Some(ref color) = new.fg_color {
1348 result.push_str(&color.to_ansi_fg());
1349 } else {
1350 result.push_str("\x1b[39m"); }
1352 }
1353 if old.bg_color != new.bg_color {
1354 if let Some(ref color) = new.bg_color {
1355 result.push_str(&color.to_ansi_bg());
1356 } else {
1357 result.push_str("\x1b[49m"); }
1359 }
1360
1361 result
1362}
1363
1364pub fn tsetattrs(attrs: &TextAttrs) -> String {
1366 apply_text_attributes(attrs)
1367}
1368
1369pub fn tunsetattrs(attrs: &TextAttrs) -> String {
1371 let mut result = String::new();
1372 if attrs.bold {
1373 result.push_str("\x1b[22m");
1374 }
1375 if attrs.underline {
1376 result.push_str("\x1b[24m");
1377 }
1378 if attrs.standout {
1379 result.push_str("\x1b[27m");
1380 }
1381 if attrs.fg_color.is_some() {
1382 result.push_str("\x1b[39m");
1383 }
1384 if attrs.bg_color.is_some() {
1385 result.push_str("\x1b[49m");
1386 }
1387 result
1388}
1389
1390pub fn match_colour(spec: &str, is_fg: bool) -> Option<String> {
1392 if let Some(code) = match_named_colour(spec) {
1394 return Some(output_colour(code, is_fg));
1395 }
1396 if spec.starts_with('#') && spec.len() == 7 {
1398 let r = u8::from_str_radix(&spec[1..3], 16).ok()?;
1399 let g = u8::from_str_radix(&spec[3..5], 16).ok()?;
1400 let b = u8::from_str_radix(&spec[5..7], 16).ok()?;
1401 return Some(output_truecolor(r, g, b, is_fg));
1402 }
1403 if let Ok(n) = spec.parse::<u8>() {
1405 return Some(output_colour(n, is_fg));
1406 }
1407 None
1408}
1409
1410pub fn match_highlight(spec: &str) -> (TextAttrs, TextAttrs) {
1412 let attrs = parsehighlight(spec);
1413 let mask = TextAttrs {
1414 bold: attrs.bold,
1415 underline: attrs.underline,
1416 standout: attrs.standout,
1417 fg_color: if attrs.fg_color.is_some() {
1418 Some(Color::Default)
1419 } else {
1420 None
1421 },
1422 bg_color: if attrs.bg_color.is_some() {
1423 Some(Color::Default)
1424 } else {
1425 None
1426 },
1427 };
1428 (attrs, mask)
1429}
1430
1431pub fn output_highlight(attrs: &TextAttrs) -> String {
1433 apply_text_attributes(attrs)
1434}
1435
1436#[cfg(test)]
1437mod tests {
1438 use super::*;
1439
1440 fn test_ctx() -> PromptContext {
1441 PromptContext {
1442 pwd: "/home/user/projects/test".to_string(),
1443 home: "/home/user".to_string(),
1444 user: "testuser".to_string(),
1445 host: "myhost.example.com".to_string(),
1446 host_short: "myhost".to_string(),
1447 tty: "/dev/pts/0".to_string(),
1448 lastval: 0,
1449 histnum: 42,
1450 shlvl: 2,
1451 num_jobs: 1,
1452 is_root: false,
1453 cmd_stack: vec![],
1454 psvar: vec!["one".to_string(), "two".to_string()],
1455 term_width: 80,
1456 lineno: 10,
1457 }
1458 }
1459
1460 #[test]
1461 fn test_directory() {
1462 let ctx = test_ctx();
1463 assert_eq!(expand_prompt("%~", &ctx), "~/projects/test");
1464 assert_eq!(expand_prompt("%/", &ctx), "/home/user/projects/test");
1465 assert_eq!(expand_prompt("%d", &ctx), "/home/user/projects/test");
1466 assert_eq!(expand_prompt("%1~", &ctx), "test");
1467 assert_eq!(expand_prompt("%2~", &ctx), "projects/test");
1468 assert_eq!(expand_prompt("%c", &ctx), "test");
1469 assert_eq!(expand_prompt("%2c", &ctx), "projects/test");
1470 }
1471
1472 #[test]
1473 fn test_user_host() {
1474 let ctx = test_ctx();
1475 assert_eq!(expand_prompt("%n", &ctx), "testuser");
1476 assert_eq!(expand_prompt("%M", &ctx), "myhost.example.com");
1477 assert_eq!(expand_prompt("%m", &ctx), "myhost");
1478 assert_eq!(expand_prompt("%2m", &ctx), "myhost.example");
1479 }
1480
1481 #[test]
1482 fn test_status() {
1483 let mut ctx = test_ctx();
1484 ctx.lastval = 127;
1485 assert_eq!(expand_prompt("%?", &ctx), "127");
1486 assert_eq!(expand_prompt("%#", &ctx), "%");
1487 }
1488
1489 #[test]
1490 fn test_history() {
1491 let ctx = test_ctx();
1492 assert_eq!(expand_prompt("%h", &ctx), "42");
1493 assert_eq!(expand_prompt("%!", &ctx), "42");
1494 }
1495
1496 #[test]
1497 fn test_misc() {
1498 let ctx = test_ctx();
1499 assert_eq!(expand_prompt("%L", &ctx), "2");
1500 assert_eq!(expand_prompt("%j", &ctx), "1");
1501 assert_eq!(expand_prompt("%i", &ctx), "10");
1502 assert_eq!(expand_prompt("%%", &ctx), "%");
1503 }
1504
1505 #[test]
1506 fn test_psvar() {
1507 let ctx = test_ctx();
1508 assert_eq!(expand_prompt("%v", &ctx), "one");
1509 assert_eq!(expand_prompt("%1v", &ctx), "one");
1510 assert_eq!(expand_prompt("%2v", &ctx), "two");
1511 assert_eq!(expand_prompt("%3v", &ctx), ""); }
1513
1514 #[test]
1515 fn test_conditional() {
1516 let mut ctx = test_ctx();
1517 ctx.lastval = 0;
1518 assert_eq!(expand_prompt("%(?.ok.fail)", &ctx), "ok");
1519 ctx.lastval = 1;
1520 assert_eq!(expand_prompt("%(?.ok.fail)", &ctx), "fail");
1521 }
1522
1523 #[test]
1524 fn test_time_format() {
1525 let fmt = convert_zsh_time_format("%Y-%m-%d %H:%M:%S");
1526 assert_eq!(fmt, "%Y-%m-%d %H:%M:%S");
1527 }
1528
1529 #[test]
1530 fn test_bang_expansion() {
1531 let ctx = test_ctx();
1532 let exp = PromptExpander::new("cmd !!", &ctx).with_prompt_bang(true);
1533 assert_eq!(exp.expand(), "cmd !");
1534
1535 let exp2 = PromptExpander::new("cmd !", &ctx).with_prompt_bang(true);
1536 assert_eq!(exp2.expand(), "cmd 42");
1537 }
1538}
1539
1540pub fn putpromptchar(c: char, ctx: &PromptContext, buf: &mut String) {
1550 if c == '%' {
1551 buf.push(c);
1555 } else {
1556 buf.push(c);
1557 }
1558}
1559
1560pub fn mixattrs(primary: &TextAttrs, mask: &TextAttrs, secondary: &TextAttrs) -> TextAttrs {
1565 TextAttrs {
1566 bold: if mask.bold {
1567 primary.bold
1568 } else {
1569 secondary.bold
1570 },
1571 underline: if mask.underline {
1572 primary.underline
1573 } else {
1574 secondary.underline
1575 },
1576 standout: if mask.standout {
1577 primary.standout
1578 } else {
1579 secondary.standout
1580 },
1581 fg_color: if mask.fg_color.is_some() {
1582 primary.fg_color.clone()
1583 } else {
1584 secondary.fg_color.clone()
1585 },
1586 bg_color: if mask.bg_color.is_some() {
1587 primary.bg_color.clone()
1588 } else {
1589 secondary.bg_color.clone()
1590 },
1591 }
1592}
1593
1594pub fn truecolor_terminal() -> bool {
1596 if let Ok(ct) = std::env::var("COLORTERM") {
1598 if ct == "truecolor" || ct == "24bit" {
1599 return true;
1600 }
1601 }
1602 if let Ok(term) = std::env::var("TERM") {
1604 if term.contains("256color") || term.contains("direct") || term.contains("kitty") {
1605 return true;
1606 }
1607 }
1608 false
1609}
1610
1611pub fn set_colour_code(spec: &str) -> Option<String> {
1613 match_colour(spec, true)
1614}
1615
1616pub fn allocate_colour_buffer() {
1618 }
1620
1621pub fn free_colour_buffer() {
1623 }
1625
1626pub fn set_colour_attribute(color: &Color, is_fg: bool) -> String {
1628 if is_fg {
1629 color.to_ansi_fg()
1630 } else {
1631 color.to_ansi_bg()
1632 }
1633}