use std::borrow::Cow;
use anyhow::Context;
use anyhow::Result;
use serde_yaml_ng::from_reader;
use serde_yaml_ng::Mapping;
use crate::app::Status;
use crate::common::CLI_PATH;
use crate::common::{is_in_path, tilde};
use crate::impl_draw_menu_with_char;
use crate::io::execute_with_ansi_colors;
use crate::modes::shell_command_parser;
use crate::{impl_content, impl_selectable, log_info, log_line};
pub trait Execute<T> {
fn execute(&self, status: &Status) -> Result<T>;
}
#[derive(Clone)]
pub struct CliCommand {
pub executable: String,
parsable_command: String,
pub desc: String,
}
impl CliCommand {
fn new(desc: String, args: String) -> Option<Self> {
let executable = args.split(' ').next()?;
if !is_in_path(executable) {
return None;
}
let desc = desc.replace('_', " ");
Some(Self {
executable: executable.to_owned(),
parsable_command: args,
desc,
})
}
}
impl Execute<(String, String)> for CliCommand {
fn execute(&self, status: &Status) -> Result<(String, String)> {
let args = shell_command_parser(&self.parsable_command, status)?;
log_info!("execute. {args:?}");
log_line!("Executed {args:?}");
let command_output = execute_with_ansi_colors(&args)?;
let text_output = String::from_utf8(command_output.stdout)?;
if !command_output.status.success() {
log_info!(
"Command {a} exited with error code {e}",
a = args[0],
e = command_output.status
);
};
Ok((text_output, self.parsable_command.to_owned()))
}
}
pub trait TerminalApplications<T: Execute<U>, U>: Sized + Default + Content<T> {
fn update_from_config(&mut self, config_file: &str) {
let Ok(file) = std::fs::File::open(std::path::Path::new(&tilde(config_file).to_string()))
else {
log_info!("Couldn't open cli file at {config_file}. Using default");
return;
};
match from_reader(file) {
Ok(yaml) => {
self.parse_yaml(&yaml);
}
Err(error) => {
log_info!("error parsing yaml file {config_file}. Error: {error:?}");
}
}
}
fn parse_yaml(&mut self, yaml: &Mapping);
fn execute(&self, status: &Status) -> Result<U> {
self.selected().context("")?.execute(status)
}
}
#[derive(Clone, Default)]
pub struct CliApplications {
pub content: Vec<CliCommand>,
index: usize,
pub desc_size: usize,
}
impl CliApplications {
pub fn setup(&mut self) {
self.update_from_config(CLI_PATH);
self.update_desc_size();
}
pub fn update_desc_size(&mut self) {
let desc_size = self
.content
.iter()
.map(|cli| cli.desc.len())
.fold(usize::MIN, |a, b| a.max(b));
self.desc_size = desc_size;
}
}
impl TerminalApplications<CliCommand, (String, String)> for CliApplications {
fn parse_yaml(&mut self, yaml: &Mapping) {
for (key, mapping) in yaml {
let Some(name) = key.as_str() else {
continue;
};
let Some(command) = mapping.get("command") else {
continue;
};
let Some(command) = command.as_str() else {
continue;
};
let Some(cli_command) = CliCommand::new(name.to_owned(), command.to_owned()) else {
continue;
};
self.content.push(cli_command)
}
}
}
impl CowStr for CliCommand {
fn cow_str(&self) -> Cow<'_, str> {
let desc_size = 20_usize.saturating_sub(self.desc.len());
format!(
"{desc}{space:<desc_size$}{exe}",
desc = self.desc,
exe = self.executable,
space = " "
)
.into()
}
}
impl_content!(CliApplications, CliCommand);
impl_draw_menu_with_char!(CliApplications, CliCommand);