Make HelpProvider colors configurable (#1408)

This commit is contained in:
Frank Ray 2024-01-18 14:31:28 +00:00 committed by GitHub
parent d5b4621233
commit 9cc888e5ad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 633 additions and 121 deletions

View File

@ -15,6 +15,34 @@ The help is also context aware and tailored depending on what has been specified
`HelpProvider` is the `Spectre.Console` class responsible for determining context and preparing the help text to write to the console. It is an implementation of the public interface `IHelpProvider`. `HelpProvider` is the `Spectre.Console` class responsible for determining context and preparing the help text to write to the console. It is an implementation of the public interface `IHelpProvider`.
## Styling the help text
Basic styling is applied to the generated help text by default, however this is configurable.
`HelpProviderStyle` is the `Spectre.Console` class that holds the style information for the help text.
The default theme shipped with Spectre.Console is provided by a factory method, `HelpProviderStyle.Default`.
However, you can explicitly set a custom theme when configuring a CommandApp, for example:
```csharp
config.Settings.HelpProviderStyles = new HelpProviderStyle()
{
Description = new DescriptionStyle()
{
Header = "bold",
},
};
```
Removing all styling from help text is also possible, a good choice for ensuring maximum accessibility. This is configured by clearing the style provider entirely:
```csharp
config.Settings.HelpProviderStyles = null;
```
See [Markup](../markup) for information about the use of markup in Spectre.Console, and [Styles](xref:styles) for a listing of supported styles.
## Custom help providers ## Custom help providers
Whilst it shouldn't be common place to implement your own help provider, it is however possible. Whilst it shouldn't be common place to implement your own help provider, it is however possible.

View File

@ -1,4 +1,5 @@
using Spectre.Console.Cli; using Spectre.Console.Cli;
using Spectre.Console.Cli.Help;
namespace Help; namespace Help;
@ -12,6 +13,9 @@ public static class Program
{ {
// Register the custom help provider // Register the custom help provider
config.SetHelpProvider(new CustomHelpProvider(config.Settings)); config.SetHelpProvider(new CustomHelpProvider(config.Settings));
// Render an unstyled help text for maximum accessibility
config.Settings.HelpProviderStyles = null;
}); });
return app.Run(args); return app.Run(args);

View File

