use anyhow::Context;
use chrono::{DateTime, Utc};
use colored::Colorize;
use serde_json::Value;
use std::fs;
use std::io::{self, Write};
use tabled::Tabled;
use unicode_segmentation::UnicodeSegmentation;
use std::io::IsTerminal;
use crate::error::{RedisCtlError, Result as CliResult};
#[derive(Tabled)]
pub struct DetailRow {
#[tabled(rename = "FIELD")]
pub field: String,
#[tabled(rename = "VALUE")]
pub value: String,
}
pub fn truncate_string(s: &str, max_len: usize) -> String {
let graphemes: Vec<&str> = s.graphemes(true).collect();
if graphemes.len() <= max_len {
s.to_string()
} else if max_len > 3 {
let truncated: String = graphemes[..max_len - 3].join("");
format!("{}...", truncated)
} else {
graphemes[..max_len].join("")
}
}
pub fn extract_field(value: &Value, field: &str, default: &str) -> String {
value
.get(field)
.and_then(|v| match v {
Value::String(s) => Some(s.clone()),
Value::Number(n) => Some(n.to_string()),
Value::Bool(b) => Some(b.to_string()),
_ => None,
})
.unwrap_or_else(|| default.to_string())
}
pub fn output_with_pager(content: &str) {
use std::io::Write;
use std::process::{Command, Stdio};
let lines: Vec<&str> = content.lines().collect();
if should_use_pager(&lines) {
let default_pager = if cfg!(windows) { "more" } else { "less -R" };
let pager_cmd = std::env::var("PAGER").unwrap_or_else(|_| default_pager.to_string());
let mut parts = pager_cmd.split_whitespace();
let default_program = if cfg!(windows) { "more" } else { "less" };
let program = parts.next().unwrap_or(default_program);
let args: Vec<&str> = parts.collect();
match Command::new(program)
.args(&args)
.stdin(Stdio::piped())
.spawn()
{
Ok(mut child) => {
if let Some(mut stdin) = child.stdin.take() {
let _ = stdin.write_all(content.as_bytes());
let _ = stdin.flush();
drop(stdin);
}
let _ = child.wait();
return;
}
Err(_) => {
}
}
}
println!("{}", content);
}
fn should_use_pager(lines: &[&str]) -> bool {
if !std::io::stdout().is_terminal() {
return false;
}
if let Some((_, height)) = terminal_size::terminal_size() {
let term_height = height.0 as usize;
return lines.len() > (term_height * 8 / 10);
}
lines.len() > 20
}
pub fn format_status(status: String) -> String {
match status.to_lowercase().as_str() {
"active" => status.green().to_string(),
"pending" => status.yellow().to_string(),
"error" | "failed" => status.red().to_string(),
_ => status,
}
}
pub fn format_status_text(status: &str) -> String {
match status.to_lowercase().as_str() {
"active" => status.green().to_string(),
"suspended" | "inactive" => status.red().to_string(),
"pending" => status.yellow().to_string(),
_ => status.to_string(),
}
}
pub fn format_date(date_str: String) -> String {
if date_str.is_empty() || date_str == "β" {
return "β".to_string();
}
if date_str.contains(' ') && !date_str.contains('T') {
return date_str;
}
if let Ok(dt) = DateTime::parse_from_rfc3339(&date_str) {
let utc: DateTime<Utc> = dt.into();
let now = Utc::now();
let duration = now.signed_duration_since(utc);
if duration.num_days() == 0 {
if duration.num_hours() == 0 {
return format!("{} min ago", duration.num_minutes());
}
return format!("{} hours ago", duration.num_hours());
} else if duration.num_days() < 7 {
return format!("{} days ago", duration.num_days());
}
return utc.format("%Y-%m-%d").to_string();
}
date_str
}
pub fn format_memory_size(gb: f64) -> String {
if gb < 1.0 {
format!("{:.0}MB", gb * 1024.0)
} else {
format!("{:.1}GB", gb)
}
}
pub fn provider_short_name(provider: &str) -> &str {
match provider.to_lowercase().as_str() {
"aws" => "AWS",
"gcp" | "google" => "GCP",
"azure" => "Azure",
_ => provider,
}
}
pub use crate::output::{apply_jmespath, handle_output, print_formatted_output, resolve_auto};
pub fn confirm_action(message: &str) -> CliResult<bool> {
print!("Are you sure you want to {}? [y/N]: ", message);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
Ok(input.trim().eq_ignore_ascii_case("y") || input.trim().eq_ignore_ascii_case("yes"))
}
pub fn read_file_input(input: &str) -> CliResult<String> {
if let Some(filename) = input.strip_prefix('@') {
fs::read_to_string(filename)
.with_context(|| format!("Failed to read file: {}", filename))
.map_err(|e| RedisCtlError::FileError {
path: filename.to_string(),
message: e.to_string(),
})
} else {
Ok(input.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_truncate_string_ascii() {
assert_eq!(truncate_string("hello", 10), "hello");
assert_eq!(truncate_string("hello world", 8), "hello...");
assert_eq!(truncate_string("hello", 5), "hello");
assert_eq!(truncate_string("hello", 4), "h...");
assert_eq!(truncate_string("abc", 2), "ab");
}
#[test]
fn test_truncate_string_unicode() {
assert_eq!(truncate_string("Hello π World", 10), "Hello π...");
assert_eq!(truncate_string("ππππ", 6), "ππππ");
assert_eq!(truncate_string("ππππ", 3), "πππ");
assert_eq!(truncate_string("ππππ", 2), "ππ");
assert_eq!(truncate_string("π¨βπ©βπ§βπ¦π", 2), "π¨βπ©βπ§βπ¦π");
assert_eq!(truncate_string("π¨βπ©βπ§βπ¦ππ", 3), "π¨βπ©βπ§βπ¦ππ");
assert_eq!(truncate_string("π¨βπ©βπ§βπ¦ππ", 2), "π¨βπ©βπ§βπ¦π");
}
#[test]
fn test_truncate_string_cjk() {
assert_eq!(truncate_string("δ½ ε₯½δΈη", 10), "δ½ ε₯½δΈη");
assert_eq!(truncate_string("δ½ ε₯½δΈη", 3), "δ½ ε₯½δΈ");
assert_eq!(truncate_string("δ½ ε₯½δΈη", 2), "δ½ ε₯½");
assert_eq!(truncate_string("γγγ«γ‘γ―", 10), "γγγ«γ‘γ―");
assert_eq!(truncate_string("γγγ«γ‘γ―", 4), "γ...");
assert_eq!(truncate_string("μλ
νμΈμ", 10), "μλ
νμΈμ");
assert_eq!(truncate_string("μλ
νμΈμ", 4), "μ...");
}
#[test]
fn test_truncate_string_mixed() {
assert_eq!(truncate_string("Hello δΈη", 10), "Hello δΈη");
assert_eq!(truncate_string("Hello δΈη", 8), "Hello δΈη");
assert_eq!(truncate_string("Hello δΈη", 7), "Hell...");
assert_eq!(truncate_string("RedisπFast", 10), "RedisπFast");
}
#[test]
fn test_truncate_string_edge_cases() {
assert_eq!(truncate_string("", 10), "");
assert_eq!(truncate_string("hello", 0), "");
assert_eq!(truncate_string("hello", 1), "h");
assert_eq!(truncate_string("hello", 2), "he");
assert_eq!(truncate_string("hello", 3), "hel");
assert_eq!(truncate_string("abc", 3), "abc");
assert_eq!(truncate_string("abcd", 4), "abcd");
}
#[test]
fn test_truncate_string_doesnt_panic() {
let _ = truncate_string("Hello π World π", 10);
let _ = truncate_string("π", 5);
let _ = truncate_string("δ½ ε₯½δΈη", 3);
let _ = truncate_string("π¨βπ©βπ§βπ¦", 2);
let _ = truncate_string("Γ©", 1); let _ = truncate_string("πΊπΈ", 1); let _ = truncate_string("ππ½", 1); }
}