use serde::Serialize;
use std::fmt;
use std::str::FromStr;
pub use clap;
pub use clap_complete;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputFormat {
Text,
Json,
Yaml,
Table,
Csv,
}
impl FromStr for OutputFormat {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"text" | "human" => Ok(OutputFormat::Text),
"json" => Ok(OutputFormat::Json),
"yaml" | "yml" => Ok(OutputFormat::Yaml),
"table" => Ok(OutputFormat::Table),
"csv" => Ok(OutputFormat::Csv),
_ => Err(format!("Unknown output format: {s}")),
}
}
}
impl fmt::Display for OutputFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
OutputFormat::Text => write!(f, "text"),
OutputFormat::Json => write!(f, "json"),
OutputFormat::Yaml => write!(f, "yaml"),
OutputFormat::Table => write!(f, "table"),
OutputFormat::Csv => write!(f, "csv"),
}
}
}
pub trait CliFormatter: Serialize {
fn format_text(&self) -> String {
serde_json::to_string_pretty(self).unwrap_or_else(|_| "Failed to format".to_string())
}
fn format_table(&self) -> Vec<Vec<String>> {
vec![]
}
fn format_csv(&self) -> Vec<Vec<String>> {
self.format_table()
}
fn format_output(&self, format: OutputFormat) -> Result<String, crate::error::ToolError> {
match format {
OutputFormat::Text => Ok(self.format_text()),
OutputFormat::Json => serde_json::to_string_pretty(self).map_err(|e| {
crate::error::ToolError::new(
crate::error::ErrorCode::SerializationError,
format!("Failed to serialize to JSON: {e}"),
)
}),
OutputFormat::Yaml => serde_yaml::to_string(self).map_err(|e| {
crate::error::ToolError::new(
crate::error::ErrorCode::SerializationError,
format!("Failed to serialize to YAML: {e}"),
)
}),
OutputFormat::Table => {
use tabled::builder::Builder;
let rows = self.format_table();
if rows.is_empty() {
Ok("No data to display".to_string())
} else {
let mut builder = Builder::default();
for row in rows {
builder.push_record(row);
}
Ok(builder.build().to_string())
}
}
OutputFormat::Csv => {
use std::io::Cursor;
let mut buffer = Cursor::new(Vec::new());
{
let mut writer = csv::Writer::from_writer(&mut buffer);
for row in self.format_csv() {
writer.write_record(&row).map_err(|e| {
crate::error::ToolError::new(
crate::error::ErrorCode::SerializationError,
format!("Failed to write CSV: {e}"),
)
})?;
}
writer.flush().map_err(|e| {
crate::error::ToolError::new(
crate::error::ErrorCode::SerializationError,
format!("Failed to flush CSV writer: {e}"),
)
})?;
}
String::from_utf8(buffer.into_inner()).map_err(|e| {
crate::error::ToolError::new(
crate::error::ErrorCode::SerializationError,
format!("Failed to convert CSV to string: {e}"),
)
})
}
}
}
}
pub struct ProgressReporter {
_style: ProgressStyle,
bar: Option<indicatif::ProgressBar>,
}
#[derive(Debug, Clone, Copy)]
pub enum ProgressStyle {
Bar,
Spinner,
Dots,
None,
}
impl ProgressReporter {
pub fn new(style: ProgressStyle, total: Option<u64>) -> Self {
use indicatif::{ProgressBar, ProgressStyle as IndicatifStyle};
let bar = match style {
ProgressStyle::Bar => {
let pb = if let Some(total) = total {
ProgressBar::new(total)
} else {
ProgressBar::new_spinner()
};
pb.set_style(
IndicatifStyle::default_bar()
.template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} ({eta})")
.unwrap()
.progress_chars("#>-"),
);
Some(pb)
}
ProgressStyle::Spinner => {
let pb = ProgressBar::new_spinner();
pb.set_style(
IndicatifStyle::default_spinner()
.template("{spinner:.green} {msg}")
.unwrap(),
);
Some(pb)
}
ProgressStyle::Dots => {
let pb = ProgressBar::new_spinner();
pb.set_style(
IndicatifStyle::default_spinner()
.tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"])
.template("{spinner:.green} {msg}")
.unwrap(),
);
Some(pb)
}
ProgressStyle::None => None,
};
if let Some(ref pb) = bar {
pb.enable_steady_tick(std::time::Duration::from_millis(100));
}
Self { _style: style, bar }
}
pub fn set_progress(&self, current: u64) {
if let Some(ref bar) = self.bar {
bar.set_position(current);
}
}
pub fn set_message(&self, message: impl Into<String>) {
if let Some(ref bar) = self.bar {
bar.set_message(message.into());
}
}
pub fn inc(&self, delta: u64) {
if let Some(ref bar) = self.bar {
bar.inc(delta);
}
}
pub fn finish_with_message(&self, message: impl Into<String>) {
if let Some(ref bar) = self.bar {
bar.finish_with_message(message.into());
}
}
pub fn finish_and_clear(&self) {
if let Some(ref bar) = self.bar {
bar.finish_and_clear();
}
}
}
pub mod interactive {
use crate::error::{ErrorCode, ToolError};
pub fn input(prompt: &str) -> Result<String, ToolError> {
dialoguer::Input::new()
.with_prompt(prompt)
.interact_text()
.map_err(|e| ToolError::new(ErrorCode::IoError, format!("Failed to read input: {e}")))
}
pub fn input_with_validation<F>(prompt: &str, validator: F) -> Result<String, ToolError>
where
F: Fn(&String) -> Result<(), String> + Clone,
{
dialoguer::Input::new()
.with_prompt(prompt)
.validate_with(validator)
.interact_text()
.map_err(|e| ToolError::new(ErrorCode::IoError, format!("Failed to read input: {e}")))
}
pub fn select<T>(prompt: &str, items: &[T]) -> Result<usize, ToolError>
where
T: ToString,
{
dialoguer::Select::new()
.with_prompt(prompt)
.items(items)
.interact()
.map_err(|e| {
ToolError::new(ErrorCode::IoError, format!("Failed to read selection: {e}"))
})
}
pub fn multi_select<T>(prompt: &str, items: &[T]) -> Result<Vec<usize>, ToolError>
where
T: ToString,
{
dialoguer::MultiSelect::new()
.with_prompt(prompt)
.items(items)
.interact()
.map_err(|e| {
ToolError::new(
ErrorCode::IoError,
format!("Failed to read selections: {e}"),
)
})
}
pub fn confirm(prompt: &str, default: bool) -> Result<bool, ToolError> {
dialoguer::Confirm::new()
.with_prompt(prompt)
.default(default)
.interact()
.map_err(|e| {
ToolError::new(
ErrorCode::IoError,
format!("Failed to read confirmation: {e}"),
)
})
}
}
pub fn is_stdin_tty() -> bool {
atty::is(atty::Stream::Stdin)
}
pub fn is_stdout_tty() -> bool {
atty::is(atty::Stream::Stdout)
}
pub fn read_stdin() -> Result<Option<String>, crate::error::ToolError> {
use std::io::Read;
if !is_stdin_tty() {
let mut buffer = String::new();
std::io::stdin().read_to_string(&mut buffer).map_err(|e| {
crate::error::ToolError::new(
crate::error::ErrorCode::IoError,
format!("Failed to read from stdin: {e}"),
)
})?;
Ok(Some(buffer))
} else {
Ok(None)
}
}