use super::*;
pub fn expand_command_alias(
line: &str,
aliases: &std::collections::HashMap<String, String>,
) -> String {
let line = line.trim();
if aliases.is_empty() || line.is_empty() {
return line.to_string();
}
let mut parts = line.splitn(2, char::is_whitespace);
let first = match parts.next() {
Some(s) => s,
None => return line.to_string(),
};
let Some(expansion) = aliases.get(first) else {
return line.to_string();
};
match parts.next() {
Some(rest) => format!("{expansion} {rest}"),
None => expansion.clone(),
}
}
pub fn humanize_short_age(d: Duration) -> String {
let secs = d.as_secs();
if secs < 60 {
format!("{secs}s")
} else if secs < 3600 {
format!("{}m", secs / 60)
} else if secs < 86_400 {
format!("{}h", secs / 3600)
} else {
format!("{}d", secs / 86_400)
}
}
pub fn parse_tag_args(rest: &[&str]) -> Option<(String, String)> {
let key = (*rest.first()?).to_string();
if rest.len() < 2 {
return None;
}
let value = rest[1..].join(" ");
if key.is_empty() || value.is_empty() {
return None;
}
Some((key, value))
}
pub fn delta_toast_key(text: &str) -> Option<String> {
let trimmed = text.trim_start();
let mut chars = trimmed.chars();
let first = chars.next()?;
if first != '▲' && first != '▼' {
return None;
}
let rest: String = chars.collect();
let first_rest = rest.chars().next()?;
if !first_rest.is_ascii_digit() {
return None;
}
let bucket_start = rest.find(|c: char| !c.is_ascii_digit())?;
let after_digits = &rest[bucket_start..];
let bucket = after_digits.trim_start();
if bucket.is_empty() || !bucket.starts_with(|c: char| c.is_ascii_alphabetic()) {
return None;
}
let word: String = bucket
.chars()
.take_while(|c| c.is_ascii_alphabetic())
.collect();
Some(word)
}
pub(crate) fn redact_for_log(value: &str, on: bool) -> String {
if !on || value.is_empty() || value == "—" {
return value.to_string();
}
"▓".repeat(value.chars().count())
}
pub(crate) fn extract_quoted_after(msg: &str, needle: &str) -> Option<String> {
let lower = msg.to_lowercase();
let needle_lower = needle.to_lowercase();
let after = lower.find(&needle_lower)? + needle_lower.len();
let tail = msg.get(after..)?;
let start = tail.find('\'')?;
let body = &tail[start + 1..];
let end = body.find('\'')?;
Some(body[..end].to_string())
}
pub(crate) fn parse_sort(raw: Option<&str>) -> (SortKey, bool) {
let Some(s) = raw else {
return (SortKey::App, false);
};
let (k, dir) = s.split_once(':').unwrap_or((s, "asc"));
let key = SortKey::parse(k.trim()).unwrap_or(SortKey::App);
let desc = dir.trim().eq_ignore_ascii_case("desc");
(key, desc)
}
pub(crate) fn health_rank(h: &str) -> u8 {
match h.to_lowercase().as_str() {
"green" | "ok" => 0,
"grey" | "gray" | "info" | "no data" | "pending" => 1,
"yellow" | "warning" => 2,
"red" | "severe" | "degraded" => 3,
_ => 4,
}
}
pub(crate) fn parse_toggle(arg: Option<&str>, current: bool) -> bool {
match arg.map(str::to_ascii_lowercase).as_deref() {
Some("on") | Some("true") | Some("yes") | Some("1") => true,
Some("off") | Some("false") | Some("no") | Some("0") => false,
_ => !current,
}
}
pub(crate) fn scroll_apply(current: u16, delta: i32) -> u16 {
let next = current as i32 + delta;
next.max(0) as u16
}
pub fn parse_metric_extra_args(args: &[&str]) -> (String, Vec<(String, String)>) {
let mut stat: Option<String> = None;
let mut dims: Vec<(String, String)> = Vec::new();
for tok in args {
if tok.contains('=') {
for kv in tok.split(',') {
if let Some((k, v)) = kv.split_once('=') {
let k = k.trim();
let v = v.trim();
if !k.is_empty() && !v.is_empty() {
dims.push((k.to_string(), v.to_string()));
}
}
}
} else if stat.is_none() {
stat = Some(tok.to_string());
}
}
(stat.unwrap_or_else(|| "Average".into()), dims)
}
pub fn parse_s3_url(raw: &str) -> Option<(String, String)> {
let rest = raw.strip_prefix("s3://")?;
let (bucket, key) = rest.split_once('/')?;
if bucket.is_empty() || key.is_empty() {
return None;
}
Some((bucket.to_string(), key.to_string()))
}
pub fn expand_tilde(path: &str) -> String {
if let Some(rest) = path.strip_prefix("~/") {
if let Some(home) = std::env::var_os("HOME") {
let mut p = std::path::PathBuf::from(home);
p.push(rest);
return p.display().to_string();
}
}
path.to_string()
}
pub fn derive_version_label(path: &str, unix_ts: i64) -> String {
let stem = std::path::Path::new(path)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("bundle");
let sanitised: String = stem
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.' {
c
} else {
'_'
}
})
.collect();
format!("{sanitised}_{unix_ts}")
}
pub fn pick_default_log_group(groups: &[String]) -> Option<String> {
const PRIORITIES: &[&str] = &[
"/var/log/web.stdout.log",
"/var/log/eb-engine.log",
"/var/log/eb-hooks.log",
"/var/log/nginx/access.log",
];
for needle in PRIORITIES {
if let Some(g) = groups.iter().find(|g| g.ends_with(needle)) {
return Some(g.clone());
}
}
groups.first().cloned()
}
pub fn parse_named_arg<T: std::str::FromStr>(rest: &[&str], flag: &str) -> Option<T> {
let pos = rest.iter().position(|s| *s == flag)?;
rest.get(pos + 1).and_then(|v| v.parse().ok())
}
pub fn alarm_kind_to_metric(kind: &str) -> Option<(&'static str, &'static str, &'static str)> {
match kind {
"health" => Some(("EnvironmentHealth", "LessThanOrEqualToThreshold", "Maximum")),
"4xx" | "req4xx" => Some(("ApplicationRequests4xx", "GreaterThanThreshold", "Sum")),
"5xx" | "req5xx" => Some(("ApplicationRequests5xx", "GreaterThanThreshold", "Sum")),
"latency" | "p90" => Some(("ApplicationLatencyP90", "GreaterThanThreshold", "Average")),
_ => None,
}
}
pub fn wrap_with_hanging_indent(text: &str, width: usize, lead: &str, cont: &str) -> String {
if text.is_empty() {
return lead.to_string();
}
let body_width = width.saturating_sub(lead.chars().count()).max(1);
let mut out = String::new();
let mut first = true;
let mut current = String::new();
let prefix = |first: bool| if first { lead } else { cont };
for word in text.split_whitespace() {
if word.chars().count() > body_width {
if !current.is_empty() {
out.push_str(prefix(first));
out.push_str(¤t);
out.push('\n');
first = false;
current.clear();
}
let mut chars = word.chars();
loop {
let chunk: String = (&mut chars).take(body_width).collect();
if chunk.is_empty() {
break;
}
out.push_str(prefix(first));
out.push_str(&chunk);
out.push('\n');
first = false;
}
continue;
}
let candidate_len = if current.is_empty() {
word.chars().count()
} else {
current.chars().count() + 1 + word.chars().count()
};
if candidate_len > body_width {
out.push_str(prefix(first));
out.push_str(¤t);
out.push('\n');
first = false;
current.clear();
}
if !current.is_empty() {
current.push(' ');
}
current.push_str(word);
}
if !current.is_empty() {
out.push_str(prefix(first));
out.push_str(¤t);
out.push('\n');
}
out.pop(); out
}
pub(crate) fn shell_quote(s: &str) -> String {
if s.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | '/'))
{
s.to_string()
} else {
let escaped = s.replace('\'', "'\\''");
format!("'{escaped}'")
}
}
pub(crate) fn md_escape(s: &str) -> String {
s.replace('\\', "\\\\").replace('|', "\\|")
}
pub(crate) fn edit_distance(a: &str, b: &str) -> usize {
let a_bytes = a.as_bytes();
let b_bytes = b.as_bytes();
if a_bytes.is_empty() {
return b_bytes.len();
}
if b_bytes.is_empty() {
return a_bytes.len();
}
let (short, long) = if a_bytes.len() < b_bytes.len() {
(a_bytes, b_bytes)
} else {
(b_bytes, a_bytes)
};
let mut prev: Vec<usize> = (0..=short.len()).collect();
let mut curr: Vec<usize> = vec![0; short.len() + 1];
for (i, lc) in long.iter().enumerate() {
curr[0] = i + 1;
for (j, sc) in short.iter().enumerate() {
let cost = if lc == sc { 0 } else { 1 };
curr[j + 1] = (prev[j + 1] + 1).min(curr[j] + 1).min(prev[j] + cost);
}
std::mem::swap(&mut prev, &mut curr);
}
prev[short.len()]
}
pub(crate) fn suggest_command(input: &str) -> Option<String> {
let threshold = if input.len() <= 3 { 1 } else { 2 };
let mut best: Option<(usize, String)> = None;
for name in crate::commands::all_names() {
let d = edit_distance(input, name);
if d <= threshold && best.as_ref().is_none_or(|(bd, _)| d < *bd) {
best = Some((d, name.to_string()));
}
}
best.map(|(_, name)| name)
}
pub(crate) fn completion_candidates(prefix: &str) -> Vec<String> {
let mut names: Vec<String> = crate::commands::all_names()
.into_iter()
.filter(|n| n.starts_with(prefix))
.map(String::from)
.collect();
names.sort();
names.dedup();
names
}
pub(crate) fn command_takes_env_arg(cmd: &str) -> bool {
crate::commands::COMMANDS
.iter()
.any(|c| c.env_arg && (c.name == cmd || c.aliases.contains(&cmd)))
}
pub(crate) fn format_age(
now: chrono::DateTime<chrono::Utc>,
t: chrono::DateTime<chrono::Utc>,
) -> String {
let d = now.signed_duration_since(t);
let secs = d.num_seconds().max(0);
if secs < 60 {
return format!("{secs}s ago");
}
let mins = secs / 60;
if mins < 60 {
return format!("{mins}m ago");
}
let hrs = mins / 60;
if hrs < 48 {
return format!("{hrs}h ago");
}
let days = hrs / 24;
if days < 60 {
return format!("{days}d ago");
}
let months = days / 30;
if months < 24 {
return format!("~{months}mo ago");
}
format!("~{}y ago", days / 365)
}
pub(crate) fn urlencode(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
if c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' {
out.push(c);
} else {
for b in c.to_string().bytes() {
out.push_str(&format!("%{b:02X}"));
}
}
}
out
}