@ -1,5 +1,3 @@
using Spectre.Console.Cli.Resources;
namespace Spectre.Console.Cli.Help; namespace Spectre.Console.Cli.Help;
/// <summary> /// <summary>
@ -10,7 +8,8 @@ namespace Spectre.Console.Cli.Help;
/// </remarks> /// </remarks>
public class HelpProvider : IHelpProvider public class HelpProvider : IHelpProvider
{ {
private HelpProviderResources resources; private readonly HelpProviderResources resources;
private readonly HelpProviderStyle? helpStyles;
/// <summary> /// <summary>
/// Gets a value indicating how many examples from direct children to show in the help text. /// Gets a value indicating how many examples from direct children to show in the help text.
@ -27,6 +26,14 @@ public class HelpProvider : IHelpProvider
/// </summary> /// </summary>
protected virtual bool TrimTrailingPeriod { get; } protected virtual bool TrimTrailingPeriod { get; }
/// <summary>
/// Gets a value indicating whether to emit the markup styles, inline, when rendering the help text.
/// </summary>
/// <remarks>
/// Useful for unit testing different styling of the same help text.
/// </remarks>
protected virtual bool RenderMarkupInline { get; } = false;
private sealed class HelpArgument private sealed class HelpArgument
{ {
public string Name { get; } public string Name { get; }
@ -94,6 +101,11 @@ public class HelpProvider : IHelpProvider
} }
} }
internal Composer NewComposer()
{
return new Composer(RenderMarkupInline);
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="HelpProvider"/> class. /// Initializes a new instance of the <see cref="HelpProvider"/> class.
/// </summary> /// </summary>
@ -104,6 +116,10 @@ public class HelpProvider : IHelpProvider
this.MaximumIndirectExamples = settings.MaximumIndirectExamples; this.MaximumIndirectExamples = settings.MaximumIndirectExamples;
this.TrimTrailingPeriod = settings.TrimTrailingPeriod; this.TrimTrailingPeriod = settings.TrimTrailingPeriod;
// Don't provide a default style if HelpProviderStyles is null,
// as the user will have explicitly done this to output unstyled help text
this.helpStyles = settings.HelpProviderStyles;
resources = new HelpProviderResources(settings.Culture); resources = new HelpProviderResources(settings.Culture);
} }
@ -148,8 +164,8 @@ public class HelpProvider : IHelpProvider
yield break; yield break;
} }
var composer = new Composer(); var composer = NewComposer();
composer.Style("yellow", $"{resources.Description}:").LineBreak(); composer.Style(helpStyles?.Description?.Header ?? Style.Plain, $"{resources.Description}:").LineBreak();
composer.Text(command.Description).LineBreak(); composer.Text(command.Description).LineBreak();
yield return composer.LineBreak(); yield return composer.LineBreak();
} }
@ -162,16 +178,16 @@ public class HelpProvider : IHelpProvider
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns> /// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
public virtual IEnumerable<IRenderable> GetUsage(ICommandModel model, ICommandInfo? command) public virtual IEnumerable<IRenderable> GetUsage(ICommandModel model, ICommandInfo? command)
{ {
var composer = new Composer(); var composer = NewComposer();
composer.Style("yellow", $"{resources.Usage}:").LineBreak(); composer.Style(helpStyles?.Usage?.Header ?? Style.Plain, $"{resources.Usage}:").LineBreak();
composer.Tab().Text(model.ApplicationName); composer.Tab().Text(model.ApplicationName);
var parameters = new List<string>(); var parameters = new List<Composer>();
if (command == null) if (command == null)
{ {
parameters.Add($"[grey][[{resources.Options}]][/]"); parameters.Add(NewComposer().Style(helpStyles?.Usage?.Options ?? Style.Plain, $"[{resources.Options}]"));
parameters.Add($"[aqua]<{resources.Command}>[/]"); parameters.Add(NewComposer().Style(helpStyles?.Usage?.Command ?? Style.Plain, $"<{resources.Command}>"));
} }
else else
{ {
@ -183,11 +199,11 @@ public class HelpProvider : IHelpProvider
{ {
if (isCurrent) if (isCurrent)
{ {
parameters.Add($"[underline]{current.Name.EscapeMarkup()}[/]"); parameters.Add(NewComposer().Style(helpStyles?.Usage?.CurrentCommand ?? Style.Plain, $"{current.Name}"));
} }
else else
{ {
parameters.Add($"{current.Name.EscapeMarkup()}"); parameters.Add(NewComposer().Text(current.Name));
} }
} }
@ -198,7 +214,7 @@ public class HelpProvider : IHelpProvider
foreach (var argument in current.Parameters.OfType<ICommandArgument>() foreach (var argument in current.Parameters.OfType<ICommandArgument>()
.Where(a => a.Required).OrderBy(a => a.Position).ToArray()) .Where(a => a.Required).OrderBy(a => a.Position).ToArray())
{ {
parameters.Add($"[aqua]<{argument.Value.EscapeMarkup()}>[/]"); parameters.Add(NewComposer().Style(helpStyles?.Usage?.RequiredArgument ?? Style.Plain, $"<{argument.Value}>"));
} }
} }
@ -207,27 +223,27 @@ public class HelpProvider : IHelpProvider
{ {
foreach (var optionalArgument in optionalArguments) foreach (var optionalArgument in optionalArguments)
{ {
parameters.Add($"[silver][[{optionalArgument.Value.EscapeMarkup()}]][/]"); parameters.Add(NewComposer().Style(helpStyles?.Usage?.OptionalArgument ?? Style.Plain, $"[{optionalArgument.Value}]"));
} }
} }
} }
if (isCurrent) if (isCurrent)
{ {
parameters.Add($"[grey][[{resources.Options}]][/]"); parameters.Add(NewComposer().Style(helpStyles?.Usage?.Options ?? Style.Plain, $"[{resources.Options}]"));
} }
} }
if (command.IsBranch && command.DefaultCommand == null) if (command.IsBranch && command.DefaultCommand == null)
{ {
// The user must specify the command // The user must specify the command
parameters.Add($"[aqua]<{resources.Command}>[/]"); parameters.Add(NewComposer().Style(helpStyles?.Usage?.Command ?? Style.Plain, $"<{resources.Command}>"));
} }
else if (command.IsBranch && command.DefaultCommand != null && command.Commands.Count > 0) else if (command.IsBranch && command.DefaultCommand != null && command.Commands.Count > 0)
{ {
// We are on a branch with a default command // We are on a branch with a default command
// The user can optionally specify the command // The user can optionally specify the command
parameters.Add($"[aqua][[{resources.Command}]][/]"); parameters.Add(NewComposer().Style(helpStyles?.Usage?.Command ?? Style.Plain, $"[{resources.Command}]"));
} }
else if (command.IsDefaultCommand) else if (command.IsDefaultCommand)
{ {
@ -237,7 +253,7 @@ public class HelpProvider : IHelpProvider
{ {
// Commands other than the default are present // Commands other than the default are present
// So make these optional in the usage statement // So make these optional in the usage statement
parameters.Add($"[aqua][[{resources.Command}]][/]"); parameters.Add(NewComposer().Style(helpStyles?.Usage?.Command ?? Style.Plain, $"[{resources.Command}]"));
} }
} }
} }
@ -245,10 +261,7 @@ public class HelpProvider : IHelpProvider
composer.Join(" ", parameters); composer.Join(" ", parameters);
composer.LineBreak(); composer.LineBreak();
return new[] return new[] { composer };
{
composer,
};
} }
/// <summary> /// <summary>
@ -302,14 +315,14 @@ public class HelpProvider : IHelpProvider
if (Math.Min(maxExamples, examples.Count) > 0) if (Math.Min(maxExamples, examples.Count) > 0)
{ {
var composer = new Composer(); var composer = NewComposer();
composer.LineBreak(); composer.LineBreak();
composer.Style("yellow", $"{resources.Examples}:").LineBreak(); composer.Style(helpStyles?.Examples?.Header ?? Style.Plain, $"{resources.Examples}:").LineBreak();
for (var index = 0; index < Math.Min(maxExamples, examples.Count); index++) for (var index = 0; index < Math.Min(maxExamples, examples.Count); index++)
{ {
var args = string.Join(" ", examples[index]); var args = string.Join(" ", examples[index]);
composer.Tab().Text(model.ApplicationName).Space().Style("grey", args); composer.Tab().Text(model.ApplicationName).Space().Style(helpStyles?.Examples?.Arguments ?? Style.Plain, args);
composer.LineBreak(); composer.LineBreak();
} }
@ -334,11 +347,9 @@ public class HelpProvider : IHelpProvider
} }
var result = new List<IRenderable> var result = new List<IRenderable>
{ {
new Markup(Environment.NewLine), NewComposer().LineBreak().Style(helpStyles?.Arguments?.Header ?? Style.Plain, $"{resources.Arguments}:").LineBreak(),
new Markup($"[yellow]{resources.Arguments}:[/]"), };
new Markup(Environment.NewLine),
};
var grid = new Grid(); var grid = new Grid();
grid.AddColumn(new GridColumn { Padding = new Padding(4, 4), NoWrap = true }); grid.AddColumn(new GridColumn { Padding = new Padding(4, 4), NoWrap = true });
@ -347,15 +358,15 @@ public class HelpProvider : IHelpProvider
foreach (var argument in arguments.Where(x => x.Required).OrderBy(x => x.Position)) foreach (var argument in arguments.Where(x => x.Required).OrderBy(x => x.Position))
{ {
grid.AddRow( grid.AddRow(
$"[silver]<{argument.Name.EscapeMarkup()}>[/]", NewComposer().Style(helpStyles?.Arguments?.RequiredArgument ?? Style.Plain, $"<{argument.Name}>"),
argument.Description?.TrimEnd('.') ?? " "); NewComposer().Text(argument.Description?.TrimEnd('.') ?? " "));
} }
foreach (var argument in arguments.Where(x => !x.Required).OrderBy(x => x.Position)) foreach (var argument in arguments.Where(x => !x.Required).OrderBy(x => x.Position))
{ {
grid.AddRow( grid.AddRow(
$"[grey][[{argument.Name.EscapeMarkup()}]][/]", NewComposer().Style(helpStyles?.Arguments?.OptionalArgument ?? Style.Plain, $"[{argument.Name}]"),
argument.Description?.TrimEnd('.') ?? " "); NewComposer().Text(argument.Description?.TrimEnd('.') ?? " "));
} }
result.Add(grid); result.Add(grid);
@ -379,11 +390,9 @@ public class HelpProvider : IHelpProvider
} }
var result = new List<IRenderable> var result = new List<IRenderable>
{ {
new Markup(Environment.NewLine), NewComposer().LineBreak().Style(helpStyles?.Options?.Header ?? Style.Plain, $"{resources.Options}:").LineBreak(),
new Markup($"[yellow]{resources.Options}:[/]"), };
new Markup(Environment.NewLine),
};
var helpOptions = parameters.ToArray(); var helpOptions = parameters.ToArray();
var defaultValueColumn = ShowOptionDefaultValues && helpOptions.Any(e => e.DefaultValue != null); var defaultValueColumn = ShowOptionDefaultValues && helpOptions.Any(e => e.DefaultValue != null);
@ -397,71 +406,24 @@ public class HelpProvider : IHelpProvider
grid.AddColumn(new GridColumn { Padding = new Padding(0, 0) }); grid.AddColumn(new GridColumn { Padding = new Padding(0, 0) });
static string GetOptionParts(HelpOption option)
{
var builder = new StringBuilder();
if (option.Short != null)
{
builder.Append('-').Append(option.Short.EscapeMarkup());
if (option.Long != null)
{
builder.Append(", ");
}
}
else
{
builder.Append(" ");
if (option.Long != null)
{
builder.Append(" ");
}
}
if (option.Long != null)
{
builder.Append("--").Append(option.Long.EscapeMarkup());
}
if (option.Value != null)
{
builder.Append(' ');
if (option.ValueIsOptional ?? false)
{
builder.Append("[grey][[").Append(option.Value.EscapeMarkup()).Append("]][/]");
}
else
{
builder.Append("[silver]<").Append(option.Value.EscapeMarkup()).Append(">[/]");
}
}
return builder.ToString();
}
if (defaultValueColumn) if (defaultValueColumn)
{ {
grid.AddRow(" ", $"[lime]{resources.Default}[/]", " "); grid.AddRow(
NewComposer().Space(),
NewComposer().Style(helpStyles?.Options?.DefaultValueHeader ?? Style.Plain, resources.Default),
NewComposer().Space());
} }
foreach (var option in helpOptions) foreach (var option in helpOptions)
{ {
var columns = new List<string> { GetOptionParts(option) }; var columns = new List<IRenderable>() { GetOptionParts(option) };
if (defaultValueColumn) if (defaultValueColumn)
{ {
static string Bold(object obj) => $"[bold]{obj.ToString().EscapeMarkup()}[/]"; columns.Add(GetOptionDefaultValue(option.DefaultValue));
var defaultValue = option.DefaultValue switch
{
null => " ",
"" => " ",
Array { Length: 0 } => " ",
Array array => string.Join(", ", array.Cast<object>().Select(Bold)),
_ => Bold(option.DefaultValue),
};
columns.Add(defaultValue);
} }
columns.Add(option.Description?.TrimEnd('.') ?? " "); columns.Add(NewComposer().Text(option.Description?.TrimEnd('.') ?? " "));
grid.AddRow(columns.ToArray()); grid.AddRow(columns.ToArray());
} }
@ -471,6 +433,60 @@ public class HelpProvider : IHelpProvider
return result; return result;
} }
private IRenderable GetOptionParts(HelpOption option)
{
var composer = NewComposer();
if (option.Short != null)
{
composer.Text("-").Text(option.Short);
if (option.Long != null)
{
composer.Text(", ");
}
}
else
{
composer.Text(" ");
if (option.Long != null)
{
composer.Text(" ");
}
}
if (option.Long != null)
{
composer.Text("--").Text(option.Long);
}
if (option.Value != null)
{
composer.Text(" ");
if (option.ValueIsOptional ?? false)
{
composer.Style(helpStyles?.Options?.OptionalOption ?? Style.Plain, $"[{option.Value}]");
}
else
{
composer.Style(helpStyles?.Options?.RequiredOption ?? Style.Plain, $"<{option.Value}>");
}
}
return composer;
}
private IRenderable GetOptionDefaultValue(object? defaultValue)
{
return defaultValue switch
{
null => NewComposer().Text(" "),
"" => NewComposer().Text(" "),
Array { Length: 0 } => NewComposer().Text(" "),
Array array => NewComposer().Join(", ", array.Cast<object>().Select(o => NewComposer().Style(helpStyles?.Options?.DefaultValue ?? Style.Plain, o.ToString() ?? string.Empty))),
_ => NewComposer().Style(helpStyles?.Options?.DefaultValue ?? Style.Plain, defaultValue?.ToString() ?? string.Empty),
};
}
/// <summary> /// <summary>
/// Gets the commands section of the help information. /// Gets the commands section of the help information.
/// </summary> /// </summary>
@ -491,11 +507,9 @@ public class HelpProvider : IHelpProvider
} }
var result = new List<IRenderable> var result = new List<IRenderable>
{ {
new Markup(Environment.NewLine), NewComposer().LineBreak().Style(helpStyles?.Commands?.Header ?? Style.Plain, $"{resources.Commands}:").LineBreak(),
new Markup($"[yellow]{resources.Commands}:[/]"), };
new Markup(Environment.NewLine),
};
var grid = new Grid(); var grid = new Grid();
grid.AddColumn(new GridColumn { Padding = new Padding(4, 4), NoWrap = true }); grid.AddColumn(new GridColumn { Padding = new Padding(4, 4), NoWrap = true });
@ -503,27 +517,27 @@ public class HelpProvider : IHelpProvider
foreach (var child in commands) foreach (var child in commands)
{ {
var arguments = new Composer(); var arguments = NewComposer();
arguments.Style("silver", child.Name.EscapeMarkup()); arguments.Style(helpStyles?.Commands?.ChildCommand ?? Style.Plain, child.Name);
arguments.Space(); arguments.Space();
foreach (var argument in HelpArgument.Get(child).Where(a => a.Required)) foreach (var argument in HelpArgument.Get(child).Where(a => a.Required))
{ {
arguments.Style("silver", $"<{argument.Name.EscapeMarkup()}>"); arguments.Style(helpStyles?.Commands?.RequiredArgument ?? Style.Plain, $"<{argument.Name}>");
arguments.Space(); arguments.Space();
} }
if (TrimTrailingPeriod) if (TrimTrailingPeriod)
{ {
grid.AddRow( grid.AddRow(
arguments.ToString().TrimEnd(), NewComposer().Text(arguments.ToString().TrimEnd()),
child.Description?.TrimEnd('.') ?? " "); NewComposer().Text(child.Description?.TrimEnd('.') ?? " "));
} }
else else
{ {
grid.AddRow( grid.AddRow(
arguments.ToString().TrimEnd(), NewComposer().Text(arguments.ToString().TrimEnd()),
child.Description ?? " "); NewComposer().Text(child.Description ?? " "));
} }
} }

