Skip to content

Commit 106ab6f

Browse files
authored
Merge pull request #55 from tgiachi/feature/consolecommands
feat: SquidStd.ConsoleCommands - interactive console commands with a fixed prompt
2 parents b9cce98 + 310e8e2 commit 106ab6f

34 files changed

Lines changed: 2144 additions & 0 deletions

SquidStd.slnx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<Project Path="src/SquidStd.Caching.Abstractions/SquidStd.Caching.Abstractions.csproj"/>
1111
<Project Path="src/SquidStd.Caching.Redis/SquidStd.Caching.Redis.csproj"/>
1212
<Project Path="src/SquidStd.Caching/SquidStd.Caching.csproj"/>
13+
<Project Path="src/SquidStd.ConsoleCommands/SquidStd.ConsoleCommands.csproj"/>
1314
<Project Path="src/SquidStd.Core/SquidStd.Core.csproj"/>
1415
<Project Path="src/SquidStd.Database.Abstractions/SquidStd.Database.Abstractions.csproj"/>
1516
<Project Path="src/SquidStd.Database/SquidStd.Database.csproj"/>
@@ -55,6 +56,7 @@
5556
<Project Path="samples/SquidStd.Samples.Networking/SquidStd.Samples.Networking.csproj"/>
5657
<Project Path="samples/SquidStd.Samples.Persistence/SquidStd.Samples.Persistence.csproj"/>
5758
<Project Path="samples/SquidStd.Samples.Commands/SquidStd.Samples.Commands.csproj"/>
59+
<Project Path="samples/SquidStd.Samples.ConsoleCommands/SquidStd.Samples.ConsoleCommands.csproj"/>
5860
<Project Path="samples/SquidStd.Samples.Email/SquidStd.Samples.Email.csproj"/>
5961
<Project Path="samples/SquidStd.Samples.Database/SquidStd.Samples.Database.csproj"/>
6062
<Project Path="samples/SquidStd.Samples.AspNetCore/SquidStd.Samples.AspNetCore.csproj"/>

docs/articles/consolecommands.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[!include[](../../src/SquidStd.ConsoleCommands/README.md)]

