TUI: переработать shell и адаптацию layout

This commit is contained in:
2026-04-14 01:23:55 +03:00
parent 7224a423fa
commit d12e9873f0
9 changed files with 346 additions and 43 deletions

View File

@@ -9,37 +9,31 @@
@implements IDisposable
<Rows Expand="true">
<Panel Title="@($"LazyBear MCP [{Localization.Label}]")"
TitleColor="@UiPalette.Accent"
BorderColor="@UiPalette.Frame"
<Columns Expand="true">
<Markup Content="@GetHeaderProjectSegment()"
Foreground="@UiPalette.Text"
Background="@UiPalette.HeaderBrandBackground"
Decoration="@Spectre.Console.Decoration.Bold" />
@foreach (var segment in BuildHeaderSegments())
{
<Markup Content="@segment.Content"
Foreground="@segment.Foreground"
Background="@segment.Background"
Decoration="@segment.Decoration" />
}
</Columns>
<Panel BorderColor="@UiPalette.Frame"
Expand="true"
Height="@GetPanelHeight()"
Padding="@(new Spectre.Console.Padding(1, 0, 1, 0))">
<Rows Expand="true">
<Markup Content="@Localization.Current.HintBar" Foreground="@UiPalette.TextMuted" />
<Markup Content=" " />
<Columns>
@foreach (var tab in _tabs)
{
var isActive = _activeTab == tab;
<Markup Content="@($" {GetTabLabel(tab)} ")"
Foreground="@(isActive ? UiPalette.SelectionForeground : UiPalette.Text)"
Background="@(isActive ? UiPalette.Accent : UiPalette.SurfaceMuted)"
Decoration="@(isActive ? Spectre.Console.Decoration.Bold : Spectre.Console.Decoration.None)" />
<Markup Content=" " />
}
</Columns>
<Markup Content=" " />
@if (_activeTab == Tab.Overview)
{
<OverviewTab Rows="@GetOverviewRows()"
SelectedIndex="@_overviewSelection"
SelectedIndexChanged="@OnOverviewSelectionChanged"
ViewportRows="@GetOverviewViewportRows()"
Endpoint="@_mcpEndpoint"
Loc="@Localization.Current" />
}
else if (_activeTab == Tab.Logs)
@@ -62,9 +56,38 @@
}
</Rows>
</Panel>
<Markup Content="@BuildStatusBar(UiMetrics.ConsoleWidth)"
Foreground="@UiPalette.Text"
Background="@UiPalette.SurfaceMuted" />
<ModalWindow IsOpened="@_isHelpOpen">
<Panel Title="@Localization.Current.HelpModalTitle"
TitleColor="@UiPalette.Accent"
BorderColor="@UiPalette.AccentSoft"
Width="@GetHelpModalWidth()"
Padding="@(new Spectre.Console.Padding(1, 0, 1, 0))">
<Rows>
<Markup Content="@Localization.Current.HintBar"
Foreground="@UiPalette.Text" />
<Markup Content=" " />
<Markup Content="@GetHelpLine(Tab.Overview, Localization.Current.OverviewHint)"
Foreground="@UiPalette.TextMuted" />
<Markup Content="@GetHelpLine(Tab.Logs, Localization.Current.LogsHint)"
Foreground="@UiPalette.TextMuted" />
<Markup Content="@GetHelpLine(Tab.Settings, Localization.Current.SettingsHint)"
Foreground="@UiPalette.TextMuted" />
<Markup Content=" " />
<Markup Content="@Localization.Current.HelpCloseHint"
Foreground="@UiPalette.AccentSoft"
Decoration="@Spectre.Console.Decoration.Bold" />
</Rows>
</Panel>
</ModalWindow>
</Rows>
@code {
private const char Nbsp = '\u00A0';
private enum Tab
{
Overview,
@@ -72,6 +95,12 @@
Settings
}
private readonly record struct HeaderSegment(
string Content,
Spectre.Console.Color Foreground,
Spectre.Console.Color Background,
Spectre.Console.Decoration Decoration);
private static readonly Tab[] _tabs = [Tab.Overview, Tab.Logs, Tab.Settings];
private static readonly string[] _logFilters = ["All", "Info", "Warn", "Error"];
private readonly HashSet<string> _expandedModules = new(StringComparer.Ordinal);
@@ -82,14 +111,16 @@
private int _logSelection;
private int _settingsSelection;
private bool _logsStickToBottom = true;
private bool _isHelpOpen;
private static readonly string _mcpEndpoint =
Environment.GetEnvironmentVariable("ASPNETCORE_URLS") ?? "http://localhost:5000";
private static int GetPanelHeight() => Math.Max(Console.WindowHeight - 2, 10);
private static int GetOverviewViewportRows() => Math.Max(Console.WindowHeight - 11, 3);
private static int GetLogsViewportRows() => Math.Max(Console.WindowHeight - 16, 5);
private static int GetSettingsViewportRows() => Math.Max(Console.WindowHeight - 13, 5);
private static int GetPanelHeight() => Math.Max(UiMetrics.ConsoleHeight - 4, 10);
private static int GetOverviewViewportRows() => Math.Max(GetPanelHeight() - 6, 3);
private static int GetLogsViewportRows() => Math.Max(GetPanelHeight() - 10, 5);
private static int GetSettingsViewportRows() => Math.Max(GetPanelHeight() - 7, 5);
private static int GetHelpModalWidth() => Math.Max(Math.Min(UiMetrics.ConsoleWidth - 6, 84), 36);
protected override void OnInitialized()
{
@@ -115,6 +146,7 @@
ConsoleKey.PageUp => "PageUp",
ConsoleKey.PageDown => "PageDown",
ConsoleKey.Tab => "Tab",
ConsoleKey.Escape => "Escape",
_ => key.KeyChar == '\0' ? string.Empty : key.KeyChar.ToString()
};
@@ -145,6 +177,22 @@
private void HandleKeyDown(KeyboardEventArgs args)
{
if (args.Key is "h" or "H")
{
_isHelpOpen = !_isHelpOpen;
return;
}
if (_isHelpOpen)
{
if (args.Key == "Escape")
{
_isHelpOpen = false;
}
return;
}
if (string.Equals(args.Key, "Tab", StringComparison.Ordinal))
{
ChangeTab(args.ShiftKey ? -1 : 1);
@@ -559,6 +607,127 @@
_ => Localization.Current.TabSettings
};
private HeaderSegment[] BuildHeaderSegments()
{
var width = Math.Max(UiMetrics.ConsoleWidth, 1);
var projectSegmentWidth = GetHeaderProjectSegment().Length;
var availableTabsWidth = Math.Max(width - projectSegmentWidth, 0);
if (availableTabsWidth <= 0)
{
return [];
}
var labels = _tabs.Select(GetTabLabel).ToArray();
var labelWidths = labels.Select(label => label.Length).ToArray();
var availableLabelWidth = availableTabsWidth - (_tabs.Length * 2);
if (availableLabelWidth < _tabs.Length)
{
return
[
new HeaderSegment(
FillBar(availableTabsWidth),
UiPalette.Text,
UiPalette.HeaderTabsBackground,
Spectre.Console.Decoration.None)
];
}
while (labelWidths.Sum() > availableLabelWidth)
{
var largestIndex = Array.IndexOf(labelWidths, labelWidths.Max());
if (largestIndex < 0 || labelWidths[largestIndex] <= 1)
{
break;
}
labelWidths[largestIndex]--;
}
var segments = new List<HeaderSegment>(_tabs.Length + 1);
var usedWidth = 0;
for (var i = 0; i < _tabs.Length; i++)
{
var tab = _tabs[i];
var label = FitInline(labels[i], labelWidths[i]);
var content = WrapBarSegment(label);
segments.Add(new HeaderSegment(
content,
tab == _activeTab ? UiPalette.SelectionForeground : UiPalette.Text,
tab == _activeTab ? UiPalette.SelectionBackground : UiPalette.HeaderTabsBackground,
tab == _activeTab ? Spectre.Console.Decoration.Bold : Spectre.Console.Decoration.None));
usedWidth += content.Length;
}
var fillerWidth = Math.Max(availableTabsWidth - usedWidth, 0);
if (fillerWidth > 0)
{
segments.Add(new HeaderSegment(
FillBar(fillerWidth),
UiPalette.Text,
UiPalette.HeaderTabsBackground,
Spectre.Console.Decoration.None));
}
return [.. segments];
}
private string GetHeaderProjectSegment() => WrapBarSegment(Localization.Current.ProjectTitle);
private string GetHelpLine(Tab tab, string hint) => $"{GetTabLabel(tab)}: {hint}";
private string GetStatusBarLeftText() => $"{Localization.Label} | {Localization.Current.HelpStatusHint}";
private string GetStatusBarRightText() => $"{Localization.Current.DashboardEndpointLabel}: {_mcpEndpoint}";
private string BuildStatusBar(int width)
{
var left = GetStatusBarLeftText();
var right = GetStatusBarRightText();
width = Math.Max(width, 1);
if (right.Length >= width)
{
return right[^width..];
}
var leftWidth = Math.Max(width - right.Length - 1, 0);
var fittedLeft = leftWidth == 0 ? string.Empty : PadRightVisible(FitInline(left, leftWidth), leftWidth);
return leftWidth == 0
? PadLeftVisible(right, width)
: $"{fittedLeft} {right}";
}
private static string FillBar(int width) =>
width <= 0 ? string.Empty : new string(Nbsp, width);
private static string WrapBarSegment(string text) => $"{Nbsp}{text}{Nbsp}";
private static string PadRightVisible(string text, int width)
{
if (width <= 0) return string.Empty;
if (text.Length >= width) return text[..width];
return text + FillBar(width - text.Length);
}
private static string PadLeftVisible(string text, int width)
{
if (width <= 0) return string.Empty;
if (text.Length >= width) return text[^width..];
return FillBar(width - text.Length) + text;
}
private static string FitInline(string text, int width)
{
if (width <= 0) return string.Empty;
if (text.Length <= width) return text;
if (width <= 3) return text[..width];
return text[..(width - 3)] + "...";
}
public void Dispose()
{
Registry.StateChanged -= OnRegistryChanged;