View File

@ -0,0 +1,219 @@
namespace Spectre.Console.Cli.Help;
/// <summary>
/// Styles for the HelpProvider to use when rendering help text.
/// </summary>
public sealed class HelpProviderStyle
{
/// <summary>
/// Gets or sets the style for describing the purpose or details of a command.
/// </summary>
public DescriptionStyle? Description { get; set; }
/// <summary>
/// Gets or sets the style for specifying the usage format of a command.
/// </summary>
public UsageStyle? Usage { get; set; }
/// <summary>
/// Gets or sets the style for providing examples of command usage.
/// </summary>
public ExampleStyle? Examples { get; set; }
/// <summary>
/// Gets or sets the style for specifying arguments in a command.
/// </summary>
public ArgumentStyle? Arguments { get; set; }
/// <summary>
/// Gets or sets the style for specifying options or flags in a command.
/// </summary>
public OptionStyle? Options { get; set; }
/// <summary>
/// Gets or sets the style for specifying subcommands or nested commands.
/// </summary>
public CommandStyle? Commands { get; set; }
/// <summary>
/// Gets the default HelpProvider styles.
/// </summary>
public static HelpProviderStyle Default { get; } =
new HelpProviderStyle()
{
Description = new DescriptionStyle()
{
Header = "yellow",
},
Usage = new UsageStyle()
{
Header = "yellow",
CurrentCommand = "underline",
Command = "aqua",
Options = "grey",
RequiredArgument = "aqua",
OptionalArgument = "silver",
},
Examples = new ExampleStyle()
{
Header = "yellow",
Arguments = "grey",
},
Arguments = new ArgumentStyle()
{
Header = "yellow",
RequiredArgument = "silver",
OptionalArgument = "silver",
},
Commands = new CommandStyle()
{
Header = "yellow",
ChildCommand = "silver",
RequiredArgument = "silver",
},
Options = new OptionStyle()
{
Header = "yellow",
DefaultValueHeader = "lime",
DefaultValue = "bold",
RequiredOption = "silver",
OptionalOption = "grey",
},
};
}
/// <summary>
/// Defines styles for describing the purpose or details of a command.
/// </summary>
public sealed class DescriptionStyle
{
/// <summary>
/// Gets or sets the style for the header in the description.
/// </summary>
public Style? Header { get; set; }
}
/// <summary>
/// Defines styles for specifying the usage format of a command.
/// </summary>
public sealed class UsageStyle
{
/// <summary>
/// Gets or sets the style for the header in the usage.
/// </summary>
public Style? Header { get; set; }
/// <summary>
/// Gets or sets the style for the current command in the usage.
/// </summary>
public Style? CurrentCommand { get; set; }
/// <summary>
/// Gets or sets the style for general commands in the usage.
/// </summary>
public Style? Command { get; set; }
/// <summary>
/// Gets or sets the style for options in the usage.
/// </summary>
public Style? Options { get; set; }
/// <summary>
/// Gets or sets the style for required arguments in the usage.
/// </summary>
public Style? RequiredArgument { get; set; }
/// <summary>
/// Gets or sets the style for optional arguments in the usage.
/// </summary>
public Style? OptionalArgument { get; set; }
}
/// <summary>
/// Defines styles for providing examples of command usage.
/// </summary>
public sealed class ExampleStyle
{
/// <summary>
/// Gets or sets the style for the header in the examples.
/// </summary>
public Style? Header { get; set; }
/// <summary>
/// Gets or sets the style for arguments in the examples.
/// </summary>
public Style? Arguments { get; set; }
}
/// <summary>
/// Defines styles for specifying arguments in a command.
/// </summary>
public sealed class ArgumentStyle
{
/// <summary>
/// Gets or sets the style for the header in the arguments.
/// </summary>
public Style? Header { get; set; }
/// <summary>
/// Gets or sets the style for required arguments.
/// </summary>
public Style? RequiredArgument { get; set; }
/// <summary>
/// Gets or sets the style for optional arguments.
/// </summary>
public Style? OptionalArgument { get; set; }
}
/// <summary>
/// Defines styles for specifying subcommands or nested commands.
/// </summary>
public sealed class CommandStyle
{
/// <summary>
/// Gets or sets the style for the header in the command section.
/// </summary>
public Style? Header { get; set; }
/// <summary>
/// Gets or sets the style for child commands in the command section.
/// </summary>
public Style? ChildCommand { get; set; }
/// <summary>
/// Gets or sets the style for required arguments in the command section.
/// </summary>
public Style? RequiredArgument { get; set; }
}
/// <summary>
/// Defines styles for specifying options or flags in a command.
/// </summary>
public sealed class OptionStyle
{
/// <summary>
/// Gets or sets the style for the header in the options.
/// </summary>
public Style? Header { get; set; }
/// <summary>
/// Gets or sets the style for the header of default values in the options.
/// </summary>
public Style? DefaultValueHeader { get; set; }
/// <summary>
/// Gets or sets the style for default values in the options.
/// </summary>
public Style? DefaultValue { get; set; }
/// <summary>
/// Gets or sets the style for required options.
/// </summary>
public Style? RequiredOption { get; set; }
/// <summary>
/// Gets or sets the style for optional options.
/// </summary>
public Style? OptionalOption { get; set; }
}

