use serde_json::{Map, Value};
use unicode_width::UnicodeWidthStr;
#[derive(Debug, Clone)]
pub struct TableColumnNames {
pub name: &'static str,
pub id: &'static str,
pub status: &'static str,
pub platform: &'static str,
pub location: &'static str,
pub activity: &'static str,
}
impl Default for TableColumnNames {
fn default() -> Self {
Self {
name: "Name",
id: "ID",
status: "Status",
platform: "Platform",
location: "Location",
activity: "Activity",
}
}
}
pub trait TableDisplayable {
fn display_name(&self) -> &str;
fn id(&self) -> Option<&str> {
None
}
fn status(&self) -> Option<String> {
None
}
fn platform(&self) -> Option<&str> {
None
}
fn location(&self) -> Option<&str> {
None
}
fn activity(&self) -> Option<&str> {
None
}
fn column_names(&self) -> TableColumnNames {
TableColumnNames::default()
}
fn colored_status(&self) -> Option<String> {
self.status()
}
fn formatted_platform(&self) -> Option<String> {
self.platform().map(|p| p.to_string())
}
fn formatted_location(&self) -> Option<String> {
self.location().map(|l| l.to_string())
}
fn formatted_activity(&self) -> Option<String> {
self.activity().map(|a| a.to_string())
}
fn to_json_object(&self, options: &super::output_options::OutputOptions) -> Value {
let mut obj = Map::new();
obj.insert(
"display_name".to_string(),
Value::String(self.display_name().to_string()),
);
if options.show_id {
if let Some(id) = self.id() {
obj.insert("id".to_string(), Value::String(id.to_string()));
}
}
if options.show_status {
if let Some(status) = self.status() {
obj.insert("status".to_string(), Value::String(status));
}
}
if options.show_platform {
if let Some(platform) = self.platform() {
obj.insert("platform".to_string(), Value::String(platform.to_string()));
}
}
if options.show_location {
if let Some(location) = self.location() {
obj.insert("location".to_string(), Value::String(location.to_string()));
}
}
if options.show_activity {
if let Some(activity) = self.activity() {
obj.insert("activity".to_string(), Value::String(activity.to_string()));
}
}
Value::Object(obj)
}
}
#[derive(Debug, Clone)]
pub struct ColumnWidths {
pub name: usize,
pub id: usize,
pub status: usize,
pub platform: usize,
pub location: usize,
}
impl Default for ColumnWidths {
fn default() -> Self {
Self::new()
}
}
impl ColumnWidths {
pub fn new() -> Self {
Self {
name: 4, id: 2, status: 6, platform: 8, location: 8, }
}
pub fn calculate_from_data<T: TableDisplayable>(items: &[T]) -> Self {
let mut widths = Self::new();
for item in items {
let name_width = item.display_name().width();
widths.name = widths.name.max(name_width);
if let Some(id) = item.id() {
let id_width = id.width();
widths.id = widths.id.max(id_width);
}
if let Some(status) = item.status() {
let status_width = status.width();
widths.status = widths.status.max(status_width);
}
if let Some(formatted_platform) = item.formatted_platform() {
let platform_width = formatted_platform.width();
widths.platform = widths.platform.max(platform_width);
}
if let Some(formatted_location) = item.formatted_location() {
let location_width = formatted_location.width();
widths.location = widths.location.max(location_width);
} else if let Some(location) = item.location() {
let location_width = location.width();
widths.location = widths.location.max(location_width);
}
if let Some(formatted_activity) = item.formatted_activity() {
let _activity_width = formatted_activity.width();
} else if let Some(activity) = item.activity() {
let _activity_width = activity.width();
}
}
widths.name = (widths.name + 2).min(30);
widths.id += 2; widths.status = (widths.status + 2).min(15);
widths.platform = (widths.platform + 2).min(15);
widths.location = (widths.location + 2).min(40);
widths
}
}
pub fn format_table<T: TableDisplayable>(
items: &[T],
options: &super::output_options::OutputOptions,
) -> String {
if items.is_empty() {
return String::new();
}
let widths = ColumnWidths::calculate_from_data(items);
let mut output = String::new();
let column_names = if let Some(first_item) = items.first() {
first_item.column_names()
} else {
TableColumnNames::default()
};
let mut header = String::new();
header.push_str(&format!(
"{:<width$}",
column_names.name,
width = widths.name
));
if options.show_id {
header.push_str(&format!("{:<width$}", column_names.id, width = widths.id));
}
if options.show_status {
header.push_str(&format!(
"{:<width$}",
column_names.status,
width = widths.status
));
}
if options.show_platform {
header.push_str(&format!(
"{:<width$}",
column_names.platform,
width = widths.platform
));
}
if options.show_location {
header.push_str(&format!(
"{:<width$}",
column_names.location,
width = widths.location
));
}
if options.show_activity {
header.push_str(column_names.activity);
}
output.push_str(&header);
output.push('\n');
for item in items {
let mut row = String::new();
let name = crate::common::utils::format_text_with_width(item.display_name(), widths.name);
row.push_str(&name);
if options.show_id {
if let Some(id) = item.id() {
let formatted_id = crate::common::utils::format_text_with_width(id, widths.id);
row.push_str(&formatted_id);
} else {
row.push_str(&format!("{:<width$}", "", width = widths.id));
}
}
if options.show_status {
if let Some(colored_status) = item.colored_status() {
let formatted_status = format_colored_text_with_width(
&colored_status,
&item.status().unwrap_or_default(),
widths.status,
);
row.push_str(&formatted_status);
} else {
row.push_str(&format!("{:<width$}", "", width = widths.status));
}
}
if options.show_platform {
if let Some(formatted_platform) = item.formatted_platform() {
let platform_formatted = crate::common::utils::format_text_with_width(
&formatted_platform,
widths.platform,
);
row.push_str(&platform_formatted);
} else {
row.push_str(&format!("{:<width$}", "", width = widths.platform));
}
}
if options.show_location {
if let Some(formatted_location) = item.formatted_location() {
let location_formatted = crate::common::utils::format_text_with_width(
&formatted_location,
widths.location,
);
row.push_str(&location_formatted);
} else if let Some(location) = item.location() {
let formatted_location =
crate::common::utils::format_text_with_width(location, widths.location);
row.push_str(&formatted_location);
} else {
row.push_str(&format!("{:<width$}", "", width = widths.location));
}
}
if options.show_activity {
if let Some(formatted_activity) = item.formatted_activity() {
row.push_str(&formatted_activity);
} else if let Some(activity) = item.activity() {
row.push_str(activity);
}
}
output.push_str(&row);
output.push('\n');
}
output
}
fn format_colored_text_with_width(colored_text: &str, plain_text: &str, width: usize) -> String {
let display_width = plain_text.width();
if display_width <= width {
let padding = width - display_width;
let spaces = " ".repeat(padding);
format!("{colored_text}{spaces}")
} else {
colored_text.to_string()
}
}