using BuildTool.Platform;
using BuildTool.Prerequisites;
using BuildTool.Publishing;
using Spectre.Console;
namespace BuildTool.Interactive;
///
/// Handles interactive mode when the BuildTool is run without arguments.
/// Provides a polished menu-driven experience with proper Ctrl+C navigation.
/// Uses ModernUO brand gold color palette.
///
public static class GuidedMode
{
// Brand gold as markup string for inline use
private const string GoldMarkup = "rgb(213,191,116)";
private const string GoldLightMarkup = "rgb(223,198,136)";
///
/// Tracks menu depth. 0 = root menu, >0 = submenu.
/// Used by ShowPrompt to automatically add Back option in submenus.
///
private static int _menuDepth;
public static int Run(PlatformInfo platform, string repoRoot)
{
return ShowMainMenu(platform, repoRoot);
}
///
/// Shows a selection prompt with automatic Back support.
/// When _menuDepth > 0, appends a ":left_arrow: Back" choice.
/// Returns null if the user chose Back or pressed Ctrl+C in a submenu.
/// At the root menu, Ctrl+C requires double-press to exit (returns null on double-press).
///
private static string? ShowPrompt(string title, params string[] choices)
{
var allChoices = choices.Where(c => !string.IsNullOrEmpty(c)).ToList();
if (_menuDepth > 0)
{
allChoices.Add(":left_arrow: Back");
}
var prompt = new SelectionPrompt()
.Title($"[{GoldMarkup}]{title}[/]")
.PageSize(10)
.HighlightStyle(Branding.HighlightStyle)
.AddChoices(allChoices);
string selection;
try
{
selection = prompt.Show(AnsiConsole.Console);
}
catch (OperationCanceledException)
{
InteractiveCancellation.Instance.Reset();
if (_menuDepth > 0)
{
return null; // Back
}
// Root menu: require double-press to exit
if (CancellationTracker.Instance.IsDoublePress())
{
AnsiConsole.MarkupLine("\n[grey]Exiting...[/]");
return ":cross_mark: Exit";
}
AnsiConsole.Write(new Text("\nPress Ctrl+C again to exit\n", Branding.DimGoldStyle));
Thread.Sleep(100);
return ""; // Signal to redraw menu
}
if (selection.Contains("Back"))
{
return null;
}
return selection;
}
private static int ShowMainMenu(PlatformInfo platform, string repoRoot)
{
while (true)
{
AnsiConsole.Clear();
// Write the true-color ANSI logo directly (bypasses Spectre markup parsing)
Console.Write(Branding.UOLogoAnsi);
AnsiConsole.Write(new Text($" {Branding.Subtitle}\n", Branding.DimGoldStyle));
AnsiConsole.WriteLine();
// Show environment info
var sdkVersion = GetDotNetSdkVersion();
var table = new Table()
.Border(TableBorder.Rounded)
.BorderColor(Branding.GoldDim)
.HideHeaders()
.AddColumn("Key")
.AddColumn("Value");
table.AddRow(
new Text("Platform", Branding.DimGoldStyle),
new Markup($"[white]{Markup.Escape(platform.OsName)} {platform.ArchRid}[/]")
);
table.AddRow(
new Text(".NET SDK", Branding.DimGoldStyle),
sdkVersion is not null
? new Markup($"[white]{Markup.Escape(sdkVersion)}[/]")
: new Markup("[red]Not found[/]")
);
if (platform.DistroName is not null)
{
table.AddRow(
new Text("Distribution", Branding.DimGoldStyle),
new Markup($"[white]{Markup.Escape(platform.DistroName)}[/]")
);
}
if (platform.KernelVersion is not null)
{
table.AddRow(
new Text("Kernel", Branding.DimGoldStyle),
new Markup($"[white]{Markup.Escape(platform.KernelVersion)}[/]")
);
}
AnsiConsole.Write(table);
AnsiConsole.WriteLine();
var selection = ShowPrompt(
"What would you like to do?",
":hammer: Publish Server",
":stethoscope: Check Prerequisites",
"",
":cross_mark: Exit"
);
if (string.IsNullOrEmpty(selection))
{
continue; // Redraw (single Ctrl+C)
}
if (selection.Contains("Publish"))
{
_menuDepth++;
AnsiConsole.Clear();
AnsiConsole.MarkupLine($"[bold {GoldMarkup}]:hammer: PUBLISH SERVER[/]\n");
var result = RunPublish(platform, repoRoot);
_menuDepth--;
if (result is not null) // null = Back, skip WaitForKey
{
WaitForKey();
}
}
else if (selection.Contains("Prerequisites"))
{
_menuDepth++;
AnsiConsole.Clear();
AnsiConsole.MarkupLine($"[bold {GoldMarkup}]:stethoscope: PREREQUISITE CHECK[/]\n");
PrerequisiteChecker.CheckAll(platform, repoRoot, interactive: true);
_menuDepth--;
WaitForKey();
}
else if (selection.Contains("Exit"))
{
return 0;
}
}
}
///
/// Returns null if user navigated Back to main menu, 0 on success, non-zero on error.
/// Uses a step loop so Back goes to the previous step, not the main menu.
///
///
/// Returns null if user navigated Back to main menu, 0 on success, non-zero on error.
/// Uses a step loop so Back goes to the previous step, not the main menu.
///
private static int? RunPublish(PlatformInfo platform, string repoRoot)
{
// Step 0: SDK check (always needed for building)
if (!PrerequisiteChecker.CheckSdk(platform, repoRoot, interactive: true))
{
return 1;
}
var config = "Release";
var rid = platform.Rid;
var os = platform.OsRid;
var arch = platform.ArchRid;
// Steps 1+: interactive prompts with back navigation
var step = 1;
while (step <= 4)
{
switch (step)
{
case 1: // Configuration
{
var choice = ShowPrompt("Configuration:", "Release (Recommended)", "Debug");
if (choice is null)
{
return null; // Back from first prompt = back to main menu
}
config = choice.Contains("Release") ? "Release" : "Debug";
step++;
break;
}
case 2: // Platform confirmation
{
var choice = ShowPrompt(
$"Target platform: [{GoldLightMarkup}]{platform.Rid}[/] [grey](auto-detected)[/]",
$":check_mark: Use {platform.Rid}",
":wrench: Choose different platform"
);
if (choice is null)
{
step--;
break;
}
if (choice.Contains("Choose"))
{
step++;
}
else
{
rid = platform.Rid;
os = platform.OsRid;
step = 5; // Skip OS/arch selection, go to publish
}
break;
}
case 3: // OS selection (only if custom platform)
{
var choice = ShowPrompt(
"Operating System:",
"win (Windows)",
"osx (macOS)",
"linux (Linux)"
);
if (choice is null)
{
step--;
break;
}
os = choice.Split(' ')[0].Trim();
step++;
break;
}
case 4: // Architecture selection (only if custom platform)
{
var choice = ShowPrompt(
"Architecture:",
"x64 (Intel/AMD 64-bit)",
"arm64 (ARM 64-bit)"
);
if (choice is null)
{
step--;
break;
}
arch = choice.Split(' ')[0].Trim();
rid = $"{os}-{arch}";
step++;
break;
}
}
}
var isCrossCompile = os != platform.OsRid;
// Native library checks — only relevant when targeting the current OS
if (!isCrossCompile)
{
if (!PrerequisiteChecker.CheckNativeLibraries(platform, interactive: true))
{
return 1;
}
}
// Run publish
var exitCode = PublishOrchestrator.Run(config, rid, interactive: true, isCrossCompile);
if (exitCode == 0)
{
CheckFirstTimeSetup(repoRoot);
if (isCrossCompile)
{
DisplayTargetRequirements(os, rid);
}
}
return exitCode;
}
private static void DisplayTargetRequirements(string targetOs, string rid)
{
var (osName, commands) = NativeLibraryChecker.GetRequirementsForTarget(targetOs);
var rows = new List
{
new Markup($"[bold {GoldMarkup}]:clipboard: Prerequisites for {Markup.Escape(osName)} target ({rid})[/]"),
new Markup("")
};
for (var i = 0; i < commands.Length; i++)
{
var cmd = commands[i];
if (i > 0)
{
rows.Add(new Markup(""));
}
rows.Add(new Markup($" [grey]\u2022[/] {Markup.Escape(cmd)}"));
}
rows.Add(new Markup(""));
rows.Add(new Markup(""));
rows.Add(new Markup("[grey]Install these on the target machine, then copy the [/][rgb(213,191,116)]Distribution[/] [grey]folder to the server and run it.[/]"));
AnsiConsole.Write(new Panel(new Rows(rows))
.Border(BoxBorder.Rounded)
.BorderColor(Branding.GoldDim)
.Padding(1, 0));
AnsiConsole.WriteLine();
}
private static void CheckFirstTimeSetup(string repoRoot)
{
var configPath = Path.Combine(repoRoot, "Distribution", "Configuration", "modernuo.json");
if (!File.Exists(configPath))
{
AnsiConsole.WriteLine();
AnsiConsole.Write(new Text(
":light_bulb: Tip: The server will prompt you for game data file locations on first run.\n",
Branding.DimGoldStyle));
}
}
private static string? GetDotNetSdkVersion()
{
var result = ProcessRunner.RunCaptured("dotnet", "--version");
return result.Success ? result.StandardOutput.Trim() : null;
}
private static void WaitForKey()
{
AnsiConsole.WriteLine();
AnsiConsole.MarkupLine("[grey]Press any key to continue...[/]");
Console.ReadKey(true);
}
}