View File

@ -41,6 +41,11 @@ public interface ICommandAppSettings
/// </summary> /// </summary>
bool TrimTrailingPeriod { get; set; } bool TrimTrailingPeriod { get; set; }
/// <summary>
/// Gets or sets the styles to used when rendering the help text.
/// </summary>
HelpProviderStyle? HelpProviderStyles { get; set; }
/// <summary> /// <summary>
/// Gets or sets the <see cref="IAnsiConsole"/>. /// Gets or sets the <see cref="IAnsiConsole"/>.
/// </summary> /// </summary>

View File

@ -4,22 +4,43 @@ internal sealed class Composer : IRenderable
{ {
private readonly StringBuilder _content; private readonly StringBuilder _content;
/// <summary>
/// Whether to emit the markup styles, inline, when rendering the content.
/// </summary>
private readonly bool _renderMarkup = false;
public Composer() public Composer()
{ {
_content = new StringBuilder(); _content = new StringBuilder();
} }
public Composer(bool renderMarkup)
: this()
{
_renderMarkup = renderMarkup;
}
public Composer Text(string text) public Composer Text(string text)
{ {
_content.Append(text); _content.Append(text);
return this; return this;
} }
public Composer Style(Style style, string text)
{
_content.Append('[').Append(style.ToMarkup()).Append(']');
_content.Append(text.EscapeMarkup());
_content.Append("[/]");
return this;
}
public Composer Style(string style, string text) public Composer Style(string style, string text)
{ {
_content.Append('[').Append(style).Append(']'); _content.Append('[').Append(style).Append(']');
_content.Append(text.EscapeMarkup()); _content.Append(text.EscapeMarkup());
_content.Append("[/]"); _content.Append("[/]");
return this; return this;
} }
@ -28,6 +49,7 @@ internal sealed class Composer : IRenderable
_content.Append('[').Append(style).Append(']'); _content.Append('[').Append(style).Append(']');
action(this); action(this);
_content.Append("[/]"); _content.Append("[/]");
return this; return this;
} }
@ -72,12 +94,19 @@ internal sealed class Composer : IRenderable
return this; return this;
} }
public Composer Join(string separator, IEnumerable<string> composers) public Composer Join(string separator, IEnumerable<Composer> composers)
{ {
if (composers != null) if (composers != null)
{ {
Space(); foreach (var composer in composers)
Text(string.Join(separator, composers)); {
if (_content.ToString().Length > 0)
{
Text(separator);
}
Text(composer.ToString());
}
} }
return this; return this;
@ -85,12 +114,26 @@ internal sealed class Composer : IRenderable
public Measurement Measure(RenderOptions options, int maxWidth) public Measurement Measure(RenderOptions options, int maxWidth)
{ {
return ((IRenderable)new Markup(_content.ToString())).Measure(options, maxWidth); if (_renderMarkup)
{
return ((IRenderable)new Paragraph(_content.ToString())).Measure(options, maxWidth);
}
else
{
return ((IRenderable)new Markup(_content.ToString())).Measure(options, maxWidth);
}
} }
public IEnumerable<Segment> Render(RenderOptions options, int maxWidth) public IEnumerable<Segment> Render(RenderOptions options, int maxWidth)
{ {
return ((IRenderable)new Markup(_content.ToString())).Render(options, maxWidth); if (_renderMarkup)
{
return ((IRenderable)new Paragraph(_content.ToString())).Render(options, maxWidth);
}
else
{
return ((IRenderable)new Markup(_content.ToString())).Render(options, maxWidth);
}
} }
public override string ToString() public override string ToString()

View File

@ -14,9 +14,10 @@ internal sealed class CommandAppSettings : ICommandAppSettings
public CaseSensitivity CaseSensitivity { get; set; } public CaseSensitivity CaseSensitivity { get; set; }
public bool PropagateExceptions { get; set; } public bool PropagateExceptions { get; set; }
public bool ValidateExamples { get; set; } public bool ValidateExamples { get; set; }
public bool TrimTrailingPeriod { get; set; } = true; public bool TrimTrailingPeriod { get; set; }
public HelpProviderStyle? HelpProviderStyles { get; set; }
public bool StrictParsing { get; set; } public bool StrictParsing { get; set; }
public bool ConvertFlagsToRemainingArguments { get; set; } = false; public bool ConvertFlagsToRemainingArguments { get; set; }
public ParsingMode ParsingMode => public ParsingMode ParsingMode =>
StrictParsing ? ParsingMode.Strict : ParsingMode.Relaxed; StrictParsing ? ParsingMode.Strict : ParsingMode.Relaxed;
@ -29,6 +30,9 @@ internal sealed class CommandAppSettings : ICommandAppSettings
CaseSensitivity = CaseSensitivity.All; CaseSensitivity = CaseSensitivity.All;
ShowOptionDefaultValues = true; ShowOptionDefaultValues = true;
MaximumIndirectExamples = 5; MaximumIndirectExamples = 5;
TrimTrailingPeriod = true;
HelpProviderStyles = HelpProviderStyle.Default;
ConvertFlagsToRemainingArguments = false;
} }
public bool IsTrue(Func<CommandAppSettings, bool> func, string environmentVariableName) public bool IsTrue(Func<CommandAppSettings, bool> func, string environmentVariableName)

