journal/Journal.DevTool/Tui/WorkspaceScreen.cs

175 lines
7.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}