use std::string::ToString; use super::{Context, Module, RootModuleConfig}; 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), } /// Creates a module with the status of the last command /// /// Will display the status only if it is not 0 pub fn module<'a>(context: &'a Context) -> Option> { 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 and pipestatus is all zero or disabled/missing if exit_code == "0" && (match pipestatus_status { PipeStatusStatus::Pipe(ps) => ps.iter().all(|s| s == "0"), _ => true, }) { return None; } // Create pipestatus string let pipestatus = match pipestatus_status { PipeStatusStatus::Pipe(pipestatus) => pipestatus .iter() .map( |ec| match format_exit_code(ec.as_str(), config.format, None, &config, context) { Ok(segments) => segments .into_iter() .map(|s| s.to_string()) .collect::>() .join(""), Err(_) => "".to_string(), }, ) .collect::>() .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.to_string(), 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, StringFormatterError> { // First, parse as i64 to accept both i32 or u32, then normalize to i32. let exit_code_int: ExitCode = match exit_code.parse::() { 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_else(|| 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(""), 1 => Some("ERROR"), 2 => Some("USAGE"), 126 => Some("NOPERM"), 127 => Some("NOTFOUND"), _ => None, } } fn status_to_signal(ex: ExitCode) -> Option { 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() { let expected = None; // Status code 0 let actual = ModuleRenderer::new("status") .config(toml::toml! { [status] disabled = false }) .status(0) .collect(); assert_eq!(expected, actual); // No status code let actual = ModuleRenderer::new("status") .config(toml::toml! { [status] 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, -2147467260, 2147500036]; 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); } } }