Kestrel hace 1 semana
commit
6630aebfaf
Se han modificado 7 ficheros con 158 adiciones y 0 borrados
  1. 3 0
      .gitignore
  2. 1 0
      .vimrc
  3. 9 0
      Cargo.toml
  4. 11 0
      derive/Cargo.toml
  5. 61 0
      derive/src/lib.rs
  6. 10 0
      examples/action_prompt.rs
  7. 63 0
      src/lib.rs

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+/target
+/Cargo.lock
+.*.sw?

+ 1 - 0
.vimrc

@@ -0,0 +1 @@
+set wildignore+=target

+ 9 - 0
Cargo.toml

@@ -0,0 +1,9 @@
+[package]
+name = "cliask"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+cliask-derive = { path = "derive", version = "0.1.0" }
+
+console = { version = "0.16", default-features = false, features = [ "std" ] }

+ 11 - 0
derive/Cargo.toml

@@ -0,0 +1,11 @@
+[package]
+name = "cliask-derive"
+version = "0.1.0"
+edition = "2024"
+
+[lib]
+proc-macro = true
+
+[dependencies]
+syn = { version = "2.0", default-features = false, features = ["derive", "parsing", "proc-macro"] }
+quote = { version = "1.0" }

+ 61 - 0
derive/src/lib.rs

@@ -0,0 +1,61 @@
+use std::collections::BTreeMap;
+
+use quote::quote;
+
+fn do_action(input: syn::DeriveInput) -> Result<proc_macro::TokenStream, syn::Error> {
+    assert_eq!(input.generics.params.len(), 0);
+
+    let syn::Data::Enum(enumdata) = input.data else {
+        return Err(syn::Error::new(input.ident.span(), "must be enum"));
+    };
+
+    // confirm all variants are units
+    for var in enumdata.variants.iter() {
+        assert!(var.fields.is_empty());
+    }
+
+    let mut keys = vec![];
+
+    for var in enumdata.variants.iter() {
+        let name = var.ident.to_string();
+        println!("variant name: {}", name);
+
+        let Some(key) = name.chars().filter(|v| v.is_uppercase() && !keys.contains(v)).next() else {
+            return Err(syn::Error::new(var.ident.span(), "no unused key for action entry"))
+        };
+        keys.push(key);
+    }
+
+    let enum_ident = input.ident;
+    let action_count = enumdata.variants.len();
+    let idents = enumdata.variants.iter().map(|v| v.ident.clone()).collect::<Vec<_>>();
+    let labels = enumdata.variants.iter().map(|v| v.ident.to_string()).collect::<Vec<_>>();
+
+    Ok(quote! {
+        impl ::cliask::ActionEnum for #enum_ident {
+            const ACTION_COUNT : usize = #action_count;
+            const LABELS : &'static [&'static str] = &[ #(#labels),* ];
+            const KEYS : &'static [char] = &[ #(#keys),* ];
+
+            fn try_parse(from: char) -> Option<#enum_ident> {
+                match from.to_uppercase().next().unwrap() {
+                    #(
+                        #keys => Some ( Self :: #idents ),
+                    )*
+                    _ => None
+                }
+            }
+        }
+    }.into())
+}
+
+#[proc_macro_derive(ActionEnum)]
+pub fn action(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
+    let input = syn::parse_macro_input!(input as syn::DeriveInput);
+
+    match do_action(input) {
+        Ok(ts) => ts,
+        Err(err) => err.to_compile_error().into()
+    }
+}
+

+ 10 - 0
examples/action_prompt.rs

@@ -0,0 +1,10 @@
+
+#[derive(cliask::ActionEnum)]
+enum YesNoPrompt {
+    Yes,
+    No
+}
+
+fn main() {
+    let answer : Result<YesNoPrompt, _> = cliask::ActionPrompt::new().run();
+}

+ 63 - 0
src/lib.rs

@@ -0,0 +1,63 @@
+use std::io::Write;
+
+pub use cliask_derive::ActionEnum;
+
+pub enum AskError {
+    IOError(std::io::Error)
+}
+
+impl From<std::io::Error> for AskError {
+    fn from(value: std::io::Error) -> Self {
+        Self::IOError(value)
+    }
+}
+
+pub trait ActionEnum: 'static {
+    const ACTION_COUNT : usize;
+    const LABELS : &'static [&'static str];
+    const KEYS : &'static [char];
+    fn try_parse(from: char) -> Option<Self> where Self: Sized;
+}
+
+pub struct ActionPrompt<'l, AE: ActionEnum> {
+    default_action: Option<AE>,
+    _ghost: std::marker::PhantomData<&'l AE>
+}
+
+impl<'l, AE: ActionEnum> ActionPrompt<'l, AE> {
+    pub fn new() -> Self {
+        Self { default_action: None, _ghost: std::marker::PhantomData }
+    }
+    pub fn run(self) -> Result<AE, AskError> {
+        let mut stdout = console::Term::stdout();
+
+        let mut options = String::new();
+        let mut first = true;
+        for (label,key) in AE::LABELS.iter().zip(AE::KEYS) {
+            let key_string = key.to_string();
+            let emphasized_label = label.replace(&key_string, format!("({})", console::style(&key_string).underlined()).as_str());
+            if first {
+                first = false
+            } else {
+                options.push('/');
+            }
+            options += format!("{}", emphasized_label).as_str();
+        }
+
+        options.push('?');
+
+        stdout.write(options.as_bytes())?;
+
+        loop {
+            if let Some(action) = AE::try_parse(stdout.read_char()?) {
+                return Ok(action)
+            }
+        }
+    }
+}
+
+pub struct SelectPrompt<'l> {
+    prompt: &'l str,
+}
+
+