starship/src/modules/status.rs

712 lines
23 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use std::string::ToString;
use super::{Context, Module, ModuleConfig};
use crate::configs::status::StatusConfig;
use crate::formatter::{string_formatter::StringFormatterError, StringFormatter};
use crate::segment::Segment;
type ExitCode = i32;
type SignalNumber = u32;
#[derive(PartialEq)]
enum PipeStatusStatus<'a> {
Disabled,
NoPipe,
Pipe(&'a Vec<std::string::String>),
}
/// Creates a module with the status of the last command
///
/// Will display the status
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
let mut module = context.new_module("status");
let config = StatusConfig::try_load(module.config);
// As we default to disabled=true, we have to check here after loading our config module,
// before it was only checking against whatever is in the config starship.toml
if config.disabled {
return None;
};
let exit_code = context.properties.status_code.as_deref().unwrap_or("0");
let pipestatus_status = match &context.properties.pipestatus {
None => PipeStatusStatus::Disabled,
Some(ps) => match ps.len() > 1 {
true => PipeStatusStatus::Pipe(ps),
false => PipeStatusStatus::NoPipe,
},
};
let pipestatus_status = match config.pipestatus {
true => pipestatus_status,
false => PipeStatusStatus::Disabled,
};
// Exit code is zero while success_symbol and pipestatus are all zero or disabled/missing
if exit_code == "0"
&& config.success_symbol.is_empty()
&& (match pipestatus_status {
PipeStatusStatus::Pipe(ps) => ps.iter().all(|s| s == "0"),
_ => true,
})
{
return None;
}
let segment_format = config.pipestatus_segment_format.unwrap_or(config.format);
// Create pipestatus string
let pipestatus = match pipestatus_status {
PipeStatusStatus::Pipe(pipestatus) => pipestatus
.iter()
.map(
|ec| match format_exit_code(ec.as_str(), segment_format, None, &config, context) {
Ok(segments) => segments
.into_iter()
.map(|s| s.to_string())
.collect::<String>(),
Err(_) => "".to_string(),
},
)
.collect::<Vec<String>>()
.join(config.pipestatus_separator),
_ => "".to_string(),
};
let main_format = match pipestatus_status {
PipeStatusStatus::Pipe(_) => config.pipestatus_format,
_ => config.format,
};
let parsed = format_exit_code(exit_code, main_format, Some(&pipestatus), &config, context);
module.set_segments(match parsed {
Ok(segments) => segments,
Err(_error) => {
log::warn!("Error parsing format string in `status.format`");
return None;
}
});
Some(module)
}
fn format_exit_code<'a>(
exit_code: &'a str,
format: &'a str,
pipestatus: Option<&str>,
config: &'a StatusConfig,
context: &'a Context,
) -> Result<Vec<Segment>, StringFormatterError> {
// First, parse as i64 to accept both i32 or u32, then normalize to i32.
let exit_code_int: ExitCode = match exit_code.parse::<i64>() {
Ok(i) => i as ExitCode,
Err(_) => {
log::warn!("Error parsing exit_code string to int");
return Ok(Vec::new());
}
};
let hex_status = format!("0x{:X}", exit_code_int);
let common_meaning = status_common_meaning(exit_code_int);
let raw_signal_number = match config.recognize_signal_code {
true => status_to_signal(exit_code_int),
false => None,
};
let signal_number = raw_signal_number.map(|sn| sn.to_string());
let signal_name =
raw_signal_number.and_then(|sn| status_signal_name(sn).or(signal_number.as_deref()));
// If not a signal and not a common meaning, it should at least print the raw exit code number
let maybe_exit_code_number = match common_meaning.is_none() && signal_name.is_none() {
true => Some(exit_code),
false => None,
};
StringFormatter::new(format).and_then(|formatter| {
formatter
.map_meta(|var, _| match var {
"symbol" => match exit_code_int {
0 => Some(config.success_symbol),
126 if config.map_symbol => Some(config.not_executable_symbol),
127 if config.map_symbol => Some(config.not_found_symbol),
130 if config.recognize_signal_code && config.map_symbol => {
Some(config.sigint_symbol)
}
x if (129..256).contains(&x)
&& config.recognize_signal_code
&& config.map_symbol =>
{
Some(config.signal_symbol)
}
_ => Some(config.symbol),
},
_ => None,
})
.map_style(|variable| match variable {
"style" => Some(Ok(config.style)),
_ => None,
})
.map(|variable| match variable {
"status" => Some(Ok(exit_code)),
"hex_status" => Some(Ok(hex_status.as_ref())),
"int" => Some(Ok(exit_code)),
"maybe_int" => Ok(maybe_exit_code_number).transpose(),
"common_meaning" => Ok(common_meaning).transpose(),
"signal_number" => Ok(signal_number.as_deref()).transpose(),
"signal_name" => Ok(signal_name).transpose(),
"pipestatus" => {
let pipestatus = pipestatus.unwrap_or_else(|| {
// We might enter this case if pipestatus hasn't
// been processed yet, which means that it has been
// set in format
log::warn!("pipestatus variable is only available in pipestatus_format");
""
});
Some(Ok(pipestatus))
}
_ => None,
})
.parse(None, Some(context))
})
}
fn status_common_meaning(ex: ExitCode) -> Option<&'static str> {
// Over 128 are Signal exit code
if ex > 128 {
return None;
}
match ex {
0 => Some(""), // SUCCESS can be defined by $success_symbol if the user wishes too.
1 => Some("ERROR"),
2 => Some("USAGE"),
126 => Some("NOPERM"),
127 => Some("NOTFOUND"),
_ => None,
}
}
fn status_to_signal(ex: ExitCode) -> Option<SignalNumber> {
if ex < 129 {
return None;
}
let sn = ex - 128;
Some(sn as u32)
}
fn status_signal_name(signal: SignalNumber) -> Option<&'static str> {
match signal {
1 => Some("HUP"), // 128 + 1
2 => Some("INT"), // 128 + 2
3 => Some("QUIT"), // 128 + 3
4 => Some("ILL"), // 128 + 4
5 => Some("TRAP"), // 128 + 5
6 => Some("IOT"), // 128 + 6
7 => Some("BUS"), // 128 + 7
8 => Some("FPE"), // 128 + 8
9 => Some("KILL"), // 128 + 9
10 => Some("USR1"), // 128 + 10
11 => Some("SEGV"), // 128 + 11
12 => Some("USR2"), // 128 + 12
13 => Some("PIPE"), // 128 + 13
14 => Some("ALRM"), // 128 + 14
15 => Some("TERM"), // 128 + 15
16 => Some("STKFLT"), // 128 + 16
17 => Some("CHLD"), // 128 + 17
18 => Some("CONT"), // 128 + 18
19 => Some("STOP"), // 128 + 19
20 => Some("TSTP"), // 128 + 20
21 => Some("TTIN"), // 128 + 21
22 => Some("TTOU"), // 128 + 22
_ => None,
}
}
#[cfg(test)]
mod tests {
use ansi_term::Color;
use crate::test::ModuleRenderer;
#[test]
fn success_status_success_symbol_empty() {
let expected = None;
// Status code 0 and success_symbol = ""
let actual = ModuleRenderer::new("status")
.config(toml::toml! {
[status]
success_symbol = ""
disabled = false
})
.status(0)
.collect();
assert_eq!(expected, actual);
// Status code 0 and success_symbol is missing
let actual = ModuleRenderer::new("status")
.config(toml::toml! {
[status]
disabled = false
})
.status(0)
.collect();
assert_eq!(expected, actual);
// No status code and success_symbol = ""
let actual = ModuleRenderer::new("status")
.config(toml::toml! {
[status]
success_symbol = ""
disabled = false
})
.collect();
assert_eq!(expected, actual);
// No status code and success_symbol is missing
let actual = ModuleRenderer::new("status")
.config(toml::toml! {
[status]
disabled = false
})
.collect();
assert_eq!(expected, actual);
}
#[test]
fn success_status_success_symbol_filled() {
let expected = Some(format!("{} ", Color::Red.bold().paint("0")));
// Status code 0
let actual = ModuleRenderer::new("status")
.config(toml::toml! {
[status]
success_symbol = "✔️"
disabled = false
})
.status(0)
.collect();
assert_eq!(expected, actual);
// No status code
let actual = ModuleRenderer::new("status")
.config(toml::toml! {
[status]
success_symbol = "✔️"
disabled = false
})
.collect();
assert_eq!(expected, actual);
}
#[test]
fn not_enabled() {
let expected = None;
let actual = ModuleRenderer::new("status").status(1).collect();
assert_eq!(expected, actual);
}
#[test]
fn failure_status() {
let exit_values = [1, 2, 130];
for status in &exit_values {
let expected = Some(format!(
"{} ",
Color::Red.bold().paint(format!("{}", status))
));
let actual = ModuleRenderer::new("status")
.config(toml::toml! {
[status]
symbol = ""
disabled = false
})
.status(*status)
.collect();
assert_eq!(expected, actual);
}
}
#[test]
fn failure_hex_status() {
let exit_values = [1, 2, 130, -2_147_467_260, 2_147_500_036];
let string_values = ["0x1", "0x2", "0x82", "0x80004004", "0x80004004"];
for (exit_value, string_value) in exit_values.iter().zip(string_values) {
let expected = Some(format!(
"{} ",
Color::Red.bold().paint(format!("{}", string_value))
));
let actual = ModuleRenderer::new("status")
.config(toml::toml! {
[status]
symbol = ""
disabled = false
format = "[${symbol}${hex_status}]($style) "
})
.status(*exit_value)
.collect();
assert_eq!(expected, actual);
}
}
#[test]
fn signal_name() {
let exit_values = [1, 2, 126, 127, 130, 101];
let exit_values_name = [
Some("ERROR"),
Some("USAGE"),
Some("NOPERM"),
Some("NOTFOUND"),
Some("INT"),
None,
];
for (status, name) in exit_values.iter().zip(&exit_values_name) {
let expected = name.map(std::string::ToString::to_string);
let actual = ModuleRenderer::new("status")
.config(toml::toml! {
[status]
format = "$common_meaning$signal_name"
disabled = false
})
.status(*status)
.collect();
assert_eq!(expected, actual);
}
}
#[test]
fn exit_code_name_no_signal() {
let exit_values = [1, 2, 126, 127, 130, 101, 132];
let exit_values_name = [
Some("ERROR"),
Some("USAGE"),
Some("NOPERM"),
Some("NOTFOUND"),
None,
None,
None,
];
for (status, name) in exit_values.iter().zip(&exit_values_name) {
let expected = name.map(std::string::ToString::to_string);
let actual = ModuleRenderer::new("status")
.config(toml::toml! {
[status]
format = "$common_meaning$signal_name"
recognize_signal_code = false
disabled = false
})
.status(*status)
.collect();
assert_eq!(expected, actual);
}
}
#[test]
fn maybe_exit_code_number() {
let exit_values = [1, 2, 126, 127, 130, 101, 6, -3];
let exit_values_name = [
None,
None,
None,
None,
None,
Some("101"),
Some("6"),
Some("-3"),
];
for (status, name) in exit_values.iter().zip(&exit_values_name) {
let expected = name.map(std::string::ToString::to_string);
let actual = ModuleRenderer::new("status")
.config(toml::toml! {
[status]
format = "$maybe_int"
disabled = false
})
.status(*status)
.collect();
assert_eq!(expected, actual);
}
}
#[test]
fn special_symbols() {
let exit_values = [1, 126, 127, 130, 131];
let exit_values_name = ["🔴", "🚫", "🔍", "🧱", ""];
for (status, name) in exit_values.iter().zip(&exit_values_name) {
let expected = Some((*name).to_string());
let actual = ModuleRenderer::new("status")
.config(toml::toml! {
[status]
format = "$symbol"
symbol = "🔴"
not_executable_symbol = "🚫"
not_found_symbol = "🔍"
sigint_symbol = "🧱"
signal_symbol = ""
recognize_signal_code = true
map_symbol = true
disabled = false
})
.status(*status)
.collect();
assert_eq!(expected, actual);
}
}
#[test]
fn special_symbols_no_signals() {
let exit_values = [1, 126, 127, 130, 131];
let exit_values_name = ["🔴", "🚫", "🔍", "🔴", "🔴"];
for (status, name) in exit_values.iter().zip(&exit_values_name) {
let expected = Some((*name).to_string());
let actual = ModuleRenderer::new("status")
.config(toml::toml! {
[status]
format = "$symbol"
symbol = "🔴"
not_executable_symbol = "🚫"
not_found_symbol = "🔍"
sigint_symbol = "🧱"
signal_symbol = ""
recognize_signal_code = false
map_symbol = true
disabled = false
})
.status(*status)
.collect();
assert_eq!(expected, actual);
}
}
#[test]
fn pipeline_uses_pipestatus_format() {
let exit_values = [
[0, 1, 0, 0],
[0, 1, 2, 3],
[130, 126, 131, 127],
[1, 1, 1, 1],
];
let exit_values_rendered = [
"PSF 🟢=🔴 🟢 🟢",
"PSF 🟢=🔴 🔴 🔴",
"PSF 🧱=🚫 ⚡ 🔍",
"PSF 🔴=🔴 🔴 🔴",
];
for (status, rendered) in exit_values.iter().zip(&exit_values_rendered) {
let main_exit_code = status[0];
let pipe_exit_code = &status[1..];
let expected = Some((*rendered).to_string());
let actual = ModuleRenderer::new("status")
.config(toml::toml! {
[status]
format = "$symbol"
symbol = "🔴"
success_symbol = "🟢"
not_executable_symbol = "🚫"
not_found_symbol = "🔍"
sigint_symbol = "🧱"
signal_symbol = ""
recognize_signal_code = true
map_symbol = true
pipestatus = true
pipestatus_separator = " "
pipestatus_format = "PSF $symbol=$pipestatus"
disabled = false
})
.status(main_exit_code)
.pipestatus(pipe_exit_code)
.collect();
assert_eq!(expected, actual);
}
}
#[test]
fn pipeline_no_map_symbols() {
let exit_values = [
[0, 1, 0, 0],
[0, 1, 2, 3],
[130, 126, 131, 127],
[1, 1, 1, 1],
];
let exit_values_rendered = [
"PSF 🟢=🔴1 🟢0 🟢0",
"PSF 🟢=🔴1 🔴2 🔴3",
"PSF INT🔴=🔴126 🔴1313 🔴127",
"PSF 🔴=🔴1 🔴1 🔴1",
];
for (status, rendered) in exit_values.iter().zip(&exit_values_rendered) {
let main_exit_code = status[0];
let pipe_exit_code = &status[1..];
let expected = Some((*rendered).to_string());
let actual = ModuleRenderer::new("status")
.config(toml::toml! {
[status]
format = "$symbol$int$signal_number"
symbol = "🔴"
success_symbol = "🟢"
not_executable_symbol = "🚫"
not_found_symbol = "🔍"
sigint_symbol = "🧱"
signal_symbol = ""
recognize_signal_code = true
map_symbol = false
pipestatus = true
pipestatus_separator = " "
pipestatus_format = "PSF $signal_name$symbol=$pipestatus"
disabled = false
})
.status(main_exit_code)
.pipestatus(pipe_exit_code)
.collect();
assert_eq!(expected, actual);
}
}
#[test]
fn successful_pipeline() {
let pipe_exit_code = [0, 0, 0];
let main_exit_code = 0;
let expected = None;
let actual = ModuleRenderer::new("status")
.config(toml::toml! {
[status]
disabled = false
})
.status(main_exit_code)
.pipestatus(&pipe_exit_code)
.collect();
assert_eq!(expected, actual);
}
#[test]
fn successful_pipeline_pipestatus_enabled() {
let pipe_exit_code = [0, 0, 0];
let main_exit_code = 0;
let expected = None;
let actual = ModuleRenderer::new("status")
.config(toml::toml! {
[status]
disabled = false
pipestatus = true
})
.status(main_exit_code)
.pipestatus(&pipe_exit_code)
.collect();
assert_eq!(expected, actual);
}
#[test]
fn pipeline_disabled() {
let exit_values = [[130, 126, 131, 127], [1, 1, 1, 1]];
let exit_values_rendered = ["F 🧱", "F 🔴"];
for (status, rendered) in exit_values.iter().zip(&exit_values_rendered) {
let main_exit_code = status[0];
let pipe_exit_code = &status[1..];
let expected = Some((*rendered).to_string());
let actual = ModuleRenderer::new("status")
.config(toml::toml! {
[status]
format = "F $symbol"
symbol = "🔴"
success_symbol = "🟢"
not_executable_symbol = "🚫"
not_found_symbol = "🔍"
sigint_symbol = "🧱"
signal_symbol = ""
recognize_signal_code = true
map_symbol = true
pipestatus = false
pipestatus_separator = " "
pipestatus_format = "PSF $symbol=$pipestatus"
disabled = false
})
.status(main_exit_code)
.pipestatus(pipe_exit_code)
.collect();
assert_eq!(expected, actual);
}
}
#[test]
fn pipeline_long() {
let exit_values = [
[130, 0, 0, 0, 30, 1, 2, 3, 142, 0, 0, 0, 130],
[1, 0, 0, 0, 30, 127, 126, 3, 142, 0, 230, 0, 2],
];
let exit_values_rendered = [
"PSF 130INT=🟢|🟢|🟢|🔴30|🔴|🔴|🔴3|⚡|🟢|🟢|🟢|🧱",
"PSF 1ERROR=🟢|🟢|🟢|🔴30|🔍|🚫|🔴3|⚡|🟢|⚡|🟢|🔴",
];
for (status, rendered) in exit_values.iter().zip(&exit_values_rendered) {
let main_exit_code = status[0];
let pipe_exit_code = &status[1..];
let expected = Some((*rendered).to_string());
let actual = ModuleRenderer::new("status")
.config(toml::toml! {
[status]
format = "$symbol$maybe_int"
symbol = "🔴"
success_symbol = "🟢"
not_executable_symbol = "🚫"
not_found_symbol = "🔍"
sigint_symbol = "🧱"
signal_symbol = ""
recognize_signal_code = true
map_symbol = true
pipestatus = true
pipestatus_separator = "|"
pipestatus_format = "PSF $int$common_meaning$signal_name=$pipestatus"
disabled = false
})
.status(main_exit_code)
.pipestatus(pipe_exit_code)
.collect();
assert_eq!(expected, actual);
}
}
#[test]
fn pipestatus_segment_format() {
let pipe_exit_code = &[0, 1];
let main_exit_code = 1;
let expected = Some("[0]|[1] => <1>".to_string());
let actual = ModuleRenderer::new("status")
.config(toml::toml! {
[status]
format = "\\($status\\)"
pipestatus = true
pipestatus_separator = "|"
pipestatus_format = "$pipestatus => <$status>"
pipestatus_segment_format = "\\[$status\\]"
disabled = false
})
.status(main_exit_code)
.pipestatus(pipe_exit_code)
.collect();
assert_eq!(expected, actual);
}
}