feat: добавить локализацию TUI (en/ru) с переключением клавишей L
- Добавлены TuiResources (sealed record), Locale, LocalizationService - Все строки интерфейса вынесены из .razor-файлов в TuiResources - App.razor: клавиша L циклически переключает локаль, заголовок показывает [EN]/[RU] - Дочерние компоненты получают Loc как параметр (stateless) - Создан AGENT.tui.md с правилами работы с TUI для агентов - Обновлены AGENTS.md и CLAUDE.md со ссылками на AGENT.tui.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
61
AGENT.tui.md
Normal file
61
AGENT.tui.md
Normal file
@@ -0,0 +1,61 @@
|
||||
## AGENT.tui.md
|
||||
|
||||
Read this file whenever you touch anything in `LazyBear.MCP/TUI/`.
|
||||
|
||||
### TUI Structure
|
||||
|
||||
- Entry point: `TUI/Components/App.razor` — owns all keyboard input and tab routing.
|
||||
- Keyboard input: `TUI/GlobalKeyboardService.cs` — single source; dedicated blocking thread on `Console.ReadKey(intercept: true)`. Do not add other console readers.
|
||||
- Localization: `TUI/Localization/` — `LocalizationService` singleton + `TuiResources` record with `En`/`Ru` static instances.
|
||||
- Components: `OverviewTab`, `LogsTab`, `SettingsTab` — pure display; receive state as parameters, fire no keyboard events.
|
||||
- Models: `TUI/Models/` — `OverviewRow`, `SettingsEntry`.
|
||||
- Palette: `TUI/UiPalette.cs` — all colors. Do not hardcode `Spectre.Console.Color` values in components.
|
||||
|
||||
### Keyboard Rules
|
||||
|
||||
- All keys are handled in `App.razor.HandleKeyDown()`. Do not attach `@onkeydown` to `<Select>` or any other RazorConsole component — framework intercepts Tab and arrows before they reach component-level callbacks.
|
||||
- `GlobalKeyboardService` fires `OnKeyPressed(ConsoleKeyInfo)`. `App.razor` converts via `ConvertKey()` and calls `HandleKeyDown()` inside `InvokeAsync`.
|
||||
- Do not use `Console.KeyAvailable` polling — it acquires the console mutex on every call and causes rendering lag. Always use blocking `Console.ReadKey` in a dedicated thread.
|
||||
- Keyboard shortcuts: `Tab`/`Shift+Tab` — switch tabs; Arrows — navigate list; `Space` — toggle; `Enter` — open/expand; `L` — cycle language.
|
||||
|
||||
### RazorConsole Gotchas (0.5.0)
|
||||
|
||||
- `@onkeydown` on `<Select>` is captured as `AdditionalAttributes` and not called for Tab/arrow keys.
|
||||
- `FocusManager` intercepts Tab globally — do not call `FocusNextAsync()` in `OnAfterRenderAsync` unconditionally; it shifts focus on every re-render.
|
||||
- No public global key-intercept API exists in 0.5.0.
|
||||
- `Select.Value` = committed selection (updated on Enter). `Select.FocusedValue` = highlighted item during navigation. Use `FocusedValue` to reflect external state immediately.
|
||||
- `StateHasChanged()` from a background thread must go through `InvokeAsync(() => { /* mutate state */ StateHasChanged(); })`.
|
||||
- Every `StateHasChanged()` triggers a full terminal redraw. Batch log-driven re-renders to avoid visible lag.
|
||||
|
||||
### Localization Rules
|
||||
|
||||
- All UI strings live in `TUI/Localization/TuiResources.cs` only. No hardcoded strings in `.razor` files or other `.cs` files.
|
||||
- To add a string: add a property to `TuiResources`, fill both `En` and `Ru` static instances. Build will fail if a `required init` is missing — by design.
|
||||
- Log level names (`Info`, `Warn`, `Error`) stay in English in all locales — they are technical identifiers, not UI labels.
|
||||
- Internal filter keys in `App.razor` (`"All"`, `"Info"`, `"Warn"`, `"Error"`) are English regardless of locale; `LogsTab` maps `"All"` → `Loc.FilterAll` for display.
|
||||
- Language toggle: `L` key cycles through locales. `LocalizationService.SwitchNext()` fires `OnChanged`; `App.razor` re-renders via `OnLocaleChanged()`.
|
||||
- Locale indicator shown in panel title: `"LazyBear MCP [EN]"` / `"LazyBear MCP [RU]"`.
|
||||
- Do not inject `LocalizationService` into child tab components — `App.razor` passes the current `TuiResources` as `Loc` parameter. Child components are stateless regarding locale.
|
||||
|
||||
### Component Contract
|
||||
|
||||
- Child tab components (`OverviewTab`, `LogsTab`, `SettingsTab`) accept `[Parameter] TuiResources Loc` — always pass it from `App.razor`.
|
||||
- Child components must not subscribe to events or inject services. Keep them as pure render components.
|
||||
- `SelectedIndexChanged` callbacks exist for forward-compatibility; actual selection state is managed exclusively in `App.razor`.
|
||||
|
||||
### DI Registration Pattern
|
||||
|
||||
Services that must be both injectable and run as hosted services:
|
||||
```csharp
|
||||
services.AddSingleton<MyService>();
|
||||
services.AddHostedService(sp => sp.GetRequiredService<MyService>());
|
||||
```
|
||||
`AddHostedService<T>()` alone creates a transient instance — unusable as injectable singleton.
|
||||
|
||||
### Working Rules
|
||||
|
||||
- Read `App.razor` before touching any keyboard or tab logic.
|
||||
- Match the style of the file being edited.
|
||||
- After changes, run `dotnet build`.
|
||||
- Do not add new hardcoded strings to `.razor` files — add to `TuiResources` instead.
|
||||
- Do not add new `Console.KeyAvailable` or `Console.ReadKey` calls outside `GlobalKeyboardService`.
|
||||
@@ -50,6 +50,7 @@
|
||||
- Project file structure and metadata indexed in memory via MCP memory system.
|
||||
|
||||
### Documentation
|
||||
- RazorConsole gotchas and TUI keyboard notes: `docs/tui_log.md`. Read before touching TUI or input handling.
|
||||
- **TUI work:** read `AGENT.tui.md` first — keyboard, localization, RazorConsole gotchas, component contract.
|
||||
- RazorConsole gotchas and session notes: `docs/tui_log.md`.
|
||||
- RazorConsole library docs: `docs/razorconsole/` (`overview.md`, `components.md`, `custom-translators.md`).
|
||||
- OpenCode question policy: `docs/opencode/question-policy.md`.
|
||||
|
||||
@@ -50,6 +50,10 @@ Each client (`K8sClientProvider`, `JiraClientProvider`, `ConfluenceClientProvide
|
||||
|
||||
`InMemoryLogSink` maintains a 500-entry circular `ConcurrentQueue`. All .NET logs flow through `InMemoryLoggerProvider` → sink → `OnLog` event → TUI Logs tab live view.
|
||||
|
||||
### TUI Agent Reference
|
||||
|
||||
**Read `AGENT.tui.md` before making any changes to `TUI/`.** It covers keyboard handling, localization rules, RazorConsole gotchas, and component contract.
|
||||
|
||||
### TUI Keyboard Input
|
||||
|
||||
`GlobalKeyboardService` (`TUI/GlobalKeyboardService.cs`) is the **single source of keyboard events** for the entire TUI. It runs a dedicated background thread with a blocking `Console.ReadKey(intercept: true)` — no polling. `App.razor` subscribes to `OnKeyPressed`, converts `ConsoleKeyInfo` to `KeyboardEventArgs`, and dispatches via `InvokeAsync`.
|
||||
|
||||
@@ -6,6 +6,7 @@ using LazyBear.MCP.Services.Mcp;
|
||||
using LazyBear.MCP.Services.ToolRegistry;
|
||||
using LazyBear.MCP.TUI;
|
||||
using LazyBear.MCP.TUI.Components;
|
||||
using LazyBear.MCP.TUI.Localization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -36,6 +37,9 @@ var host = Host.CreateDefaultBuilder(args)
|
||||
// Глобальный читатель клавиш — единственный источник клавишных событий для TUI
|
||||
services.AddSingleton<GlobalKeyboardService>();
|
||||
services.AddHostedService(sp => sp.GetRequiredService<GlobalKeyboardService>());
|
||||
|
||||
// Локализация TUI (en/ru, переключение клавишей L)
|
||||
services.AddSingleton<LocalizationService>();
|
||||
})
|
||||
.ConfigureLogging(logging =>
|
||||
{
|
||||
|
||||
@@ -3,18 +3,19 @@
|
||||
@inject ToolRegistryService Registry
|
||||
@inject InMemoryLogSink LogSink
|
||||
@inject GlobalKeyboardService KeyboardService
|
||||
@inject LocalizationService Localization
|
||||
|
||||
@implements IDisposable
|
||||
|
||||
<Rows Expand="true">
|
||||
<Panel Title="LazyBear MCP"
|
||||
<Panel Title="@($"LazyBear MCP [{Localization.Label}]")"
|
||||
TitleColor="@UiPalette.Accent"
|
||||
BorderColor="@UiPalette.Frame"
|
||||
Expand="true"
|
||||
Height="@GetPanelHeight()"
|
||||
Padding="@(new Spectre.Console.Padding(1, 0, 1, 0))">
|
||||
<Rows Expand="true">
|
||||
<Markup Content="Tab: switch tabs | Arrows: navigate | Space: toggle | Enter: open" Foreground="@UiPalette.TextMuted" />
|
||||
<Markup Content="@Localization.Current.HintBar" Foreground="@UiPalette.TextMuted" />
|
||||
<Markup Content=" " />
|
||||
|
||||
<Columns>
|
||||
@@ -36,7 +37,8 @@
|
||||
<OverviewTab Rows="@GetOverviewRows()"
|
||||
SelectedIndex="@_overviewSelection"
|
||||
SelectedIndexChanged="@OnOverviewSelectionChanged"
|
||||
ViewportRows="@GetOverviewViewportRows()" />
|
||||
ViewportRows="@GetOverviewViewportRows()"
|
||||
Loc="@Localization.Current" />
|
||||
}
|
||||
else if (_activeTab == Tab.Logs)
|
||||
{
|
||||
@@ -45,14 +47,16 @@
|
||||
SelectedIndexChanged="@OnLogSelectionChanged"
|
||||
SelectedFilter="@_logFilters[_logFilterIndex]"
|
||||
ViewportRows="@GetLogsViewportRows()"
|
||||
IsStickyToBottom="@_logsStickToBottom" />
|
||||
IsStickyToBottom="@_logsStickToBottom"
|
||||
Loc="@Localization.Current" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<SettingsTab Entries="@GetSettingsEntries()"
|
||||
SelectedIndex="@_settingsSelection"
|
||||
SelectedIndexChanged="@OnSettingsSelectionChanged"
|
||||
ViewportRows="@GetSettingsViewportRows()" />
|
||||
ViewportRows="@GetSettingsViewportRows()"
|
||||
Loc="@Localization.Current" />
|
||||
}
|
||||
</Rows>
|
||||
</Panel>
|
||||
@@ -87,6 +91,7 @@
|
||||
Registry.StateChanged += OnRegistryChanged;
|
||||
LogSink.OnLog += OnNewLog;
|
||||
KeyboardService.OnKeyPressed += OnConsoleKeyPressed;
|
||||
Localization.OnChanged += OnLocaleChanged;
|
||||
}
|
||||
|
||||
// Конвертация ConsoleKeyInfo → KeyboardEventArgs для переиспользования существующей логики
|
||||
@@ -130,6 +135,9 @@
|
||||
});
|
||||
}
|
||||
|
||||
private void OnLocaleChanged() =>
|
||||
InvokeAsync(StateHasChanged);
|
||||
|
||||
private void HandleKeyDown(KeyboardEventArgs args)
|
||||
{
|
||||
if (string.Equals(args.Key, "Tab", StringComparison.Ordinal))
|
||||
@@ -138,6 +146,12 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Key is "l" or "L")
|
||||
{
|
||||
Localization.SwitchNext();
|
||||
return; // StateHasChanged вызовет OnLocaleChanged
|
||||
}
|
||||
|
||||
switch (_activeTab)
|
||||
{
|
||||
case Tab.Overview:
|
||||
@@ -447,7 +461,7 @@
|
||||
toolName,
|
||||
isModuleEnabled
|
||||
? $"{module.ModuleName} / {toolName}"
|
||||
: $"{module.ModuleName} / {toolName} (module is OFF, tool state is preserved)",
|
||||
: $"{module.ModuleName} / {toolName} {Localization.Current.ModuleOffPreserved}",
|
||||
isConfigured,
|
||||
isModuleEnabled,
|
||||
false,
|
||||
@@ -527,11 +541,11 @@
|
||||
private static bool IsInfoLevel(LogEntry entry) =>
|
||||
entry.Level is LogLevel.Information or LogLevel.Debug or LogLevel.Trace;
|
||||
|
||||
private static string GetTabLabel(Tab tab) => tab switch
|
||||
private string GetTabLabel(Tab tab) => tab switch
|
||||
{
|
||||
Tab.Overview => "Overview",
|
||||
Tab.Logs => "Logs",
|
||||
_ => "Settings"
|
||||
Tab.Overview => Localization.Current.TabOverview,
|
||||
Tab.Logs => Localization.Current.TabLogs,
|
||||
_ => Localization.Current.TabSettings
|
||||
};
|
||||
|
||||
public void Dispose()
|
||||
@@ -539,5 +553,6 @@
|
||||
Registry.StateChanged -= OnRegistryChanged;
|
||||
LogSink.OnLog -= OnNewLog;
|
||||
KeyboardService.OnKeyPressed -= OnConsoleKeyPressed;
|
||||
Localization.OnChanged -= OnLocaleChanged;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
@using LazyBear.MCP.Services.Logging
|
||||
|
||||
<Rows>
|
||||
<Markup Content="Runtime Logs" Foreground="@UiPalette.Text" Decoration="@Spectre.Console.Decoration.Bold" />
|
||||
<Markup Content="Left/Right change level. Up/Down move. PageUp/PageDown/Home/End scroll." Foreground="@UiPalette.TextMuted" />
|
||||
<Markup Content="@Loc.LogsTitle" Foreground="@UiPalette.Text" Decoration="@Spectre.Console.Decoration.Bold" />
|
||||
<Markup Content="@Loc.LogsHint" Foreground="@UiPalette.TextMuted" />
|
||||
<Markup Content=" " />
|
||||
|
||||
<Columns>
|
||||
@foreach (var filter in Filters)
|
||||
{
|
||||
var isActive = string.Equals(filter, SelectedFilter, StringComparison.Ordinal);
|
||||
<Markup Content="@($" {filter} ")"
|
||||
<Markup Content="@($" {FilterDisplay(filter)} ")"
|
||||
Foreground="@(isActive ? UiPalette.SelectionForeground : UiPalette.Text)"
|
||||
Background="@(isActive ? UiPalette.AccentSoft : UiPalette.SurfaceMuted)"
|
||||
Decoration="@(isActive ? Spectre.Console.Decoration.Bold : Spectre.Console.Decoration.None)" />
|
||||
@@ -22,7 +22,7 @@
|
||||
@if (Entries.Count == 0)
|
||||
{
|
||||
<Border BorderColor="@UiPalette.Frame" BoxBorder="@Spectre.Console.BoxBorder.Rounded" Padding="@(new Spectre.Console.Padding(0, 0, 0, 0))">
|
||||
<Markup Content="No log entries yet." Foreground="@UiPalette.TextDim" />
|
||||
<Markup Content="@Loc.LogsEmpty" Foreground="@UiPalette.TextDim" />
|
||||
</Border>
|
||||
}
|
||||
else
|
||||
@@ -43,6 +43,7 @@
|
||||
</Rows>
|
||||
|
||||
@code {
|
||||
// Внутренние ключи фильтров — не локализуются (используются в логике App.razor)
|
||||
private static readonly string[] Filters = ["All", "Info", "Warn", "Error"];
|
||||
|
||||
[Parameter, EditorRequired] public IReadOnlyList<LogEntry> Entries { get; set; } = Array.Empty<LogEntry>();
|
||||
@@ -51,21 +52,26 @@
|
||||
[Parameter] public string SelectedFilter { get; set; } = "All";
|
||||
[Parameter] public int ViewportRows { get; set; } = 5;
|
||||
[Parameter] public bool IsStickyToBottom { get; set; }
|
||||
[Parameter] public TuiResources Loc { get; set; } = TuiResources.En;
|
||||
|
||||
private int[] GetOptions() => Enumerable.Range(0, Entries.Count).ToArray();
|
||||
|
||||
private int GetNormalizedIndex() => Entries.Count == 0 ? 0 : Math.Clamp(SelectedIndex, 0, Entries.Count - 1);
|
||||
|
||||
// "All" локализуется; уровни логов (Info/Warn/Error) остаются на английском в любой локали
|
||||
private string FilterDisplay(string filter) =>
|
||||
filter == "All" ? Loc.FilterAll : filter;
|
||||
|
||||
private string GetDetailsHeader()
|
||||
{
|
||||
if (Entries.Count == 0)
|
||||
{
|
||||
return $"Filter: {SelectedFilter}";
|
||||
return $"{Loc.FilterLabel}: {FilterDisplay(SelectedFilter)}";
|
||||
}
|
||||
|
||||
var selected = Entries[Math.Clamp(SelectedIndex, 0, Entries.Count - 1)];
|
||||
var position = Math.Clamp(SelectedIndex, 0, Entries.Count - 1) + 1;
|
||||
var sticky = IsStickyToBottom ? "sticky" : "manual";
|
||||
var sticky = IsStickyToBottom ? Loc.LogsSticky : Loc.LogsManual;
|
||||
return $"{position}/{Entries.Count} | {selected.Timestamp:HH:mm:ss} | {selected.Level} | {selected.ShortCategory} | {sticky}";
|
||||
}
|
||||
|
||||
@@ -73,7 +79,7 @@
|
||||
{
|
||||
if (Entries.Count == 0)
|
||||
{
|
||||
return "Incoming log entries will appear here.";
|
||||
return Loc.LogsPlaceholder;
|
||||
}
|
||||
|
||||
var selected = Entries[Math.Clamp(SelectedIndex, 0, Entries.Count - 1)];
|
||||
@@ -84,25 +90,17 @@
|
||||
return Fit(details, Math.Max(Console.WindowWidth - 12, 32));
|
||||
}
|
||||
|
||||
private static Spectre.Console.Color GetLevelColor(LogEntry entry) => entry.Level switch
|
||||
{
|
||||
LogLevel.Error or LogLevel.Critical => UiPalette.Danger,
|
||||
LogLevel.Warning => UiPalette.Warning,
|
||||
LogLevel.Information => UiPalette.Text,
|
||||
_ => UiPalette.TextMuted
|
||||
};
|
||||
|
||||
private string FormatEntry(int index)
|
||||
{
|
||||
var entry = Entries[index];
|
||||
var level = entry.Level switch
|
||||
{
|
||||
LogLevel.Error => "ERR",
|
||||
LogLevel.Critical => "CRT",
|
||||
LogLevel.Warning => "WRN",
|
||||
LogLevel.Error => "ERR",
|
||||
LogLevel.Critical => "CRT",
|
||||
LogLevel.Warning => "WRN",
|
||||
LogLevel.Information => "INF",
|
||||
LogLevel.Debug => "DBG",
|
||||
_ => "TRC"
|
||||
LogLevel.Debug => "DBG",
|
||||
_ => "TRC"
|
||||
};
|
||||
|
||||
var text = $"{entry.Timestamp:HH:mm:ss} {level,-3} {entry.ShortCategory,-18} {entry.Message}";
|
||||
@@ -111,21 +109,9 @@
|
||||
|
||||
private static string Fit(string text, int width)
|
||||
{
|
||||
if (width <= 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (text.Length <= width)
|
||||
{
|
||||
return text.PadRight(width);
|
||||
}
|
||||
|
||||
if (width <= 3)
|
||||
{
|
||||
return text[..width];
|
||||
}
|
||||
|
||||
if (width <= 0) return string.Empty;
|
||||
if (text.Length <= width) return text.PadRight(width);
|
||||
if (width <= 3) return text[..width];
|
||||
return text[..(width - 3)] + "...";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<Rows>
|
||||
<Markup Content="Module Overview" Foreground="@UiPalette.Text" Decoration="@Spectre.Console.Decoration.Bold" />
|
||||
<Markup Content="Up/Down select a module. Enter opens its settings." Foreground="@UiPalette.TextMuted" />
|
||||
<Markup Content="@Loc.OverviewTitle" Foreground="@UiPalette.Text" Decoration="@Spectre.Console.Decoration.Bold" />
|
||||
<Markup Content="@Loc.OverviewHint" Foreground="@UiPalette.TextMuted" />
|
||||
<Markup Content=" " />
|
||||
|
||||
@if (Rows.Count == 0)
|
||||
{
|
||||
<Border BorderColor="@UiPalette.Frame" BoxBorder="@Spectre.Console.BoxBorder.Rounded" Padding="@(new Spectre.Console.Padding(0, 0, 0, 0))">
|
||||
<Markup Content="No modules registered." Foreground="@UiPalette.TextDim" />
|
||||
<Markup Content="@Loc.OverviewEmpty" Foreground="@UiPalette.TextDim" />
|
||||
</Border>
|
||||
}
|
||||
else
|
||||
@@ -30,6 +30,7 @@
|
||||
[Parameter] public int SelectedIndex { get; set; }
|
||||
[Parameter] public EventCallback<int> SelectedIndexChanged { get; set; }
|
||||
[Parameter] public int ViewportRows { get; set; } = 3;
|
||||
[Parameter] public TuiResources Loc { get; set; } = TuiResources.En;
|
||||
|
||||
private int[] GetOptions() => Enumerable.Range(0, Rows.Count).ToArray();
|
||||
|
||||
@@ -39,42 +40,27 @@
|
||||
{
|
||||
if (Rows.Count == 0)
|
||||
{
|
||||
return "No integration modules available.";
|
||||
return Loc.OverviewEmpty;
|
||||
}
|
||||
|
||||
var selected = Rows[Math.Clamp(SelectedIndex, 0, Rows.Count - 1)];
|
||||
var state = selected.IsModuleEnabled ? "ON" : "OFF";
|
||||
return $"{selected.ModuleName}: {selected.Description} | Module {state} | Tools {selected.ConfiguredTools}/{selected.TotalTools}";
|
||||
var state = selected.IsModuleEnabled ? Loc.StateOn : Loc.StateOff;
|
||||
return $"{selected.ModuleName}: {selected.Description} | {Loc.FooterModule} {state} | {Loc.FooterTools} {selected.ConfiguredTools}/{selected.TotalTools}";
|
||||
}
|
||||
|
||||
private static Spectre.Console.Color GetRowForeground(OverviewRow row) =>
|
||||
row.IsModuleEnabled ? UiPalette.Text : UiPalette.TextMuted;
|
||||
|
||||
private string FormatRow(int index)
|
||||
{
|
||||
var row = Rows[index];
|
||||
var status = row.IsModuleEnabled ? "[ON] " : "[OFF]";
|
||||
var status = row.IsModuleEnabled ? $"[{Loc.StateOn}] " : $"[{Loc.StateOff}]";
|
||||
var text = $"{row.ModuleName,-12} {status} {row.ConfiguredTools,2}/{row.TotalTools,-2} {row.Description}";
|
||||
return Fit(text, Math.Max(Console.WindowWidth - 12, 32));
|
||||
}
|
||||
|
||||
private static string Fit(string text, int width)
|
||||
{
|
||||
if (width <= 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (text.Length <= width)
|
||||
{
|
||||
return text.PadRight(width);
|
||||
}
|
||||
|
||||
if (width <= 3)
|
||||
{
|
||||
return text[..width];
|
||||
}
|
||||
|
||||
if (width <= 0) return string.Empty;
|
||||
if (text.Length <= width) return text.PadRight(width);
|
||||
if (width <= 3) return text[..width];
|
||||
return text[..(width - 3)] + "...";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<Rows>
|
||||
<Markup Content="Tool Registry" Foreground="@UiPalette.Text" Decoration="@Spectre.Console.Decoration.Bold" />
|
||||
<Markup Content="Up/Down select. Left/Right collapse or expand. Space toggles state." Foreground="@UiPalette.TextMuted" />
|
||||
<Markup Content="@Loc.SettingsTitle" Foreground="@UiPalette.Text" Decoration="@Spectre.Console.Decoration.Bold" />
|
||||
<Markup Content="@Loc.SettingsHint" Foreground="@UiPalette.TextMuted" />
|
||||
<Markup Content=" " />
|
||||
|
||||
@if (Entries.Count == 0)
|
||||
{
|
||||
<Border BorderColor="@UiPalette.Frame" BoxBorder="@Spectre.Console.BoxBorder.Rounded" Padding="@(new Spectre.Console.Padding(0, 0, 0, 0))">
|
||||
<Markup Content="No modules available." Foreground="@UiPalette.TextDim" />
|
||||
<Markup Content="@Loc.SettingsEmpty" Foreground="@UiPalette.TextDim" />
|
||||
</Border>
|
||||
}
|
||||
else
|
||||
@@ -30,6 +30,7 @@
|
||||
[Parameter] public int SelectedIndex { get; set; }
|
||||
[Parameter] public EventCallback<int> SelectedIndexChanged { get; set; }
|
||||
[Parameter] public int ViewportRows { get; set; } = 5;
|
||||
[Parameter] public TuiResources Loc { get; set; } = TuiResources.En;
|
||||
|
||||
private int[] GetOptions() => Enumerable.Range(0, Entries.Count).ToArray();
|
||||
|
||||
@@ -39,7 +40,7 @@
|
||||
{
|
||||
if (Entries.Count == 0)
|
||||
{
|
||||
return "Runtime enable/disable settings are unavailable.";
|
||||
return Loc.SettingsUnavailable;
|
||||
}
|
||||
|
||||
var selected = Entries[Math.Clamp(SelectedIndex, 0, Entries.Count - 1)];
|
||||
@@ -51,7 +52,9 @@
|
||||
var entry = Entries[index];
|
||||
var indent = new string(' ', entry.Depth * 4);
|
||||
var checkbox = entry.IsChecked ? "[x]" : "[ ]";
|
||||
var disabledSuffix = entry.Kind == SettingsEntryKind.Tool && !entry.IsModuleEnabled ? " (module off)" : string.Empty;
|
||||
var disabledSuffix = entry.Kind == SettingsEntryKind.Tool && !entry.IsModuleEnabled
|
||||
? $" {Loc.ModuleOff}"
|
||||
: string.Empty;
|
||||
|
||||
string text;
|
||||
if (entry.Kind == SettingsEntryKind.Module)
|
||||
@@ -69,21 +72,9 @@
|
||||
|
||||
private static string Fit(string text, int width)
|
||||
{
|
||||
if (width <= 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (text.Length <= width)
|
||||
{
|
||||
return text.PadRight(width);
|
||||
}
|
||||
|
||||
if (width <= 3)
|
||||
{
|
||||
return text[..width];
|
||||
}
|
||||
|
||||
if (width <= 0) return string.Empty;
|
||||
if (text.Length <= width) return text.PadRight(width);
|
||||
if (width <= 3) return text[..width];
|
||||
return text[..(width - 3)] + "...";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,5 +4,6 @@
|
||||
@using RazorConsole.Core
|
||||
@using RazorConsole.Core.Rendering
|
||||
@using LazyBear.MCP.TUI
|
||||
@using LazyBear.MCP.TUI.Localization
|
||||
@using LazyBear.MCP.TUI.Models
|
||||
@using LazyBear.MCP.TUI.Components
|
||||
|
||||
7
LazyBear.MCP/TUI/Localization/Locale.cs
Normal file
7
LazyBear.MCP/TUI/Localization/Locale.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace LazyBear.MCP.TUI.Localization;
|
||||
|
||||
public enum Locale
|
||||
{
|
||||
En = 0,
|
||||
Ru = 1
|
||||
}
|
||||
25
LazyBear.MCP/TUI/Localization/LocalizationService.cs
Normal file
25
LazyBear.MCP/TUI/Localization/LocalizationService.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
namespace LazyBear.MCP.TUI.Localization;
|
||||
|
||||
/// <summary>
|
||||
/// Синглтон, хранящий текущую локаль TUI. Переключение — клавиша L.
|
||||
/// Компоненты подписываются на OnChanged для перерисовки при смене языка.
|
||||
/// </summary>
|
||||
public sealed class LocalizationService
|
||||
{
|
||||
private static readonly TuiResources[] All = [TuiResources.En, TuiResources.Ru];
|
||||
private static readonly string[] Labels = ["EN", "RU"];
|
||||
|
||||
private int _index;
|
||||
|
||||
public TuiResources Current => All[_index];
|
||||
public string Label => Labels[_index];
|
||||
public Locale Locale => (Locale)_index;
|
||||
|
||||
public event Action? OnChanged;
|
||||
|
||||
public void SwitchNext()
|
||||
{
|
||||
_index = (_index + 1) % All.Length;
|
||||
OnChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
107
LazyBear.MCP/TUI/Localization/TuiResources.cs
Normal file
107
LazyBear.MCP/TUI/Localization/TuiResources.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
namespace LazyBear.MCP.TUI.Localization;
|
||||
|
||||
/// <summary>
|
||||
/// Все строки TUI для одной локали.
|
||||
/// При добавлении новой строки: добавить свойство сюда и перевод в оба статических экземпляра.
|
||||
/// </summary>
|
||||
public sealed record TuiResources
|
||||
{
|
||||
// ── Подсказка и вкладки ──────────────────────────────────────────────────
|
||||
public string HintBar { get; init; } = "";
|
||||
public string TabOverview { get; init; } = "";
|
||||
public string TabLogs { get; init; } = "";
|
||||
public string TabSettings { get; init; } = "";
|
||||
|
||||
// ── Overview ─────────────────────────────────────────────────────────────
|
||||
public string OverviewTitle { get; init; } = "";
|
||||
public string OverviewHint { get; init; } = "";
|
||||
public string OverviewEmpty { get; init; } = "";
|
||||
public string StateOn { get; init; } = "";
|
||||
public string StateOff { get; init; } = "";
|
||||
public string FooterModule { get; init; } = "";
|
||||
public string FooterTools { get; init; } = "";
|
||||
|
||||
// ── Logs ─────────────────────────────────────────────────────────────────
|
||||
public string LogsTitle { get; init; } = "";
|
||||
public string LogsHint { get; init; } = "";
|
||||
public string LogsEmpty { get; init; } = "";
|
||||
public string LogsPlaceholder { get; init; } = "";
|
||||
public string LogsSticky { get; init; } = "";
|
||||
public string LogsManual { get; init; } = "";
|
||||
public string FilterLabel { get; init; } = "";
|
||||
public string FilterAll { get; init; } = "";
|
||||
|
||||
// ── Settings ─────────────────────────────────────────────────────────────
|
||||
public string SettingsTitle { get; init; } = "";
|
||||
public string SettingsHint { get; init; } = "";
|
||||
public string SettingsEmpty { get; init; } = "";
|
||||
public string SettingsUnavailable { get; init; } = "";
|
||||
public string ModuleOff { get; init; } = "";
|
||||
public string ModuleOffPreserved { get; init; } = "";
|
||||
|
||||
// ── Локали ───────────────────────────────────────────────────────────────
|
||||
|
||||
public static readonly TuiResources En = new()
|
||||
{
|
||||
HintBar = "Tab: tabs | Arrows: navigate | Space: toggle | Enter: open | L: language",
|
||||
TabOverview = "Overview",
|
||||
TabLogs = "Logs",
|
||||
TabSettings = "Settings",
|
||||
|
||||
OverviewTitle = "Module Overview",
|
||||
OverviewHint = "Up/Down: select module. Enter: open settings.",
|
||||
OverviewEmpty = "No modules registered.",
|
||||
StateOn = "ON",
|
||||
StateOff = "OFF",
|
||||
FooterModule = "Module",
|
||||
FooterTools = "Tools",
|
||||
|
||||
LogsTitle = "Runtime Logs",
|
||||
LogsHint = "Left/Right: filter | Up/Down: scroll | PageUp/Down: page",
|
||||
LogsEmpty = "No log entries yet.",
|
||||
LogsPlaceholder = "Incoming log entries will appear here.",
|
||||
LogsSticky = "sticky",
|
||||
LogsManual = "manual",
|
||||
FilterLabel = "Filter",
|
||||
FilterAll = "All",
|
||||
|
||||
SettingsTitle = "Tool Registry",
|
||||
SettingsHint = "Up/Down: select | Left/Right: expand/collapse | Space: toggle",
|
||||
SettingsEmpty = "No modules available.",
|
||||
SettingsUnavailable = "Runtime enable/disable settings are unavailable.",
|
||||
ModuleOff = "(module off)",
|
||||
ModuleOffPreserved = "(module is OFF, tool state is preserved)"
|
||||
};
|
||||
|
||||
public static readonly TuiResources Ru = new()
|
||||
{
|
||||
HintBar = "Tab: вкладки | Стрелки: навигация | Space: вкл/выкл | Enter: открыть | L: язык",
|
||||
TabOverview = "Обзор",
|
||||
TabLogs = "Логи",
|
||||
TabSettings = "Настройки",
|
||||
|
||||
OverviewTitle = "Модули",
|
||||
OverviewHint = "Вверх/вниз: выбор модуля. Enter: открыть настройки.",
|
||||
OverviewEmpty = "Нет зарегистрированных модулей.",
|
||||
StateOn = "ВКЛ",
|
||||
StateOff = "ВЫКЛ",
|
||||
FooterModule = "Модуль",
|
||||
FooterTools = "Инструменты",
|
||||
|
||||
LogsTitle = "Логи",
|
||||
LogsHint = "Лево/право: фильтр | Вверх/вниз: прокрутка | PageUp/Down: страница",
|
||||
LogsEmpty = "Записей пока нет.",
|
||||
LogsPlaceholder = "Новые записи будут появляться здесь.",
|
||||
LogsSticky = "следить",
|
||||
LogsManual = "вручную",
|
||||
FilterLabel = "Фильтр",
|
||||
FilterAll = "Все",
|
||||
|
||||
SettingsTitle = "Реестр инструментов",
|
||||
SettingsHint = "Вверх/вниз: выбор | Лево/право: развернуть/свернуть | Space: вкл/выкл",
|
||||
SettingsEmpty = "Нет доступных модулей.",
|
||||
SettingsUnavailable = "Настройки включения/выключения недоступны.",
|
||||
ModuleOff = "(модуль выкл)",
|
||||
ModuleOffPreserved = "(модуль ВЫКЛ, состояние инструментов сохранено)"
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user