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

XFESystem

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

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Globalization;
using System.Text;

namespace XFESystem.Console;

/// <summary>
/// Bounded, incremental VT text terminal model. A caller must serialize writes
/// and snapshots, for example through <see cref="TerminalTextWriter"/>.
/// OSC/DCS data is discarded and never interpreted as a file, URL or clipboard command.
/// </summary>
public sealed class TerminalBuffer
{
    private enum ParserState { Text, Escape, Csi, CsiDiscard, Osc, OscEscape, String, StringEscape }

    private const int MaximumCells = 4_194_304;
    private const int MaximumCsiLength = 128;
    private readonly StringBuilder csi = new(MaximumCsiLength);
    private readonly Queue<TerminalCell[]> scrollback = new();
    private readonly int scrollbackLimit;
    private TerminalCell[] cells;
    private ParserState state;
    private char highSurrogate;
    private int savedColumn;
    private int savedRow;
    private int scrollTop;
    private int scrollBottom;
    private bool wrapPending;
    private bool autoWrap = true;
    private PrimaryScreen? primaryScreen;

    public TerminalBuffer(int columns, int rows, TerminalStyle? defaultStyle = null, int scrollbackLimit = 1_000)
    {
        ArgumentOutOfRangeException.ThrowIfLessThan(columns, 1);
        ArgumentOutOfRangeException.ThrowIfLessThan(rows, 1);
        if ((long)columns * rows > MaximumCells)
            throw new ArgumentOutOfRangeException(nameof(columns), "The terminal exceeds its cell limit.");
        ArgumentOutOfRangeException.ThrowIfNegative(scrollbackLimit);
        ArgumentOutOfRangeException.ThrowIfGreaterThan(scrollbackLimit, 100_000);
        Columns = columns;
        Rows = rows;
        DefaultStyle = defaultStyle ?? TerminalStyle.Default;
        CurrentStyle = DefaultStyle;
        this.scrollbackLimit = scrollbackLimit;
        scrollBottom = rows - 1;
        cells = new TerminalCell[columns * rows];
        Array.Fill(cells, TerminalCell.Blank(DefaultStyle));
    }

    public int Columns { get; }
    public int Rows { get; }
    public int CursorColumn { get; private set; }
    public int CursorRow { get; private set; }
    public bool CursorVisible { get; private set; } = true;
    public TerminalStyle DefaultStyle { get; }
    public TerminalStyle CurrentStyle { get; private set; }
    public int ScrollbackCount => scrollback.Count;
    public bool IsAlternateScreen => primaryScreen is not null;

    public TerminalCell GetCell(int column, int row)
    {
        CheckPosition(column, row);
        return cells[row * Columns + column];
    }

    public TerminalSnapshot Snapshot() => new(Columns, Rows, CursorColumn, CursorRow,
        CursorVisible, (TerminalCell[])cells.Clone());

    public string GetLine(int row, bool trimEnd = true)
    {
        CheckPosition(0, row);
        var text = new StringBuilder();
        for (var column = 0; column < Columns; column++)
        {
            var cell = cells[row * Columns + column];
            if (!cell.IsContinuation)
                text.Append(cell.Text);
        }
        return trimEnd ? text.ToString().TrimEnd(' ') : text.ToString();
    }

    public string GetText() => string.Join('\n', Enumerable.Range(0, Rows).Select(row => GetLine(row)));

    public void Write(string? value)
    {
        if (value is not null)
            Write(value.AsSpan());
    }

    public void Write(ReadOnlySpan<char> value)
    {
        foreach (var character in value)
            Consume(character);
    }

    /// <summary>Complete a stream: replace an incomplete surrogate and discard an unfinished escape.</summary>
    public void Complete()
    {
        if (highSurrogate != '\0')
            PutRune(Rune.ReplacementChar);
        highSurrogate = '\0';
        state = ParserState.Text;
        csi.Clear();
    }

    public void Reset()
    {
        primaryScreen = null;
        CurrentStyle = DefaultStyle;
        Array.Fill(cells, TerminalCell.Blank(DefaultStyle));
        scrollback.Clear();
        CursorColumn = CursorRow = savedColumn = savedRow = scrollTop = 0;
        scrollBottom = Rows - 1;
        CursorVisible = autoWrap = true;
        wrapPending = false;
        highSurrogate = '\0';
        csi.Clear();
        state = ParserState.Text;
    }

    private void Consume(char character)
    {
        switch (state)
        {
            case ParserState.Osc:
                if (character == '\a') state = ParserState.Text;
                else if (character == '\u001b') state = ParserState.OscEscape;
                return;
            case ParserState.OscEscape:
                state = character == '\\' || character == '\a' ? ParserState.Text :
                    character == '\u001b' ? ParserState.OscEscape : ParserState.Osc;
                return;
            case ParserState.String:
                if (character == '\u001b') state = ParserState.StringEscape;
                return;
            case ParserState.StringEscape:
                state = character == '\\' ? ParserState.Text :
                    character == '\u001b' ? ParserState.StringEscape : ParserState.String;
                return;
            case ParserState.Csi:
            case ParserState.CsiDiscard:
                if (character is >= '@' and <= '~')
                {
                    if (state == ParserState.Csi)
                        ExecuteCsi(character, csi.ToString());
                    csi.Clear();
                    state = ParserState.Text;
                }
                else if (character == '\u001b')
                {
                    csi.Clear();
                    state = ParserState.Escape;
                }
                else if (character is '\u0018' or '\u001a')
                {
                    csi.Clear();
                    state = ParserState.Text;
                }
                else if (state == ParserState.Csi && character is >= ' ' and <= '?')
                {
                    if (csi.Length < MaximumCsiLength) csi.Append(character);
                    else state = ParserState.CsiDiscard;
                }
                return;
            case ParserState.Escape:
                state = ParserState.Text;
                switch (character)
                {
                    case '[': state = ParserState.Csi; csi.Clear(); break;
                    case ']': state = ParserState.Osc; break;
                    case 'P': case '^': case '_': state = ParserState.String; break;
                    case '7': SaveCursor(); break;
                    case '8': RestoreCursor(); break;
                    case 'D': LineFeed(); break;
                    case 'E': CursorColumn = 0; LineFeed(); break;
                    case 'M': ReverseIndex(); break;
                    case 'c': Reset(); break;
                    case '\u001b': state = ParserState.Escape; break;
                }
                return;
        }

        if (character == '\u001b')
        {
            FlushSurrogate();
            state = ParserState.Escape;
            return;
        }

        if (character < ' ' || character == '\u007f')
        {
            FlushSurrogate();
            switch (character)
            {
                case '\r': CursorColumn = 0; wrapPending = false; break;
                case '\n': case '\v': case '\f': LineFeed(); break;
                case '\b': CursorColumn = Math.Max(0, CursorColumn - 1); wrapPending = false; break;
                case '\t': CursorColumn = Math.Min(Columns - 1, (CursorColumn / 8 + 1) * 8); wrapPending = false; break;
            }
            return;
        }

        if (highSurrogate != '\0')
        {
            if (char.IsLowSurrogate(character))
            {
                var rune = new Rune(highSurrogate, character);
                highSurrogate = '\0';
                PutRune(rune);
                return;
            }
            FlushSurrogate();
        }

        if (char.IsHighSurrogate(character)) highSurrogate = character;
        else PutRune(char.IsLowSurrogate(character) ? Rune.ReplacementChar : new Rune(character));
    }

    private void FlushSurrogate()
    {
        if (highSurrogate == '\0') return;
        highSurrogate = '\0';
        PutRune(Rune.ReplacementChar);
    }

    private void PutRune(Rune rune)
    {
        var category = Rune.GetUnicodeCategory(rune);
        if (category is UnicodeCategory.NonSpacingMark or UnicodeCategory.EnclosingMark or UnicodeCategory.Format)
        {
            var previousColumn = wrapPending ? CursorColumn : CursorColumn - 1;
            if (previousColumn >= 0)
            {
                var index = CursorRow * Columns + previousColumn;
                if (cells[index].IsContinuation && previousColumn > 0) index--;
                cells[index] = cells[index] with { Text = cells[index].Text + rune };
            }
            return;
        }

        var width = IsWide(rune.Value) ? 2 : 1;
        if (Columns == 1 && width == 2)
        {
            width = 1;
            rune = Rune.ReplacementChar;
        }
        if (wrapPending || CursorColumn + width > Columns)
        {
            if (autoWrap)
            {
                CursorColumn = 0;
                LineFeed();
            }
            else if (width == 2 && CursorColumn == Columns - 1)
            {
                return;
            }
            wrapPending = false;
        }

        ClearGlyphAt(CursorColumn, CursorRow);
        if (width == 2) ClearGlyphAt(CursorColumn + 1, CursorRow);
        var offset = CursorRow * Columns + CursorColumn;
        cells[offset] = new(rune.ToString(), CurrentStyle, width);
        if (width == 2) cells[offset + 1] = new(string.Empty, CurrentStyle, 0);
        if (CursorColumn + width >= Columns)
        {
            CursorColumn = Columns - 1;
            wrapPending = autoWrap;
        }
        else CursorColumn += width;
    }

    private static bool IsWide(int value) => value is
        >= 0x1100 and <= 0x115f or >= 0x2329 and <= 0x232a or
        >= 0x2e80 and <= 0xa4cf or >= 0xac00 and <= 0xd7a3 or
        >= 0xf900 and <= 0xfaff or >= 0xfe10 and <= 0xfe19 or
        >= 0xfe30 and <= 0xfe6f or >= 0xff00 and <= 0xff60 or
        >= 0xffe0 and <= 0xffe6 or >= 0x1f300 and <= 0x1faff or
        >= 0x20000 and <= 0x3fffd;

    private void ExecuteCsi(char final, string text)
    {
        var privateMode = text.StartsWith('?');
        if (privateMode) text = text[1..];
        var parts = text.Split(';');
        if (parts.Length > 32) return;
        var parameters = new int[parts.Length];
        for (var index = 0; index < parts.Length; index++)
        {
            if (parts[index].Length == 0) continue;
            if (!int.TryParse(parts[index], NumberStyles.None, CultureInfo.InvariantCulture, out parameters[index]))
                return;
        }

        int Parameter(int index, int fallback = 1) => index >= parameters.Length || parameters[index] == 0
            ? fallback : parameters[index];

        if (privateMode)
        {
            if (final is not ('h' or 'l')) return;
            foreach (var parameter in parameters)
            {
                switch (parameter)
                {
                    case 7: autoWrap = final == 'h'; wrapPending = false; break;
                    case 25: CursorVisible = final == 'h'; break;
                    case 1049: SetAlternateScreen(final == 'h'); break;
                }
            }
            return;
        }

        var count = Parameter(0);
        switch (final)
        {
            case 'm': SetGraphics(parameters); return;
            case 'A': CursorRow = Clamp((long)CursorRow - count, Rows); break;
            case 'B': case 'e': CursorRow = Clamp((long)CursorRow + count, Rows); break;
            case 'C': case 'a': CursorColumn = Clamp((long)CursorColumn + count, Columns); break;
            case 'D': CursorColumn = Clamp((long)CursorColumn - count, Columns); break;
            case 'E': CursorRow = Clamp((long)CursorRow + count, Rows); CursorColumn = 0; break;
            case 'F': CursorRow = Clamp((long)CursorRow - count, Rows); CursorColumn = 0; break;
            case 'G': case '`': CursorColumn = Clamp((long)count - 1, Columns); break;
            case 'd': CursorRow = Clamp((long)count - 1, Rows); break;
            case 'H': case 'f': CursorRow = Clamp((long)count - 1, Rows); CursorColumn = Clamp((long)Parameter(1) - 1, Columns); break;
            case 'J': EraseDisplay(parameters[0]); break;
            case 'K': EraseLine(parameters[0]); break;
            case 's': SaveCursor(); break;
            case 'u': RestoreCursor(); break;
            case 'S': ScrollUp(scrollTop, scrollBottom, count); break;
            case 'T': ScrollDown(scrollTop, scrollBottom, count); break;
            case 'L': if (CursorRow >= scrollTop && CursorRow <= scrollBottom) ScrollDown(CursorRow, scrollBottom, count); break;
            case 'M': if (CursorRow >= scrollTop && CursorRow <= scrollBottom) ScrollUp(CursorRow, scrollBottom, count); break;
            case '@': InsertCharacters(count); break;
            case 'P': DeleteCharacters(count); break;
            case 'X': EraseRange(CursorRow * Columns + CursorColumn, Math.Min(count, Columns - CursorColumn)); break;
            case 'r':
                var top = Parameter(0) - 1;
                var bottom = Parameter(1, Rows) - 1;
                if (top >= 0 && bottom < Rows && top < bottom)
                {
                    scrollTop = top;
                    scrollBottom = bottom;
                    CursorRow = CursorColumn = 0;
                }
                break;
        }
        wrapPending = false;
    }

    private void SetGraphics(int[] parameters)
    {
        for (var index = 0; index < parameters.Length; index++)
        {
            var value = parameters[index];
            switch (value)
            {
                case 0: CurrentStyle = DefaultStyle; break;
                case 1: CurrentStyle = CurrentStyle with { Bold = true }; break;
                case 2: CurrentStyle = CurrentStyle with { Dim = true }; break;
                case 3: CurrentStyle = CurrentStyle with { Italic = true }; break;
                case 4: CurrentStyle = CurrentStyle with { Underline = true }; break;
                case 7: CurrentStyle = CurrentStyle with { Inverse = true }; break;
                case 8: CurrentStyle = CurrentStyle with { Hidden = true }; break;
                case 9: CurrentStyle = CurrentStyle with { Strike = true }; break;
                case 22: CurrentStyle = CurrentStyle with { Bold = false, Dim = false }; break;
                case 23: CurrentStyle = CurrentStyle with { Italic = false }; break;
                case 24: CurrentStyle = CurrentStyle with { Underline = false }; break;
                case 27: CurrentStyle = CurrentStyle with { Inverse = false }; break;
                case 28: CurrentStyle = CurrentStyle with { Hidden = false }; break;
                case 29: CurrentStyle = CurrentStyle with { Strike = false }; break;
                case >= 30 and <= 37: SetColor(TerminalColor.FromIndexed((byte)(value - 30)), true); break;
                case >= 40 and <= 47: SetColor(TerminalColor.FromIndexed((byte)(value - 40)), false); break;
                case >= 90 and <= 97: SetColor(TerminalColor.FromIndexed((byte)(value - 90 + 8)), true); break;
                case >= 100 and <= 107: SetColor(TerminalColor.FromIndexed((byte)(value - 100 + 8)), false); break;
                case 39: SetColor(DefaultStyle.Foreground, true); break;
                case 49: SetColor(DefaultStyle.Background, false); break;
                case 38: case 48:
                    if (index + 1 >= parameters.Length) return;
                    var mode = parameters[++index];
                    if (mode == 5)
                    {
                        if (index + 1 >= parameters.Length) return;
                        var paletteIndex = parameters[++index];
                        if (paletteIndex <= 255) SetColor(TerminalColor.FromIndexed((byte)paletteIndex), value == 38);
                    }
                    else if (mode == 2)
                    {
                        if (index + 3 >= parameters.Length) return;
                        var red = parameters[++index];
                        var green = parameters[++index];
                        var blue = parameters[++index];
                        if (red <= 255 && green <= 255 && blue <= 255)
                            SetColor(new((byte)red, (byte)green, (byte)blue), value == 38);
                    }
                    else return;
                    break;
            }
        }
    }

    private void SetColor(TerminalColor color, bool foreground) => CurrentStyle = foreground
        ? CurrentStyle with { Foreground = color } : CurrentStyle with { Background = color };

    private void LineFeed()
    {
        wrapPending = false;
        if (CursorRow == scrollBottom) ScrollUp(scrollTop, scrollBottom, 1);
        else CursorRow = Math.Min(Rows - 1, CursorRow + 1);
    }

    private void ReverseIndex()
    {
        wrapPending = false;
        if (CursorRow == scrollTop) ScrollDown(scrollTop, scrollBottom, 1);
        else CursorRow = Math.Max(0, CursorRow - 1);
    }

    private void ScrollUp(int top, int bottom, int requested)
    {
        var count = Math.Min(requested, bottom - top + 1);
        if (top == 0 && bottom == Rows - 1 && primaryScreen is null && scrollbackLimit > 0)
        {
            for (var row = 0; row < count; row++)
            {
                var line = new TerminalCell[Columns];
                Array.Copy(cells, row * Columns, line, 0, Columns);
                scrollback.Enqueue(line);
                while (scrollback.Count > scrollbackLimit) scrollback.Dequeue();
            }
        }
        Array.Copy(cells, (top + count) * Columns, cells, top * Columns, (bottom - top + 1 - count) * Columns);
        Array.Fill(cells, TerminalCell.Blank(CurrentStyle), (bottom - count + 1) * Columns, count * Columns);
    }

    private void ScrollDown(int top, int bottom, int requested)
    {
        var count = Math.Min(requested, bottom - top + 1);
        Array.Copy(cells, top * Columns, cells, (top + count) * Columns, (bottom - top + 1 - count) * Columns);
        Array.Fill(cells, TerminalCell.Blank(CurrentStyle), top * Columns, count * Columns);
    }

    private void EraseDisplay(int mode)
    {
        var position = CursorRow * Columns + CursorColumn;
        switch (mode)
        {
            case 0: EraseRange(position, cells.Length - position); break;
            case 1: EraseRange(0, position + 1); break;
            case 2: Array.Fill(cells, TerminalCell.Blank(CurrentStyle)); break;
            case 3: Array.Fill(cells, TerminalCell.Blank(CurrentStyle)); scrollback.Clear(); break;
        }
    }

    private void EraseLine(int mode)
    {
        var start = CursorRow * Columns;
        switch (mode)
        {
            case 0: EraseRange(start + CursorColumn, Columns - CursorColumn); break;
            case 1: EraseRange(start, CursorColumn + 1); break;
            case 2: EraseRange(start, Columns); break;
        }
    }

    private void EraseRange(int start, int count)
    {
        if (count <= 0) return;
        ClearGlyphAt(start % Columns, start / Columns);
        var end = start + count - 1;
        ClearGlyphAt(end % Columns, end / Columns);
        Array.Fill(cells, TerminalCell.Blank(CurrentStyle), start, count);
    }

    private void ClearGlyphAt(int column, int row)
    {
        var index = row * Columns + column;
        if (cells[index].IsContinuation && column > 0)
            cells[index - 1] = TerminalCell.Blank(CurrentStyle);
        else if (cells[index].DisplayWidth == 2 && column + 1 < Columns)
            cells[index + 1] = TerminalCell.Blank(CurrentStyle);
        cells[index] = TerminalCell.Blank(CurrentStyle);
    }

    private void InsertCharacters(int requested)
    {
        var count = Math.Min(requested, Columns - CursorColumn);
        var start = CursorRow * Columns + CursorColumn;
        Array.Copy(cells, start, cells, start + count, Columns - CursorColumn - count);
        Array.Fill(cells, TerminalCell.Blank(CurrentStyle), start, count);
        NormalizeWideRow(CursorRow);
    }

    private void DeleteCharacters(int requested)
    {
        var count = Math.Min(requested, Columns - CursorColumn);
        var start = CursorRow * Columns + CursorColumn;
        Array.Copy(cells, start + count, cells, start, Columns - CursorColumn - count);
        Array.Fill(cells, TerminalCell.Blank(CurrentStyle), (CursorRow + 1) * Columns - count, count);
        NormalizeWideRow(CursorRow);
    }

    private void NormalizeWideRow(int row)
    {
        for (var column = 0; column < Columns; column++)
        {
            var index = row * Columns + column;
            if (cells[index].IsContinuation && (column == 0 || cells[index - 1].DisplayWidth != 2) ||
                cells[index].DisplayWidth == 2 && (column + 1 == Columns || !cells[index + 1].IsContinuation))
                cells[index] = TerminalCell.Blank(CurrentStyle);
        }
    }

    private void SaveCursor() { savedColumn = CursorColumn; savedRow = CursorRow; }
    private void RestoreCursor() { CursorColumn = savedColumn; CursorRow = savedRow; wrapPending = false; }

    private void SetAlternateScreen(bool enabled)
    {
        if (enabled && primaryScreen is null)
        {
            primaryScreen = new(cells, CursorColumn, CursorRow, savedColumn, savedRow,
                scrollTop, scrollBottom, wrapPending, CurrentStyle);
            cells = new TerminalCell[Columns * Rows];
            Array.Fill(cells, TerminalCell.Blank(DefaultStyle));
            CursorColumn = CursorRow = savedColumn = savedRow = scrollTop = 0;
            scrollBottom = Rows - 1;
            wrapPending = false;
            CurrentStyle = DefaultStyle;
        }
        else if (!enabled && primaryScreen is not null)
        {
            var saved = primaryScreen;
            primaryScreen = null;
            cells = saved.Cells;
            CursorColumn = saved.Column;
            CursorRow = saved.Row;
            savedColumn = saved.SavedColumn;
            savedRow = saved.SavedRow;
            scrollTop = saved.ScrollTop;
            scrollBottom = saved.ScrollBottom;
            wrapPending = saved.WrapPending;
            CurrentStyle = saved.Style;
        }
    }

    private void CheckPosition(int column, int row)
    {
        ArgumentOutOfRangeException.ThrowIfNegative(column);
        ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(column, Columns);
        ArgumentOutOfRangeException.ThrowIfNegative(row);
        ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(row, Rows);
    }

    private static int Clamp(long value, int dimension) => (int)Math.Clamp(value, 0, dimension - 1);

    private sealed record PrimaryScreen(TerminalCell[] Cells, int Column, int Row, int SavedColumn,
        int SavedRow, int ScrollTop, int ScrollBottom, bool WrapPending, TerminalStyle Style);
}