View File

@ -26,7 +26,7 @@ internal static class MarkupParser
if (token.Kind == MarkupTokenKind.Open) if (token.Kind == MarkupTokenKind.Open)
{ {
var parsedStyle = StyleParser.Parse(token.Value); var parsedStyle = string.IsNullOrEmpty(token.Value) ? Style.Plain : StyleParser.Parse(token.Value);
stack.Push(parsedStyle); stack.Push(parsedStyle);
} }
else if (token.Kind == MarkupTokenKind.Close) else if (token.Kind == MarkupTokenKind.Close)

View File

@ -0,0 +1,11 @@
namespace Spectre.Console.Cli.Tests.Data.Help;
internal class RenderMarkupHelpProvider : HelpProvider
{
protected override bool RenderMarkupInline { get; } = true;
public RenderMarkupHelpProvider(ICommandAppSettings settings)
: base(settings)
{
}
}

View File

@ -14,4 +14,8 @@ public class LionSettings : CatSettings
[Description("The days the lion goes hunting.")] [Description("The days the lion goes hunting.")]
[DefaultValue(new[] { DayOfWeek.Monday, DayOfWeek.Thursday })] [DefaultValue(new[] { DayOfWeek.Monday, DayOfWeek.Thursday })]
public DayOfWeek[] HuntDays { get; set; } public DayOfWeek[] HuntDays { get; set; }
[CommandOption("-w|--weight [WEIGHT]")]
[Description("The weight of the lion, in kgs.")]
public FlagValue<int?> Weight { get; set; }
} }

