Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Derive a form (multiple prompts) from struct #11

Open
simonsan opened this issue Mar 14, 2024 · 15 comments · May be fixed by #17
Open

Derive a form (multiple prompts) from struct #11

simonsan opened this issue Mar 14, 2024 · 15 comments · May be fixed by #17

Comments

@simonsan
Copy link

simonsan commented Mar 14, 2024

I would like to easily annotate a configuration struct, to ask the user for input for various fields and give a hint on validation:

use derive_bla::AskUser;

#[derive(Debug, AskUser)]
struct MyConf {
    #[ask(q="Please enter password:", hide_input=true, min_len=12)]
    password: Option<String>,

    #[ask(q="Please enter username:", min_len=3)]
    username: Option<String>,

    #[ask(q="Please enter age:", range=18..=85)]
    age: Option<u8>,
}

fn main() -> Result<()> {
    let mut conf = MyConf::default()
    conf.password()?.validate()?;
    conf.username()?.validate()?;
    conf.age()?.validate()?;

    println!("{conf#?}");
}

I would be interested if something like this is already existing, e.g. based on inquire, promkit, or if there is any interest to add this?

There are some ideas already in inquire, but it feels a bit out of scope.

CC: mikaelmello/inquire#212
CC: mikaelmello/inquire#65

EDIT: It would basically be the clap way of doing interactive prompts. Annotate something and give it to the user to let it be filled out. I think it comes with its own advantages and disadvantages, and excels a lot in a workflow where you suddenly want the user to fill out some data you already have bundled in a struct.

I think it could be a game changing feature, though. Because it would make dealing with interactive prompts much easier for a lot of users. Also, it would be excellent to have for people that don't want the full-fledged control of a ratatui TUI, but still a bit more ergonomic when it comes to interactivity and developer experience creating such.

@ynqa
Copy link
Owner

ynqa commented Mar 15, 2024

@simonsan Thank you for your proposal!

I would be interested if something like this is already existing, e.g. based on inquire, promkit, or if there is any interest to add this?

Currently, we do not offer such a feature (this is simply because I am not yet well-versed in Derive/macro). However, setting aside the specifics of the interface, I understand what you are looking to achieve and I am very interested in it.

@simonsan
Copy link
Author

Nice! In one issue, there is an example/demo of how such a thing could work: https://github.com/IniterWorker/inquire_derive

Maybe it helps to get started. Also, there is the The Little Book of Rust Macros and there is the proc-macro workshop by dtolnay. I'm saying this, because I'm trying to support and hope that such a thing will exist at one point in time, as I think it would be really valuable to the ecosystem. (:

@ynqa
Copy link
Owner

ynqa commented Mar 16, 2024

@simonsan I've started by creating a prototype. I believe it needs to be refined further, but what do you think?

use promkit::{crossterm::style::Color, style::StyleBuilder, Result};
use promkit_derive::Promkit;

#[derive(Default, Debug, Promkit)]
struct MyStruct {
    #[ask(
        prefix = "What is your name?",
        prefix_style = StyleBuilder::new().fgc(Color::DarkCyan).build(),
    )]
    name: String,

    #[ask(prefix = "How old are you?", ignore_invalid_attr = "nothing")]
    age: usize,
}

fn main() -> Result {
    let ret = MyStruct::default().ask_name()?.ask_age()?;
    dbg!(ret);
    Ok(())
}

@ynqa ynqa added the v0.4.0 label Mar 16, 2024
@simonsan
Copy link
Author

simonsan commented Mar 16, 2024

Looks really nice! 👍🏽 I need to try it out, to see if the API is ergonomic for me. But the way you instantiate and do it, is exactly how I would imagine it. In the end, it's like a builder pattern, that is being exposed directly to the user. For example, if you would like to ask for each field in the struct it would be pleasant to have a short calling method like MyStruct::ask().

Which would internally call Default::default() on all fields, if #[ask(default)] is set on this field.

  • returning a result, so we can return early with the try operator after each step

  • validate could be implemented on top of it at later stage, there is a validator crate, which API is ergonomic and could be inspiring. T.validate_args((77, 555)).is_ok() sound like a good pattern to me as well

  • for attributes to annotate TypedBuilder is interesting to look at for inspiration I think

Great work! 🌷

@ynqa
Copy link
Owner

ynqa commented Mar 16, 2024

@simonsan Thank you for your comment! 👍 Please feel free to continue posting your feedback after you have had a chance to try it out. As a favor, could I ask for the opportunity to have you take another review the macro after I've refined it based on your feedback?

@simonsan
Copy link
Author

@simonsan Thank you for your comment! 👍 Please feel free to continue post your feedback after you have had a chance to try it out. As a favor, could I ask for the opportunity to have you take another look at the macro after I've refined it based on your feedback?

For sure! :) 👍🏽

@ynqa ynqa linked a pull request Mar 18, 2024 that will close this issue
@ynqa
Copy link
Owner

ynqa commented Mar 19, 2024

@simonsan I refined it in PR #17. Here are the changes:

  • Changed the term ask to readline. This indicates the use of the preset readline. Any parameters that can be set via the macro attribute will directly apply as parameters for readline.
  • Added #[readline(default)]. This defines readline as the default.
  • The supported field types are Option and T (where T satisfies FromStr). This is because it incorporates parse().

