use serde_json::{json, Map, Value};
#[derive(Debug, Clone, Default)]
pub struct ShellSegmentInfo {
pub jobnum: Option<i32>,
pub last_exit_code: Option<i32>,
pub last_pipe_status: Vec<i32>,
pub mode: Option<String>,
pub default_mode: Option<String>,
pub parser_state: Option<String>,
pub shortened_path: Option<String>,
}
pub fn jobnum(_pl: &(), segment_info: &ShellSegmentInfo, show_zero: bool) -> Option<String> {
let jobnum = segment_info.jobnum?;
if !show_zero && jobnum == 0 {
return None;
}
Some(jobnum.to_string())
}
#[allow(non_upper_case_globals)]
pub fn exit_codes(n: i32) -> Option<&'static str> {
match n {
1 => Some("SIGHUP"),
2 => Some("SIGINT"),
3 => Some("SIGQUIT"),
4 => Some("SIGILL"),
6 => Some("SIGABRT"),
8 => Some("SIGFPE"),
9 => Some("SIGKILL"),
11 => Some("SIGSEGV"),
13 => Some("SIGPIPE"),
14 => Some("SIGALRM"),
15 => Some("SIGTERM"),
_ => None,
}
}
pub fn last_status(
_pl: &(),
segment_info: &ShellSegmentInfo,
signal_names: bool,
) -> Option<Vec<Value>> {
let last_exit_code = segment_info.last_exit_code?;
if last_exit_code == 0 {
return None;
}
if signal_names {
if let Some(name) = exit_codes(last_exit_code - 128) {
return Some(vec![json!({
"contents": name,
"highlight_groups": ["exit_fail"],
})]);
}
}
Some(vec![json!({
"contents": last_exit_code.to_string(),
"highlight_groups": ["exit_fail"],
})])
}
pub fn last_pipe_status(
_pl: &(),
segment_info: &ShellSegmentInfo,
signal_names: bool,
) -> Option<Vec<Value>> {
let statuses: Vec<i32> = if !segment_info.last_pipe_status.is_empty() {
segment_info.last_pipe_status.clone()
} else {
match segment_info.last_exit_code {
Some(code) => vec![code],
None => return None,
}
};
if !statuses.iter().any(|&s| s != 0) {
return None;
}
let segments: Vec<Value> = statuses
.iter()
.map(|&status| {
let contents = if signal_names {
exit_codes(status - 128)
.map(String::from)
.unwrap_or_else(|| status.to_string())
} else {
status.to_string()
};
let highlight = if status != 0 {
"exit_fail"
} else {
"exit_success"
};
json!({
"contents": contents,
"highlight_groups": [highlight],
"draw_inner_divider": true,
})
})
.collect();
Some(segments)
}
pub fn mode(
_pl: &(),
segment_info: &ShellSegmentInfo,
override_table: &Map<String, Value>,
default: Option<&str>,
) -> Option<String> {
let mode = segment_info.mode.as_ref()?;
if mode.is_empty() {
return None;
}
let default = default
.map(String::from)
.or_else(|| segment_info.default_mode.clone());
if Some(mode.clone()) == default {
return None;
}
if let Some(override_val) = override_table.get(mode).and_then(|v| v.as_str()) {
return Some(override_val.to_string());
}
Some(mode.to_uppercase())
}
pub fn continuation(
_pl: &(),
segment_info: &ShellSegmentInfo,
omit_cmdsubst: bool,
right_align: bool,
renames: &Map<String, Value>,
) -> Vec<Value> {
let parser_state = match &segment_info.parser_state {
Some(s) if !s.is_empty() => s,
_ => {
return vec![json!({
"contents": "",
"width": "auto",
"highlight_groups": ["continuation:current", "continuation"],
})];
}
};
let mut ret: Vec<Value> = Vec::new();
for state in parser_state.split_whitespace() {
let renamed = renames.get(state).and_then(|v| v.as_str()).unwrap_or(state);
if renamed.is_empty() {
continue;
}
ret.push(json!({
"contents": renamed,
"highlight_groups": ["continuation"],
"draw_inner_divider": true,
}));
}
if omit_cmdsubst {
if let Some(last) = ret.last() {
if last.get("contents").and_then(|v| v.as_str()) == Some("cmdsubst") {
ret.pop();
}
}
}
if ret.is_empty() {
ret.push(json!({"contents": ""}));
}
if right_align {
if let Some(Value::Object(map)) = ret.first_mut() {
map.insert("width".into(), Value::String("auto".into()));
map.insert("align".into(), Value::String("r".into()));
}
if let Some(Value::Object(map)) = ret.last_mut() {
map.insert(
"highlight_groups".into(),
json!(["continuation:current", "continuation"]),
);
}
} else if let Some(Value::Object(map)) = ret.last_mut() {
map.insert("width".into(), Value::String("auto".into()));
map.insert("align".into(), Value::String("l".into()));
map.insert(
"highlight_groups".into(),
json!(["continuation:current", "continuation"]),
);
}
ret
}
pub fn get_shortened_path<F>(
segment_info: &serde_json::Map<String, serde_json::Value>,
use_shortened_path: bool,
super_get_shortened_path: F,
) -> Result<String, std::io::Error>
where
F: FnOnce() -> Result<String, std::io::Error>,
{
if use_shortened_path {
if let Some(v) = segment_info.get("shortened_path").and_then(|v| v.as_str()) {
return Ok(crate::ported::lib::unicode::out_u_str(v));
}
}
super_get_shortened_path()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn get_shortened_path_uses_segment_info_shortened_path_when_present() {
let mut info = serde_json::Map::new();
info.insert(
"shortened_path".to_string(),
serde_json::Value::String("/sh/path".to_string()),
);
let result = get_shortened_path(&info, true, || {
panic!("super should not be called");
})
.unwrap();
assert_eq!(result, "/sh/path");
}
#[test]
fn get_shortened_path_falls_back_to_super_when_use_shortened_false() {
let mut info = serde_json::Map::new();
info.insert(
"shortened_path".to_string(),
serde_json::Value::String("/sh/path".to_string()),
);
let result = get_shortened_path(&info, false, || Ok("/full/path".to_string())).unwrap();
assert_eq!(result, "/full/path");
}
#[test]
fn get_shortened_path_falls_back_to_super_when_key_missing() {
let info = serde_json::Map::new();
let result = get_shortened_path(&info, true, || Ok("/full/path".to_string())).unwrap();
assert_eq!(result, "/full/path");
}
#[test]
fn jobnum_returns_none_when_zero_and_no_show_zero() {
let info = ShellSegmentInfo {
jobnum: Some(0),
..Default::default()
};
assert!(jobnum(&(), &info, false).is_none());
}
#[test]
fn jobnum_returns_zero_when_show_zero() {
let info = ShellSegmentInfo {
jobnum: Some(0),
..Default::default()
};
assert_eq!(jobnum(&(), &info, true), Some("0".into()));
}
#[test]
fn jobnum_returns_count_string() {
let info = ShellSegmentInfo {
jobnum: Some(3),
..Default::default()
};
assert_eq!(jobnum(&(), &info, false), Some("3".into()));
}
#[test]
fn jobnum_none_jobnum_returns_none() {
let info = ShellSegmentInfo::default();
assert!(jobnum(&(), &info, false).is_none());
}
#[test]
fn exit_codes_known_signals() {
assert_eq!(exit_codes(9), Some("SIGKILL"));
assert_eq!(exit_codes(15), Some("SIGTERM"));
assert_eq!(exit_codes(2), Some("SIGINT"));
assert_eq!(exit_codes(999), None);
}
#[test]
fn last_status_zero_returns_none() {
let info = ShellSegmentInfo {
last_exit_code: Some(0),
..Default::default()
};
assert!(last_status(&(), &info, true).is_none());
}
#[test]
fn last_status_signal_kills() {
let info = ShellSegmentInfo {
last_exit_code: Some(137),
..Default::default()
};
let result = last_status(&(), &info, true).unwrap();
assert_eq!(result[0]["contents"], "SIGKILL");
assert_eq!(result[0]["highlight_groups"], json!(["exit_fail"]));
}
#[test]
fn last_status_non_signal_returns_number() {
let info = ShellSegmentInfo {
last_exit_code: Some(42),
..Default::default()
};
let result = last_status(&(), &info, true).unwrap();
assert_eq!(result[0]["contents"], "42");
}
#[test]
fn last_pipe_status_all_zero_returns_none() {
let info = ShellSegmentInfo {
last_pipe_status: vec![0, 0, 0],
..Default::default()
};
assert!(last_pipe_status(&(), &info, true).is_none());
}
#[test]
fn last_pipe_status_mixed_emits_per_status_segments() {
let info = ShellSegmentInfo {
last_pipe_status: vec![0, 1, 137],
..Default::default()
};
let result = last_pipe_status(&(), &info, true).unwrap();
assert_eq!(result.len(), 3);
assert_eq!(result[0]["highlight_groups"], json!(["exit_success"]));
assert_eq!(result[1]["highlight_groups"], json!(["exit_fail"]));
assert_eq!(result[2]["contents"], "SIGKILL");
}
#[test]
fn mode_returns_uppercase_when_no_override() {
let info = ShellSegmentInfo {
mode: Some("normal".into()),
..Default::default()
};
let override_table = Map::new();
assert_eq!(
mode(&(), &info, &override_table, None),
Some("NORMAL".into())
);
}
#[test]
fn mode_returns_override_when_present() {
let info = ShellSegmentInfo {
mode: Some("vicmd".into()),
..Default::default()
};
let mut override_table = Map::new();
override_table.insert("vicmd".into(), json!("COMMND"));
assert_eq!(
mode(&(), &info, &override_table, None),
Some("COMMND".into())
);
}
#[test]
fn mode_returns_none_when_matches_default() {
let info = ShellSegmentInfo {
mode: Some("insert".into()),
..Default::default()
};
let override_table = Map::new();
assert!(mode(&(), &info, &override_table, Some("insert")).is_none());
}
#[test]
fn continuation_no_parser_state_returns_empty_segment() {
let info = ShellSegmentInfo::default();
let renames = Map::new();
let r = continuation(&(), &info, true, false, &renames);
assert_eq!(r.len(), 1);
assert_eq!(r[0]["contents"], "");
assert_eq!(r[0]["width"], "auto");
}
#[test]
fn continuation_splits_parser_state() {
let info = ShellSegmentInfo {
parser_state: Some("if while for".into()),
..Default::default()
};
let renames = Map::new();
let r = continuation(&(), &info, true, false, &renames);
assert_eq!(r.len(), 3);
assert_eq!(r[0]["contents"], "if");
assert_eq!(
r[2]["highlight_groups"],
json!(["continuation:current", "continuation"])
);
}
#[test]
fn continuation_omits_cmdsubst_last() {
let info = ShellSegmentInfo {
parser_state: Some("if cmdsubst".into()),
..Default::default()
};
let renames = Map::new();
let r = continuation(&(), &info, true, false, &renames);
assert_eq!(r.len(), 1);
assert_eq!(r[0]["contents"], "if");
}
#[test]
fn continuation_right_align_sets_first_align() {
let info = ShellSegmentInfo {
parser_state: Some("if while".into()),
..Default::default()
};
let renames = Map::new();
let r = continuation(&(), &info, true, true, &renames);
assert_eq!(r[0]["align"], "r");
assert_eq!(r[0]["width"], "auto");
}
}