docs/articles/toc.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@
120120
href: aws-abstractions.md
121121
- name: SquidStd.Tui
122122
href: tui.md
123+
- name: SquidStd.ConsoleCommands
124+
href: consolecommands.md
123125
- name: SquidStd.Crypto
124126
href: crypto.md
125127
- name: SquidStd.Secrets.Aws
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
using SquidStd.ConsoleCommands.Attributes;
2+
using SquidStd.ConsoleCommands.Data;
3+
using SquidStd.ConsoleCommands.Extensions;
4+
using SquidStd.ConsoleCommands.Interfaces;
5+
using SquidStd.Core.Data.Bootstrap;
6+
using SquidStd.Generators.ConsoleCommands;
7+
using SquidStd.Services.Core.Extensions;
8+
using SquidStd.Services.Core.Services.Bootstrap;
9+
10+
var bootstrap = SquidStdBootstrap.Create(
11+
new SquidStdOptions
12+
{
13+
ConfigName = "squidstd",
14+
RootDirectory = AppContext.BaseDirectory,
15+
AppName = "ConsoleDemo"
16+
}
17+
);
18+
19+
bootstrap.ConfigureServices(
20+
container =>
21+
{
22+
container.RegisterCoreServices();
23+
container.AddConsoleCommands();
24+
container.RegisterGeneratedConsoleCommands();
25+
26+
return container;
27+
}
28+
);
29+
30+
// The prompt sink renders the log lines: disable the standard console sink to avoid duplicates.
31+
bootstrap.OnConfigLoaded<SquidStdLoggerOptions>(o => o.EnableConsole = false);
32+
33+
await bootstrap.StartAsync();
34+
35+
var commands = bootstrap.Resolve<ICommandSystemService>();
36+
commands.RegisterCommand(
37+
"echo",
38+
ctx =>
39+
{
40+
ctx.WriteLine(string.Join(' ', ctx.Arguments));
41+
42+
return Task.CompletedTask;
43+
},
44+
"Echoes the arguments back."
45+
);
46+
47+
foreach (var line in await commands.ExecuteCommandWithOutputAsync("help"))
48+
{
49+
Console.WriteLine(line);
50+
}
51+
52+
foreach (var line in await commands.ExecuteCommandWithOutputAsync("ping"))
53+
{
54+
Console.WriteLine(line);
55+
}
56+
57+
await bootstrap.StopAsync();
58+
59+
[RegisterConsoleCommand("ping|p", "Replies pong.")]
60+
internal sealed class PingCommand : IConsoleCommandExecutor
61+
{
62+
public string Description => "Replies pong.";
63+
64+
public Task ExecuteAsync(ConsoleCommandContext context)
65+
{
66+
context.WriteLine("pong");
67+
68+
return Task.CompletedTask;
69+
}
70+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net10.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
<IsPackable>false</IsPackable>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<ProjectReference Include="..\..\src\SquidStd.ConsoleCommands\SquidStd.ConsoleCommands.csproj"/>
13+
<ProjectReference Include="..\..\src\SquidStd.Services.Core\SquidStd.Services.Core.csproj"/>
14+
<ProjectReference Include="..\..\src\SquidStd.Generators\SquidStd.Generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
15+
</ItemGroup>
16+
17+
</Project>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
namespace SquidStd.ConsoleCommands.Attributes;
2+
3+
/// <summary>
4+
/// Marks an <see cref="Interfaces.IConsoleCommandExecutor" /> class for source-generated registration.
5+
/// </summary>
6+
[AttributeUsage(AttributeTargets.Class)]
7+
public sealed class RegisterConsoleCommandAttribute : Attribute
8+
{
9+
/// <summary>Primary command name or aliases separated by <c>|</c>.</summary>
10+
public string CommandName { get; }
11+
12+
/// <summary>Help description.</summary>
13+
public string Description { get; }
14+
15+
/// <summary>Initializes the attribute.</summary>
16+
/// <param name="commandName">Primary command name or aliases separated by <c>|</c>.</param>
17+
/// <param name="description">Help description.</param>
18+
public RegisterConsoleCommandAttribute(string commandName, string description = "")
19+
{
20+
CommandName = commandName;
21+
Description = description;
22+
}
23+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace SquidStd.ConsoleCommands.Data.Config;
2+
3+
/// <summary>
4+
/// Configuration for the interactive console command prompt.
5+
/// </summary>
6+
public sealed class ConsoleCommandsConfig
7+
{
8+
/// <summary>Prompt prefix shown on the input row.</summary>
9+
public string Prompt { get; set; } = "squid> ";
10+
11+
/// <summary>Whether the prompt starts locked.</summary>
12+
public bool StartLocked { get; set; } = true;
13+
14+
/// <summary>Character that unlocks the prompt when locked.</summary>
15+
public char UnlockCharacter { get; set; } = '*';
16+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
namespace SquidStd.ConsoleCommands.Data;
2+
3+
/// <summary>
4+
/// Execution context handed to a console command handler.
5+
/// </summary>
6+
public sealed class ConsoleCommandContext
7+
{
8+
private readonly Action<string> _writeLine;
9+
10+
/// <summary>The raw command line as typed.</summary>
11+
public string RawText { get; }
12+
13+
/// <summary>Parsed arguments (command name excluded).</summary>
14+
public IReadOnlyList<string> Arguments { get; }
15+
16+
/// <summary>Initializes the context.</summary>
17+
/// <param name="rawText">Raw command line.</param>
18+
/// <param name="arguments">Parsed arguments.</param>
19+
/// <param name="writeLine">Writer for command output lines.</param>
20+
public ConsoleCommandContext(string rawText, IReadOnlyList<string> arguments, Action<string> writeLine)
21+
{
22+
ArgumentNullException.ThrowIfNull(writeLine);
23+
24+
RawText = rawText;
25+
Arguments = arguments;
26+
_writeLine = writeLine;
27+
}
28+
29+
/// <summary>Writes one output line.</summary>
30+
public void WriteLine(string line)
31+
=> _writeLine(line);
32+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace SquidStd.ConsoleCommands.Data;
2+
3+
/// <summary>
4+
/// Describes a registered console command.
5+
/// </summary>
6+
/// <param name="Name">Primary command name.</param>
7+
/// <param name="Aliases">Additional aliases.</param>
8+
/// <param name="Description">Help description.</param>
9+
public sealed record ConsoleCommandDefinition(string Name, IReadOnlyList<string> Aliases, string Description);
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using DryIoc;
2+
using Serilog.Core;
3+
using SquidStd.Abstractions.Extensions.Config;
4+
using SquidStd.Abstractions.Extensions.Services;
5+
using SquidStd.ConsoleCommands.Data.Config;
6+
using SquidStd.ConsoleCommands.Interfaces;
7+
using SquidStd.ConsoleCommands.Internal;
8+
using SquidStd.ConsoleCommands.Internal.Logging;
9+
using SquidStd.ConsoleCommands.Services;
10+
using SquidStd.Core.Interfaces.Bootstrap;
11+
12+
namespace SquidStd.ConsoleCommands.Extensions;
13+
14+
/// <summary>
15+
/// Registration helpers for the interactive console command stack.
16+
/// </summary>
17+
public static class ConsoleCommandsRegistrationExtensions
18+
{
19+
/// <param name="container">Container that receives the registrations.</param>
20+
extension(IContainer container)
21+
{
22+
/// <summary>
23+
/// Registers the console command stack: the prompt UI, the command system with the
24+
/// built-in help/clear/exit commands, and the Serilog sink that renders log lines above
25+
/// the prompt. Set <c>logger.EnableConsole: false</c> so the sink replaces the standard
26+
/// console sink.
27+
/// </summary>
28+
/// <returns>The same container for chaining.</returns>
29+
public IContainer AddConsoleCommands()
30+
{
31+
container.RegisterConfigSection("consoleCommands", static () => new ConsoleCommandsConfig(), -50);
32+
container.Register<IConsoleUiService, ConsoleUiService>(Reuse.Singleton);
33+
container.RegisterDelegate<ICommandSystemService>(
34+
resolver =>
35+
{
36+
var ui = resolver.Resolve<IConsoleUiService>();
37+
var system = new CommandSystemService(ui.WriteLine);
38+
var bootstrap = resolver.Resolve<ISquidStdBootstrap>(IfUnresolved.ReturnDefault);
39+
BuiltinConsoleCommands.Register(
40+
system,
41+
bootstrap,
42+
static () =>
43+
{
44+
if (!Console.IsOutputRedirected)
45+
{
46+
Console.Clear();
47+
}
48+
}
49+
);
50+
51+
return system;
52+
},
53+
Reuse.Singleton
54+
);
55+
container.RegisterDelegate<ILogEventSink>(
56+
resolver => new ConsolePromptLogSink(resolver.Resolve<IConsoleUiService>()),
57+
Reuse.Singleton
58+
);
59+
container.RegisterStdService<ConsoleInputLoopService, ConsoleInputLoopService>(500);
60+
61+
return container;
62+
}
63+
}
64+
}

0 commit comments

Comments
 (0)