View File

@ -17,3 +17,4 @@ OPTIONS:
--agility <VALUE> 10 The agility between 0 and 100 --agility <VALUE> 10 The agility between 0 and 100
-c <CHILDREN> The number of children the lion has -c <CHILDREN> The number of children the lion has
-d <DAY> Monday, Thursday The days the lion goes hunting -d <DAY> Monday, Thursday The days the lion goes hunting
-w, --weight [WEIGHT] The weight of the lion, in kgs

View File

@ -17,3 +17,4 @@ OPTIONS:
--agility <VALUE> 10 The agility between 0 and 100 --agility <VALUE> 10 The agility between 0 and 100
-c <CHILDREN> The number of children the lion has -c <CHILDREN> The number of children the lion has
-d <DAY> Monday, Thursday The days the lion goes hunting -d <DAY> Monday, Thursday The days the lion goes hunting
-w, --weight [WEIGHT] The weight of the lion, in kgs

View File

@ -20,6 +20,7 @@ OPTIONEN:
--agility <VALUE> 10 The agility between 0 and 100 --agility <VALUE> 10 The agility between 0 and 100
-c <CHILDREN> The number of children the lion has -c <CHILDREN> The number of children the lion has
-d <DAY> Monday, Thursday The days the lion goes hunting -d <DAY> Monday, Thursday The days the lion goes hunting
-w, --weight [WEIGHT] The weight of the lion, in kgs
KOMMANDOS: KOMMANDOS:
giraffe <LENGTH> The giraffe command giraffe <LENGTH> The giraffe command

