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

Cli fixes and escape hatches #2457

Merged
merged 6 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading