Skip to content

Commit

Permalink
Merge pull request #1 from jakenuts/feature-singular-addcommand
Browse files Browse the repository at this point in the history
Feature singular addcommand
  • Loading branch information
jakenuts authored Dec 31, 2023
2 parents 46f6056 + 1666eb8 commit a03e860
Show file tree
Hide file tree
Showing 11 changed files with 216 additions and 43 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Spectre.Console;
using Spectre.Console.Cli;

namespace Community.Extensions.Spectre.Cli.Hosting.Sample.Commands;

/// <summary>
/// Another command, just to show that multiple commands can be added
/// </summary>
public class OtherCommand : AsyncCommand<OtherCommand.Options>
{
private readonly IAnsiConsole _console;

/// <summary>
/// Creates a OtherCommand with access to the console and logging
/// </summary>
/// <param name="console"></param>
/// <param name="log"></param>
public OtherCommand(IAnsiConsole console, ILogger<HelloCommand> log)
{
_console = console;
}

/// <summary>Executes the command.</summary>
/// <param name="context">The command context.</param>
/// <param name="options">The command options.</param>
/// <returns>An integer indicating whether or not the command executed successfully.</returns>
public override async Task<int> ExecuteAsync(CommandContext context, Options options)
{
_console.MarkupLineInterpolated($"[springgreen2_1] Other {options.Stuff}![/]");

return 0;
}

[Description("OtherOptions")]
public class Options : CommandSettings
{
[Description("Other Stuff")]
[CommandArgument(0, "<stuff>")]
public string? Stuff { get; set; }
}
}
29 changes: 15 additions & 14 deletions src/Community.Extensions.Spectre.Cli.Hosting.Sample/Program.cs
Original file line number Diff line number Diff line change
@@ -1,33 +1,34 @@
using System.Diagnostics;
using Community.Extensions.Spectre.Cli.Hosting;
using Community.Extensions.Spectre.Cli.Hosting.Sample.Commands;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Spectre.Console;
using Spectre.Console.Cli;
using Community.Extensions.Spectre.Cli.Hosting;
using Community.Extensions.Spectre.Cli.Hosting.Sample.Commands;

var builder = Host.CreateApplicationBuilder(args);
builder.Logging.AddSimpleConsole();

// Yes this is duplicated, this is the one we'll use and that
// can receive services from the outer host service provider.
builder.Services.AddCommand<HelloCommand, HelloCommand.Options>();
// Add a command and optionally configure it.
builder.Services.AddCommand<HelloCommand>("hello", cmd =>
{
cmd.WithDescription("A command that says hello");
});

// Add another command
builder.Services.AddCommand<OtherCommand>("other");

//
// The standard call save for the commands will be pre-added & configured
//
builder.UseSpectreConsole<HelloCommand>(config =>
{
// All commands above are passed to config.AddCommand() by this point
#if DEBUG
config.PropagateExceptions();
config.ValidateExamples();
#endif
config.SetApplicationName("hello");
config.SetExceptionHandler(BasicExceptionHandler.WriteException);
// This configures the command with the internal service provider.
// Unfortunately, it comes after the external service provider & host have
// already been built. In future configuration should be extracted to a builder
// that can be configured prior to service provider creation allowing the two
// AddCommand calls to be combined.
config.AddCommand<HelloCommand>("hello");
config.UseBasicExceptionHandler();
});

