Skip to content

Commit

Permalink
Cli fixes and escape hatches (#2457)
Browse files Browse the repository at this point in the history
* Fix up the CLI on windows, add module support to the CLI

* Add support for deleting specific items from specific groups from the CLI

* Fix the CLI tests a bit

* Handle other OS feedback
  • Loading branch information
halgari authored Jan 9, 2025
1 parent e39ee0d commit 24847f3
Show file tree
Hide file tree
Showing 21 changed files with 366 additions and 158 deletions.
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<PackageVersion Include="Bannerlord.ModuleManager" Version="6.0.246" />
<PackageVersion Include="BsDiff" Version="1.1.0" />
<PackageVersion Include="LinqGen" Version="0.3.1" />
<PackageVersion Include="Microsoft.Extensions.FileSystemGlobbing" Version="9.0.0" />
<PackageVersion Include="LinuxDesktopUtils.XDGDesktopPortal" Version="1.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.0" />
Expand Down
52 changes: 52 additions & 0 deletions src/Abstractions/NexusMods.Abstractions.Cli/RendererExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Runtime.CompilerServices;
using NexusMods.ProxyConsole.Abstractions;
using NexusMods.ProxyConsole.Abstractions.Implementations;

Expand All @@ -23,6 +24,45 @@ await renderer.RenderAsync(new Table
});
}

/// <summary>
/// A table renderer for when you have a collection of tuples to render
/// </summary>
public static ValueTask Table<T>(this IRenderer renderer, IEnumerable<T> rows, params ReadOnlySpan<string> columnNames)
where T : ITuple
{
var namesPrepared = GC.AllocateArray<IRenderable>(columnNames.Length);
for (var i = 0; i < columnNames.Length; i++)
{
namesPrepared[i] = Renderable.Text(columnNames[i]);
}

static IRenderable[] PrepareRow(T row)
{
var rowPrepared = GC.AllocateArray<IRenderable>(row.Length);
for (var i = 0; i < row.Length; i++)
{
rowPrepared[i] = Renderable.Text(row[i]!.ToString()!);
}
return rowPrepared;
}

return renderer.RenderAsync(new Table
{
Columns = namesPrepared,
Rows = rows.Select(PrepareRow).ToArray(),
}
);
}

/// <summary>
/// Renders the data in the given rows to a table
/// </summary>
public static ValueTask RenderTable<T>(this IEnumerable<T> rows, IRenderer renderer, params ReadOnlySpan<string> columnNames)
where T : ITuple
{
return renderer.Table(rows, columnNames);
}

/// <summary>
/// Renders the given text to the renderer
/// </summary>
Expand All @@ -43,6 +83,18 @@ public static async ValueTask Text(this IRenderer renderer, string template, par
// Todo: implement custom conversion and formatting for the arguments
await renderer.RenderAsync(Renderable.Text(template, args.Select(a => a.ToString()!).ToArray()));
}

/// <summary>
/// Renders the text to the renderer with the given arguments and template
/// </summary>
/// <param name="renderer"></param>
/// <param name="text"></param>
public static async ValueTask<int> InputError(this IRenderer renderer, string template, params object[] args)
{
// Todo: implement custom conversion and formatting for the arguments
await renderer.RenderAsync(Renderable.Text(template, args.Select(a => a.ToString()!).ToArray()));
return -1;
}

/// <summary>
/// Runs the fn in a context that will render a progress bar while the user waits
Expand Down
24 changes: 13 additions & 11 deletions src/Networking/NexusMods.Networking.NexusWebApi/NexusApiVerbs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,33 @@ namespace NexusMods.Networking.NexusWebApi;
internal static class NexusApiVerbs
{
internal static IServiceCollection AddNexusApiVerbs(this IServiceCollection collection) =>
collection.AddVerb(() => NexusApiVerify)
collection
.AddModule("nexus", "Commands for interacting with the Nexus Mods API")
.AddVerb(() => NexusApiVerify)
.AddVerb(() => NexusDownloadLinks);


[Verb("nexus-api-verify", "Verifies the logged in account via the Nexus API")]
[Verb("nexus verify", "Verifies the logged in account via the Nexus API")]
private static async Task<int> NexusApiVerify([Injected] IRenderer renderer,
[Injected] NexusApiClient nexusApiClient,
[Injected] IAuthenticatingMessageFactory messageFactory,
[Injected] CancellationToken token)
{
var userInfo = await messageFactory.Verify(nexusApiClient, token);
await renderer.Table(new[] { "Name", "Premium" },
new[]
{
new object[]
{
userInfo?.Name ?? "<Not logged in>",

await renderer.Table(["Name", "Premium"],
[
[
userInfo?.Name ?? "<Not logged in>",
userInfo?.IsPremium ?? false,
}
});
],
]
);

return 0;
}

[Verb("nexus-download-links", "Generates download links for a given file")]
[Verb("nexus download-links", "Generates download links for a given file")]
private static async Task<int> NexusDownloadLinks([Injected] IRenderer renderer,
[Option("g", "gameDomain", "Game domain")] string gameDomain,
[Option("m", "modId", "Mod ID")] ModId modId,
Expand Down
24 changes: 23 additions & 1 deletion src/NexusMods.App.Cli/OptionParsers/LoadoutParser.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.Globalization;
using JetBrains.Annotations;
using NexusMods.Abstractions.Games;
using NexusMods.Abstractions.Loadouts;
using NexusMods.Extensions.BCL;
using NexusMods.MnemonicDB.Abstractions;
using NexusMods.ProxyConsole.Abstractions.VerbDefinitions;

Expand All @@ -10,7 +12,7 @@ namespace NexusMods.CLI.OptionParsers;
/// Parses a string into a loadout marker
/// </summary>
[UsedImplicitly]
internal class LoadoutParser(IConnection conn) : IOptionParser<Loadout.ReadOnly>
internal class LoadoutParser(IConnection conn, IOptionParser<IGame> gameParser) : IOptionParser<Loadout.ReadOnly>
{
public bool TryParse(string input, out Loadout.ReadOnly value, out string error)
{
Expand All @@ -36,6 +38,26 @@ public bool TryParse(string input, out Loadout.ReadOnly value, out string error)
return true;
}
}

// In the format of "<Game>/<ShortName>"
if (input.Contains("/"))
{
var parts = input.Split('/');
var game = parts[0];
var shortName = parts[1];

if (gameParser.TryParse(game, out var gameValue, out _))
{
if (Loadout
.FindByShortName(db, shortName)
.TryGetFirst(l => l.Installation.GameId == gameValue.GameId, out var foundLoadout))
{
value = foundLoadout;
return true;
}

}
}

var found = Loadout.FindByName(db, input).ToArray();

Expand Down
25 changes: 25 additions & 0 deletions src/NexusMods.App.Cli/OptionParsers/MatcherParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Microsoft.Extensions.FileSystemGlobbing;
using NexusMods.ProxyConsole.Abstractions.VerbDefinitions;

namespace NexusMods.CLI.OptionParsers;

internal class MatcherParser : IOptionParser<Matcher>
{
/// <inheritdoc />
public bool TryParse(string toParse, out Matcher value, out string error)
{
try
{
value = new Matcher();
value.AddInclude(toParse);
error = string.Empty;
return true;
}
catch (Exception exception)
{
value = null!;
error = exception.Message;
return false;
}
}
}
2 changes: 2 additions & 0 deletions src/NexusMods.App.Cli/Services.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileSystemGlobbing;
using NexusMods.Abstractions.Games;
using NexusMods.Abstractions.Loadouts;
using NexusMods.CLI.OptionParsers;
Expand Down Expand Up @@ -28,6 +29,7 @@ public static IServiceCollection AddCLI(this IServiceCollection services)
.AddOptionParser<Uri>(u => (new Uri(u), null))
.AddOptionParser<Version>(v => (Version.Parse(v), null))
.AddOptionParser<string>(s => (s, null))
.AddOptionParser<Matcher, MatcherParser>()
.AddOptionParser<ITool, ToolParser>();

// Protocol Handlers
Expand Down
32 changes: 32 additions & 0 deletions src/NexusMods.App/ConsoleHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Runtime.InteropServices;
using System.Runtime.Versioning;

namespace NexusMods.App;

/// <summary>
/// Helpers for consoles on Windows
/// </summary>
[SupportedOSPlatform("windows")]
public static class ConsoleHelper
{
// ReSharper disable once InconsistentNaming
private const int ATTACH_PARENT_PROCESS = -1;

[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool AttachConsole(int dwProcessId);

[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool AllocConsole();

/// <summary>
/// Attempt to attach to the console, if it fails, create a new console window if desired
/// </summary>
/// <param name="forceNewConsoleIfNoParent">If there is no parent console, should one be created?</param>
public static void EnsureConsole(bool forceNewConsoleIfNoParent = false)
{
if (!AttachConsole(ATTACH_PARENT_PROCESS) && !forceNewConsoleIfNoParent)
{
AllocConsole();
}
}
}
2 changes: 1 addition & 1 deletion src/NexusMods.App/NexusMods.App.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<OutputType>Exe</OutputType>
<ApplicationIcon>icon.ico</ApplicationIcon>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
Expand Down
9 changes: 6 additions & 3 deletions src/NexusMods.App/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
using NLog.Targets;
using ReactiveUI;
using Spectre.Console;
using Spectre.Console.Advanced;

namespace NexusMods.App;

Expand Down Expand Up @@ -90,11 +91,12 @@ public static int Main(string[] args)

try
{
if (OperatingSystem.IsWindows())
ConsoleHelper.EnsureConsole();

if (startupMode.RunAsMain)
{
LogMessages.StartingProcess(_logger, Environment.ProcessPath, Environment.ProcessId,
args
);
LogMessages.StartingProcess(_logger, Environment.ProcessPath, Environment.ProcessId, args);

if (startupMode.ShowUI)
{
Expand Down Expand Up @@ -180,6 +182,7 @@ private static Task<int> RunCliTaskAsMain(IServiceProvider provider, StartupMode
if (!startupMode.ExecuteCli)
return Task.FromResult(0);
var configurator = provider.GetRequiredService<CommandLineConfigurator>();
_logger.LogInformation("Starting with Spectre.Cli");
return configurator.RunAsync(startupMode.Args, new SpectreRenderer(AnsiConsole.Console), CancellationToken.None);
}

Expand Down
Loading

0 comments on commit 24847f3

Please sign in to comment.