use crate::Klask; use clap::{Arg, ArgSettings, ValueHint}; use eframe::egui::{widgets::Widget, ComboBox, Response, TextEdit, Ui}; use inflector::Inflector; use native_dialog::FileDialog; use uuid::Uuid; #[derive(Debug, Clone)] pub struct ArgState { pub name: String, pub call_name: Option, pub desc: Option, pub optional: bool, pub use_equals: bool, pub forbid_empty: bool, pub kind: ArgKind, pub validation_error: Option, } #[derive(Debug, Clone)] pub enum ArgKind { String { value: (String, Uuid), default: Option, possible: Vec, value_hint: ValueHint, }, MultipleStrings { values: Vec<(String, Uuid)>, default: Vec, possible: Vec, multiple_values: bool, multiple_occurrences: bool, use_delimiter: bool, req_delimiter: bool, value_hint: ValueHint, }, Occurences(i32), Bool(bool), } impl ArgState { pub fn new(arg: &Arg) -> Self { let kind = if arg.is_set(ArgSettings::TakesValue) { let mut default = arg .get_default_values() .iter() .map(|s| s.to_string_lossy().into_owned()); let possible = arg .get_possible_values() .unwrap_or_default() .iter() .map(|v| v.get_name().to_string()) .collect(); let multiple_values = arg.is_set(ArgSettings::MultipleValues); let multiple_occurrences = arg.is_set(ArgSettings::MultipleOccurrences); if multiple_occurrences | multiple_values { ArgKind::MultipleStrings { values: vec![], default: default.collect(), possible, multiple_values, multiple_occurrences, use_delimiter: arg.is_set(ArgSettings::UseValueDelimiter) | arg.is_set(ArgSettings::RequireDelimiter), req_delimiter: arg.is_set(ArgSettings::RequireDelimiter), value_hint: arg.get_value_hint(), } } else { ArgKind::String { value: ("".to_string(), Uuid::new_v4()), default: default.next(), possible, value_hint: arg.get_value_hint(), } } } else if arg.is_set(ArgSettings::MultipleOccurrences) { ArgKind::Occurences(0) } else { ArgKind::Bool(false) }; Self { name: arg.get_name().to_string().to_sentence_case(), call_name: arg .get_long() .map(|s| format!("--{}", s)) .or_else(|| arg.get_short().map(|c| format!("-{}", c))), desc: arg .get_long_help() .map(ToString::to_string) .or_else(|| arg.get_help().map(ToString::to_string)), optional: !arg.is_set(ArgSettings::Required), use_equals: arg.is_set(ArgSettings::RequireEquals), forbid_empty: arg.is_set(ArgSettings::ForbidEmptyValues), kind, validation_error: None, } } pub fn update_validation_error(&mut self, name: &str, message: &str) { self.validation_error = (self.name == name).then(|| message.to_string()); } pub fn ui_single_row( ui: &mut Ui, (value, id): &mut (String, Uuid), default: &Option, possible: &[String], value_hint: ValueHint, optional: bool, validation_error: bool, ) -> Response { let is_error = (!optional && value.is_empty()) || validation_error; if is_error { ui.set_style(Klask::error_style()); } let inner_response = if possible.is_empty() { ui.horizontal(|ui| { if matches!( value_hint, ValueHint::AnyPath | ValueHint::FilePath | ValueHint::ExecutablePath ) && ui.button("Select file...").clicked() { if let Some(file) = FileDialog::new().show_open_single_file().ok().flatten() { *value = file.to_string_lossy().into_owned(); } } if matches!(value_hint, ValueHint::AnyPath | ValueHint::DirPath) && ui.button("Select directory...").clicked() { if let Some(file) = FileDialog::new().show_open_single_dir().ok().flatten() { *value = file.to_string_lossy().into_owned(); } } ui.add( TextEdit::singleline(value).hint_text(match (default, optional) { (Some(default), _) => default.as_str(), (_, true) => "(Optional)", (_, false) => "", }), ); Some(()) }) } else { ComboBox::from_id_source(id) .selected_text(&value) .show_ui(ui, |ui| { if optional { ui.selectable_value(value, String::new(), "None"); } for p in possible { ui.selectable_value(value, p.clone(), p); } }) }; if is_error { ui.set_style(Klask::klask_style()); } inner_response.response } pub fn get_cmd_args(&self, mut args: Vec) -> Result, String> { match &self.kind { ArgKind::String { value: (value, _), .. } => { if !value.is_empty() { if let Some(call_name) = self.call_name.as_ref() { if self.use_equals { args.push(format!("{}={}", call_name, value)); } else { args.extend_from_slice(&[call_name.clone(), value.clone()]); } } else { args.push(value.clone()); } } else if !self.optional { return Err(format!("{} is required.", self.name)); } } ArgKind::MultipleStrings { values, multiple_values, multiple_occurrences, use_delimiter, req_delimiter, .. } => { if !values.is_empty() { if let Some(call_name) = &self.call_name { let single = *use_delimiter || values.len() == 1; match ( self.use_equals, *multiple_values, *multiple_occurrences, single, ) { (true, true, _, true) => { args.push(format!( "{}={}", call_name, &values .iter() .map(|(s, _)| format!(",{}", s)) .collect::()[1..] )); } (false, true, _, _) => { args.push(call_name.clone()); if *req_delimiter { args.push( (&values .iter() .map(|(s, _)| format!(",{}", s)) .collect::()[1..]) .to_string(), ); } else { for value in values { args.push(value.0.clone()); } } } (true, _, true, _) => { for value in values { args.push(format!("{}={}", call_name, value.0)); } } (false, _, true, _) => { for value in values { args.extend_from_slice(&[call_name.clone(), value.0.clone()]); } } (_, false, false, _) => unreachable!( "Either multiple_values or multiple_occurrences must be true" ), (true, true, false, false) => return Err("Can't be represented".into()), } } else { for value in values { args.push(value.0.clone()); } } } } &ArgKind::Occurences(i) => { for _ in 0..i { args.push( self.call_name .clone() .ok_or_else(|| "Internal error.".to_string())?, ); } } &ArgKind::Bool(bool) => { if bool { args.push( self.call_name .clone() .ok_or_else(|| "Internal error.".to_string())?, ); } } } Ok(args) } } impl Widget for &mut ArgState { fn ui(self, ui: &mut Ui) -> eframe::egui::Response { let label = ui.label(&self.name); if let Some(desc) = &self.desc { label.on_hover_text(desc); } // Grid column automatically switches here let is_validation_error = self.validation_error.is_some(); match &mut self.kind { ArgKind::String { value, default, possible, value_hint, } => ArgState::ui_single_row( ui, value, default, possible, *value_hint, self.optional && !self.forbid_empty, is_validation_error, ), ArgKind::MultipleStrings { values, default, possible, value_hint, .. } => { let forbid_empty = self.forbid_empty; let mut list = ui .vertical(|ui| { let mut remove_index = None; for (index, value) in values.iter_mut().enumerate() { ui.horizontal(|ui| { if ui.small_button("-").clicked() { remove_index = Some(index); } ArgState::ui_single_row( ui, value, &None, possible, *value_hint, !forbid_empty, is_validation_error, ); }); } if let Some(index) = remove_index { values.remove(index); } ui.horizontal(|ui| { if ui.button("New value").clicked() { values.push(("".into(), Uuid::new_v4())); } let text = if default.is_empty() { "Reset" } else { "Reset to default" }; ui.add_space(20.0); if ui.button(text).clicked() { *values = default .iter() .map(|s| (s.to_string(), Uuid::new_v4())) .collect(); } }); }) .response; if let Some(message) = &mut self.validation_error { list = list.on_hover_text(message); if list.changed() { self.validation_error = None; } } list } ArgKind::Occurences(i) => { ui.horizontal(|ui| { if ui.small_button("-").clicked() { *i = (*i - 1).max(0); } ui.label(i.to_string()); if ui.small_button("+").clicked() { *i += 1; } }) .response } ArgKind::Bool(bool) => ui.checkbox(bool, ""), } } }