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

Dynamic subcommands #5109

Closed
2 tasks done
diegofariasm opened this issue Sep 1, 2023 · 1 comment
Closed
2 tasks done

Dynamic subcommands #5109

diegofariasm opened this issue Sep 1, 2023 · 1 comment
Labels
C-enhancement Category: Raise on the bar on expectations

Comments

@diegofariasm
Copy link

Please complete the following tasks

Clap Version

clap = { version = "4.3.1", features = ["derive"] }

Describe your use case

What i want to do:

In my use case, i want to be able to make the cli adapt to ther user environment.
That means that i want the cli to be able to change depending on what it is fed.

Examples

User has two monitors
Runs the cli
Gets subcommands for two monitors

User has one monitor
Runs the cli
Gets subcommands for one monitor

Describe the solution you'd like

I though about having, for example, a way to read subcommands from a file and feed it to the cli.

For example:

config.toml

[volume]
increase = "increase volume"
decrease = "decrease volume"

That would look something like this:

const CLAP_JSON = read_from_file("cli,json)"

let app: clap::App = serde_json::from_str::<clap_serde::CommandWrap>(CLAP_JSON)
    .expect("parse failed")
    .into();


Alternatives, if applicable

I though about using clap_serde, which seems to actually fit my requirements. But, in the other hand, seems to be deprecated.

Additional Context

No response

@diegofariasm diegofariasm added the C-enhancement Category: Raise on the bar on expectations label Sep 1, 2023
@diegofariasm diegofariasm changed the title Subcommands based on other inputs Dynamic subcommands Sep 2, 2023
@diegofariasm
Copy link
Author

I have actually managed to do it. Not the prettiest code, but you can check it out:

Disclamer: i am not really that used to rust. As you can see, it's just not my best code.

use clap::ArgMatches;
use std::collections::HashMap;
use std::path::PathBuf;
mod config;
mod utils;

fn string_to_static_str(s: String) -> &'static str {
    Box::leak(s.into_boxed_str())
}

fn iterate_groups(group: &config::CommandGroup, group_name: String) -> clap::Command {
    let group_command_about = group.about.clone();
    let group_command_name = string_to_static_str(group_name);

    let mut group_command = clap::Command::new(group_command_name)
        .about(group_command_about)
        .subcommand_required(true);

    if let Some(commands) = &group.commands {
        for (command_name, command) in commands {
            let about = command.about.clone();
            let name = string_to_static_str(command_name.clone());

            group_command = group_command.subcommand(clap::Command::new(name).about(about));
        }
    }

    if let Some(groups) = &group.groups {
        for (sub_group_name, sub_group) in groups.iter() {
            let sub_group_app = iterate_groups(sub_group, sub_group_name.clone());

            group_command = group_command.subcommand(sub_group_app);
        }
    }

    group_command
}

fn collect_subcommand_names(matches: &ArgMatches) -> Vec<String> {
    let mut names = Vec::new();

    if let Some(subcommand_name) = matches.subcommand_name() {
        names.push(subcommand_name.to_string());

        if let Some(submatches) = matches.subcommand_matches(subcommand_name) {
            names.extend(collect_subcommand_names(submatches));
        }
    }

    names
}

fn cli_config() -> HashMap<String, config::CommandGroup> {
    let config_file_path = PathBuf::from("maiden.toml"); // Replace with your actual file path

    let parsed_config = match config::load(config_file_path) {
        Ok(parsed_config) => parsed_config,
        Err(err) => {
            panic!("Failed to load config. {}", err);
        }
    };

    parsed_config
}

fn cli(parsed_config: &HashMap<String, config::CommandGroup>) -> clap::Command {
    let mut app = clap::Command::new("maiden")
        .about("abouter tool for controlling things like volume, brightness and etc.")
        .subcommand_required(true);

    for (name, group) in parsed_config {
        // Add the matched subcommand to the cli.
        // The iterate through will automatically go through
        // any of the nested groups.
        app = app.subcommand(iterate_groups(&group, name.clone()));
    }

    app
}

fn find_command(
    group: &config::CommandGroup,
    names: &[String],
    cli_cfg: &HashMap<String, config::CommandGroup>,
) {
    if let Some(commands) = &group.commands {
        for (command_name, command) in commands {
            if names.contains(&command_name) {
                // Run the "bin" specified in the toml file.
                // This also uses the args defined there.
                match utils::run_command(&command.bin, &command.args) {
                    Ok(output) => {
                        // Print the output so other things can use.
                        // For example: if using deflisten on eww,
                        // you could use this wrapper to run the scripts.
                        println!("{}", output)
                    }
                    Err(e) => {
                        panic!("Failed to run: {}", e);
                    }
                }
            }
        }
    }
    if let Some(groups) = &group.groups {
        for (sub_group_name, sub_group) in groups.iter() {
            if names.contains(&sub_group_name) {
                find_command(sub_group, names, cli_cfg);
            }
        }
    }
}

fn main() {
    let cli_cfg = cli_config();
    let cli = cli(&cli_cfg);

    let matches = cli.get_matches();
    let names = collect_subcommand_names(&matches);

    // Match the used command.
    // It is matched against the config.
    // Later, i will rework this so you can also add normal commands.
    for name in &names {
        if let Some(group) = cli_cfg.get(name) {
            find_command(group, &names, &cli_cfg);
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-enhancement Category: Raise on the bar on expectations
Projects
None yet
Development

No branches or pull requests

1 participant