var app = builder.Build();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
{
"profiles": {
"Community.Extensions.Spectre.Cli.Hosting.Sample": {
"Sample": {
"commandName": "Project",
"commandLineArgs": "You -p Sadie"
//"commandLineArgs": "You -p Sadie"
//"commandLineArgs": "other stuff"
"commandLineArgs": "--help"
},
"Interactive": {
"commandName": "Executable",
"executablePath": "pwsh.exe",
"commandLineArgs": "-NoExit -c \"Set-Alias -Name hello -Value \"$(TargetDir)$(AssemblyName).exe\"",
"workingDirectory": "$(ProjectDir)"
}
/*, For reference only
"WT": {
"commandName": "Executable",
"executablePath": "wt.exe",
"commandLineArgs": "pwsh.exe -NoExit -c \"Set-Alias -Name hello -Value \"$(TargetDir)$(AssemblyName).exe\"",
"workingDirectory": "$(ProjectDir)"
}*/
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ namespace Community.Extensions.Spectre.Cli.Hosting;
/// </summary>
public static class BasicExceptionHandler
{
/// <summary>
/// Sets the exception handler to write the exception to the AnsiConsole.
/// </summary>
/// <param name="configurator"></param>
/// <returns></returns>
public static IConfigurator UseBasicExceptionHandler(this IConfigurator configurator)
{
return configurator.SetExceptionHandler(WriteException);
}
/// <summary>
/// Writes the exception to the AnsiConsole.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Spectre.Console.Cli;

namespace Community.Extensions.Spectre.Cli.Hosting.Internal;

/// <summary>
/// A typed registration class for commands with their types and name
/// </summary>
/// <param name="Name"></param>
/// <typeparam name="TCommand"></typeparam>
public record CommandRegistration<TCommand>(string Name, Action<ICommandConfigurator>? CommandConfigurator = null)
: CommandRegistration(typeof(TCommand), Name) where TCommand : class, ICommand
{
/// <summary>
/// </summary>
/// <param name="configuration"></param>
public override void Configure(IConfigurator configuration)
{
// Add the command to Spectre's configuration
var cmdConfig = configuration.AddCommand<TCommand>(Name);

// Optionally configure the command
CommandConfigurator?.Invoke(cmdConfig);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Spectre.Console.Cli;

namespace Community.Extensions.Spectre.Cli.Hosting.Internal;

/// <summary>
/// A base registration class for commands with their types and name
/// </summary>
/// <param name="CommandType"></param>
/// <param name="Name"></param>
public abstract record CommandRegistration(Type CommandType, string Name)
{
/// <summary>
/// </summary>
/// <param name="configuration"></param>
public abstract void Configure(IConfigurator configuration);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Hosting.Internal;
using Spectre.Console;
using Spectre.Console.Cli;

namespace Community.Extensions.Spectre.Cli.Hosting.Internal;

/// <summary>
/// Extends <see cref="IHostBuilder" /> with SpectreConsole commands.
/// </summary>
public static class CommandRegistrationExtensions
{

/// <summary>
/// Returns registered commands
/// </summary>
/// <param name="serviceProvider"></param>
/// <returns></returns>
public static IEnumerable<CommandRegistration> GetRegisteredCommands(this IServiceProvider serviceProvider) =>
serviceProvider.GetServices<CommandRegistration>();

/// <summary>
/// Registers a command with it's primary type, name and optional configuration action
/// </summary>
/// <param name="services"></param>
/// <param name="name"></param>
/// <param name="commandConfigurator"></param>
/// <typeparam name="TCommand"></typeparam>
/// <returns></returns>
public static IServiceCollection RegisterCommand<TCommand>(this IServiceCollection services, string name,
Action<ICommandConfigurator>? commandConfigurator = null)
where TCommand : class, ICommand
{
return services.AddTransient<CommandRegistration, CommandRegistration<TCommand>>(c =>
new CommandRegistration<TCommand>(name, commandConfigurator));
}

/// <summary>
/// Adds registered commands to the provided app and allows further customization of the app and commands
/// </summary>
/// <param name="app"></param>
/// <param name="provider"></param>
/// <param name="configureCommandApp"></param>
/// <returns></returns>
internal static ICommandApp ConfigureAppAndRegisteredCommands(this ICommandApp app, IServiceProvider provider, Action<IConfigurator>? configureCommandApp = null)
{
app.Configure(config =>
{
// Add/Configure registered commands
foreach (var cmd in provider.GetRegisteredCommands())
{
cmd.Configure(config);
}
// Optionally allow caller to configure the command app
configureCommandApp?.Invoke(config);
});

return app;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
using Spectre.Console.Cli;


namespace Community.Extensions.Spectre.Cli.Hosting;
namespace Community.Extensions.Spectre.Cli.Hosting.Internal;

internal sealed class CustomTypeRegistrar : ITypeRegistrar
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using Microsoft.Extensions.Logging;
using Spectre.Console.Cli;

namespace Community.Extensions.Spectre.Cli.Hosting;
namespace Community.Extensions.Spectre.Cli.Hosting.Internal;

internal sealed class CustomTypeResolver : ITypeResolver, IDisposable
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text;
using Community.Extensions.Spectre.Cli.Hosting.Internal;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Hosting.Internal;
Expand All @@ -13,31 +14,32 @@ namespace Community.Extensions.Spectre.Cli.Hosting;
public static class SpectreConsoleHostBuilderExtensions
{
/// <summary>
/// Adds a command and it's options to the service collection
/// Adds a command and it's options to the service collection. Also registers the command
/// to be added & configured during the UseSpectreConsole call.
/// </summary>
/// <typeparam name="TCommand"></typeparam>
/// <typeparam name="TOptions"></typeparam>
/// <param name="services"></param>
/// <param name="name"></param>
/// <param name="commandConfigurator">The configuration action applied to the command</param>
/// <returns></returns>
public static IServiceCollection AddCommand<TCommand, TOptions>(this IServiceCollection services)
where TCommand : class, ICommand<TOptions>
where TOptions : CommandSettings
public static IServiceCollection AddCommand<TCommand>(this IServiceCollection services, string name,
Action<ICommandConfigurator>? commandConfigurator = null)
where TCommand : class, ICommand

{
// Could use ConfigurationHelper.GetSettingsType(typeof(TCommand)) but I want options flexible
services.AddSingleton<TCommand>();
services.AddTransient<TOptions>();
services.RegisterCommand<TCommand>(name, commandConfigurator);
return services;
}

/// <summary>
/// Adds the internal services to the host builder.
/// Adds the internal services to the host builder.
/// </summary>
/// <param name="builder"></param>
/// <returns></returns>
private static HostApplicationBuilder AddInternalServices(HostApplicationBuilder builder)
{
System.Console.OutputEncoding = Encoding.Default;
Console.OutputEncoding = Encoding.Default;

builder.Services.AddHostedService<SpectreConsoleWorker>();
builder.Services.AddSingleton(x => AnsiConsole.Console);
Expand All @@ -60,14 +62,8 @@ public static HostApplicationBuilder UseSpectreConsole(this HostApplicationBuild

builder.Services.AddSingleton<ICommandApp>(x =>
{
var command = new CommandApp(new CustomTypeRegistrar(builder.Services, x));
if (configureCommandApp != null)
{
command.Configure(configureCommandApp);
}
return command;
var app = new CommandApp(new CustomTypeRegistrar(builder.Services, x));
return app.ConfigureAppAndRegisteredCommands(x, configureCommandApp);
});

return AddInternalServices(builder);
Expand All @@ -89,14 +85,9 @@ public static HostApplicationBuilder UseSpectreConsole<TDefaultCommand>(this Hos

builder.Services.AddSingleton<ICommandApp>(x =>
{
var command = new CommandApp<TDefaultCommand>(new CustomTypeRegistrar(builder.Services, x));
if (configureCommandApp != null)
{
command.Configure(configureCommandApp);
}
return command;
// Create the command app
var app = new CommandApp<TDefaultCommand>(new CustomTypeRegistrar(builder.Services, x));
return app.ConfigureAppAndRegisteredCommands(x, configureCommandApp);
});

return AddInternalServices(builder);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

namespace Community.Extensions.Spectre.Cli.Hosting;

/// <summary>
/// A background service that runs the Spectre Console App
/// </summary>
public class SpectreConsoleWorker : BackgroundService
{
private readonly ICommandApp _commandApp;
Expand All @@ -14,6 +17,12 @@ public class SpectreConsoleWorker : BackgroundService

private int _exitCode;

/// <summary>
///
/// </summary>
/// <param name="logger"></param>
/// <param name="commandApp"></param>
/// <param name="hostLifetime"></param>
public SpectreConsoleWorker(ILogger<SpectreConsoleWorker> logger, ICommandApp commandApp,
IHostApplicationLifetime hostLifetime)
{
Expand Down

0 comments on commit a03e860

Please sign in to comment.