View File

@ -20,6 +20,7 @@ OPTIONS:
--agility <VALUE> 10 The agility between 0 and 100 --agility <VALUE> 10 The agility between 0 and 100
-c <CHILDREN> The number of children the lion has -c <CHILDREN> The number of children the lion has
-d <DAY> Monday, Thursday The days the lion goes hunting -d <DAY> Monday, Thursday The days the lion goes hunting
-w, --weight [WEIGHT] The weight of the lion, in kgs
COMMANDS: COMMANDS:
giraffe <LENGTH> The giraffe command giraffe <LENGTH> The giraffe command

View File

@ -20,6 +20,7 @@ OPTIONS:
--agility <VALUE> 10 The agility between 0 and 100 --agility <VALUE> 10 The agility between 0 and 100
-c <CHILDREN> The number of children the lion has -c <CHILDREN> The number of children the lion has
-d <DAY> Monday, Thursday The days the lion goes hunting -d <DAY> Monday, Thursday The days the lion goes hunting
-w, --weight [WEIGHT] The weight of the lion, in kgs
COMMANDES: COMMANDES:
giraffe <LENGTH> The giraffe command giraffe <LENGTH> The giraffe command

View File

@ -20,6 +20,7 @@ VAL:
--agility <VALUE> 10 The agility between 0 and 100 --agility <VALUE> 10 The agility between 0 and 100
-c <CHILDREN> The number of children the lion has -c <CHILDREN> The number of children the lion has
-d <DAY> Monday, Thursday The days the lion goes hunting -d <DAY> Monday, Thursday The days the lion goes hunting
-w, --weight [WEIGHT] The weight of the lion, in kgs
KOMMANDON: KOMMANDON:
giraffe <LENGTH> The giraffe command giraffe <LENGTH> The giraffe command

View File

@ -0,0 +1,26 @@
[bold]DESCRIPTION:[/]
The lion command.
[bold]USAGE:[/]
myapp []<TEETH>[/] [][[LEGS]][/] [][[OPTIONS]][/] [][[COMMAND]][/]
[bold]EXAMPLES:[/]
myapp []20 --alive[/]
[bold]ARGUMENTS:[/]
[]<TEETH>[/] The number of teeth the lion has
[][[LEGS]][/] The number of legs
[]OPTIONS:[/]
[]DEFAULT[/]
-h, --help Prints help information
-v, --version Prints version information
-a, --alive Indicates whether or not the animal is alive
-n, --name []<VALUE>[/]
--agility []<VALUE>[/] []10[/] The agility between 0 and 100
-c []<CHILDREN>[/] The number of children the lion has
-d []<DAY>[/] []Monday[/], []Thursday[/] The days the lion goes hunting
-w, --weight [][[WEIGHT]][/] The weight of the lion, in kgs
[bold]COMMANDS:[/]
[]giraffe[/] []<LENGTH>[/] The giraffe command

View File

@ -0,0 +1,26 @@
[yellow]DESCRIPTION:[/]
The lion command.
[yellow]USAGE:[/]
myapp [aqua]<TEETH>[/] [silver][[LEGS]][/] [grey][[OPTIONS]][/] [aqua][[COMMAND]][/]
[yellow]EXAMPLES:[/]
myapp [grey]20 --alive[/]
[yellow]ARGUMENTS:[/]
[silver]<TEETH>[/] The number of teeth the lion has
[silver][[LEGS]][/] The number of legs
[yellow]OPTIONS:[/]
[lime]DEFAULT[/]
-h, --help Prints help information
-v, --version Prints version information
-a, --alive Indicates whether or not the animal is alive
-n, --name [silver]<VALUE>[/]
--agility [silver]<VALUE>[/] [bold]10[/] The agility between 0 and 100
-c [silver]<CHILDREN>[/] The number of children the lion has
-d [silver]<DAY>[/] [bold]Monday[/], [bold]Thursday[/] The days the lion goes hunting
-w, --weight [grey][[WEIGHT]][/] The weight of the lion, in kgs
[yellow]COMMANDS:[/]
[silver]giraffe[/] [silver]<LENGTH>[/] The giraffe command

View File

@ -0,0 +1,26 @@
[]DESCRIPTION:[/]
The lion command.
[]USAGE:[/]
myapp []<TEETH>[/] [][[LEGS]][/] [][[OPTIONS]][/] [][[COMMAND]][/]
[]EXAMPLES:[/]
myapp []20 --alive[/]
[]ARGUMENTS:[/]
[]<TEETH>[/] The number of teeth the lion has
[][[LEGS]][/] The number of legs
[]OPTIONS:[/]
[]DEFAULT[/]
-h, --help Prints help information
-v, --version Prints version information
-a, --alive Indicates whether or not the animal is alive
-n, --name []<VALUE>[/]
--agility []<VALUE>[/] []10[/] The agility between 0 and 100
-c []<CHILDREN>[/] The number of children the lion has
-d []<DAY>[/] []Monday[/], []Thursday[/] The days the lion goes hunting
-w, --weight [][[WEIGHT]][/] The weight of the lion, in kgs
[]COMMANDS:[/]
[]giraffe[/] []<LENGTH>[/] The giraffe command

View File

@ -1,4 +1,4 @@
DESCRIPTION: DESCRIPTION:
The lion command. The lion command.
USAGE: USAGE:
@ -8,7 +8,8 @@ ARGUMENTS:
<TEETH> The number of teeth the lion has <TEETH> The number of teeth the lion has
OPTIONS: OPTIONS:
DEFAULT DEFAULT
-h, --help Prints help information -h, --help Prints help information
-c <CHILDREN> The number of children the lion has -c <CHILDREN> The number of children the lion has
-d <DAY> Monday, Thursday The days the lion goes hunting -d <DAY> Monday, Thursday The days the lion goes hunting
-w, --weight [WEIGHT] The weight of the lion, in kgs

View File

@ -264,6 +264,101 @@ public sealed partial class CommandAppTests
return Verifier.Verify(result.Output, settings).UseTextForParameters(expectationPrefix); return Verifier.Verify(result.Output, settings).UseTextForParameters(expectationPrefix);
} }
[Fact]
[Expectation("Default_Without_Args_Additional_Style_Default")]
public Task Should_Output_Default_Command_And_Additional_Commands_When_Default_Command_Has_Required_Parameters_And_Is_Called_Without_Args_Style_Default()
{
// Given
var fixture = new CommandAppTester();
fixture.SetDefaultCommand<LionCommand>();
fixture.Configure(configurator =>
{
configurator.SetApplicationName("myapp");
configurator.AddExample("20", "--alive");
configurator.AddCommand<GiraffeCommand>("giraffe");
configurator.SetHelpProvider(new RenderMarkupHelpProvider(configurator.Settings));
});
// When
var result = fixture.Run();
// Then
return Verifier.Verify(result.Output);
}
[Fact]
[Expectation("Default_Without_Args_Additional_Style_BoldHeadings")]
public Task Should_Output_Default_Command_And_Additional_Commands_When_Default_Command_Has_Required_Parameters_And_Is_Called_Without_Args_Style_BoldHeadings()
{
// Bold headings in the help text
var styles = new HelpProviderStyle()
{
Description = new DescriptionStyle()
{
Header = "bold",
},
Usage = new UsageStyle()
{
Header = "bold",
},
Examples = new ExampleStyle()
{
Header = "bold",
},
Arguments = new ArgumentStyle()
{
Header = "bold",
},
Commands = new CommandStyle()
{
Header = "bold",
},
// Omit OptionStyle to ensure coverage of at least one null style class
};
// Given
var fixture = new CommandAppTester();
fixture.SetDefaultCommand<LionCommand>();
fixture.Configure(configurator =>
{
configurator.SetApplicationName("myapp");
configurator.AddExample("20", "--alive");
configurator.AddCommand<GiraffeCommand>("giraffe");
configurator.Settings.HelpProviderStyles = styles;
configurator.SetHelpProvider(new RenderMarkupHelpProvider(configurator.Settings));
});
// When
var result = fixture.Run();
// Then
return Verifier.Verify(result.Output);
}
[Fact]
[Expectation("Default_Without_Args_Additional_Style_None")]
public Task Should_Output_Default_Command_And_Additional_Commands_When_Default_Command_Has_Required_Parameters_And_Is_Called_Without_Args_Style_None()
{
// Given
var fixture = new CommandAppTester();
fixture.SetDefaultCommand<LionCommand>();
fixture.Configure(configurator =>
{
configurator.SetApplicationName("myapp");
configurator.AddExample("20", "--alive");
configurator.AddCommand<GiraffeCommand>("giraffe");
configurator.Settings.HelpProviderStyles = null;
configurator.SetHelpProvider(new RenderMarkupHelpProvider(configurator.Settings));
});
// When
var result = fixture.Run();
// Then
return Verifier.Verify(result.Output);
}
[Fact] [Fact]
[Expectation("Default_Greeter")] [Expectation("Default_Greeter")]
public Task Should_Not_Output_Default_Command_When_Command_Has_No_Required_Parameters_And_Is_Called_Without_Args() public Task Should_Not_Output_Default_Command_When_Command_Has_No_Required_Parameters_And_Is_Called_Without_Args()