175 lines
7.2 KiB
C#
175 lines
7.2 KiB
C#
using Sdt.Config;
|
||
using Spectre.Console;
|
||
|
||
namespace Sdt.Tui;
|
||
|
||
public sealed class WorkspaceScreen(WorkspaceConfig workspace, string workspaceRoot, string currentProjectRoot)
|
||
{
|
||
private readonly WorkspaceConfig _workspace = workspace;
|
||
private readonly string _workspaceRoot = workspaceRoot;
|
||
private readonly string _currentProjectRoot = currentProjectRoot;
|
||
|
||
/// <summary>
|
||
/// Shows the project switcher. Returns the absolute path to the selected project root,
|
||
/// or null if the user cancelled.
|
||
/// </summary>
|
||
public string? SelectProject()
|
||
{
|
||
while (true)
|
||
{
|
||
AnsiConsole.Clear();
|
||
AnsiConsole.Write(Theme.SectionRule("WORKSPACE — " + _workspace.Name));
|
||
AnsiConsole.WriteLine();
|
||
|
||
var projects = _workspace.Projects;
|
||
if (projects.Count == 0)
|
||
{
|
||
AnsiConsole.MarkupLine(Theme.Warn("No projects defined in sdt-workspace.json."));
|
||
AnsiConsole.MarkupLine(Theme.Faint("Add entries to the \"projects\" array."));
|
||
if (AnsiConsole.Confirm($"[{Theme.Amber}]Add an external project now?[/]", defaultValue: true))
|
||
{
|
||
AddExternalProject();
|
||
continue;
|
||
}
|
||
|
||
AnsiConsole.MarkupLine("\n" + Theme.Faint("Press any key to go back..."));
|
||
Console.ReadKey(intercept: true);
|
||
return null;
|
||
}
|
||
|
||
// Build choice list with current project marked
|
||
var choices = new List<WorkspaceMenuItem>();
|
||
foreach (var proj in projects)
|
||
{
|
||
var absPath = WorkspaceLoader.ResolveProjectRoot(_workspaceRoot, proj);
|
||
var devtoolPath = Path.Combine(absPath, "devtool.json");
|
||
var isCurrent = string.Equals(absPath, _currentProjectRoot, StringComparison.OrdinalIgnoreCase);
|
||
var exists = File.Exists(devtoolPath);
|
||
|
||
var label = isCurrent
|
||
? $"[bold {Theme.GreenBold}]► {Markup.Escape(proj.Name)}[/] [{Theme.GreenDim}](current)[/]"
|
||
: $"[{Theme.Green}] {Markup.Escape(proj.Name)}[/]";
|
||
if (proj.Disabled)
|
||
label += $" [{Theme.Amber}](disabled)[/]";
|
||
|
||
var desc = !exists
|
||
? $" [{Theme.Red}]devtool.json not found[/]"
|
||
: string.IsNullOrWhiteSpace(proj.Description)
|
||
? $" [{Theme.GreenDim}]{Markup.Escape(absPath)}[/]"
|
||
: $" [{Theme.GreenDim}]{Markup.Escape(proj.Description)}[/]";
|
||
if (proj.Tags.Count > 0)
|
||
desc += $" [{Theme.GreenDim}]tags: {Markup.Escape(string.Join(",", proj.Tags))}[/]";
|
||
|
||
choices.Add(new WorkspaceMenuItem(label + "\n" + desc, absPath, exists && !isCurrent && !proj.Disabled));
|
||
}
|
||
|
||
choices.Add(new WorkspaceMenuItem($"[{Theme.Green}]+ Add external project[/]", "__add__", true));
|
||
choices.Add(new WorkspaceMenuItem($"[{Theme.GreenDim}]← Cancel[/]", null, true));
|
||
|
||
// Show project table for overview
|
||
var table = new Table()
|
||
.Border(TableBorder.Rounded)
|
||
.BorderStyle(Theme.DimStyle)
|
||
.AddColumn(new TableColumn($"[{Theme.Amber}]Project[/]"))
|
||
.AddColumn(new TableColumn($"[{Theme.Amber}]Path[/]"))
|
||
.AddColumn(new TableColumn($"[{Theme.Amber}]Status[/]").Width(12));
|
||
|
||
foreach (var proj in projects)
|
||
{
|
||
var absPath = WorkspaceLoader.ResolveProjectRoot(_workspaceRoot, proj);
|
||
var isCurrent = string.Equals(absPath, _currentProjectRoot, StringComparison.OrdinalIgnoreCase);
|
||
var hasConfig = File.Exists(Path.Combine(absPath, "devtool.json"));
|
||
var status = proj.Disabled
|
||
? Theme.Warn("disabled")
|
||
: hasConfig ? Theme.Ok("ready") : Theme.Fail("no config");
|
||
|
||
table.AddRow(
|
||
isCurrent
|
||
? $"[bold {Theme.GreenBold}]► {Markup.Escape(proj.Name)}[/]"
|
||
: Theme.G(proj.Name),
|
||
Theme.Faint(proj.Path),
|
||
status);
|
||
}
|
||
|
||
AnsiConsole.Write(table);
|
||
AnsiConsole.WriteLine();
|
||
|
||
var switchable = choices.Where(c => c.Selectable).ToList();
|
||
if (switchable.Count == 1) // only Cancel
|
||
{
|
||
AnsiConsole.MarkupLine(Theme.Warn("No other projects available to switch to."));
|
||
AnsiConsole.MarkupLine("\n" + Theme.Faint("Press any key to go back..."));
|
||
Console.ReadKey(intercept: true);
|
||
return null;
|
||
}
|
||
|
||
var selected = AnsiConsole.Prompt(
|
||
new SelectionPrompt<WorkspaceMenuItem>()
|
||
.Title($"[{Theme.Green}]Switch to project:[/]")
|
||
.PageSize(15)
|
||
.UseConverter(m => m.Display)
|
||
.AddChoices(switchable));
|
||
|
||
if (selected.AbsPath == "__add__")
|
||
{
|
||
AddExternalProject();
|
||
continue;
|
||
}
|
||
|
||
return selected.AbsPath; // null = cancelled
|
||
}
|
||
}
|
||
|
||
private void AddExternalProject()
|
||
{
|
||
var raw = AnsiConsole.Ask<string>($"[{Theme.Amber}]Project root path[/]");
|
||
if (string.IsNullOrWhiteSpace(raw))
|
||
return;
|
||
|
||
var absolutePath = Path.GetFullPath(raw.Trim());
|
||
if (!Directory.Exists(absolutePath))
|
||
{
|
||
AnsiConsole.MarkupLine(Theme.Fail("Directory does not exist."));
|
||
Thread.Sleep(700);
|
||
return;
|
||
}
|
||
|
||
var configPath = Path.Combine(absolutePath, "devtool.json");
|
||
if (!File.Exists(configPath))
|
||
{
|
||
var create = AnsiConsole.Confirm(
|
||
$"[{Theme.Amber}]No devtool.json found. Create a minimal template?[/]",
|
||
defaultValue: true);
|
||
if (!create)
|
||
return;
|
||
|
||
File.WriteAllText(configPath, "{\n \"name\": \"SDT Project\",\n \"version\": \"0.1.0\",\n \"workflows\": []\n}\n");
|
||
}
|
||
|
||
if (_workspace.Projects.Any(p =>
|
||
string.Equals(WorkspaceLoader.ResolveProjectRoot(_workspaceRoot, p), absolutePath, StringComparison.OrdinalIgnoreCase)))
|
||
{
|
||
AnsiConsole.MarkupLine(Theme.Warn("Project already exists in workspace."));
|
||
Thread.Sleep(700);
|
||
return;
|
||
}
|
||
|
||
var relativePath = Path.GetRelativePath(_workspaceRoot, absolutePath);
|
||
var useRelative = !relativePath.StartsWith("..", StringComparison.OrdinalIgnoreCase) && !Path.IsPathRooted(relativePath);
|
||
var projectEntry = new WorkspaceProject
|
||
{
|
||
Name = new DirectoryInfo(absolutePath).Name,
|
||
Description = $"External project at {absolutePath}",
|
||
Path = useRelative ? relativePath : absolutePath,
|
||
Disabled = false,
|
||
};
|
||
|
||
_workspace.Projects.Add(projectEntry);
|
||
WorkspaceLoader.Save(_workspaceRoot, _workspace);
|
||
AnsiConsole.MarkupLine(Theme.Ok("Project added to workspace."));
|
||
Thread.Sleep(700);
|
||
}
|
||
|
||
private sealed record WorkspaceMenuItem(string Display, string? AbsPath, bool Selectable);
|
||
}
|