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

Choice menu for all the registered commands #1654

Open
flupkede opened this issue Sep 23, 2024 · 12 comments
Open

Choice menu for all the registered commands #1654

flupkede opened this issue Sep 23, 2024 · 12 comments

Comments

@flupkede
Copy link

Description
When we build console programs we often want to invoke them with direct commands and parameters from a commandline, as well as to run them interactively from some kind of menu system to select a command from that you want to run. To achieve this I added a default command "Menu" that shows a numbered list of all the commands and uses a selectionprompt to chose the command. I know the menu system wouldn't handle the command arguments, however we use appsettings a lot.

Describe the solution you'd like
It would be nice to have such a command OOB, and if that is a to specific request to have at least the possibility to have somewhere a list of the registered commands available in the context in a way we can also Run them.

Describe alternatives you've considered
An awfull solution I quickly put together, however it works for me.
It was (at least to my understanding) rather difficult to achieve this currently and I had to do a lot of workaround to make it work, the execute method of the default Menu command:
public override int Execute(CommandContext context)
{
var type = GetType().Assembly.GetName();
_con.MarkupLine($"[yellow]{type.Name} v.{type.Version.Major}.{type.Version.Minor}[/] dotnetcore REST Client");
_con.MarkupLine("[blue]--help for info[/]");

    var commandAppService = _provider.GetService(typeof(Spectre.Console.Cli.ICommandApp));
    var commandApp = commandAppService.GetType().GetField("_app", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(commandAppService);
    var configurator = commandApp.GetType().GetField("_configurator", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(commandApp);
    var commands = configurator.GetType().GetProperty("Commands").GetValue(configurator) as IList;

    var prompt = new SelectionPrompt<SelectionCommandItem>() { SearchEnabled = true }
        .Title("Select command to run:")
        .PageSize(20)
        .MoreChoicesText("[yellow][[more]][/]");
    prompt.Converter = item => { return $"[[{item.Sequence + 1:000}]] - {item.CommandTitle}"; };

    string commandTitle;
    for (byte count = 0; count < commands.Count - 1; count++)
    {
        var configuredCommandType = commands[count].GetType();
        var commandType = configuredCommandType.GetProperty("CommandType").GetValue(commands[count]) as Type;
        var executeType = (commandType.BaseType.Name?.StartsWith("Async") == true) ? "Async" : string.Empty;
        var settingsType = configuredCommandType.GetProperty("SettingsType").GetValue(commands[count]) as Type;
        commandTitle = configuredCommandType.GetProperty("Name").GetValue(commands[count]) as string;
        prompt.AddChoice(new SelectionCommandItem(count, commandTitle, commandType, settingsType, executeType));
    }

    prompt.AddChoice(new SelectionCommandItem(commands.Count, "QUIT", null, null, null));

    var selection = _con.Prompt(prompt);

    if (selection.CommandTitle.Equals("QUIT"))
        return 0;

    _con.MarkupLine($"Executing command: [yellow]{selection.CommandTitle}[/]");

    var command = ActivatorUtilities.CreateInstance(_provider, selection.CommandType);
    var settings = ActivatorUtilities.CreateInstance(_provider, selection.SettingsType);
    
    selection.CommandType.InvokeMember($"Execute{selection.ExecuteType}", BindingFlags.InvokeMethod, null, command, [context, settings]);

    return 0;
}

and

public sealed class SelectionCommandItem(byte sequence, string commandTitle, Type commandType, Type settingsType, string executeType)
{
public byte Sequence { get; } = sequence;
public string CommandTitle { get; } = commandTitle;
public Type CommandType { get; } = commandType;
public Type SettingsType { get; } = settingsType;
public string ExecuteType { get; } = executeType;
}


Please upvote 👍 this issue if you are interested in it.

@flupkede flupkede changed the title Choice menu for all the command Choice menu for all the registered commands Sep 23, 2024
@FrankRay78
Copy link
Contributor

An interesting idea.

@patriksvensson
Copy link
Contributor

Although interesting, I think this is outside the scope of the project, and probably more suited as an external extension to Spectre.Console.

@FrankRay78
Copy link
Contributor

Yes, indeed. We still need to think about what other wigets may qualify for it too, and see how a new project/NuGet package for them might be setup.

@flupkede
Copy link
Author

flupkede commented Oct 2, 2024

An external extension would be fine, however it would mean that some of the sealed classes need to become public to be able to achieve this ?

@patriksvensson
Copy link
Contributor

@flupkede What classes would that be?

@flupkede
Copy link
Author

flupkede commented Oct 2, 2024

@patriksvensson , to my understanding which might be wrong.
I used public sealed class CommandApp to get the configurator
I used internal sealed class CommandConfigurator to get the registered commands.

@FrankRay78
Copy link
Contributor

A recent discussion about this idea FYI, see: #1610

@FrankRay78
Copy link
Contributor

FrankRay78 commented Oct 4, 2024

@flupkede, @patriksvensson - I would be keen to investigate this further, as the idea of future community extensions is of personal interest to me. However @flupkede, I probably won't have spare capacity for another 4 weeks until my current CLI work is complete. Don't hesitate to ping me around that time to ensure this hasn't slipped my mind.

(nb. happy for others to work on this ahead of me, if they wish)

@flupkede
Copy link
Author

flupkede commented Oct 4, 2024

@FrankRay78 I would be more then happy to collaborate on this project, however I will require some of your guidance and direction.

@FrankRay78
Copy link
Contributor

@FrankRay78 I would be more then happy to collaborate on this project, however I will require some of your guidance and direction.

@flupkede I've got a proposal for you, which I will write up shortly for your consideration. My support comes free 😉

@FrankRay78
Copy link
Contributor

Hello @flupkede, apologies for the delay - I had a few things to finish off first. So, I'm not entirely sure why, but I'm personally interested in the idea of extensibility points and enabling community plugins (perhaps more so than other maintainers... 😉). I extended the help provider to make it pluggable, see here, a worthy labour of love. It's not yet entirely done in my eyes, but the foundation has been set.

There is a long-standing, not to be broken, design decision within Spectre.Console that internal, sealed classes will never be exposed. However, as per the help provider above, exposing necessary functionality through interfaces and DI is allowable, if well reasoned. I've spoken with the other maintainers, and whilst there is no guarantee of merging, I've got the greenlight to proceed with investigating this further, and potentially making the case for exposing necessary interfaces for your Choice menu.

The most mature community extension we currently have is Spectre.Console AutoCompletion, however, that's been implemented without input from the core maintainer team, so the author 'liberated' internal classes (in a similar way to your use of Reflection). Access modifiers were basically turned off on a blanket basis ie:

<PackageReference Include="IgnoresAccessChecksToGenerator" Version="0.6.0" PrivateAssets="All" />

See here.

My proposal to you, and the same I put to the other maintainers, is you could create a repo somewhere for your Choice widget, reference Spectre.Console and turn off access checks by including the above in your project file. Author the widget to a good professional standard, and once done, we'd remove the line from the project file, enforcing access checks to show exactly where the project fails to compile. We would then have a clear understanding of what classes need to be exposed, and could look to POC that through interfaces on a branch.

Baring an unforeseen act of god, I can commit to supporting you with this, up to the point where the Spectre.Console team make their decision. I'd be happy to 'present' the findings of this piece of work, so it doesn't end up ignored. Have a think about it and let me know your thoughts.

@Redth
Copy link

Redth commented Oct 18, 2024

Really interesting timing running across this. I've started building another dotnet global tool and naturally Spectre is my first choice as I've used it in the past and have enjoyed it (Thanks!!)

In this new console app, I was searching if there was an existing way to have the app be able to pivot between specifying the full command / branches and options without requiring any interactivity, but I'd also like to have the app intelligent enough to prompt for the information that's required but not specified...

So for instance I'm working on an app to help synchronize dev certificates and profiles for mobile apps...

I might have a command like gizmo create apple certificate --type=Development --common-name="Acme". I'd also like the user to be able to invoke the app with gizmo create apple or gizmo create or gizmo and have it prompt for branching selections based on how the CommandApp is composed. So if I start with gizmo create I get a list of choices (eg: Apple, Google, Token, etc) and as I make choices I navigate the branches.

Similarly I'd like to be able to 'fill in' Command Settings for those not specified. So, prompt for --type and as an enum, give me the list of choices. If it's text/numeric/etc input, parse out the input from a prompt. The command option description and name could be displayed during this interactive set of prompts. The information is all basically here, we just need a way to drive the experience :)

Unfortunately I won't have any time to help contribute myself, so I'm merely adding a +1 request and some thoughts for how I'd like to use something like this, and will again express my gratitude to everyone who's put effort into this project as I've enjoyed using it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Todo 🕑
Development

No branches or pull requests

4 participants