use std::io::IsTerminal;
use kaish_kernel::interpreter::{EntryType, ExecResult, OutputData, OutputNode};
use kaish_kernel::tools::OutputContext;
pub fn format_output(result: &ExecResult, context: OutputContext) -> String {
if let Some(ref output) = result.output {
return format_output_data(output, context);
}
result.out.clone()
}
pub fn format_output_data(output: &OutputData, context: OutputContext) -> String {
if !matches!(context, OutputContext::Interactive) {
return output.to_canonical_string();
}
if let Some(text) = output.as_text() {
return text.to_string();
}
if !output.is_flat() {
return format_tree_from_output_data(output);
}
if output.is_tabular() {
return format_table_from_output_data(output);
}
format_columns_from_output_data(output)
}
fn format_tree_from_output_data(output: &OutputData) -> String {
let mut result = String::new();
for (i, node) in output.root.iter().enumerate() {
if i > 0 {
result.push('\n');
}
format_tree_node(&mut result, node, "", true);
}
result.trim_end().to_string()
}
fn format_tree_node(output: &mut String, node: &OutputNode, prefix: &str, is_last: bool) {
let connector = if is_last { "└── " } else { "├── " };
let name = if node.name.is_empty() {
node.text.as_deref().unwrap_or("")
} else {
&node.name
};
let suffix = if node.entry_type == EntryType::Directory && node.children.is_empty() {
"/"
} else {
""
};
output.push_str(prefix);
output.push_str(connector);
output.push_str(&colorize_entry(name, Some(node.entry_type)));
output.push_str(suffix);
output.push('\n');
let child_prefix = format!("{}{} ", prefix, if is_last { " " } else { "│" });
let children: Vec<_> = node.children.iter().collect();
for (i, child) in children.iter().enumerate() {
let is_last_child = i == children.len() - 1;
format_tree_node(output, child, &child_prefix, is_last_child);
}
}
fn format_table_from_output_data(output: &OutputData) -> String {
if output.root.is_empty() {
return String::new();
}
let rows: Vec<Vec<&str>> = output.root.iter().map(|node| {
let mut row = Vec::new();
row.push(node.display_name());
for cell in &node.cells {
row.push(cell.as_str());
}
row
}).collect();
let num_cols = rows.iter().map(|r| r.len()).max().unwrap_or(0);
let mut col_widths = vec![0; num_cols];
if let Some(ref headers) = output.headers {
for (i, header) in headers.iter().enumerate() {
if i < col_widths.len() {
col_widths[i] = col_widths[i].max(header.len());
}
}
}
for row in &rows {
for (i, cell) in row.iter().enumerate() {
if i < col_widths.len() {
col_widths[i] = col_widths[i].max(cell.len());
}
}
}
let mut result = String::new();
if let Some(ref headers) = output.headers {
for (i, header) in headers.iter().enumerate() {
if i > 0 {
result.push_str(" ");
}
result.push_str(header);
if i < headers.len() - 1 {
let padding = col_widths[i].saturating_sub(header.len());
for _ in 0..padding {
result.push(' ');
}
}
}
result.push('\n');
}
for (row_idx, row) in rows.iter().enumerate() {
for (i, cell) in row.iter().enumerate() {
if i > 0 {
result.push_str(" ");
}
let colored_cell = if i == 0 {
colorize_entry(cell, Some(output.root[row_idx].entry_type))
} else {
(*cell).to_string()
};
result.push_str(&colored_cell);
if i < row.len() - 1 {
let padding = col_widths[i].saturating_sub(cell.len());
for _ in 0..padding {
result.push(' ');
}
}
}
result.push('\n');
}
result.trim_end().to_string()
}
fn format_columns_from_output_data(output: &OutputData) -> String {
if output.root.is_empty() {
return String::new();
}
let term_width = terminal_size::terminal_size()
.map(|(w, _)| w.0 as usize)
.unwrap_or(80);
let items: Vec<_> = output.root.iter().collect();
let max_len = items.iter()
.map(|n| n.display_name().len())
.max()
.unwrap_or(0);
let col_width = max_len + 2;
let num_cols = (term_width / col_width).max(1);
let mut result = String::new();
let mut col = 0;
for (i, node) in items.iter().enumerate() {
let colored_item = colorize_entry(node.display_name(), Some(node.entry_type));
if col > 0 && col >= num_cols {
result.push('\n');
col = 0;
}
if col > 0 {
let prev_len = items.get(i.saturating_sub(1))
.map(|n| n.display_name().len())
.unwrap_or(0);
let padding = col_width.saturating_sub(prev_len);
for _ in 0..padding {
result.push(' ');
}
}
result.push_str(&colored_item);
col += 1;
}
result
}
pub fn detect_context() -> OutputContext {
if std::io::stdout().is_terminal() {
OutputContext::Interactive
} else {
OutputContext::Piped
}
}
fn colorize_entry(name: &str, entry_type: Option<EntryType>) -> String {
use owo_colors::OwoColorize;
if std::env::var("NO_COLOR").is_ok() {
return name.to_string();
}
if std::env::var("TERM").map(|t| t == "dumb").unwrap_or(false) {
return name.to_string();
}
match entry_type {
Some(EntryType::Directory) => name.blue().bold().to_string(),
Some(EntryType::Executable) => name.green().bold().to_string(),
Some(EntryType::Symlink) => name.cyan().to_string(),
Some(EntryType::File) | Some(EntryType::Text) | None => name.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_output_raw() {
let result = ExecResult::success("hello world");
let output = format_output(&result, OutputContext::Interactive);
assert_eq!(output, "hello world");
}
#[test]
fn test_detect_context_not_terminal() {
let context = detect_context();
assert_eq!(context, OutputContext::Piped);
}
#[test]
fn test_colorize_plain_file() {
let result = colorize_entry("test.txt", Some(EntryType::File));
assert_eq!(result, "test.txt");
}
#[test]
fn test_output_data_simple_text() {
let output_data = OutputData::text("hello world");
let result = ExecResult::with_output(output_data);
let formatted = format_output(&result, OutputContext::Interactive);
assert_eq!(formatted, "hello world");
}
#[test]
fn test_output_data_text_piped() {
let output_data = OutputData::text("hello world");
let result = ExecResult::with_output(output_data);
let formatted = format_output(&result, OutputContext::Piped);
assert_eq!(formatted, "hello world");
}
#[test]
fn test_output_data_flat_nodes_interactive() {
let nodes = vec![
OutputNode::new("file1.txt").with_entry_type(EntryType::File),
OutputNode::new("file2.txt").with_entry_type(EntryType::File),
OutputNode::new("dir").with_entry_type(EntryType::Directory),
];
let output_data = OutputData::nodes(nodes);
let result = ExecResult::with_output(output_data);
let formatted = format_output(&result, OutputContext::Interactive);
assert!(formatted.contains("file1.txt"));
assert!(formatted.contains("file2.txt"));
assert!(formatted.contains("dir"));
}
#[test]
fn test_output_data_flat_nodes_piped() {
let nodes = vec![
OutputNode::new("file1.txt").with_entry_type(EntryType::File),
OutputNode::new("file2.txt").with_entry_type(EntryType::File),
];
let output_data = OutputData::nodes(nodes);
let result = ExecResult::with_output(output_data);
let formatted = format_output(&result, OutputContext::Piped);
assert_eq!(formatted, "file1.txt\nfile2.txt");
}
#[test]
fn test_output_data_table_with_cells() {
let nodes = vec![
OutputNode::new("file1.txt")
.with_cells(vec!["1024".to_string()])
.with_entry_type(EntryType::File),
OutputNode::new("file2.txt")
.with_cells(vec!["2048".to_string()])
.with_entry_type(EntryType::File),
];
let output_data = OutputData::table(
vec!["Name".to_string(), "Size".to_string()],
nodes,
);
let result = ExecResult::with_output(output_data);
let formatted = format_output(&result, OutputContext::Interactive);
assert!(formatted.contains("Name"));
assert!(formatted.contains("Size"));
assert!(formatted.contains("file1.txt"));
assert!(formatted.contains("1024"));
}
#[test]
fn test_output_data_nested_children_piped() {
let child = OutputNode::new("main.rs").with_entry_type(EntryType::File);
let parent = OutputNode::new("src")
.with_entry_type(EntryType::Directory)
.with_children(vec![child]);
let output_data = OutputData::nodes(vec![parent]);
let result = ExecResult::with_output(output_data);
let formatted = format_output(&result, OutputContext::Piped);
assert!(formatted.contains("src"));
assert!(formatted.contains("main.rs"));
}
#[test]
fn test_format_output_data_direct() {
let output_data = OutputData::text("direct test");
let formatted = format_output_data(&output_data, OutputContext::Interactive);
assert_eq!(formatted, "direct test");
}
}