The concerns are as follows, and I would appreciate your opinions:

  • Changing the attribute from ask to readline
  • The name of the Derive (currently Promkit)

Example:

use promkit::{crossterm::style::Color, style::StyleBuilder, Result};
use promkit_derive::Promkit;

#[derive(Default, Debug, Promkit)]
struct Profile {
    #[readline(
        prefix = "What is your name?",
        prefix_style = StyleBuilder::new().fgc(Color::DarkCyan).build(),
    )]
    name: String,

    #[readline(default)]
    hobby: Option<String>,

    #[readline(prefix = "How old are you?", ignore_invalid_attr = "nothing")]
    age: usize,
}

fn main() -> Result {
    let mut ret = Profile::default();
    ret.readline_name()?;
    ret.readline_hobby()?;
    ret.readline_age()?;
    dbg!(ret);
    Ok(())
}

Thanks 👍

@simonsan
Copy link
Author

simonsan commented Mar 19, 2024

I'm on the way to going out, so only tiny feedback:

  • I don't mind, if it's called readline, or ask, or something. I think it should make clear the intent, of what is happening, and readline might be sufficient for that. Could also be input for example.

  • prefix I personally find a bit confusing, I would rather call it prompt as this would make it clear what it will do

  • for the supported fields I think something like a Vec and a HashSet (although we can build that from a Vec might be good as well, like clap does it:

    /// Tags for the activity
    #[cfg_attr(
        feature = "clap",
        clap(
            short,
            long,
            group = "adjust",
            name = "Tags",
            value_name = "Tags",
            visible_alias = "tag",
            value_delimiter = ','
        )
    )]
    tags: Option<Vec<String>>,
  • for understanding better: what does 'defines readline as default' mean in this context? my assumption would be: it prompts the user for a value, and if not given, as it's optional, it takes the default value for that type?

@simonsan
Copy link
Author

simonsan commented Mar 19, 2024

Some naming ideas:

For the readline attribute:

  • user_prompt
  • input_prompt
  • question
  • user_input

For the prefix attribute:

  • prompt_text
  • question_text
  • message
  • query

@ynqa
Copy link
Owner

ynqa commented Mar 19, 2024

@simonsan Thanks for your comments! 👍

Indeed, the terminology needs to be clearer about what can be done. It felt like it could also be offered as something separate from presets.

Accepting collections with delimiters is a good idea. On the other hand, incorporating various parser logics might make the macro complex (it already seemed quite complex when allowing for Option). Therefore, what do you think about accepting parser functions in the attribute for types beyond primitives, like str -> Vec<T>, str -> Option<T> (I believe this was also in clap)?

@simonsan
Copy link
Author

simonsan commented Mar 19, 2024

Some idea:

use promkit::{crossterm::style::Color, style::StyleBuilder, Result};
use promkit_derive::Form;

#[derive(Default, Debug, Form)]
struct Profile {
    #[input(
        prompt = "What is your name?",
        prompt_style = StyleBuilder::new().fgc(Color::DarkCyan).build(),
    )]
    name: String,

    #[input(default)]
    hobby: Option<String>,

    #[input(prompt = "How old are you?", ignore_invalid_attr = "nothing")]
    age: usize,
}

fn main() -> Result {
    let mut ret = Profile::default();
    ret.prompt_name()?;
    ret.prompt_hobby()?;
    ret.prompt_age()?;
    dbg!(ret);
    Ok(())
}

FormBuilder could be also a viable option, as in:

use promkit_derive::FormBuilder;

#[derive(Default, Debug, FormBuilder)]

@simonsan
Copy link
Author

A form could be even layouted like this:

Please fill out the form:
------------------------
What is your name?: John Doe
What is your hobby? (Leave empty if none):
How old are you?: 25

Use Up/Down to navigate through the inputs
Press Enter if you are ready to continue

I don't know if that is easily possible, though 😅 It's not a dealbreaker, would just double down on the Form

@simonsan
Copy link
Author

simonsan commented Mar 19, 2024

Accepting collections with delimiters is a good idea. On the other hand, incorporating various parser logics might make the macro complex (it already seemed quite complex when allowing for Option). Therefore, what do you think about accepting parser functions in the attribute for types beyond primitives, like str -> Vec<T>, str -> Option<T> (I believe this was also in clap)?

I agree completely. We don't want to make it too complex. I think clap has https://docs.rs/clap/latest/clap/builder/struct.ValueParser.html for this.

@ynqa ynqa added v0.5.0 and removed v0.4.0 labels Jun 4, 2024
@simonsan
Copy link
Author

I found this one from the Go ecosystem, which might be also interesting from a design perspective:
https://github.com/charmbracelet/huh

@ynqa ynqa removed the v0.5.0 label Oct 23, 2024
@ynqa
Copy link
Owner

ynqa commented Oct 31, 2024

When you are feeling ready for a review, let me know, I'll be able to look over it then. 🫂

From this comment.

@simonsan I was on a bit of a journey to working on some projects like jnv and sig (and also doing the actual travel). I'm gradually returning to this development. Apologies for the delay in my response.

A form could be even layouted like this:

I’ve prepared the form-like UI (ref) mentioned in your previous comment and I'm currently updating the derive to use this form mechanism instead of readline. Once that is complete, I'll formally submit a pull request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants