XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESystem

【Rust】XFE系统,基于Rust和汇编

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Text;
using XFESystem.Console;
using XFESystem.Core;

namespace XFESystem.Desktop;

/// <summary>The desktop publishes this on its UI thread. The native adapter
/// queues execution and exclusively owns Output until it reports completion.</summary>
public sealed record TerminalProgramRequest(int WindowId, string Path, TerminalTextWriter Output);

public sealed partial class DesktopSession
{
    public string TerminalVersion { get; init; } = "0.3.0-dev.4";
    public Action<TerminalProgramRequest>? TerminalProgramRequested { get; init; }
    private const int TerminalCellWidth = 8, TerminalRowHeight = 20;
    private static DesktopBounds TerminalViewport(DesktopWindow window) => new(window.Bounds.X + 2, window.Bounds.Y + 35,
        Math.Max(1, window.Bounds.Width - 4), Math.Max(1, window.Bounds.Height - 64));

    private void InitializeTerminal(DesktopWindow window)
    {
        int columns = Math.Clamp((window.Bounds.Width - 28) / TerminalCellWidth, 24, 240);
        window.Shell = new(_storage, TerminalVersion, columns, 128);
        window.Terminal = window.Shell.ReadOutput(); window.Content = "";
        window.Location = window.Shell.CurrentDirectory; window.Status = "";
        RestartTerminalCaret(window);
    }

    private DesktopWindow GetTerminalWindow(int? windowId = null)
    {
        var window = windowId is { } id ? _windows.Find(value => value.Id == id && value.Application == DesktopApplication.Terminal)
            : ActiveWindow?.Application == DesktopApplication.Terminal ? ActiveWindow : _windows.FindLast(value => value.Application == DesktopApplication.Terminal);
        if (window is null && windowId is not null) throw new InvalidOperationException("The terminal window is closed.");
        window ??= Open(DesktopApplication.Terminal);
        if (window.Shell is null) InitializeTerminal(window);
        return window;
    }

    /// <summary>UI-thread completion only. The worker must finish writing before
    /// its queued completion is drained. No worker touches desktop/window state.</summary>
    public void RefreshTerminalOutput(int windowId)
    {
        var window = _windows.Find(value => value.Id == windowId && value.Application == DesktopApplication.Terminal);
        if (window?.Shell is null) return; // A closed terminal never reopens on worker completion.
        window.TerminalProgramRunning = false; window.Terminal = window.Shell.ReadOutput();
        window.Location = window.Shell.CurrentDirectory; window.TerminalScrollRows = 0; window.Status = "";
        RestartTerminalCaret(window); InvalidateScene(window.Bounds);
        Trace?.Invoke("XFE_TERMINAL_OUTPUT_READY|Window|" + window.Id + "\n");
    }

    /// <summary>Short UI-owned output. Long/application output uses the request
    /// writer on its worker, followed by RefreshTerminalOutput on the UI thread.</summary>
    public void WriteTerminalOutput(string text, int? windowId = null)
    {
        var window = GetTerminalWindow(windowId);
        if (window.TerminalProgramRunning) throw new InvalidOperationException("The running program owns terminal output.");
        window.Shell!.ProgramOutput.Write(text); window.Shell.ProgramOutput.Complete(); RefreshTerminalOutput(window.Id);
    }

    /// <summary>Literal single-line paste. It does not execute pasted commands.</summary>
    public bool PasteTerminalInput(string text)
    {
        if (IsStartMenuOpen || ActiveWindow is not { Application: DesktopApplication.Terminal, Shell: { } shell } window || window.TerminalProgramRunning) return false;
        bool accepted = shell.InsertText(text);
        if (accepted) { window.TerminalScrollRows = 0; RestartTerminalCaret(window); InvalidateScene(TerminalViewport(window)); }
        return accepted;
    }

    private void TerminalKey(DesktopWindow window, DesktopKey key, char character)
    {
        if (window.TerminalProgramRunning)
        {
            if (key == DesktopKey.Cancel)
            {
                window.Status = "Program is running; this runtime cannot interrupt it yet.";
                InvalidateScene(window.Bounds);
            }
            return;
        }
        var shell = window.Shell!;
        TerminalShellKey? mapped = key switch
        {
            DesktopKey.Character => TerminalShellKey.Character, DesktopKey.Enter => TerminalShellKey.Enter,
            DesktopKey.Backspace => TerminalShellKey.Backspace, DesktopKey.Delete => TerminalShellKey.Delete,
            DesktopKey.Left => TerminalShellKey.Left, DesktopKey.Right => TerminalShellKey.Right,
            DesktopKey.Up => TerminalShellKey.Up, DesktopKey.Down => TerminalShellKey.Down,
            DesktopKey.Home => TerminalShellKey.Home, DesktopKey.End => TerminalShellKey.End,
            DesktopKey.Insert => TerminalShellKey.Insert, DesktopKey.Cancel or DesktopKey.Escape => TerminalShellKey.Cancel,
            _ => null
        };
        if (mapped is null) return;
        var result = shell.HandleKey(mapped.Value, key == DesktopKey.Character ? character.ToString() : null);
        window.Location = shell.CurrentDirectory; window.TerminalScrollRows = 0; RestartTerminalCaret(window);
        if (key is DesktopKey.Enter or DesktopKey.Cancel or DesktopKey.Escape) window.Terminal = shell.ReadOutput();
        if (result is not null)
        {
            Trace?.Invoke("XFE_TERMINAL_COMMAND|Window|" + window.Id + "|Exit|" + result.ExitCode + "\n");
            if (result.CloseRequested) { Close(window.Id); return; }
            if (result.OpenPath is { } path)
            {
                bool directory = path == SystemPaths.Root || _storage.List(SystemPaths.Parent(path)).Any(entry => entry.IsDirectory && entry.Path.Equals(path, StringComparison.OrdinalIgnoreCase));
                if (directory) Open(DesktopApplication.Files, path); else OpenFile(path);
            }
            if (result.ManagedProgramPath is { } program)
            {
                if (TerminalProgramRequested is not null)
                {
                    // Capture before publication, then render only this immutable
                    // snapshot until the UI drains the worker's completion.
                    window.TerminalProgramRunning = true; window.Status = "Running " + SystemPaths.Name(program) + "...";
                    try { TerminalProgramRequested(new(window.Id, program, shell.ProgramOutput)); }
                    catch (Exception error) when (error is IOException or InvalidOperationException or ArgumentException)
                    { window.TerminalProgramRunning = false; shell.ProgramOutput.WriteLine("Program could not start: " + error.Message); RefreshTerminalOutput(window.Id); }
                }
                else if (_managedApplication is not null) _managedApplication(program);
                else { shell.ProgramOutput.WriteLine("Managed application execution is unavailable."); window.Terminal = shell.ReadOutput(); }
            }
        }
        InvalidateScene(TerminalViewport(window));
    }

    private void RestartTerminalCaret(DesktopWindow window)
    { window.TerminalCaretStarted = _monotonicMilliseconds?.Invoke() ?? _feedbackNow; window.TerminalCaretVisible = true; }

    private void AdvanceTerminalCarets(long now)
    {
        if (ActiveWindow is not { Application: DesktopApplication.Terminal } window || window.TerminalProgramRunning) return;
        bool visible = Math.Max(0, now - window.TerminalCaretStarted) / 500 % 2 == 0;
        if (visible != window.TerminalCaretVisible) { window.TerminalCaretVisible = visible; InvalidateScene(TerminalViewport(window)); }
    }

    private static int TerminalColumns(DesktopBounds body) => Math.Max(1, (body.Width - 24) / TerminalCellWidth);
    private static int TerminalRows(DesktopBounds body) => Math.Max(1, (body.Height - 16) / TerminalRowHeight);

    private static List<(int Row, int Start, int Count)> TerminalRowsForDisplay(TerminalSnapshot frame, int columns)
    {
        var rows = new List<(int Row, int Start, int Count)>();
        bool promptNeedsNewRow = false;
        for (int row = 0; row <= Math.Min(frame.CursorRow, frame.Rows - 1); row++)
        {
            int used = row == frame.CursorRow ? frame.CursorColumn : 0;
            for (int col = frame.Columns - 1; col >= 0; col--)
            {
                var cell = frame.GetCell(col, row);
                if (!string.IsNullOrWhiteSpace(cell.Text) || cell.Style.DisplayBackground != TerminalColor.ThemeBackground)
                { used = Math.Max(used, col + Math.Max(1, cell.DisplayWidth)); break; }
            }
            int chunks = Math.Max(1, (used + columns - 1) / columns);
            for (int chunk = 0; chunk < chunks; chunk++) rows.Add((row, chunk * columns, Math.Min(columns, frame.Columns - chunk * columns)));
            if (row == frame.CursorRow) promptNeedsNewRow = used > 0;
        }
        // Console.Write and carriage-return progress output need not end in a
        // newline. Preserve their row rather than clearing it for the prompt.
        if (promptNeedsNewRow) rows.Add((-1, 0, 0));
        return rows;
    }

    private void DrawTerminal(DesktopWindow window)
    {
        var body = TerminalViewport(window); var shell = window.Shell!;
        var frame = window.Terminal!; // Never snapshot a worker-owned program during rendering.
        int columns = Math.Min(frame.Columns, TerminalColumns(body)), visibleRows = TerminalRows(body);
        var rows = TerminalRowsForDisplay(frame, columns);
        int maximumScroll = Math.Max(0, rows.Count - visibleRows);
        window.TerminalScrollRows = Math.Clamp(window.TerminalScrollRows, 0, maximumScroll);
        int first = Math.Max(0, rows.Count - visibleRows - window.TerminalScrollRows);
        _canvas.Fill(TerminalBackground, body.X, body.Y, body.Width, body.Height);
        for (int index = first; index < Math.Min(rows.Count, first + visibleRows); index++)
        {
            var line = rows[index]; int y = body.Y + 8 + (index - first) * TerminalRowHeight;
            for (int col = 0; col < line.Count; col++)
            {
                var cell = frame.GetCell(line.Start + col, line.Row); if (cell.IsContinuation) continue;
                int x = body.X + 12 + col * TerminalCellWidth;
                if (cell.Style.DisplayBackground != TerminalColor.ThemeBackground)
                    _canvas.Fill(Rgb(cell.Style.DisplayBackground), x, y, TerminalCellWidth * Math.Max(1, cell.DisplayWidth), TerminalRowHeight);
                if (cell.Text.Length != 0 && cell.Text != " ") _canvas.Text(cell.Text, Rgb(cell.Style.DisplayForeground), x, y);
                if (cell.Style.Underline) _canvas.Line(Rgb(cell.Style.DisplayForeground), x, y + 17, x + TerminalCellWidth * Math.Max(1, cell.DisplayWidth) - 1, y + 17);
            }
        }
        if (!window.TerminalProgramRunning && window.TerminalScrollRows == 0)
        {
            int row = rows.Count - 1 - first;
            DrawTerminalInput(window, body.X + 12, body.Y + 8 + row * TerminalRowHeight, body.Width - 24);
        }
    }

    private static uint Rgb(TerminalColor color) => (uint)(color.Red << 16 | color.Green << 8 | color.Blue);

    private (string Prompt, int Start, string Visible) TerminalInputLayout(DesktopWindow window, int width)
    {
        var shell = window.Shell!; string prompt = shell.Prompt;
        int promptBudget = Math.Max(24, width / 2);
        while (prompt.Length > 1 && _canvas.TextWidth(prompt) > promptBudget) prompt = prompt[1..];
        if (prompt.Length != shell.Prompt.Length) prompt = "…" + prompt;
        int available = Math.Max(8, width - _canvas.TextWidth(prompt) - 3);
        int start = Math.Clamp(window.TerminalInputStart, 0, shell.Caret);
        if (start > 0 && start < shell.Input.Length && char.IsLowSurrogate(shell.Input[start])) start--;
        while (start < shell.Caret && _canvas.TextWidth(shell.Input[start..shell.Caret]) > available)
            start += char.IsHighSurrogate(shell.Input[start]) && start + 1 < shell.Input.Length ? 2 : 1;
        window.TerminalInputStart = start;
        int end = start;
        while (end < shell.Input.Length)
        {
            int next = end + (char.IsHighSurrogate(shell.Input[end]) && end + 1 < shell.Input.Length ? 2 : 1);
            if (_canvas.TextWidth(shell.Input[start..next]) > available) break;
            end = next;
        }
        return (prompt, start, shell.Input[start..end]);
    }

