1use crate::error;
4
5#[derive(Clone, Debug)]
7#[cfg_attr(
8 any(test, feature = "serde"),
9 derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
10)]
11pub enum PromptPiece {
12 AsciiCharacter(u32),
14 Backslash,
16 BellCharacter,
18 CarriageReturn,
20 CurrentCommandNumber,
22 CurrentHistoryNumber,
24 CurrentUser,
26 CurrentWorkingDirectory {
28 tilde_replaced: bool,
30 basename: bool,
32 },
33 Date(PromptDateFormat),
35 DollarOrPound,
37 EndNonPrintingSequence,
40 EscapeCharacter,
42 EscapedSequence(String),
44 Hostname {
46 only_up_to_first_dot: bool,
48 },
49 Literal(String),
51 Newline,
53 NumberOfManagedJobs,
55 ShellBaseName,
57 ShellRelease,
59 ShellVersion,
61 StartNonPrintingSequence,
64 TerminalDeviceBaseName,
66 Time(PromptTimeFormat),
68}
69
70#[derive(Clone, Debug)]
72#[cfg_attr(
73 any(test, feature = "serde"),
74 derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
75)]
76pub enum PromptDateFormat {
77 WeekdayMonthDate,
79 Custom(String),
81}
82
83#[derive(Clone, Debug)]
85#[cfg_attr(
86 any(test, feature = "serde"),
87 derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
88)]
89pub enum PromptTimeFormat {
90 TwelveHourAM,
92 TwelveHourHHMMSS,
94 TwentyFourHourHHMM,
96 TwentyFourHourHHMMSS,
98}
99
100peg::parser! {
101 grammar prompt_parser() for str {
102 pub(crate) rule prompt() -> Vec<PromptPiece> =
103 pieces:prompt_piece()*
104
105 rule prompt_piece() -> PromptPiece =
106 special_sequence() /
107 literal_sequence()
108
109 rule special_sequence() -> PromptPiece =
113 "\\a" { PromptPiece::BellCharacter } /
114 "\\A" { PromptPiece::Time(PromptTimeFormat::TwentyFourHourHHMM) } /
115 "\\d" { PromptPiece::Date(PromptDateFormat::WeekdayMonthDate) } /
116 "\\D{" f:date_format() "}" { PromptPiece::Date(PromptDateFormat::Custom(f)) } /
117 "\\e" { PromptPiece::EscapeCharacter } /
118 "\\h" { PromptPiece::Hostname { only_up_to_first_dot: true } } /
119 "\\H" { PromptPiece::Hostname { only_up_to_first_dot: false } } /
120 "\\j" { PromptPiece::NumberOfManagedJobs } /
121 "\\l" { PromptPiece::TerminalDeviceBaseName } /
122 "\\n" { PromptPiece::Newline } /
123 "\\r" { PromptPiece::CarriageReturn } /
124 "\\s" { PromptPiece::ShellBaseName } /
125 "\\t" { PromptPiece::Time(PromptTimeFormat::TwentyFourHourHHMMSS ) } /
126 "\\T" { PromptPiece::Time(PromptTimeFormat::TwelveHourHHMMSS ) } /
127 "\\@" { PromptPiece::Time(PromptTimeFormat::TwelveHourAM ) } /
128 "\\u" { PromptPiece::CurrentUser } /
129 "\\v" { PromptPiece::ShellVersion } /
130 "\\V" { PromptPiece::ShellRelease } /
131 "\\w" { PromptPiece::CurrentWorkingDirectory { tilde_replaced: true, basename: false, } } /
132 "\\W" { PromptPiece::CurrentWorkingDirectory { tilde_replaced: true, basename: true, } } /
133 "\\!" { PromptPiece::CurrentHistoryNumber } /
134 "\\#" { PromptPiece::CurrentCommandNumber } /
135 "\\$" { PromptPiece::DollarOrPound } /
136 "\\" n:octal_number() { PromptPiece::AsciiCharacter(n) } /
137 "\\\\" { PromptPiece::Backslash } /
138 "\\[" { PromptPiece::StartNonPrintingSequence } /
139 "\\]" { PromptPiece::EndNonPrintingSequence } /
140 s:$("\\" [_]) { PromptPiece::EscapedSequence(s.to_owned()) }
141
142 rule literal_sequence() -> PromptPiece =
143 s:$((!special_sequence() [c])+) { PromptPiece::Literal(s.to_owned()) }
144
145 rule date_format() -> String =
146 s:$([c if c != '}']*) { s.to_owned() }
147
148 rule octal_number() -> u32 =
149 s:$(['0'..='7']*<1,3>) {? u32::from_str_radix(s, 8).or(Err("invalid octal number")) }
150 }
151}
152
153pub fn parse(s: &str) -> Result<Vec<PromptPiece>, error::WordParseError> {
159 let result = prompt_parser::prompt(s).map_err(|e| error::WordParseError::Prompt(e.into()))?;
160 Ok(result)
161}
162
163#[cfg(test)]
164mod tests {
165 use anyhow::Result;
166 use pretty_assertions::assert_eq;
167
168 use super::*;
169
170 #[test]
171 fn basic_prompt() -> Result<()> {
172 assert_eq!(
173 parse(r"\u@\h:\w$ ")?,
174 &[
175 PromptPiece::CurrentUser,
176 PromptPiece::Literal("@".to_owned()),
177 PromptPiece::Hostname {
178 only_up_to_first_dot: true
179 },
180 PromptPiece::Literal(":".to_owned()),
181 PromptPiece::CurrentWorkingDirectory {
182 tilde_replaced: true,
183 basename: false
184 },
185 PromptPiece::Literal("$ ".to_owned()),
186 ]
187 );
188
189 Ok(())
190 }
191
192 #[test]
193 fn brackets_and_vars() -> Result<()> {
194 assert_eq!(
195 parse(r"\[$foo\]\u > ")?,
196 &[
197 PromptPiece::StartNonPrintingSequence,
198 PromptPiece::Literal("$foo".to_owned()),
199 PromptPiece::EndNonPrintingSequence,
200 PromptPiece::CurrentUser,
201 PromptPiece::Literal(" > ".to_owned()),
202 ]
203 );
204
205 Ok(())
206 }
207}