use std::io::{self, Write};
pub(crate) const REQUEST_MAGIC: &[u8] = b"ZTREQ";
pub(crate) const REQUEST_VERSION: &str = "3";
pub(crate) const REQUEST_FIELDS: &[&str] = &[
"PATH",
"HOME",
"GIT_DIR",
"GIT_WORK_TREE",
"GIT_CEILING_DIRECTORIES",
"VIRTUAL_ENV",
"CONDA_PREFIX",
"CONDA_DEFAULT_ENV",
"PERLBREW_PERL",
"PLENV_VERSION",
"PYENV_VERSION",
"PYENV_DIR",
"RUSTUP_TOOLCHAIN",
"RUSTUP_HOME",
"RBENV_DIR",
"RBENV_VERSION",
"NODENV_VERSION",
"NODENV_DIR",
"PLENV_DIR",
"RUBY_VERSION",
"JAVA_HOME",
"GOTOOLCHAIN",
"DOTNET_ROOT",
"JULIAUP_CHANNEL",
"JULIAUP_DEPOT_PATH",
"JULIA_PROJECT",
"JULIA_LOAD_PATH",
"JULIA_DEPOT_PATH",
"R_ARCH",
];
pub(crate) const CONTEXT_EXCLUDED: &[&str] = &["PATH", "HOME", "GIT_CEILING_DIRECTORIES"];
pub(crate) fn request_field_lines() -> String {
REQUEST_FIELDS
.iter()
.map(|field| format!(" request_line+=\"${{{field}:-}}\"$'\\0'"))
.collect::<Vec<_>>()
.join("\n")
}
pub(crate) fn context_field_lines() -> String {
REQUEST_FIELDS
.iter()
.copied()
.filter(|field| !CONTEXT_EXCLUDED.contains(field))
.map(|field| format!(" context_key+=\"|${{{field}:-}}\""))
.collect::<Vec<_>>()
.join("\n")
}
const MAGIC: &str = "ZTHEME1";
pub fn prompt_text(value: &str) -> String {
let mut escaped = String::with_capacity(value.len());
for character in value.chars() {
match character {
'%' => escaped.push_str("%%"),
'\t' | '\r' | '\n' => escaped.push(' '),
character if character.is_control() => escaped.push('?'),
character => escaped.push(character),
}
}
escaped
}
pub fn write_segment(
output: &mut impl Write,
generation: u64,
segment: &str,
fragment: &str,
) -> io::Result<()> {
write_fields(output, generation, &["segment", segment, fragment])
}
pub fn write_error(
output: &mut impl Write,
generation: u64,
segment: &str,
message: &str,
) -> io::Result<()> {
let message = prompt_text(message);
write_fields(output, generation, &["error", segment, &message])
}
pub fn write_done(output: &mut impl Write, generation: u64) -> io::Result<()> {
write_fields(output, generation, &["done"])
}
pub fn write_complete(output: &mut impl Write, generation: u64, group: &str) -> io::Result<()> {
write_fields(output, generation, &["complete", group])
}
fn write_fields(output: &mut impl Write, generation: u64, fields: &[&str]) -> io::Result<()> {
write!(output, "{MAGIC}\t{generation}")?;
for field in fields {
write!(output, "\t{field}")?;
}
writeln!(output)?;
output.flush()
}
#[cfg(test)]
mod tests {
use super::{prompt_text, write_complete, write_done, write_error, write_segment};
#[test]
fn prompt_text_escapes_prompt_sequences_and_controls() {
assert_eq!(prompt_text("100%\tready\n\u{1b}"), "100%% ready ?");
assert_eq!(prompt_text("Grüße 🚀"), "Grüße 🚀");
}
#[test]
fn records_have_exact_tab_delimited_framing() {
let mut output = Vec::new();
write_segment(&mut output, 7, "git", " main").unwrap();
write_complete(&mut output, 7, "git").unwrap();
write_error(&mut output, 7, "runtime", "bad\tvalue\nnext").unwrap();
write_complete(&mut output, 7, "runtime").unwrap();
write_done(&mut output, 7).unwrap();
assert_eq!(
String::from_utf8(output).unwrap(),
"ZTHEME1\t7\tsegment\tgit\t main\n\
ZTHEME1\t7\tcomplete\tgit\n\
ZTHEME1\t7\terror\truntime\tbad value next\n\
ZTHEME1\t7\tcomplete\truntime\n\
ZTHEME1\t7\tdone\n"
);
}
}