    private void DrawTerminalInput(DesktopWindow window, int x, int y, int width)
    {
        var shell = window.Shell!; var line = TerminalInputLayout(window, width);
        _canvas.Fill(TerminalBackground, x, y, width, TerminalRowHeight);
        _canvas.Text(line.Prompt, TerminalForeground, x, y); int inputX = x + _canvas.TextWidth(line.Prompt);
        _canvas.Text(line.Visible, TerminalForeground, inputX, y);
        if (window == ActiveWindow && window.TerminalCaretVisible)
        {
            int caretX = inputX + _canvas.TextWidth(shell.Input[line.Start..shell.Caret]);
            if (shell.InsertMode) _canvas.Fill(TerminalForeground, caretX, y + 1, 2, 16);
            else _canvas.Fill(TerminalForeground, caretX, y + 16, TerminalCellWidth, 2);
        }
    }

    private void TerminalClick(DesktopWindow window, int x, int y)
    {
        if (window.TerminalProgramRunning || window.TerminalScrollRows != 0) return;
        var body = TerminalViewport(window); var frame = window.Terminal!; var shell = window.Shell!;
        int columns = Math.Min(frame.Columns, TerminalColumns(body)); var rows = TerminalRowsForDisplay(frame, columns);
        int inputY = body.Y + 8 + Math.Min(rows.Count - 1, TerminalRows(body) - 1) * TerminalRowHeight;
        if (y < inputY || y >= inputY + TerminalRowHeight) return;
        var line = TerminalInputLayout(window, body.Width - 24);
        int pixels = Math.Max(0, x - body.X - 12 - _canvas.TextWidth(line.Prompt)), index = line.Start;
        while (index < shell.Input.Length)
        {
            int next = index + (char.IsHighSurrogate(shell.Input[index]) && index + 1 < shell.Input.Length ? 2 : 1);
            int advance = _canvas.TextWidth(shell.Input[index..next]); if (pixels < (advance + 1) / 2) break;
            pixels -= advance; index = next;
        }
        shell.MoveCaret(index); RestartTerminalCaret(window); InvalidateScene(body);
    }

    private void ScrollTerminal(DesktopWindow window, int wheel)
    {
        var body = TerminalViewport(window); var frame = window.Terminal!;
        int maximum = Math.Max(0, TerminalRowsForDisplay(frame, Math.Min(frame.Columns, TerminalColumns(body))).Count - TerminalRows(body));
        window.TerminalScrollRows = Math.Clamp(window.TerminalScrollRows + wheel * 3, 0, maximum); InvalidateScene(body);
    }

    private static void AppendTerminalSnapshot(TerminalShell shell, TerminalSnapshot snapshot)
    {
        var writer = shell.ProgramOutput; TerminalStyle? previous = null;
        for (int row = 0; row < snapshot.Rows; row++)
        {
            int last = snapshot.Columns - 1;
            while (last >= 0 && string.IsNullOrWhiteSpace(snapshot.GetCell(last, row).Text) && snapshot.GetCell(last, row).Style == TerminalStyle.Default) last--;
            if (last < 0 && row > snapshot.CursorRow) break;
            var text = new StringBuilder();
            for (int col = 0; col <= last; col++)
            {
                var cell = snapshot.GetCell(col, row); if (cell.IsContinuation) continue;
                if (previous != cell.Style)
                {
                    var style = cell.Style; var foreground = style.Foreground; var background = style.Background;
                    text.Append("\u001b[0;38;2;").Append(foreground.Red).Append(';').Append(foreground.Green).Append(';').Append(foreground.Blue)
                        .Append(";48;2;").Append(background.Red).Append(';').Append(background.Green).Append(';').Append(background.Blue);
                    if (style.Bold) text.Append(";1"); if (style.Dim) text.Append(";2"); if (style.Italic) text.Append(";3");
                    if (style.Underline) text.Append(";4"); if (style.Inverse) text.Append(";7"); if (style.Hidden) text.Append(";8"); if (style.Strike) text.Append(";9");
                    text.Append('m'); previous = style;
                }
                text.Append(cell.Text);
            }
            writer.WriteLine(text.ToString());
        }
        writer.Write("\u001b[0m"); writer.Complete();
    }
}