diff --git a/KubeUI.sln b/KubeUI.sln index 73d2917e..957a1ad0 100644 --- a/KubeUI.sln +++ b/KubeUI.sln @@ -23,6 +23,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KubeUI.Tests", "tests\KubeU EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KubeUI.Core", "src\KubeUI.Core\KubeUI.Core.csproj", "{57504622-EB10-4E5C-9436-B2DAA35F4335}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XtermSharp", "src\XtermSharp\XtermSharp.csproj", "{51CC524C-8557-D5ED-7ABB-7CC8FEC3E242}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -45,6 +47,10 @@ Global {57504622-EB10-4E5C-9436-B2DAA35F4335}.Debug|Any CPU.Build.0 = Debug|Any CPU {57504622-EB10-4E5C-9436-B2DAA35F4335}.Release|Any CPU.ActiveCfg = Release|Any CPU {57504622-EB10-4E5C-9436-B2DAA35F4335}.Release|Any CPU.Build.0 = Release|Any CPU + {51CC524C-8557-D5ED-7ABB-7CC8FEC3E242}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {51CC524C-8557-D5ED-7ABB-7CC8FEC3E242}.Debug|Any CPU.Build.0 = Debug|Any CPU + {51CC524C-8557-D5ED-7ABB-7CC8FEC3E242}.Release|Any CPU.ActiveCfg = Release|Any CPU + {51CC524C-8557-D5ED-7ABB-7CC8FEC3E242}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/KubeUI.Desktop/Program.cs b/src/KubeUI.Desktop/Program.cs index 21bc0b0d..3ba2be25 100644 --- a/src/KubeUI.Desktop/Program.cs +++ b/src/KubeUI.Desktop/Program.cs @@ -1,5 +1,6 @@ using System; using Avalonia; +using Avalonia.Fonts.Inter; using Velopack; namespace KubeUI.Desktop; @@ -21,5 +22,9 @@ public static void Main(string[] args) public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UsePlatformDetect() + .ConfigureFonts(fontManager => + { + fontManager.AddFontCollection(new CascadiaMonoFontCollection()); + }) .WithInterFont(); } diff --git a/src/KubeUI/Assets/Fonts/CascadiaMono-Regular.otf b/src/KubeUI/Assets/Fonts/CascadiaMono-Regular.otf new file mode 100644 index 00000000..e99c3040 Binary files /dev/null and b/src/KubeUI/Assets/Fonts/CascadiaMono-Regular.otf differ diff --git a/src/KubeUI/Assets/Fonts/CascadiaMono-SemiBold.otf b/src/KubeUI/Assets/Fonts/CascadiaMono-SemiBold.otf new file mode 100644 index 00000000..47267c90 Binary files /dev/null and b/src/KubeUI/Assets/Fonts/CascadiaMono-SemiBold.otf differ diff --git a/src/KubeUI/Assets/Fonts/CascadiaMono-SemiBoldItalic.otf b/src/KubeUI/Assets/Fonts/CascadiaMono-SemiBoldItalic.otf new file mode 100644 index 00000000..031067ae Binary files /dev/null and b/src/KubeUI/Assets/Fonts/CascadiaMono-SemiBoldItalic.otf differ diff --git a/src/KubeUI/Assets/Fonts/CascadiaMono-SemiLight.otf b/src/KubeUI/Assets/Fonts/CascadiaMono-SemiLight.otf new file mode 100644 index 00000000..2ac6b919 Binary files /dev/null and b/src/KubeUI/Assets/Fonts/CascadiaMono-SemiLight.otf differ diff --git a/src/KubeUI/Assets/Fonts/CascadiaMono-SemiLightItalic.otf b/src/KubeUI/Assets/Fonts/CascadiaMono-SemiLightItalic.otf new file mode 100644 index 00000000..d08210db Binary files /dev/null and b/src/KubeUI/Assets/Fonts/CascadiaMono-SemiLightItalic.otf differ diff --git a/src/KubeUI/CascadiaMonoFontCollection.cs b/src/KubeUI/CascadiaMonoFontCollection.cs new file mode 100644 index 00000000..a325e241 --- /dev/null +++ b/src/KubeUI/CascadiaMonoFontCollection.cs @@ -0,0 +1,10 @@ +using Avalonia.Media.Fonts; + +public sealed class CascadiaMonoFontCollection : EmbeddedFontCollection +{ + public CascadiaMonoFontCollection() : base( + new Uri("fonts:Cascadia Mono", UriKind.Absolute), + new Uri("avares://KubeUI/Assets", UriKind.Absolute)) + { + } +} diff --git a/src/KubeUI/KubeUI.csproj b/src/KubeUI/KubeUI.csproj index 2a37a354..0ef49c5e 100644 --- a/src/KubeUI/KubeUI.csproj +++ b/src/KubeUI/KubeUI.csproj @@ -70,6 +70,7 @@ + diff --git a/src/KubeUI/ViewModels/ResourceListViewModel.cs b/src/KubeUI/ViewModels/ResourceListViewModel.cs index 21c7fd4b..115fda5c 100644 --- a/src/KubeUI/ViewModels/ResourceListViewModel.cs +++ b/src/KubeUI/ViewModels/ResourceListViewModel.cs @@ -1721,7 +1721,7 @@ private bool CanViewLogs(V1Container? container) } [RelayCommand(CanExecute = nameof(CanViewConsole))] - private async Task ViewConsole(V1Container container) + private void ViewConsole(V1Container container) { var vm = Application.Current.GetRequiredService(); vm.Cluster = Cluster; @@ -1729,18 +1729,7 @@ private async Task ViewConsole(V1Container container) vm.ContainerName = container.Name; vm.Id = $"{nameof(ViewConsole)}-{Cluster.Name}-{((KeyValuePair)SelectedItem).Key}-{container.Name}"; - if (Factory.AddToBottom(vm)) - { - try - { - await vm.Connect(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error connecting to console"); - return; - } - } + Factory.AddToBottom(vm); } private bool CanViewConsole(V1Container? container) diff --git a/src/KubeUI/ViewModels/Workloads/Pod/PodConsoleViewModel.cs b/src/KubeUI/ViewModels/Workloads/Pod/PodConsoleViewModel.cs index 5a8d5011..d8656efe 100644 --- a/src/KubeUI/ViewModels/Workloads/Pod/PodConsoleViewModel.cs +++ b/src/KubeUI/ViewModels/Workloads/Pod/PodConsoleViewModel.cs @@ -4,7 +4,11 @@ using KubeUI.Client; using System.Net.WebSockets; using System.Text; -using System.Text.RegularExpressions; +using XtermSharp; +using AvaloniaEdit.Highlighting; +using Color = Avalonia.Media.Color; +using System.Text.Json; +using Avalonia.Media.TextFormatting; namespace KubeUI.ViewModels; @@ -30,10 +34,137 @@ public PodConsoleViewModel(ILogger logger) [ObservableProperty] public partial TextDocument Console { get; set; } = new(); + [ObservableProperty] + public partial RichTextModel ConsoleColor { get; set; } = new(); + + [ObservableProperty] + public partial Terminal Terminal { get; set; } = new(); + + [ObservableProperty] + public partial double Width { get; set; } + + [ObservableProperty] + public partial double Height { get; set; } + + [ObservableProperty] + public partial int FontSize { get; set; } = 14; + + [ObservableProperty] + public partial int BufferLength { get; set; } + + [ObservableProperty] + public partial string FontFamily { get; set; } = "Cascadia Mono"; + private WebSocket? _webSocket; private StreamDemuxer? _streamDemuxer; private Stream? _stream; - private StreamReader? _streamReader; + private Stream? _refreshStream; + + /// + /// Gets a value indicating whether or not the user can scroll the terminal contents + /// + public bool CanScroll + { + get + { + var shouldBeEnabled = !Terminal.Buffers.IsAlternateBuffer; + shouldBeEnabled = shouldBeEnabled && Terminal.Buffer.HasScrollback; + shouldBeEnabled = shouldBeEnabled && Terminal.Buffer.Lines.Length > Terminal.Rows; + return shouldBeEnabled; + } + } + + /// + /// Gets a value indicating the scroll thumbsize + /// + public float ScrollThumbsize + { + get + { + if (Terminal.Buffers.IsAlternateBuffer) + return 0; + + // the thumb size is the proportion of the visible content of the + // entire content but don't make it too small + return Math.Max((float)Terminal.Rows / (float)Terminal.Buffer.Lines.Length, 0.01f); + } + } + + /// + /// Gets a value indicating the relative position of the terminal scroller + /// + public double ScrollPosition + { + get + { + if (Terminal.Buffers.IsAlternateBuffer) + return 0; + + // strictly speaking these ought not to be outside these bounds + if (Terminal.Buffer.YDisp <= 0) + return 0; + + var maxScrollback = Terminal.Buffer.Lines.Length - Terminal.Rows; + if (Terminal.Buffer.YDisp >= maxScrollback) + return 1; + + return (double)Terminal.Buffer.YDisp / (double)maxScrollback; + } + } + + public override void OnVisibleBoundsChanged(double x, double y, double width, double height) + { + base.OnVisibleBoundsChanged(x, y, width, height); + + Width = width; + Height = height; + + const double toolHeaderHeight = 23; + + if (Width > 0 && Height > 0) + { + var size = CalculateTextSize("a", FontFamily, FontSize); + + var cols = (int)((width - 16) / size.Width); + var rows = (int)((height - toolHeaderHeight) / (size.Height * 1.17)); + + Terminal.Resize(cols, rows); + Terminal.Delegate.SizeChanged(Terminal); + SendResize(); + BufferLength = rows + Terminal.Options.Scrollback ?? 0; + } + } + + public static Size CalculateTextSize(string text, string fontName, int myFontSize) + { + var myFont = Avalonia.Media.FontFamily.Parse(fontName) ?? throw new ArgumentException($"The resource {fontName} is not a FontFamily."); + + var typeface = new Typeface(myFont); + var shaped = TextShaper.Current.ShapeText(text, new TextShaperOptions(typeface.GlyphTypeface, myFontSize)); + var run = new ShapedTextRun(shaped, new GenericTextRunProperties(typeface, myFontSize)); + return run.Size; + } + + public void SendResize() + { + var size = new TerminalSize + { + Width = (ushort)Terminal.Cols, + Height = (ushort)Terminal.Rows, + }; + + if (_refreshStream?.CanWrite == true) + { + try + { + _refreshStream?.Write(JsonSerializer.SerializeToUtf8Bytes(size)); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error Sending Resize to console"); + } + } + } public async Task Connect() { @@ -41,7 +172,7 @@ public async Task Connect() { "sh", "-c", - "clear; (bash || ash || sh || echo 'No Shell Found!')" + "clear; (bash || ash || sh || echo 'No Shell Found!')", }; _webSocket = await Cluster.Client.WebSocketNamespacedPodExecAsync(Object.Name(), Object.Namespace(), command, ContainerName); @@ -50,8 +181,7 @@ public async Task Connect() _streamDemuxer.Start(); _stream = _streamDemuxer.GetStream(ChannelIndex.StdOut, ChannelIndex.StdIn); - - _streamReader = new StreamReader(_stream); + _refreshStream = _streamDemuxer.GetStream(null, ChannelIndex.Resize); _ = Task.Run(async () => { @@ -59,27 +189,16 @@ public async Task Connect() { try { - var memory = new Memory(new char[1024]); - await _streamReader.ReadAsync(memory).ConfigureAwait(false); - var str = memory.ToString() - .Replace("\0", "", StringComparison.Ordinal) // null character - .Replace("\a", "", StringComparison.Ordinal) // bell or alert - ; - str = RemoveAnsiEscapeSequences(str); - if (!string.IsNullOrEmpty(str)) + const int bufferSize = 4096; // 4KB buffer size + byte[] buffer = new byte[bufferSize]; + if (await _stream.ReadAsync(buffer, 0, bufferSize) > 0) { - await Dispatcher.UIThread.InvokeAsync(() => - { - // backspace - if (str.Equals("\b", StringComparison.Ordinal) || str.Equals("\b \b", StringComparison.Ordinal)) - { - Console.Remove(Console.TextLength - 1, 1); - } - else - { - Console.Insert(Console.TextLength, str); - } - }, DispatcherPriority.Background); + Terminal.Feed(buffer, bufferSize); + await Dispatcher.UIThread.InvokeAsync(() => + { + UpdateTerminalText(); + UpdateTerminalColors(); + }, DispatcherPriority.Background); } } catch (IOException ex) when (ex.Message.Equals("The request was aborted.")) { break; } @@ -88,18 +207,6 @@ await Dispatcher.UIThread.InvokeAsync(() => }); } - // ANSI escape sequences pattern - [GeneratedRegex(@"\x1B\[[0-?]*[ -/]*[@-~]", RegexOptions.None, matchTimeoutMilliseconds: 1000)] - private static partial Regex AnsiEscape(); - - public static string RemoveAnsiEscapeSequences(string text) - { - if (string.IsNullOrEmpty(text)) - return text; - - return AnsiEscape().Replace(text, string.Empty); - } - public void Send(string text) { if (_stream?.CanWrite == true) @@ -118,6 +225,36 @@ public void Send(string text) } } + public void Send(byte bytes) + { + if (_stream?.CanWrite == true) + { + try + { + _stream.Write([bytes]); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error sending text to console: "); + } + } + } + + public void Send(byte[] bytes) + { + if (_stream?.CanWrite == true) + { + try + { + _stream.Write(bytes); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error sending text to console: "); + } + } + } + [RelayCommand] public async Task Paste() { @@ -146,6 +283,378 @@ public void Dispose() _webSocket?.Dispose(); _streamDemuxer?.Dispose(); _stream?.Dispose(); - _streamReader?.Dispose(); + _refreshStream?.Dispose(); + } + + private void UpdateTerminalText() + { + var result = ""; + var lineText = ""; + for (var line = Terminal.Buffer.YBase; line < Terminal.Buffer.YBase + Terminal.Rows; line++) + { + lineText = ""; + for (var cell = 0; cell < Terminal.Cols; cell++) + { + var cd = Terminal.Buffer.Lines[line][cell]; + + if (cd.Code == 0) + lineText += " "; + else + lineText += (char)cd.Rune; + } + + result += lineText; + + // All except last line + if (line < Terminal.Buffer.YBase + Terminal.Rows - 1) + { + result += '\n'; + } + } + + Console.Text = result; + } + + private void UpdateTerminalColors() + { + for (var line = Terminal.Buffer.YBase; line < Terminal.Buffer.YBase + Terminal.Rows; line++) + { + for (var cell = 0; cell < Terminal.Cols; cell++) + { + var cd = Terminal.Buffer.Lines[line][cell]; + var hc = new HighlightingColor(); + var attribute = cd.Attribute; + + // ((int)flags << 18) | (fg << 9) | bg; + int bg = attribute & 0x1ff; + int fg = (attribute >> 9) & 0x1ff; + var flags = (FLAGS)(attribute >> 18); + + if (flags.HasFlag(FLAGS.INVERSE)) + { + var tmp = bg; + bg = fg; + fg = tmp; + + if (fg == Renderer.DefaultColor) + fg = Renderer.InvertedDefaultColor; + if (bg == Renderer.DefaultColor) + bg = Renderer.InvertedDefaultColor; + } + if (flags.HasFlag(FLAGS.BOLD)) + { + hc.FontWeight = FontWeight.Bold; + } + if (flags.HasFlag(FLAGS.ITALIC)) + { + hc.FontStyle = FontStyle.Italic; + } + hc.Underline = flags.HasFlag(FLAGS.UNDERLINE); + + hc.Strikethrough = flags.HasFlag(FLAGS.CrossedOut); + + if (fg <= 255) + { + hc.Foreground = new SimpleHighlightingBrush(ConvertXtermColor(fg)); + } + else if (fg == 256) // DefaultColor + { + hc.Foreground = new SimpleHighlightingBrush(ConvertXtermColor(15)); + } + else if (fg == 257) // InvertedDefaultColor + { + hc.Foreground = new SimpleHighlightingBrush(ConvertXtermColor(0)); + } + + if (bg <= 255) + { + hc.Background = new SimpleHighlightingBrush(ConvertXtermColor(bg)); + } + else if (bg == 256) // DefaultColor + { + hc.Background = new SimpleHighlightingBrush(ConvertXtermColor(0)); + } + else if (bg == 257) // InvertedDefaultColor + { + hc.Background = new SimpleHighlightingBrush(ConvertXtermColor(15)); + } + + var append = line > 0 && line < Terminal.Buffer.YBase + Terminal.Rows ? 1 : 0; + + var offset = (line * (Terminal.Cols + append)) + cell; + + ConsoleColor.SetHighlighting(offset, 1, hc); + } + } } + + private static Color ConvertXtermColor(int xtermColor) + { + return xtermColor switch + { + 0 => Color.FromRgb(0, 0, 0), + 1 => Color.FromRgb(128, 0, 0), + 2 => Color.FromRgb(0, 128, 0), + 3 => Color.FromRgb(128, 128, 0), + 4 => Color.FromRgb(0, 0, 128), + 5 => Color.FromRgb(128, 0, 128), + 6 => Color.FromRgb(0, 128, 128), + 7 => Color.FromRgb(192, 192, 192), + 8 => Color.FromRgb(128, 128, 128), + 9 => Color.FromRgb(255, 0, 0), + 10 => Color.FromRgb(0, 255, 0), + 11 => Color.FromRgb(255, 255, 0), + 12 => Color.FromRgb(0, 0, 255), + 13 => Color.FromRgb(255, 0, 255), + 14 => Color.FromRgb(0, 255, 255), + 15 => Color.FromRgb(255, 255, 255), + 16 => Color.FromRgb(0, 0, 0), + 17 => Color.FromRgb(0, 0, 95), + 18 => Color.FromRgb(0, 0, 135), + 19 => Color.FromRgb(0, 0, 175), + 20 => Color.FromRgb(0, 0, 215), + 21 => Color.FromRgb(0, 0, 255), + 22 => Color.FromRgb(0, 95, 0), + 23 => Color.FromRgb(0, 95, 95), + 24 => Color.FromRgb(0, 95, 135), + 25 => Color.FromRgb(0, 95, 175), + 26 => Color.FromRgb(0, 95, 215), + 27 => Color.FromRgb(0, 95, 255), + 28 => Color.FromRgb(0, 135, 0), + 29 => Color.FromRgb(0, 135, 95), + 30 => Color.FromRgb(0, 135, 135), + 31 => Color.FromRgb(0, 135, 175), + 32 => Color.FromRgb(0, 135, 215), + 33 => Color.FromRgb(0, 135, 255), + 34 => Color.FromRgb(0, 175, 0), + 35 => Color.FromRgb(0, 175, 95), + 36 => Color.FromRgb(0, 175, 135), + 37 => Color.FromRgb(0, 175, 175), + 38 => Color.FromRgb(0, 175, 215), + 39 => Color.FromRgb(0, 175, 255), + 40 => Color.FromRgb(0, 215, 0), + 41 => Color.FromRgb(0, 215, 95), + 42 => Color.FromRgb(0, 215, 135), + 43 => Color.FromRgb(0, 215, 175), + 44 => Color.FromRgb(0, 215, 215), + 45 => Color.FromRgb(0, 215, 255), + 46 => Color.FromRgb(0, 255, 0), + 47 => Color.FromRgb(0, 255, 95), + 48 => Color.FromRgb(0, 255, 135), + 49 => Color.FromRgb(0, 255, 175), + 50 => Color.FromRgb(0, 255, 215), + 51 => Color.FromRgb(0, 255, 255), + 52 => Color.FromRgb(95, 0, 0), + 53 => Color.FromRgb(95, 0, 95), + 54 => Color.FromRgb(95, 0, 135), + 55 => Color.FromRgb(95, 0, 175), + 56 => Color.FromRgb(95, 0, 215), + 57 => Color.FromRgb(95, 0, 255), + 58 => Color.FromRgb(95, 95, 0), + 59 => Color.FromRgb(95, 95, 95), + 60 => Color.FromRgb(95, 95, 135), + 61 => Color.FromRgb(95, 95, 175), + 62 => Color.FromRgb(95, 95, 215), + 63 => Color.FromRgb(95, 95, 255), + 64 => Color.FromRgb(95, 135, 0), + 65 => Color.FromRgb(95, 135, 95), + 66 => Color.FromRgb(95, 135, 135), + 67 => Color.FromRgb(95, 135, 175), + 68 => Color.FromRgb(95, 135, 215), + 69 => Color.FromRgb(95, 135, 255), + 70 => Color.FromRgb(95, 175, 0), + 71 => Color.FromRgb(95, 175, 95), + 72 => Color.FromRgb(95, 175, 135), + 73 => Color.FromRgb(95, 175, 175), + 74 => Color.FromRgb(95, 175, 215), + 75 => Color.FromRgb(95, 175, 255), + 76 => Color.FromRgb(95, 215, 0), + 77 => Color.FromRgb(95, 215, 95), + 78 => Color.FromRgb(95, 215, 135), + 79 => Color.FromRgb(95, 215, 175), + 80 => Color.FromRgb(95, 215, 215), + 81 => Color.FromRgb(95, 215, 255), + 82 => Color.FromRgb(95, 255, 0), + 83 => Color.FromRgb(95, 255, 95), + 84 => Color.FromRgb(95, 255, 135), + 85 => Color.FromRgb(95, 255, 175), + 86 => Color.FromRgb(95, 255, 215), + 87 => Color.FromRgb(95, 255, 255), + 88 => Color.FromRgb(135, 0, 0), + 89 => Color.FromRgb(135, 0, 95), + 90 => Color.FromRgb(135, 0, 135), + 91 => Color.FromRgb(135, 0, 175), + 92 => Color.FromRgb(135, 0, 215), + 93 => Color.FromRgb(135, 0, 255), + 94 => Color.FromRgb(135, 95, 0), + 95 => Color.FromRgb(135, 95, 95), + 96 => Color.FromRgb(135, 95, 135), + 97 => Color.FromRgb(135, 95, 175), + 98 => Color.FromRgb(135, 95, 215), + 99 => Color.FromRgb(135, 95, 255), + 100 => Color.FromRgb(135, 135, 0), + 101 => Color.FromRgb(135, 135, 95), + 102 => Color.FromRgb(135, 135, 135), + 103 => Color.FromRgb(135, 135, 175), + 104 => Color.FromRgb(135, 135, 215), + 105 => Color.FromRgb(135, 135, 255), + 106 => Color.FromRgb(135, 175, 0), + 107 => Color.FromRgb(135, 175, 95), + 108 => Color.FromRgb(135, 175, 135), + 109 => Color.FromRgb(135, 175, 175), + 110 => Color.FromRgb(135, 175, 215), + 111 => Color.FromRgb(135, 175, 255), + 112 => Color.FromRgb(135, 215, 0), + 113 => Color.FromRgb(135, 215, 95), + 114 => Color.FromRgb(135, 215, 135), + 115 => Color.FromRgb(135, 215, 175), + 116 => Color.FromRgb(135, 215, 215), + 117 => Color.FromRgb(135, 215, 255), + 118 => Color.FromRgb(135, 255, 0), + 119 => Color.FromRgb(135, 255, 95), + 120 => Color.FromRgb(135, 255, 135), + 121 => Color.FromRgb(135, 255, 175), + 122 => Color.FromRgb(135, 255, 215), + 123 => Color.FromRgb(135, 255, 255), + 124 => Color.FromRgb(175, 0, 0), + 125 => Color.FromRgb(175, 0, 95), + 126 => Color.FromRgb(175, 0, 135), + 127 => Color.FromRgb(175, 0, 175), + 128 => Color.FromRgb(175, 0, 215), + 129 => Color.FromRgb(175, 0, 255), + 130 => Color.FromRgb(175, 95, 0), + 131 => Color.FromRgb(175, 95, 95), + 132 => Color.FromRgb(175, 95, 135), + 133 => Color.FromRgb(175, 95, 175), + 134 => Color.FromRgb(175, 95, 215), + 135 => Color.FromRgb(175, 95, 255), + 136 => Color.FromRgb(175, 135, 0), + 137 => Color.FromRgb(175, 135, 95), + 138 => Color.FromRgb(175, 135, 135), + 139 => Color.FromRgb(175, 135, 175), + 140 => Color.FromRgb(175, 135, 215), + 141 => Color.FromRgb(175, 135, 255), + 142 => Color.FromRgb(175, 175, 0), + 143 => Color.FromRgb(175, 175, 95), + 144 => Color.FromRgb(175, 175, 135), + 145 => Color.FromRgb(175, 175, 175), + 146 => Color.FromRgb(175, 175, 215), + 147 => Color.FromRgb(175, 175, 255), + 148 => Color.FromRgb(175, 215, 0), + 149 => Color.FromRgb(175, 215, 95), + 150 => Color.FromRgb(175, 215, 135), + 151 => Color.FromRgb(175, 215, 175), + 152 => Color.FromRgb(175, 215, 215), + 153 => Color.FromRgb(175, 215, 255), + 154 => Color.FromRgb(175, 255, 0), + 155 => Color.FromRgb(175, 255, 95), + 156 => Color.FromRgb(175, 255, 135), + 157 => Color.FromRgb(175, 255, 175), + 158 => Color.FromRgb(175, 255, 215), + 159 => Color.FromRgb(175, 255, 255), + 160 => Color.FromRgb(215, 0, 0), + 161 => Color.FromRgb(215, 0, 95), + 162 => Color.FromRgb(215, 0, 135), + 163 => Color.FromRgb(215, 0, 175), + 164 => Color.FromRgb(215, 0, 215), + 165 => Color.FromRgb(215, 0, 255), + 166 => Color.FromRgb(215, 95, 0), + 167 => Color.FromRgb(215, 95, 95), + 168 => Color.FromRgb(215, 95, 135), + 169 => Color.FromRgb(215, 95, 175), + 170 => Color.FromRgb(215, 95, 215), + 171 => Color.FromRgb(215, 95, 255), + 172 => Color.FromRgb(215, 135, 0), + 173 => Color.FromRgb(215, 135, 95), + 174 => Color.FromRgb(215, 135, 135), + 175 => Color.FromRgb(215, 135, 175), + 176 => Color.FromRgb(215, 135, 215), + 177 => Color.FromRgb(215, 135, 255), + 178 => Color.FromRgb(215, 175, 0), + 179 => Color.FromRgb(215, 175, 95), + 180 => Color.FromRgb(215, 175, 135), + 181 => Color.FromRgb(215, 175, 175), + 182 => Color.FromRgb(215, 175, 215), + 183 => Color.FromRgb(215, 175, 255), + 184 => Color.FromRgb(215, 215, 0), + 185 => Color.FromRgb(215, 215, 95), + 186 => Color.FromRgb(215, 215, 135), + 187 => Color.FromRgb(215, 215, 175), + 188 => Color.FromRgb(215, 215, 215), + 189 => Color.FromRgb(215, 215, 255), + 190 => Color.FromRgb(215, 255, 0), + 191 => Color.FromRgb(215, 255, 95), + 192 => Color.FromRgb(215, 255, 135), + 193 => Color.FromRgb(215, 255, 175), + 194 => Color.FromRgb(215, 255, 215), + 195 => Color.FromRgb(215, 255, 255), + 196 => Color.FromRgb(255, 0, 0), + 197 => Color.FromRgb(255, 0, 95), + 198 => Color.FromRgb(255, 0, 135), + 199 => Color.FromRgb(255, 0, 175), + 200 => Color.FromRgb(255, 0, 215), + 201 => Color.FromRgb(255, 0, 255), + 202 => Color.FromRgb(255, 95, 0), + 203 => Color.FromRgb(255, 95, 95), + 204 => Color.FromRgb(255, 95, 135), + 205 => Color.FromRgb(255, 95, 175), + 206 => Color.FromRgb(255, 95, 215), + 207 => Color.FromRgb(255, 95, 255), + 208 => Color.FromRgb(255, 135, 0), + 209 => Color.FromRgb(255, 135, 95), + 210 => Color.FromRgb(255, 135, 135), + 211 => Color.FromRgb(255, 135, 175), + 212 => Color.FromRgb(255, 135, 215), + 213 => Color.FromRgb(255, 135, 255), + 214 => Color.FromRgb(255, 175, 0), + 215 => Color.FromRgb(255, 175, 95), + 216 => Color.FromRgb(255, 175, 135), + 217 => Color.FromRgb(255, 175, 175), + 218 => Color.FromRgb(255, 175, 215), + 219 => Color.FromRgb(255, 175, 255), + 220 => Color.FromRgb(255, 215, 0), + 221 => Color.FromRgb(255, 215, 95), + 222 => Color.FromRgb(255, 215, 135), + 223 => Color.FromRgb(255, 215, 175), + 224 => Color.FromRgb(255, 215, 215), + 225 => Color.FromRgb(255, 215, 255), + 226 => Color.FromRgb(255, 255, 0), + 227 => Color.FromRgb(255, 255, 95), + 228 => Color.FromRgb(255, 255, 135), + 229 => Color.FromRgb(255, 255, 175), + 230 => Color.FromRgb(255, 255, 215), + 231 => Color.FromRgb(255, 255, 255), + 232 => Color.FromRgb(8, 8, 8), + 233 => Color.FromRgb(18, 18, 18), + 234 => Color.FromRgb(28, 28, 28), + 235 => Color.FromRgb(38, 38, 38), + 236 => Color.FromRgb(48, 48, 48), + 237 => Color.FromRgb(58, 58, 58), + 238 => Color.FromRgb(68, 68, 68), + 239 => Color.FromRgb(78, 78, 78), + 240 => Color.FromRgb(88, 88, 88), + 241 => Color.FromRgb(98, 98, 98), + 242 => Color.FromRgb(108, 108, 108), + 243 => Color.FromRgb(118, 118, 118), + 244 => Color.FromRgb(128, 128, 128), + 245 => Color.FromRgb(138, 138, 138), + 246 => Color.FromRgb(148, 148, 148), + 247 => Color.FromRgb(158, 158, 158), + 248 => Color.FromRgb(168, 168, 168), + 249 => Color.FromRgb(178, 178, 178), + 250 => Color.FromRgb(188, 188, 188), + 251 => Color.FromRgb(198, 198, 198), + 252 => Color.FromRgb(208, 208, 208), + 253 => Color.FromRgb(218, 218, 218), + 254 => Color.FromRgb(228, 228, 228), + 255 => Color.FromRgb(238, 238, 238), + _ => throw new ArgumentOutOfRangeException(nameof(xtermColor), "Color code must be between 0 and 255."), + }; + } +} + +public struct TerminalSize +{ + public ushort Width { get; set; } + public ushort Height { get; set; } } diff --git a/src/KubeUI/ViewModels/Workloads/Pod/PodLogsViewModel.cs b/src/KubeUI/ViewModels/Workloads/Pod/PodLogsViewModel.cs index 032f14a7..0fff9108 100644 --- a/src/KubeUI/ViewModels/Workloads/Pod/PodLogsViewModel.cs +++ b/src/KubeUI/ViewModels/Workloads/Pod/PodLogsViewModel.cs @@ -2,8 +2,6 @@ using k8s.Models; using k8s; using KubeUI.Client; -using AvaloniaEdit; -using System.Reflection; namespace KubeUI.ViewModels; @@ -94,7 +92,7 @@ public async Task Connect() } catch (Exception ex) { - //to display notification + //todo display notification _logger.LogError(ex, "Unable to View Logs"); } } diff --git a/src/KubeUI/Views/Workloads/Pod/PodConsoleView.cs b/src/KubeUI/Views/Workloads/Pod/PodConsoleView.cs index 9006415a..f26d6106 100644 --- a/src/KubeUI/Views/Workloads/Pod/PodConsoleView.cs +++ b/src/KubeUI/Views/Workloads/Pod/PodConsoleView.cs @@ -5,6 +5,8 @@ using static AvaloniaEdit.TextMate.TextMate; using TextMateSharp.Grammars; using Avalonia.Markup.Xaml.MarkupExtensions; +using XtermSharp; +using AvaloniaEdit.Highlighting; namespace KubeUI.Views; @@ -59,95 +61,336 @@ protected override object Build(PodConsoleViewModel? vm) new Binding(nameof(PodConsoleViewModel.ContainerName)) ], StringFormat = "{0}/{1}/{2}" - }) + }), + //new Label() + // .Margin(0,0,4,0) + // .Content(new MultiBinding(){ + // Bindings = [ + // new Binding("Width"), + // new Binding("Height"), + // ], + // StringFormat = "{0}x{1}" + // }), + //new Label() + // .Margin(0,0,4,0) + // .Content(new MultiBinding(){ + // Bindings = [ + // new Binding("Terminal.Cols"), + // new Binding("Terminal.Rows"), + // ], + // StringFormat = "{0}x{1}" + // }) ]), - new TextEditor() - .Ref(out var editor) - .Row(1) - .Set(x => { - _textMateInstallation = editor.InstallTextMate(_registryOptions, false); - - x.Options.AllowScrollBelowDocument = false; - x.Options.ShowBoxForControlCharacters = false; - x.Options.EnableHyperlinks = false; - x.Options.EnableEmailHyperlinks = false; - x.TextArea.Caret.CaretBrush = Brushes.Transparent; - x.TextArea.Caret.Hide(); - }) - .OnTextChanged((e) => { - editor.ScrollToEnd(); - }) - .Document(@vm.Console, BindingMode.OneWay) - .FontFamily(new FontFamily("Consolas,Menlo,Monospace")) - .FontSize(14.0) - .FontWeight(FontWeight.Normal) - .IsReadOnly(true) - .ShowLineNumbers(false) - .Background(new DynamicResourceExtension("SystemAltHighColor")) - .HorizontalScrollBarVisibility(ScrollBarVisibility.Auto) - .VerticalScrollBarVisibility(ScrollBarVisibility.Visible) - .OnKeyUp((e) => { - if (DataContext is PodConsoleViewModel dc1) - { - if (e.KeyModifiers is KeyModifiers.Control) - { - if (e.Key == Key.C) - { - editor.Copy(); - return; - } - if (e.Key == Key.V) - { - _ = dc1.Paste(); - return; - } - } - - if (e.KeySymbol != null) - { - dc1.Send(e.KeySymbol); - } - else if (e.Key == Key.Up) - { - dc1.Send("\x1b[A"); - } - else if (e.Key == Key.Down) - { - dc1.Send("\x1b[B"); - } - else if (e.Key == Key.Left) - { - dc1.Send("\x1b[D"); - } - else if (e.Key == Key.Right) - { - dc1.Send("\x1b[C"); - } - else - { - _logger.LogInformation("Unmapped key: {0}", e.Key); - } - - e.Handled = true; - } - }) - .ContextMenu(new ContextMenu() - .Items([ - new MenuItem() - .OnClick((_) => editor.Copy()) - .Header(Assets.Resources.Action_Copy) - .InputGesture(new KeyGesture(Key.C, KeyModifiers.Control)) - .Icon(new PathIcon() { Data = (Geometry)Application.Current.FindResource("copy_regular") }), - new MenuItem() - .Command(vm.PasteCommand) - .Header(Assets.Resources.Action_Paste) - .InputGesture(new KeyGesture(Key.V, KeyModifiers.Control)) - .Icon(new PathIcon() { Data = (Geometry)Application.Current.FindResource("clipboard_paste_regular") }), - ]) - ), + new Grid() + .Children([ + + ]), + new TextEditor() + .Ref(out var editor) + .Row(1) + .Set(x => { + _textMateInstallation = editor.InstallTextMate(_registryOptions, false); + + x.Options.AllowScrollBelowDocument = false; + x.Options.ShowBoxForControlCharacters = false; + x.Options.EnableHyperlinks = false; + x.Options.EnableEmailHyperlinks = false; + + x.TextArea.OnKeyDown((e) => { + e.Handled = true; + }); + + x.TextArea.OnKeyUp((e) => { + if (DataContext is PodConsoleViewModel dc1) + { + if (e.KeyModifiers is KeyModifiers.Control) + { + switch (e.Key) + { + case Key.A: + dc1.Send(0x01); // Ctrl+A + break; + case Key.B: + dc1.Send(0x02); // Ctrl+B + break; + case Key.C: + dc1.Send(0x03); // Ctrl+C + break; + case Key.D: + dc1.Send(0x04); // Ctrl+D + break; + case Key.E: + dc1.Send(0x05); // Ctrl+E + break; + case Key.F: + dc1.Send(0x06); // Ctrl+F + break; + case Key.G: + dc1.Send(0x07); // Ctrl+G + break; + case Key.H: + dc1.Send(0x08); // Ctrl+H + break; + case Key.I: + dc1.Send(0x09); // Ctrl+I (Tab) + break; + case Key.J: + dc1.Send(0x0A); // Ctrl+J (Line Feed) + break; + case Key.K: + dc1.Send(0x0B); // Ctrl+K + break; + case Key.L: + dc1.Send(0x0C); // Ctrl+L + break; + case Key.M: + dc1.Send(0x0D); // Ctrl+M (Carriage Return) + break; + case Key.N: + dc1.Send(0x0E); // Ctrl+N + break; + case Key.O: + dc1.Send(0x0F); // Ctrl+O + break; + case Key.P: + dc1.Send(0x10); // Ctrl+P + break; + case Key.Q: + dc1.Send(0x11); // Ctrl+Q + break; + case Key.R: + dc1.Send(0x12); // Ctrl+R + break; + case Key.S: + dc1.Send(0x13); // Ctrl+S + break; + case Key.T: + dc1.Send(0x14); // Ctrl+T + break; + case Key.U: + dc1.Send(0x15); // Ctrl+U + break; + case Key.V: + _ = dc1.Paste(); + //dc1.Send(0x16); // Ctrl+V + break; + case Key.W: + dc1.Send(0x17); // Ctrl+W + break; + case Key.X: + dc1.Send(0x18); // Ctrl+X + break; + case Key.Y: + dc1.Send(0x19); // Ctrl+Y + break; + case Key.Z: + dc1.Send(0x1A); // Ctrl+Z + break; + case Key.D1: // Ctrl+1 + dc1.Send(0x31); // ASCII '1' + break; + case Key.D2: // Ctrl+2 + dc1.Send(0x32); // ASCII '2' + break; + case Key.D3: // Ctrl+3 + dc1.Send(0x33); // ASCII '3' + break; + case Key.D4: // Ctrl+4 + dc1.Send(0x34); // ASCII '4' + break; + case Key.D5: // Ctrl+5 + dc1.Send(0x35); // ASCII '5' + break; + case Key.D6: // Ctrl+6 + dc1.Send(0x36); // ASCII '6' + break; + case Key.D7: // Ctrl+7 + dc1.Send(0x37); // ASCII '7' + break; + case Key.D8: // Ctrl+8 + dc1.Send(0x38); // ASCII '8' + break; + case Key.D9: // Ctrl+9 + dc1.Send(0x39); // ASCII '9' + break; + case Key.D0: // Ctrl+0 + dc1.Send(0x30); // ASCII '0' + break; + case Key.OemOpenBrackets: // Ctrl+[ + dc1.Send(0x1B); + break; + case Key.OemBackslash: // Ctrl+\ + dc1.Send(0x1C); + break; + case Key.OemCloseBrackets: // Ctrl+] + dc1.Send(0x1D); + break; + case Key.Space: // Ctrl+Space + dc1.Send(0x00); + break; + case Key.OemMinus: // Ctrl+_ + dc1.Send(0x1F); + break; + default: + dc1.Send(e.KeySymbol); + break; + } + } + if (e.KeyModifiers is KeyModifiers.Alt) + { + dc1.Send(0x1B); + dc1.Send(e.KeySymbol); + } + else + { + switch (e.Key) { + case Key.Escape: + dc1.Send(0x1b); + break; + case Key.Space: + dc1.Send(0x20); + break; + case Key.Delete: + dc1.Send(EscapeSequences.CmdDelKey); + break; + case Key.Back: + dc1.Send(0x7f); + break; + case Key.Up: + dc1.Send(vm.Terminal.ApplicationCursor ? EscapeSequences.MoveUpApp : EscapeSequences.MoveUpNormal); + break; + case Key.Down: + dc1.Send(vm.Terminal.ApplicationCursor ? EscapeSequences.MoveDownApp : EscapeSequences.MoveDownNormal); + break; + case Key.Left: + dc1.Send(vm.Terminal.ApplicationCursor ? EscapeSequences.MoveLeftApp : EscapeSequences.MoveLeftNormal); + break; + case Key.Right: + dc1.Send(vm.Terminal.ApplicationCursor ? EscapeSequences.MoveRightApp : EscapeSequences.MoveRightNormal); + break; + case Key.PageUp: + if (vm.Terminal.ApplicationCursor) + { + dc1.Send(EscapeSequences.CmdPageUp); + } else { + // TODO: view should scroll one page up. + } + break; + case Key.PageDown: + if (vm.Terminal.ApplicationCursor) + { + dc1.Send(EscapeSequences.CmdPageDown); + } else { + // TODO: view should scroll one page down + } + break; + case Key.Home: + dc1.Send(vm.Terminal.ApplicationCursor ? EscapeSequences.MoveHomeApp : EscapeSequences.MoveHomeNormal); + break; + case Key.End: + dc1.Send(vm.Terminal.ApplicationCursor ? EscapeSequences.MoveEndApp : EscapeSequences.MoveEndNormal); + break; + case Key.Insert: + break; + case Key.F1: + dc1.Send(EscapeSequences.CmdF [0]); + break; + case Key.F2: + dc1.Send(EscapeSequences.CmdF [1]); + break; + case Key.F3: + dc1.Send(EscapeSequences.CmdF [2]); + break; + case Key.F4: + dc1.Send(EscapeSequences.CmdF [3]); + break; + case Key.F5: + dc1.Send(EscapeSequences.CmdF [4]); + break; + case Key.F6: + dc1.Send(EscapeSequences.CmdF [5]); + break; + case Key.F7: + dc1.Send(EscapeSequences.CmdF [6]); + break; + case Key.F8: + dc1.Send(EscapeSequences.CmdF [7]); + break; + case Key.F9: + dc1.Send(EscapeSequences.CmdF [8]); + break; + case Key.F10: + dc1.Send(EscapeSequences.CmdF [9]); + break; + case Key.OemBackTab: + dc1.Send(EscapeSequences.CmdBackTab); + break; + case Key.Tab: + dc1.Send(EscapeSequences.CmdTab); + break; + default: + dc1.Send(e.KeySymbol); + break; + } + } + + e.Handled = true; + } + }); + + // Add Coloring + x.TextArea.TextView.LineTransformers.Add(new RichTextColorizer(vm.ConsoleColor)); + + // Remove Caret keyboard navigation + foreach (var command in x.TextArea.DefaultInputHandler.CaretNavigation.CommandBindings.Where(x => x.Command.Gesture != null).ToList()) + { + x.TextArea.DefaultInputHandler.CaretNavigation.CommandBindings.Remove(command); + } + + x.TextArea.DefaultInputHandler.CaretNavigation.KeyBindings.Clear(); + }) + .OnTextChanged((e) => { + editor.TextArea.Caret.Line = vm.Terminal.Buffer.Y - vm.Terminal.Buffer.YDisp + vm.Terminal.Buffer.YBase + 1; + editor.TextArea.Caret.Column = vm.Terminal.Buffer.X + 1; + }) + .Document(@vm.Console, BindingMode.OneWay) + .FontFamily(@vm.FontFamily) + .FontSize(@vm.FontSize) + .IsReadOnly(true) + .ShowLineNumbers(false) + .Background(new DynamicResourceExtension("SystemAltHighColor")) + .HorizontalScrollBarVisibility(ScrollBarVisibility.Disabled) + .VerticalScrollBarVisibility(ScrollBarVisibility.Disabled) + .ContextMenu(new ContextMenu() + .Items([ + new MenuItem() + .OnClick((_) => editor.Copy()) + .Header(Assets.Resources.Action_Copy) + .Icon(new PathIcon() { Data = (Geometry)Application.Current.FindResource("copy_regular") }), + new MenuItem() + .Command(vm.PasteCommand) + .Header(Assets.Resources.Action_Paste) + .InputGesture(new KeyGesture(Key.V, KeyModifiers.Control)) + .Icon(new PathIcon() { Data = (Geometry)Application.Current.FindResource("clipboard_paste_regular") }), + ]) + ), ]); } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + try + { + ViewModel.Connect().GetAwaiter(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error connecting to Console1"); + } + } + protected override void OnUnloaded(RoutedEventArgs e) { base.OnUnloaded(e); diff --git a/src/KubeUI/Views/Workloads/Pod/PodLogsView.cs b/src/KubeUI/Views/Workloads/Pod/PodLogsView.cs index 00a2b113..adb73068 100644 --- a/src/KubeUI/Views/Workloads/Pod/PodLogsView.cs +++ b/src/KubeUI/Views/Workloads/Pod/PodLogsView.cs @@ -94,7 +94,7 @@ protected override object Build(PodLogsViewModel? vm) }; }) .Document(@vm.Logs, BindingMode.OneWay) - .FontFamily(new FontFamily("Consolas,Menlo,Monospace")) + .FontFamily(new FontFamily("Cascadia Mono")) .FontSize(14.0) .FontWeight(FontWeight.Normal) .IsReadOnly(true) diff --git a/src/XtermSharp/AssemblyInfo.cs b/src/XtermSharp/AssemblyInfo.cs new file mode 100644 index 00000000..1a8a6e86 --- /dev/null +++ b/src/XtermSharp/AssemblyInfo.cs @@ -0,0 +1 @@ +[assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute ("Tests")] diff --git a/src/XtermSharp/Buffer.cs b/src/XtermSharp/Buffer.cs new file mode 100644 index 00000000..7cde2eab --- /dev/null +++ b/src/XtermSharp/Buffer.cs @@ -0,0 +1,436 @@ +using System; +using System.Collections; +using System.Diagnostics; +using NStack; + +namespace XtermSharp { + /// + /// This class represents a terminal buffer (an internal state of the terminal), where text contents, cursor and scroll position are stored. + /// + [DebuggerDisplay ("({X},{Y}) YD={YDisp}:YB={YBase} Scroll={ScrollBottom,ScrollTop}")] + public class Buffer { + CircularList lines; + int scrollTop; + int scrollBottom; + + /// + /// Initializes a new instance of the class. + /// + /// The terminal the Buffer will belong to. + /// Whether the buffer should respect the scrollback of the terminal. + public Buffer (Terminal terminal, bool hasScrollback = true) + { + rows = terminal.Rows; + cols = terminal.Cols; + MarginLeft = 0; + MarginRight = cols - 1; + Terminal = terminal; + this.hasScrollback = hasScrollback; + Clear (); + } + + /// + /// Gets the number of columns of the buffer + /// + public int Cols => cols; + + /// + /// Gets the number of rows of the buffer + /// + public int Rows => rows; + + /// + /// Gets or sets the top scrolling region in the buffer when Origin Mode is turned on + /// + public int ScrollTop { + get { + return scrollTop; + } + set { + if(value >= 0) { + scrollTop = value; + } + } + } + + /// + /// Gets or sets the top scrolling region in the buffer when Origin Mode is turned on + /// + public int ScrollBottom { + get { + return scrollBottom; + } + set { + if (value < 0) + throw new ArgumentException ("Scroll bottom cannot be less than zero", nameof(ScrollBottom)); + + scrollBottom = value; + } + } + + /// + /// Gets or sets the left margin, 0 based + /// + public int MarginLeft { get; private set; } + + /// + /// Gets or sets the right margin, 0 based + /// + public int MarginRight { get; private set; } + + public int YDisp, YBase; + public int X; + int y; + public int Y { + get => y; + set { + if (value < 0 || value > Terminal.Rows - 1) + throw new ArgumentException ("Y cannot be outside the bounds of the terminal rows", nameof(Y)); + else + y = value; + } + } + BitArray tabStops; + public int SavedX, SavedY, SavedAttr = CharData.DefaultAttr; + public Terminal Terminal { get; private set; } + bool hasScrollback; + int cols, rows; + + /// + /// Gets a value indicating whether this has scrollback. + /// + /// true if has scrollback; otherwise, false. + public bool HasScrollback => hasScrollback && lines.MaxLength > Terminal.Rows; + + public CircularList Lines => lines; + + // Gets the correct buffer length based on the rows provided, the terminal's + // scrollback and whether this buffer is flagged to have scrollback or not. + int getCorrectBufferLength (int rows) + { + if (!hasScrollback) + return rows; + var correct = rows + Terminal.Options.Scrollback ?? 0; + return correct > Int32.MaxValue ? Int32.MaxValue : correct; + } + + /// + /// Returns the CharData at the specified position in the buffer + /// + public CharData GetChar (int col, int row) + { + var bufferRow = lines [row]; + if (bufferRow == null) { + return CharData.Null; + } + + if (col >= bufferRow.Length || col < 0) + return CharData.Null; + + return bufferRow [col]; + } + + public BufferLine GetBlankLine (int attribute, bool isWrapped = false) + { + var cd = new CharData (attribute); + + return new BufferLine (Terminal.Cols, cd, isWrapped); + } + + /// + /// Clears the buffer to it's initial state, discarding all previous data. + /// + public void Clear () + { + YDisp = 0; + YBase = 0; + X = 0; + Y = 0; + lines = new CircularList (getCorrectBufferLength (Terminal.Rows)); + ScrollTop = 0; + ScrollBottom = Terminal.Rows - 1; + SetupTabStops (); + } + + public bool IsCursorInViewport { + get { + var absoluteY = YBase + Y; + var relativeY = absoluteY - YDisp; + return (relativeY >= 0 && relativeY < Terminal.Rows); + } + } + + /// + /// Sets the left and right margins + /// + public void SetMargins (int left, int right) + { + left = Math.Min (left, right); + MarginLeft = left; + MarginRight = right; + } + + /// + /// Saves the cursor position + /// + public void SaveCursor (int curAttr) + { + SavedX = X; + SavedY = Y; + SavedAttr = curAttr; + } + + /// + /// Restores the cursor + /// + public int RestoreCursor () + { + X = SavedX; + Y = SavedY; + return SavedAttr; + } + + /// + /// Fills the buffer's viewport with blank lines. + /// + public void FillViewportRows (int? attribute = null) + { + // TODO: limitation in original, this does not cope with partial fills, it is either zero or nothing + if (lines.Length != 0) + return; + var attr = attribute.HasValue ? attribute.Value : CharData.DefaultAttr; + for (int i = Terminal.Rows; i > 0; i--) + lines.Push (GetBlankLine (attr)); + } + + bool IsReflowEnabled => hasScrollback;// && Terminal.Options.WindowsMode; + + /// + /// Resize the buffer, adjusting its data accordingly + /// + /// The resize. + /// New columns. + /// New rows. + public void Resize (int newCols, int newRows) + { + var newMaxLength = getCorrectBufferLength (newRows); + if (newMaxLength > lines.MaxLength) { + lines.MaxLength = newMaxLength; + } + + if (this.lines.Length > 0) { + // Deal with columns increasing (reducing needs to happen after reflow) + if (cols < newCols) { + for (int i = 0; i < lines.MaxLength; i++) { + lines [i]?.Resize (newCols, CharData.Null); + } + } + + // Resize rows in both directions as needed + int addToY = 0; + if (rows < newRows) { + for (int y = rows; y < newRows; y++) { + if (lines.Length < newRows + YBase) { + //if (Terminal.Options.windowsMode) { + // // Just add the new missing rows on Windows as conpty reprints the screen with it's + // // view of the world. Once a line enters scrollback for conpty it remains there + // lines.Push (new BufferLine (newCols, CharData.Null)); + //} else { + { + if (YBase > 0 && lines.Length <= YBase + Y + addToY + 1) { + // There is room above the buffer and there are no empty elements below the line, + // scroll up + YBase--; + addToY++; + if (YDisp > 0) { + // Viewport is at the top of the buffer, must increase downwards + YDisp--; + } + } else { + // Add a blank line if there is no buffer left at the top to scroll to, or if there + // are blank lines after the cursor + lines.Push (new BufferLine (newCols, CharData.Null)); + } + } + } + } + } else { // (this._rows >= newRows) + for (int y = rows; y > newRows; y--) { + if (lines.Length > newRows + YBase) { + if (lines.Length > YBase + this.y + 1) { + // The line is a blank line below the cursor, remove it + lines.Pop (); + } else { + // The line is the cursor, scroll down + YBase++; + YDisp++; + } + } + } + } + + // Reduce max length if needed after adjustments, this is done after as it + // would otherwise cut data from the bottom of the buffer. + if (newMaxLength < lines.MaxLength) { + // Trim from the top of the buffer and adjust ybase and ydisp. + int amountToTrim = lines.Length - newMaxLength; + if (amountToTrim > 0) { + lines.TrimStart (amountToTrim); + YBase = Math.Max (YBase - amountToTrim, 0); + YDisp = Math.Max (YDisp - amountToTrim, 0); + SavedY = Math.Max (SavedY - amountToTrim, 0); + } + + lines.MaxLength = newMaxLength; + } + + // Make sure that the cursor stays on screen + X = Math.Min (X, newCols - 1); + Y = Math.Min (Y, newRows - 1); + if (addToY != 0) { + Y += addToY; + } + + SavedX = Math.Min (SavedX, newCols - 1); + + ScrollTop = 0; + } + + ScrollBottom = newRows - 1; + + if (IsReflowEnabled) { + this.Reflow (newCols, newRows); + + // Trim the end of the line off if cols shrunk + if (cols > newCols) { + for (int i = 0; i < lines.MaxLength; i++) { + lines [i]?.Resize (newCols, CharData.Null); + } + } + } + + rows = newRows; + cols = newCols; + if (MarginRight > newCols - 1) + MarginRight = newCols - 1; + if (MarginLeft > MarginRight) + MarginLeft = MarginRight; + } + + /// + /// Translates a buffer line to a string, with optional start and end columns. Wide characters will count as two columns in the resulting string. This + /// function is useful for getting the actual text underneath the raw selection position. + /// + /// The buffer line to string. + /// The line being translated. + /// If set to true trim whitespace to the right. + /// The column to start at. + /// The column to end at. + public ustring TranslateBufferLineToString (int lineIndex, bool trimRight, int startCol = 0, int endCol = -1) + { + try { + var line = lines [lineIndex]; + + return line.TranslateToString (trimRight, startCol, endCol); + } catch ( Exception ex) + { + return ustring.Empty; + } + } + + /// + /// Setups the tab stops. + /// + /// Index to start setting tabs stops from. + public void SetupTabStops (int index = -1) + { + if (index != -1 && tabStops != null) { + tabStops.Length = cols; + + var from = Math.Min (index, cols - 1); + if (!tabStops [from]) + index = PreviousTabStop (from); + } else { + tabStops = new BitArray (cols); + index = 0; + } + + int tabStopWidth = Terminal.Options.TabStopWidth ?? 8; + for (int i = index; i < cols; i += tabStopWidth) + tabStops [i] = true; + } + + public void TabSet (int pos) + { + if (pos < tabStops.Length) + tabStops [pos] = true; + } + + public void ClearStop (int pos) + { + if (pos < tabStops.Length) + tabStops [pos] = false; + } + + public void ClearTabStops () + { + tabStops = new BitArray (tabStops.Count); + } + + /// + /// Move the cursor to the previous tab stop from the given position (default is current). + /// + /// The tab stop. + /// The position to move the cursor to the previous tab stop. + public int PreviousTabStop (int index = -1) + { + if (index == -1) + index = X; + while (index > 0 && !tabStops [--index]) + ; + + return index >= Cols ? Cols - 1 : index; + } + + /// + /// Move the cursor one tab stop forward from the given position (default is current). + /// + /// The tab stop. + /// The position to move the cursor one tab stop forward. + public int NextTabStop (int index = -1) + { + // Users marginMode because apparently for tabs, there is no need to have originMode set + var limit = Terminal.MarginMode ? MarginRight : (Cols - 1); + if (index == -1) + index = X; + + do { + index++; + if (index > limit) + break; + + if (tabStops [index]) + break; + } while (index < limit); + + return index >= limit ? limit : index; + } + + void Reflow (int newCols, int newRows) + { + if (cols == newCols) { + return; + } + + // Iterate through rows, ignore the last one as it cannot be wrapped + ReflowStrategy strategy; + if (newCols > cols) { + strategy = new ReflowWider (this); + } else { + strategy = new ReflowNarrower (this); + } + + strategy.Reflow (newCols, newRows, cols, rows); + } + } +} diff --git a/src/XtermSharp/BufferLine.cs b/src/XtermSharp/BufferLine.cs new file mode 100644 index 00000000..02d22dc0 --- /dev/null +++ b/src/XtermSharp/BufferLine.cs @@ -0,0 +1,197 @@ +// +// Note: does not handle combined, as this code uses Runes, rather than Utf16 encoded chars +// +using System; +using System.Collections.Generic; +using System.Diagnostics; +using NStack; + +namespace XtermSharp { + [DebuggerDisplay ("Line: {DebuggerDisplay}")] + public class BufferLine { + CharData [] data = Array.Empty (); + public int Length => data.Length; + public bool IsWrapped; + + public BufferLine (int cols, CharData? fillCharData, bool isWrapped = false) + { + var fill = fillCharData ?? CharData.Null; + + data = new CharData [cols]; + for (int i = 0; i < cols; i++) + data [i] = fill; + this.IsWrapped = isWrapped; + } + + public BufferLine (BufferLine other) + { + data = new CharData [other.data.Length]; + other.data.CopyTo (data, 0); + IsWrapped = other.IsWrapped; + } + + public CharData this [int idx] { + get => data [idx]; + set { + data [idx] = value; + } + } + + public int GetWidth (int index) => data [index].Width; + + /** + * Test whether contains any chars. + * Basically an empty has no content, but other cells might differ in FG/BG + * from real empty cells. + * */ + // TODO: not sue this is completely right + public bool HasContent (int index) => !data [index].IsNullChar() || data [index].Attribute != CharData.DefaultAttr; + + public bool HasAnyContent() + { + for (int i = 0; i < data.Length; i++) { + if (HasContent (i)) return true; + } + + return false; + } + + string DebuggerDisplay { + get { + return TranslateToString (true, 0, -1).ToString (); + } + } + + public void InsertCells (int pos, int n, int rightMargin, CharData fillCharData) + { + var len = Math.Min (rightMargin + 1, Length); + pos = pos % len; + if (n < len - pos) { + for (var i = len - pos - n - 1; i >= 0; --i) + data [pos + n + i] = data [pos + i]; + for (var i = 0; i < n; i++) + data [pos + i] = fillCharData; + } else { + for (var i = pos; i < len; ++i) + data [i] = fillCharData; + } + } + + public void DeleteCells (int pos, int n, int rightMargin, CharData fillCharData) + { + var len = Math.Min(rightMargin + 1, Length); + pos %= len; + if (n < len - pos) { + for (var i = 0; i < len - pos - n; ++i) + data [pos + i] = this [pos + n + i]; + for (var i = len - n; i < len; ++i) + data [i] = fillCharData; + } else { + for (var i = pos; i < len; ++i) + data [i] = fillCharData; + } + } + + public void ReplaceCells (int start, int end, CharData fillCharData) + { + var len = Length; + + while (start < end && start < len) + data [start++] = fillCharData; + } + + public void Resize (int cols, CharData fillCharData) + { + var len = Length; + if (cols == len) + return; + + if (cols > len) { + var newData = new CharData [cols]; + if (len > 0) + data.CopyTo (newData, 0); + data = newData; + for (int i = len; i < cols; i++) + data [i] = fillCharData; + } else { + if (cols > 0) { + var newData = new CharData [cols]; + Array.Copy (data, newData, cols); + data = newData; + } else { + data = Array.Empty (); + } + } + } + + /// + /// Fills the line with fillCharData values + /// + public void Fill (CharData fillCharData) + { + var len = Length; + for (int i = 0; i < len; i++) + data [i] = fillCharData; + } + + /// + /// Fills the line with len fillCharData values from the given atCol + /// + public void Fill (CharData fillCharData, int atCol, int len) + { + for (int i = 0; i < len; i++) + data [atCol + i] = fillCharData; + } + + public void CopyFrom (BufferLine line) + { + if (data.Length != line.Length) + data = new CharData [line.Length]; + + line.data.CopyTo (data, 0); + + IsWrapped = line.IsWrapped; + } + + /// + /// Copies a subrange the given source line into the current line + /// + public void CopyFrom (BufferLine source, int sourceCol, int destCol, int len) + { + Array.Copy (source.data, sourceCol, data, destCol, len); + } + + public int GetTrimmedLength () + { + for (int i = data.Length - 1; i >= 0; --i) + if (!data [i].IsNullChar()) { + int width = 0; + for (int j = 0; j <= i; j++) + width += data [i].Width; + return width; + } + return 0; + } + + public void CopyCellsFrom (BufferLine src, int srcCol, int dstCol, int len) + { + Array.Copy (src.data, srcCol, data, dstCol, len); + } + + public ustring TranslateToString (bool trimRight = false, int startCol = 0, int endCol = -1) + { + if (endCol == -1) + endCol = data.Length; + if (trimRight) { + // make sure endCol is not before startCol if we set it to the trimmed length + endCol = Math.Max (Math.Min (endCol, GetTrimmedLength ()), startCol); + } + + Rune [] runes = new Rune [endCol - startCol]; + for (int i = startCol; i < endCol; i++) + runes [i - startCol] = data [i].Rune; + + return ustring.Make (runes); + } + } +} diff --git a/src/XtermSharp/BufferSet.cs b/src/XtermSharp/BufferSet.cs new file mode 100644 index 00000000..e0dedce9 --- /dev/null +++ b/src/XtermSharp/BufferSet.cs @@ -0,0 +1,104 @@ +// +// At: 857ae4b702b17381f6b862909a3570a6c3ab30b4 +// +using System; +namespace XtermSharp { + + /// + /// The BufferSet represents the set of two buffers used by xterm terminals (normal and alt) and + /// provides also utilities for working with them. + /// + public class BufferSet { + public Buffer Normal { get; private set; } + public Buffer Alt { get; private set; } + public Buffer Active { get; private set; } + + public BufferSet (Terminal terminal) + { + Normal = new Buffer (terminal, hasScrollback: true); + Normal.FillViewportRows (); + + // The alt buffer should never have scrollback. + // See http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer + Alt = new Buffer (terminal, hasScrollback: false); + + Active = Normal; + SetupTabStops (); + } + + /// + /// Gets a value indicating whether the active buffer is the alternate buffer + /// + public bool IsAlternateBuffer => Active == Alt; + + /// + /// Raised when a buffer is activated, the parameters is the buffer that was activated and the second parameter is the Inactive buffer. + /// + public event Action Activated; + + /// + /// Sets the normal Buffer of the BufferSet as its currently active Buffer + /// + public void ActivateNormalBuffer (bool clearAlt) + { + if (Active == Normal) + return; + + Normal.X = Alt.X; + Normal.Y = Alt.Y; + + // The alt buffer should always be cleared when we switch to the normal + // buffer. This frees up memory since the alt buffer should always be new + // when activated. + + if (clearAlt) { + Alt.Clear (); + } + + Active = Normal; + Activated?.Invoke (Normal, Alt); + } + + /// + /// Sets the alt Buffer of the BufferSet as its currently active Buffer + /// + /// Attribute to fill the screen with + public void ActivateAltBuffer (int? fillAttr) + { + if (Active == Alt) + return; + Alt.X = Normal.X; + Alt.Y = Normal.Y; + // Since the alt buffer is always cleared when the normal buffer is + // activated, we want to fill it when switching to it. + + Alt.FillViewportRows (fillAttr); + Active = Alt; + if (Activated != null) + Activated (Alt, Normal); + } + + /// + /// Resizes both normal and alt buffers, adjusting their data accordingly. + /// + /// The resize. + /// The new number of columns. + /// The new number of rows. + public void Resize (int newColumns, int newRows) + { + Normal.Resize (newColumns, newRows); + Alt.Resize (newColumns, newRows); + } + + /// + /// Setups the tab stops. + /// + /// The index to start setting up tab stops from, or -1 to do it from the start. + public void SetupTabStops (int index = -1) + { + Normal.SetupTabStops (index); + Alt.SetupTabStops (index); + } + + } +} diff --git a/src/XtermSharp/CharData.cs b/src/XtermSharp/CharData.cs new file mode 100644 index 00000000..8db879ce --- /dev/null +++ b/src/XtermSharp/CharData.cs @@ -0,0 +1,75 @@ +using System; +using System.Diagnostics; + +namespace XtermSharp { + // MIGUEL TODO: + // The original code used Rune + Code, but it really makes no sense to keep those separate, excpt for null that has a + // zero-width thing for code 0. + [DebuggerDisplay("[CharData (Attr={Attribute},Rune={Rune},W={Width},Code={Code})]")] + public struct CharData { + public int Attribute; + public Rune Rune; + public int Width; + public int Code; + + // ((int)flags << 18) | (fg << 9) | bg; + + public const int DefaultAttr = Renderer.DefaultColor << 9 | (256 << 0); + public const int InvertedAttr = Renderer.InvertedDefaultColor << 9 | (256 << 0) | Renderer.InvertedDefaultColor; + + public static CharData Null = new CharData (DefaultAttr, '\u0200', 1, 0); + public static CharData WhiteSpace = new CharData (DefaultAttr, ' ', 1, 32); + public static CharData LeftBrace = new CharData (DefaultAttr, '{', 1, 123); + public static CharData RightBrace = new CharData (DefaultAttr, '}', 1, 125); + public static CharData LeftBracket = new CharData (DefaultAttr, '[', 1, 91); + public static CharData RightBracket = new CharData (DefaultAttr, ']', 1, 93); + public static CharData LeftParenthesis = new CharData (DefaultAttr, '(', 1, 40); + public static CharData RightParenthesis = new CharData (DefaultAttr, ')', 1, 41); + public static CharData Period = new CharData (DefaultAttr, '.', 1, 46); + + public CharData (int attribute, char rune, int width) : this(attribute, rune, width, rune) + { + } + + public CharData (int attribute, char rune, int width, int code) + { + Attribute = attribute; + Rune = rune; + Width = width; + Code = code; + } + + // Returns an empty CharData with the specified attribute + public CharData (int attribute) + { + Attribute = attribute; + Rune = '\u0200'; + Width = 1; + Code = 0; + } + + /// + /// Returns true if this CharData matches the given Rune, irrespective of character attributes + /// + public bool MatchesRune(Rune rune) + { + return rune == Rune; + } + + /// + /// Returns true if this CharData matches the given Rune, irrespective of character attributes + /// + public bool MatchesRune (CharData chr) + { + return Rune == chr.Rune; + } + + /// + /// returns true if this CharData matches Null or has a code of 0 + /// + public bool IsNullChar() + { + return Rune == Null.Rune || Code == 0; + } + } +} diff --git a/src/XtermSharp/CharSets.cs b/src/XtermSharp/CharSets.cs new file mode 100644 index 00000000..5b11fbe2 --- /dev/null +++ b/src/XtermSharp/CharSets.cs @@ -0,0 +1,260 @@ +using System; +using System.Collections.Generic; + +namespace XtermSharp { + public class CharSets { + public static Dictionary> All; + + // This is the "B" charset, null + public static Dictionary Default = null; + + static CharSets () + { + All = new Dictionary> (); + // + // DEC Special Character and Line Drawing Set. + // Reference: http://vt100.net/docs/vt102-ug/table5-13.html + // A lot of curses apps use this if they see TERM=xterm. + // testing: echo -e '\e(0a\e(B' + // The xterm output sometimes seems to conflict with the + // reference above. xterm seems in line with the reference + // when running vttest however. + // The table below now uses xterm's output from vttest. + // + All [(byte)'0'] = new Dictionary () { + { (byte) '`', "\u25c6"}, // '◆' + { (byte) 'a', "\u2592"}, // '▒' + { (byte) 'b', "\u2409"}, // [ht] + { (byte) 'c', "\u240c"}, // [ff] + { (byte) 'd', "\u240d"}, // [cr] + { (byte) 'e', "\u240a"}, // [lf] + { (byte) 'f', "\u00b0"}, // '°' + { (byte) 'g', "\u00b1"}, // '±' + { (byte) 'h', "\u2424"}, // [nl] + { (byte) 'i', "\u240b"}, // [vt] + { (byte) 'j', "\u2518"}, // '┘' + { (byte) 'k', "\u2510"}, // '┐' + { (byte) 'l', "\u250c"}, // '┌' + { (byte) 'm', "\u2514"}, // '└' + { (byte) 'n', "\u253c"}, // '┼' + { (byte) 'o', "\u23ba"}, // '⎺' + { (byte) 'p', "\u23bb"}, // '⎻' + { (byte) 'q', "\u2500"}, // '─' + { (byte) 'r', "\u23bc"}, // '⎼' + { (byte) 's', "\u23bd"}, // '⎽' + { (byte) 't', "\u251c"}, // '├' + { (byte) 'u', "\u2524"}, // '┤' + { (byte) 'v', "\u2534"}, // '┴' + { (byte) 'w', "\u252c"}, // '┬' + { (byte) 'x', "\u2502"}, // '│' + { (byte) 'y', "\u2264"}, // '≤' + { (byte) 'z', "\u2265"}, // '≥' + { (byte) '{', "\u03c0"}, // 'π' + { (byte) '|', "\u2260"}, // '≠' + { (byte) '}', "\u00a3"}, // '£' + { (byte) '~', "\u00b7"} // '·' + }; + + // (DEC Alternate character ROM special graphics) + All [(byte)'2'] = All [(byte)'0']; + + /** + * British character set + * ESC (A + * Reference: http://vt100.net/docs/vt220-rm/table2-5.html + */ + All [(byte)'A'] = new Dictionary { + {(byte) '#', "£"} + }; + + /** + * United States character set + * ESC (B + */ + All [(byte)'B'] = null; + + /** + * Dutch character set + * ESC (4 + * Reference: http://vt100.net/docs/vt220-rm/table2-6.html + */ + All [(byte)'4'] = new Dictionary { + { (byte) '#', "£"}, + { (byte) '@', "¾"}, + { (byte) '[', "ij"}, + { (byte) '\\', "½"}, + { (byte) ']', "|"}, + { (byte) '{', "¨"}, + { (byte) '|', "f"}, + { (byte) '}', "¼"}, + { (byte) '~', "´"} + }; + + /** + * Finnish character set + * ESC (C or ESC (5 + * Reference: http://vt100.net/docs/vt220-rm/table2-7.html + */ + All [(byte)'C'] = + All [(byte)'5'] = new Dictionary { + { (byte) '[', "Ä"}, + { (byte) '\\', "Ö"}, + { (byte) ']', "Å"}, + { (byte) '^', "Ü"}, + { (byte) '`', "é"}, + { (byte) '{', "ä"}, + { (byte) '|', "ö"}, + { (byte) '}', "å"}, + { (byte) '~', "ü"} + }; + + /** + * French character set + * ESC (R + * Reference: http://vt100.net/docs/vt220-rm/table2-8.html + */ + All [(byte)'R'] = new Dictionary { + { (byte) '#', "£"}, + { (byte) '@', "à"}, + { (byte) '[', "°"}, + { (byte) '\\', "ç"}, + { (byte) ']', "§"}, + { (byte) '{', "é"}, + { (byte) '|', "ù"}, + { (byte) '}', "è"}, + { (byte) '~', "¨"} + }; + + /** + * French Canadian character set + * ESC (Q + * Reference: http://vt100.net/docs/vt220-rm/table2-9.html + */ + All [(byte)'Q'] = new Dictionary { + { (byte) '@', "à"}, + { (byte) '[', "â"}, + { (byte) '\\', "ç"}, + { (byte) ']', "ê"}, + { (byte) '^', "î"}, + { (byte) '`', "ô"}, + { (byte) '{', "é"}, + { (byte) '|', "ù"}, + { (byte) '}', "è"}, + { (byte) '~', "û"} + }; + + /** + * German character set + * ESC (K + * Reference: http://vt100.net/docs/vt220-rm/table2-10.html + */ + All [(byte)'K'] = new Dictionary { + { (byte) '@', "§"}, + { (byte) '[', "Ä"}, + { (byte) '\\', "Ö"}, + { (byte) ']', "Ü"}, + { (byte) '{', "ä"}, + { (byte) '|', "ö"}, + { (byte) '}', "ü"}, + { (byte) '~', "ß"} + }; + + /** + * Italian character set + * ESC (Y + * Reference: http://vt100.net/docs/vt220-rm/table2-11.html + */ + All [(byte)'Y'] = new Dictionary { + { (byte) '#', "£"}, + { (byte) '@', "§"}, + { (byte) '[', "°"}, + { (byte) '\\', "ç"}, + { (byte) ']', "é"}, + { (byte) '`', "ù"}, + { (byte) '{', "à"}, + { (byte) '|', "ò"}, + { (byte) '}', "è"}, + { (byte) '~', "ì"} + }; + + /** + * Norwegian/Danish character set + * ESC (E or ESC (6 + * Reference: http://vt100.net/docs/vt220-rm/table2-12.html + */ + All [(byte)'E'] = + All [(byte)'6'] = new Dictionary { + { (byte) '@', "Ä"}, + { (byte) '[', "Æ"}, + { (byte) '\\', "Ø"}, + { (byte) ']', "Å"}, + { (byte) '^', "Ü"}, + { (byte) '`', "ä"}, + { (byte) '{', "æ"}, + { (byte) '|', "ø"}, + { (byte) '}', "å"}, + { (byte) '~', "ü"} + }; + + /** + * Spanish character set + * ESC (Z + * Reference: http://vt100.net/docs/vt220-rm/table2-13.html + */ + + All [(byte)'Z'] = new Dictionary { + { (byte) '#', "£"}, + { (byte) '@', "§"}, + { (byte) '[', "¡"}, + { (byte) '\\', "Ñ"}, + { (byte) ']', "¿"}, + { (byte) '{', "°"}, + { (byte) '|', "ñ"}, + { (byte) '}', "ç"} + }; + + /** + * Swedish character set + * ESC (H or ESC (7 + * Reference: http://vt100.net/docs/vt220-rm/table2-14.html + */ + All [(byte)'H'] = + All [(byte)'7'] = new Dictionary { + { (byte) '@', "É"}, + { (byte) '[', "Ä"}, + { (byte) '\\', "Ö"}, + { (byte) ']', "Å"}, + { (byte) '^', "Ü"}, + { (byte) '`', "é"}, + { (byte) '{', "ä"}, + { (byte) '|', "ö"}, + { (byte) '}', "å"}, + { (byte) '~', "ü"} + }; + + /** + * Swiss character set + * ESC (= + * Reference: http://vt100.net/docs/vt220-rm/table2-15.html + */ + All [(byte)'='] = new Dictionary { + { (byte) '#', "ù"}, + { (byte) '@', "à"}, + { (byte) '[', "é"}, + { (byte) '\\', "ç"}, + { (byte) ']', "ê"}, + { (byte) '^', "î"}, + { (byte) '_', "è"}, + { (byte) '`', "ô"}, + { (byte) '{', "ä"}, + { (byte) '|', "ö"}, + { (byte) '}', "ü"}, + { (byte) '~', "û" } + }; + } + } + + public class CharSet { + + } +} diff --git a/src/XtermSharp/CharWidth.cs b/src/XtermSharp/CharWidth.cs new file mode 100644 index 00000000..3ba0ba1e --- /dev/null +++ b/src/XtermSharp/CharWidth.cs @@ -0,0 +1,179 @@ +using System; +namespace XtermSharp { + public static class RuneHelper { + // extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c + + static uint [,] combining = new uint [,] { + { 0x0300, 0x036F }, { 0x0483, 0x0486 }, { 0x0488, 0x0489 }, + { 0x0591, 0x05BD }, { 0x05BF, 0x05BF }, { 0x05C1, 0x05C2 }, + { 0x05C4, 0x05C5 }, { 0x05C7, 0x05C7 }, { 0x0600, 0x0603 }, + { 0x0610, 0x0615 }, { 0x064B, 0x065E }, { 0x0670, 0x0670 }, + { 0x06D6, 0x06E4 }, { 0x06E7, 0x06E8 }, { 0x06EA, 0x06ED }, + { 0x070F, 0x070F }, { 0x0711, 0x0711 }, { 0x0730, 0x074A }, + { 0x07A6, 0x07B0 }, { 0x07EB, 0x07F3 }, { 0x0901, 0x0902 }, + { 0x093C, 0x093C }, { 0x0941, 0x0948 }, { 0x094D, 0x094D }, + { 0x0951, 0x0954 }, { 0x0962, 0x0963 }, { 0x0981, 0x0981 }, + { 0x09BC, 0x09BC }, { 0x09C1, 0x09C4 }, { 0x09CD, 0x09CD }, + { 0x09E2, 0x09E3 }, { 0x0A01, 0x0A02 }, { 0x0A3C, 0x0A3C }, + { 0x0A41, 0x0A42 }, { 0x0A47, 0x0A48 }, { 0x0A4B, 0x0A4D }, + { 0x0A70, 0x0A71 }, { 0x0A81, 0x0A82 }, { 0x0ABC, 0x0ABC }, + { 0x0AC1, 0x0AC5 }, { 0x0AC7, 0x0AC8 }, { 0x0ACD, 0x0ACD }, + { 0x0AE2, 0x0AE3 }, { 0x0B01, 0x0B01 }, { 0x0B3C, 0x0B3C }, + { 0x0B3F, 0x0B3F }, { 0x0B41, 0x0B43 }, { 0x0B4D, 0x0B4D }, + { 0x0B56, 0x0B56 }, { 0x0B82, 0x0B82 }, { 0x0BC0, 0x0BC0 }, + { 0x0BCD, 0x0BCD }, { 0x0C3E, 0x0C40 }, { 0x0C46, 0x0C48 }, + { 0x0C4A, 0x0C4D }, { 0x0C55, 0x0C56 }, { 0x0CBC, 0x0CBC }, + { 0x0CBF, 0x0CBF }, { 0x0CC6, 0x0CC6 }, { 0x0CCC, 0x0CCD }, + { 0x0CE2, 0x0CE3 }, { 0x0D41, 0x0D43 }, { 0x0D4D, 0x0D4D }, + { 0x0DCA, 0x0DCA }, { 0x0DD2, 0x0DD4 }, { 0x0DD6, 0x0DD6 }, + { 0x0E31, 0x0E31 }, { 0x0E34, 0x0E3A }, { 0x0E47, 0x0E4E }, + { 0x0EB1, 0x0EB1 }, { 0x0EB4, 0x0EB9 }, { 0x0EBB, 0x0EBC }, + { 0x0EC8, 0x0ECD }, { 0x0F18, 0x0F19 }, { 0x0F35, 0x0F35 }, + { 0x0F37, 0x0F37 }, { 0x0F39, 0x0F39 }, { 0x0F71, 0x0F7E }, + { 0x0F80, 0x0F84 }, { 0x0F86, 0x0F87 }, { 0x0F90, 0x0F97 }, + { 0x0F99, 0x0FBC }, { 0x0FC6, 0x0FC6 }, { 0x102D, 0x1030 }, + { 0x1032, 0x1032 }, { 0x1036, 0x1037 }, { 0x1039, 0x1039 }, + { 0x1058, 0x1059 }, { 0x1160, 0x11FF }, { 0x135F, 0x135F }, + { 0x1712, 0x1714 }, { 0x1732, 0x1734 }, { 0x1752, 0x1753 }, + { 0x1772, 0x1773 }, { 0x17B4, 0x17B5 }, { 0x17B7, 0x17BD }, + { 0x17C6, 0x17C6 }, { 0x17C9, 0x17D3 }, { 0x17DD, 0x17DD }, + { 0x180B, 0x180D }, { 0x18A9, 0x18A9 }, { 0x1920, 0x1922 }, + { 0x1927, 0x1928 }, { 0x1932, 0x1932 }, { 0x1939, 0x193B }, + { 0x1A17, 0x1A18 }, { 0x1B00, 0x1B03 }, { 0x1B34, 0x1B34 }, + { 0x1B36, 0x1B3A }, { 0x1B3C, 0x1B3C }, { 0x1B42, 0x1B42 }, + { 0x1B6B, 0x1B73 }, { 0x1DC0, 0x1DCA }, { 0x1DFE, 0x1DFF }, + { 0x200B, 0x200F }, { 0x202A, 0x202E }, { 0x2060, 0x2063 }, + { 0x206A, 0x206F }, { 0x20D0, 0x20EF }, { 0x302A, 0x302F }, + { 0x3099, 0x309A }, { 0xA806, 0xA806 }, { 0xA80B, 0xA80B }, + { 0xA825, 0xA826 }, { 0xFB1E, 0xFB1E }, { 0xFE00, 0xFE0F }, + { 0xFE20, 0xFE23 }, { 0xFEFF, 0xFEFF }, { 0xFFF9, 0xFFFB }, + { 0x10A01, 0x10A03 }, { 0x10A05, 0x10A06 }, { 0x10A0C, 0x10A0F }, + { 0x10A38, 0x10A3A }, { 0x10A3F, 0x10A3F }, { 0x1D167, 0x1D169 }, + { 0x1D173, 0x1D182 }, { 0x1D185, 0x1D18B }, { 0x1D1AA, 0x1D1AD }, + { 0x1D242, 0x1D244 }, { 0xE0001, 0xE0001 }, { 0xE0020, 0xE007F }, + { 0xE0100, 0xE01EF } + }; + + /* + * The following functions are the same as mk_wcwidth() and + * mk_wcswidth(), except that spacing characters in the East Asian + * Ambiguous (A) category as defined in Unicode Technical Report #11 + * have a column width of 2. This variant might be useful for users of + * CJK legacy encodings who want to migrate to UCS without changing + * the traditional terminal character-width behaviour. It is not + * otherwise recommended for general use. + */ + + // sorted list of non-overlapping intervals of East Asian Ambiguous + // characters, generated by "uniset +WIDTH-A -cat=Me -cat=Mn -cat=Cf c" + static uint[,] ambiguous = new uint[,] { + { 0x00A1, 0x00A1 }, { 0x00A4, 0x00A4 }, { 0x00A7, 0x00A8 }, + { 0x00AA, 0x00AA }, { 0x00AE, 0x00AE }, { 0x00B0, 0x00B4 }, + { 0x00B6, 0x00BA }, { 0x00BC, 0x00BF }, { 0x00C6, 0x00C6 }, + { 0x00D0, 0x00D0 }, { 0x00D7, 0x00D8 }, { 0x00DE, 0x00E1 }, + { 0x00E6, 0x00E6 }, { 0x00E8, 0x00EA }, { 0x00EC, 0x00ED }, + { 0x00F0, 0x00F0 }, { 0x00F2, 0x00F3 }, { 0x00F7, 0x00FA }, + { 0x00FC, 0x00FC }, { 0x00FE, 0x00FE }, { 0x0101, 0x0101 }, + { 0x0111, 0x0111 }, { 0x0113, 0x0113 }, { 0x011B, 0x011B }, + { 0x0126, 0x0127 }, { 0x012B, 0x012B }, { 0x0131, 0x0133 }, + { 0x0138, 0x0138 }, { 0x013F, 0x0142 }, { 0x0144, 0x0144 }, + { 0x0148, 0x014B }, { 0x014D, 0x014D }, { 0x0152, 0x0153 }, + { 0x0166, 0x0167 }, { 0x016B, 0x016B }, { 0x01CE, 0x01CE }, + { 0x01D0, 0x01D0 }, { 0x01D2, 0x01D2 }, { 0x01D4, 0x01D4 }, + { 0x01D6, 0x01D6 }, { 0x01D8, 0x01D8 }, { 0x01DA, 0x01DA }, + { 0x01DC, 0x01DC }, { 0x0251, 0x0251 }, { 0x0261, 0x0261 }, + { 0x02C4, 0x02C4 }, { 0x02C7, 0x02C7 }, { 0x02C9, 0x02CB }, + { 0x02CD, 0x02CD }, { 0x02D0, 0x02D0 }, { 0x02D8, 0x02DB }, + { 0x02DD, 0x02DD }, { 0x02DF, 0x02DF }, { 0x0391, 0x03A1 }, + { 0x03A3, 0x03A9 }, { 0x03B1, 0x03C1 }, { 0x03C3, 0x03C9 }, + { 0x0401, 0x0401 }, { 0x0410, 0x044F }, { 0x0451, 0x0451 }, + { 0x2010, 0x2010 }, { 0x2013, 0x2016 }, { 0x2018, 0x2019 }, + { 0x201C, 0x201D }, { 0x2020, 0x2022 }, { 0x2024, 0x2027 }, + { 0x2030, 0x2030 }, { 0x2032, 0x2033 }, { 0x2035, 0x2035 }, + { 0x203B, 0x203B }, { 0x203E, 0x203E }, { 0x2074, 0x2074 }, + { 0x207F, 0x207F }, { 0x2081, 0x2084 }, { 0x20AC, 0x20AC }, + { 0x2103, 0x2103 }, { 0x2105, 0x2105 }, { 0x2109, 0x2109 }, + { 0x2113, 0x2113 }, { 0x2116, 0x2116 }, { 0x2121, 0x2122 }, + { 0x2126, 0x2126 }, { 0x212B, 0x212B }, { 0x2153, 0x2154 }, + { 0x215B, 0x215E }, { 0x2160, 0x216B }, { 0x2170, 0x2179 }, + { 0x2190, 0x2199 }, { 0x21B8, 0x21B9 }, { 0x21D2, 0x21D2 }, + { 0x21D4, 0x21D4 }, { 0x21E7, 0x21E7 }, { 0x2200, 0x2200 }, + { 0x2202, 0x2203 }, { 0x2207, 0x2208 }, { 0x220B, 0x220B }, + { 0x220F, 0x220F }, { 0x2211, 0x2211 }, { 0x2215, 0x2215 }, + { 0x221A, 0x221A }, { 0x221D, 0x2220 }, { 0x2223, 0x2223 }, + { 0x2225, 0x2225 }, { 0x2227, 0x222C }, { 0x222E, 0x222E }, + { 0x2234, 0x2237 }, { 0x223C, 0x223D }, { 0x2248, 0x2248 }, + { 0x224C, 0x224C }, { 0x2252, 0x2252 }, { 0x2260, 0x2261 }, + { 0x2264, 0x2267 }, { 0x226A, 0x226B }, { 0x226E, 0x226F }, + { 0x2282, 0x2283 }, { 0x2286, 0x2287 }, { 0x2295, 0x2295 }, + { 0x2299, 0x2299 }, { 0x22A5, 0x22A5 }, { 0x22BF, 0x22BF }, + { 0x2312, 0x2312 }, { 0x2460, 0x24E9 }, { 0x24EB, 0x254B }, + { 0x2550, 0x2573 }, { 0x2580, 0x258F }, { 0x2592, 0x2595 }, + { 0x25A0, 0x25A1 }, { 0x25A3, 0x25A9 }, { 0x25B2, 0x25B3 }, + { 0x25B6, 0x25B7 }, { 0x25BC, 0x25BD }, { 0x25C0, 0x25C1 }, + { 0x25C6, 0x25C8 }, { 0x25CB, 0x25CB }, { 0x25CE, 0x25D1 }, + { 0x25E2, 0x25E5 }, { 0x25EF, 0x25EF }, { 0x2605, 0x2606 }, + { 0x2609, 0x2609 }, { 0x260E, 0x260F }, { 0x2614, 0x2615 }, + { 0x261C, 0x261C }, { 0x261E, 0x261E }, { 0x2640, 0x2640 }, + { 0x2642, 0x2642 }, { 0x2660, 0x2661 }, { 0x2663, 0x2665 }, + { 0x2667, 0x266A }, { 0x266C, 0x266D }, { 0x266F, 0x266F }, + { 0x273D, 0x273D }, { 0x2776, 0x277F }, { 0xE000, 0xF8FF }, + { 0xFFFD, 0xFFFD }, { 0xF0000, 0xFFFFD }, { 0x100000, 0x10FFFD } + }; + + static int bisearch (uint rune, uint [,] table, int max) + { + int min = 0; + int mid; + + if (rune < table [0,0]|| rune> table[max,1]) + return 0; + while (max >= min) { + mid = (min + max) / 2; + if (rune > table [mid,1]) + min = mid + 1; + else if (rune < table [mid,0]) + max = mid - 1; + else + return 1; + } + + return 0; + } + + // + // Returns the number of columns/characters that a rune uses + // + public static int ConsoleWidth (this uint rune) + { + if (rune < 32) + return 0; + if (rune < 127) + return 1; + if (rune >= 0x7f && rune <= 0xa0) + return 0; + /* binary search in table of non-spacing characters */ + if (bisearch (rune, combining, combining.GetLength (0) - 1) != 0) + return 0; + + if (bisearch(rune, ambiguous, ambiguous.GetLength(0) - 1) != 0) + return 2; + + /* if we arrive here, ucs is not a combining or C0/C1 control character */ + return 1 + + ((rune >= 0x1100 && + (rune <= 0x115f || /* Hangul Jamo init. consonants */ + rune == 0x2329 || rune == 0x232a || + (rune >= 0x2e80 && rune <= 0xa4cf && + rune != 0x303f) || /* CJK ... Yi */ + (rune >= 0xac00 && rune <= 0xd7a3) || /* Hangul Syllables */ + (rune >= 0xf900 && rune <= 0xfaff) || /* CJK Compatibility Ideographs */ + (rune >= 0xfe10 && rune <= 0xfe19) || /* Vertical forms */ + (rune >= 0xfe30 && rune <= 0xfe6f) || /* CJK Compatibility Forms */ + (rune >= 0xff00 && rune <= 0xff60) || /* Fullwidth Forms */ + (rune >= 0xffe0 && rune <= 0xffe6) || + (rune >= 0x20000 && rune <= 0x2fffd) || + (rune >= 0x30000 && rune <= 0x3fffd))) ? 1 : 0); + } + } +} diff --git a/src/XtermSharp/CharacterAttribute.cs b/src/XtermSharp/CharacterAttribute.cs new file mode 100644 index 00000000..fb3a0e49 --- /dev/null +++ b/src/XtermSharp/CharacterAttribute.cs @@ -0,0 +1,72 @@ +using System; + +namespace XtermSharp { + // TODO: rename to CharacterAttributes or similar + [Flags] + public enum FLAGS { + BOLD = 1, + UNDERLINE = 2, + BLINK = 4, + INVERSE = 8, + INVISIBLE = 16, + DIM = 32, + ITALIC = 64, + CrossedOut = 128 + } + + public static class CharacterAttribute { + // Temporary, longer term in Attribute we will add a proper encoding + public static string ToSGR (int attribute) + { + var result = "0"; + + var ca = (FLAGS)(attribute >> 18); + if (ca.HasFlag (FLAGS.BOLD)) { + result += ";1"; + } + if (ca.HasFlag (FLAGS.UNDERLINE)) { + result += ";4"; + } + if (ca.HasFlag (FLAGS.BLINK)) { + result += ";5"; + } + if (ca.HasFlag (FLAGS.INVERSE)) { + result += ";7"; + } + if (ca.HasFlag (FLAGS.INVISIBLE)) { + result += ";8"; + } + + int fg = (attribute >> 9) & 0x1ff; + + if (fg != Renderer.DefaultColor) { + if (fg > 16) { + result += $";38;5;{fg}"; + } else { + if (fg >= 8) { + result += $";{9}{fg - 8};"; + } else { + result += $";{3}{fg};"; + } + } + } + + int bg = attribute & 0x1ff; + if (bg != Renderer.DefaultColor) { + if (bg > 16) { + result += $";48;5;{bg}"; + } else { + if (bg >= 8) { + result += $";{10}{bg - 8};"; + } else { + result += $";{4}{bg};"; + } + } + } + + result += "m"; + return result; + } + + } +} diff --git a/src/XtermSharp/Colors.cs b/src/XtermSharp/Colors.cs new file mode 100644 index 00000000..6d3708f0 --- /dev/null +++ b/src/XtermSharp/Colors.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; + +namespace XtermSharp { + + public class Color { + public byte Red, Green, Blue; + public static List DefaultAnsiColors; + public static Color DefaultForeground = new Color (0xff, 0xff, 0xff); + public static Color DefaultBackground = new Color (0, 0, 0); + + static Color () + { + DefaultAnsiColors = new List () { + // dark colors + new Color (0x2e, 0x34, 0x36), + new Color (0xcc, 0x00, 0x00), + new Color (0x4e, 0x9a, 0x06), + new Color (0xc4, 0xa0, 0x00), + new Color (0x34, 0x65, 0xa4), + new Color (0x75, 0x50, 0x7b), + new Color (0x06, 0x98, 0x9a), + new Color (0xd3, 0xd7, 0xcf), + // bright colors + new Color (0x55, 0x57, 0x53), + new Color (0xef, 0x29, 0x29), + new Color (0x8a, 0xe2, 0x34), + new Color (0xfc, 0xe9, 0x4f), + new Color (0x72, 0x9f, 0xcf), + new Color (0xad, 0x7f, 0xa8), + new Color (0x34, 0xe2, 0xe2), + new Color (0xee, 0xee, 0xec), + }; + + // Fill in the remaining 240 ANSI colors. + var v = new int [] { 0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff }; + + // Generate colors (16-231) + for (var i = 0; i < 216; i++) { + var r = v [(i / 36) % 6]; + var g = v [(i / 6) % 6]; + var b = v [i % 6]; + + DefaultAnsiColors.Add (new Color ((byte)r, (byte)g, (byte)b)); + } + + // Generate greys (232-255) + for (int i = 0; i < 24; i++) { + var c = (byte)(8 + i * 10); + DefaultAnsiColors.Add (new Color (c, c, c)); + } + } + + public Color (byte red, byte green, byte blue) + { + Red = red; + Green = green; + Blue = blue; + } + } +} diff --git a/src/XtermSharp/ControlCodes.cs b/src/XtermSharp/ControlCodes.cs new file mode 100644 index 00000000..4431ceb2 --- /dev/null +++ b/src/XtermSharp/ControlCodes.cs @@ -0,0 +1,56 @@ +using System; + +namespace XtermSharp { + public struct ControlCodes { + public static uint NUL = 0x00; + public static uint BEL = 0x07; + public static uint BS = 0x08; + public static uint HT = 0x09; + public static uint LF = 0x0a; + public static uint VT = 0x0b; + public static uint FF = 0x0c; + public static uint CR = 0x0d; + public static uint SO = 0x0e; + public static uint SI = 0x0f; + public static uint CAN = 0x18; + public static uint SUB = 0x1a; + public static uint ESC = 0x1b; + public static uint SP = 0x20; + public static uint DEL = 0x7f; + + public bool Send8bit { get; set; } + + public string PAD { get { return Send8bit ? "\u0080" : "\u001b@"; } } + public string HOP { get { return Send8bit ? "\u0081" : "\u001bA"; } } + public string BPH { get { return Send8bit ? "\u0082" : "\u001bB"; } } + public string NBH { get { return Send8bit ? "\u0083" : "\u001bC"; } } + public string IND { get { return Send8bit ? "\u0084" : "\u001bD"; } } + public string NEL { get { return Send8bit ? "\u0085" : "\u001bE"; } } + public string SSA { get { return Send8bit ? "\u0086" : "\u001bF"; } } + public string ESA { get { return Send8bit ? "\u0087" : "\u001bG"; } } + public string HTS { get { return Send8bit ? "\u0088" : "\u001bH"; } } + public string HTJ { get { return Send8bit ? "\u0089" : "\u001bI"; } } + public string VTS { get { return Send8bit ? "\u008a" : "\u001bJ"; } } + public string PLD { get { return Send8bit ? "\u008b" : "\u001bK"; } } + public string PLU { get { return Send8bit ? "\u008c" : "\u001bL"; } } + public string RI { get { return Send8bit ? "\u008d" : "\u001bM"; } } + public string SS2 { get { return Send8bit ? "\u008e" : "\u001bN"; } } + public string SS3 { get { return Send8bit ? "\u008f" : "\u001bO"; } } + public string DCS { get { return Send8bit ? "\u0090" : "\u001bP"; } } + public string PU1 { get { return Send8bit ? "\u0091" : "\u001bQ"; } } + public string PU2 { get { return Send8bit ? "\u0092" : "\u001bR"; } } + public string STS { get { return Send8bit ? "\u0093" : "\u001bS"; } } + public string CCH { get { return Send8bit ? "\u0094" : "\u001bT"; } } + public string MW { get { return Send8bit ? "\u0095" : "\u001bU"; } } + public string SPA { get { return Send8bit ? "\u0096" : "\u001bV"; } } + public string EPA { get { return Send8bit ? "\u0097" : "\u001bW"; } } + public string SOS { get { return Send8bit ? "\u0098" : "\u001bX"; } } + public string SGCI{ get { return Send8bit ? "\u0099" : "\u001bY"; } } + public string SCI { get { return Send8bit ? "\u009a" : "\u001bZ"; } } + public string CSI { get { return Send8bit ? "\u009b" : "\u001b["; } } + public string ST { get { return Send8bit ? "\u009c" : "\u001b\\"; } } + public string OSC { get { return Send8bit ? "\u009d" : "\u001b]"; } } + public string PM { get { return Send8bit ? "\u009e" : "\u001b^"; } } + public string APC { get { return Send8bit ? "\u009f" : "\u001b_"; } } + } +} diff --git a/src/XtermSharp/EscapeSequenceParser.cs b/src/XtermSharp/EscapeSequenceParser.cs new file mode 100644 index 00000000..7d314a75 --- /dev/null +++ b/src/XtermSharp/EscapeSequenceParser.cs @@ -0,0 +1,697 @@ +// +// This could use an audit for the use of Rune when dealing with "code" values, +// in particular in Execute code paths +// +using System; +using System.Collections.Generic; +using System.Linq; +using NStack; + +namespace XtermSharp { + + public interface IDcsHandler { + void Hook (string collect, int [] parameters, int flag); + unsafe void Put (byte *data, int start, int end); + void Unhook (); + } + + // Dummy DCS Handler as defaulta fallback + class DcsDummy : IDcsHandler { + public void Hook (string collect, int [] parameters, int flag) { } + public unsafe void Put (byte *data, int start, int end) { } + public void Unhook () { } + } + + public enum ParserState { + Invalid = -1, + Ground = 0, + Escape, + EscapeIntermediate, + CsiEntry, + CsiParam, + CsiIntermediate, + CsiIgnore, + SosPmApcString, + OscString, + DcsEntry, + DcsParam, + DcsIgnore, + DcsIntermediate, + DcsPassthrough + } + + public class ParsingState { + /// + /// Position in Parse String + /// + public int Position; + /// + /// Actual character code + /// + public int Code; + /// + /// Current Parser State + /// + public ParserState CurrentState; + /// + /// Print buffer start index (-1 for not set) + /// + public int Print; + /// + /// Buffer start index (-1 for not set) + /// + public int Dcs; + /// + /// Osc string buffer + /// + public string Osc; + /// + /// Collect buffer with intermediate characters + /// + public string Collect; + /// + /// Parameters buffer + /// + public int [] Parameters; + // should abort (default: false) + public bool Abort; + + } + + // + // EscapeSequenceParser. + // This class implements the ANSI/DEC compatible parser described by + // Paul Williams (https://vt100.net/emu/dec_ansi_parser). + // To implement custom ANSI compliant escape sequences it is not needed to + // alter this parser, instead consider registering a custom handler. + // For non ANSI compliant sequences change the transition table with + // the optional `transitions` contructor argument and + // reimplement the `parse` method. + // NOTE: The parameter element notation is currently not supported. + // TODO: implement error recovery hook via error handler return values + // + public class EscapeSequenceParser : IDisposable { + + + enum ParserAction { + Ignore, + Error, + Print, + Execute, + OscStart, + OscPut, + OscEnd, + CsiDispatch, + Param, + Collect, + EscDispatch, + Clear, + DcsHook, + DcsPut, + DcsUnhook + } + + class TransitionTable { + // data is packed like this: + // currentState << 8 | characterCode --> action << 4 | nextState + byte [] table; + + public TransitionTable (int length) + { + table = new byte [length]; + } + + public void Add (int code, ParserState state, ParserAction action, ParserState next = ParserState.Invalid) + { + table [(int)state << 8 | code] = (byte)((int)action << 4 | (int)(next == ParserState.Invalid ? state : next)); + } + + public void Add (int [] codes, ParserState state, ParserAction action, ParserState next = ParserState.Invalid) + { + foreach (var c in codes) + Add (c, state, action, next); + } + + public byte this [int idx] { + get => table [idx]; + } + } + static int [] PRINTABLES = r (0x20, 0x7f); + static int [] EXECUTABLES = r (0x00, 0x19).Concat (r (0x1c, 0x20)).ToArray (); + + static int [] r (int low, int high) + { + var c = high - low; + var arr = new int [c]; + while (c-- > 0) + arr [c] = --high; + return arr; + } + + static ParserState [] r (ParserState low, ParserState high) + { + var c = high - low; + var arr = new ParserState [c]; + while (c-- > 0) + arr [c] = --high; + return arr; + } + + const int NonAsciiPrintable = 0xa0; + + static TransitionTable BuildVt500TransitionTable () + { + var table = new TransitionTable (4095); + var states = r (ParserState.Ground, ParserState.DcsPassthrough + 1); + + // table with default transition + foreach (var state in states) { + for (var code = 0; code <= NonAsciiPrintable; ++code) + table.Add (code, state, ParserAction.Error, ParserState.Ground); + } + // printables + table.Add (PRINTABLES, ParserState.Ground, ParserAction.Print, ParserState.Ground); + + // global anwyhere rules + foreach (var state in states) { + table.Add (new int [] { 0x18, 0x1a, 0x99, 0x9a }, state, ParserAction.Execute, ParserState.Ground); + table.Add (r (0x80, 0x90), state, ParserAction.Execute, ParserState.Ground); + table.Add (r (0x90, 0x98), state, ParserAction.Execute, ParserState.Ground); + table.Add (0x9c, state, ParserAction.Ignore, ParserState.Ground); // ST as terminator + table.Add (0x1b, state, ParserAction.Clear, ParserState.Escape); // ESC + table.Add (0x9d, state, ParserAction.OscStart, ParserState.OscString); // OSC + table.Add (new int [] { 0x98, 0x9e, 0x9f }, state, ParserAction.Ignore, ParserState.SosPmApcString); + table.Add (0x9b, state, ParserAction.Clear, ParserState.CsiEntry); // CSI + table.Add (0x90, state, ParserAction.Clear, ParserState.DcsEntry); // DCS + } + + // rules for executable and 0x7f + table.Add (EXECUTABLES, ParserState.Ground, ParserAction.Execute, ParserState.Ground); + table.Add (EXECUTABLES, ParserState.Escape, ParserAction.Execute, ParserState.Escape); + table.Add (0x7f, ParserState.Escape, ParserAction.Ignore, ParserState.Escape); + table.Add (EXECUTABLES, ParserState.OscString, ParserAction.Ignore, ParserState.OscString); + table.Add (EXECUTABLES, ParserState.CsiEntry, ParserAction.Execute, ParserState.CsiEntry); + table.Add (0x7f, ParserState.CsiEntry, ParserAction.Ignore, ParserState.CsiEntry); + table.Add (EXECUTABLES, ParserState.CsiParam, ParserAction.Execute, ParserState.CsiParam); + table.Add (0x7f, ParserState.CsiParam, ParserAction.Ignore, ParserState.CsiParam); + table.Add (EXECUTABLES, ParserState.CsiIgnore, ParserAction.Execute, ParserState.CsiIgnore); + table.Add (EXECUTABLES, ParserState.CsiIntermediate, ParserAction.Execute, ParserState.CsiIntermediate); + table.Add (0x7f, ParserState.CsiIntermediate, ParserAction.Ignore, ParserState.CsiIntermediate); + table.Add (EXECUTABLES, ParserState.EscapeIntermediate, ParserAction.Execute, ParserState.EscapeIntermediate); + table.Add (0x7f, ParserState.EscapeIntermediate, ParserAction.Ignore, ParserState.EscapeIntermediate); + // osc + table.Add (0x5d, ParserState.Escape, ParserAction.OscStart, ParserState.OscString); + table.Add (PRINTABLES, ParserState.OscString, ParserAction.OscPut, ParserState.OscString); + table.Add (0x7f, ParserState.OscString, ParserAction.OscPut, ParserState.OscString); + table.Add (new int [] { 0x9c, 0x1b, 0x18, 0x1a, 0x07 }, ParserState.OscString, ParserAction.OscEnd, ParserState.Ground); + table.Add (r (0x1c, 0x20), ParserState.OscString, ParserAction.Ignore, ParserState.OscString); + // sos/pm/apc does nothing + table.Add (new int [] { 0x58, 0x5e, 0x5f }, ParserState.Escape, ParserAction.Ignore, ParserState.SosPmApcString); + table.Add (PRINTABLES, ParserState.SosPmApcString, ParserAction.Ignore, ParserState.SosPmApcString); + table.Add (EXECUTABLES, ParserState.SosPmApcString, ParserAction.Ignore, ParserState.SosPmApcString); + table.Add (0x9c, ParserState.SosPmApcString, ParserAction.Ignore, ParserState.Ground); + table.Add (0x7f, ParserState.SosPmApcString, ParserAction.Ignore, ParserState.SosPmApcString); + // csi entries + table.Add (0x5b, ParserState.Escape, ParserAction.Clear, ParserState.CsiEntry); + table.Add (r (0x40, 0x7f), ParserState.CsiEntry, ParserAction.CsiDispatch, ParserState.Ground); + table.Add (r (0x30, 0x3a), ParserState.CsiEntry, ParserAction.Param, ParserState.CsiParam); + table.Add (0x3b, ParserState.CsiEntry, ParserAction.Param, ParserState.CsiParam); + table.Add (new int [] { 0x3c, 0x3d, 0x3e, 0x3f }, ParserState.CsiEntry, ParserAction.Collect, ParserState.CsiParam); + table.Add (r (0x30, 0x3a), ParserState.CsiParam, ParserAction.Param, ParserState.CsiParam); + table.Add (0x3b, ParserState.CsiParam, ParserAction.Param, ParserState.CsiParam); + table.Add (r (0x40, 0x7f), ParserState.CsiParam, ParserAction.CsiDispatch, ParserState.Ground); + table.Add (new int [] { 0x3a, 0x3c, 0x3d, 0x3e, 0x3f }, ParserState.CsiParam, ParserAction.Ignore, ParserState.CsiIgnore); + table.Add (r (0x20, 0x40), ParserState.CsiIgnore, ParserAction.Ignore, ParserState.CsiIgnore); + table.Add (0x7f, ParserState.CsiIgnore, ParserAction.Ignore, ParserState.CsiIgnore); + table.Add (r (0x40, 0x7f), ParserState.CsiIgnore, ParserAction.Ignore, ParserState.Ground); + table.Add (0x3a, ParserState.CsiEntry, ParserAction.Ignore, ParserState.CsiIgnore); + table.Add (r (0x20, 0x30), ParserState.CsiEntry, ParserAction.Collect, ParserState.CsiIntermediate); + table.Add (r (0x20, 0x30), ParserState.CsiIntermediate, ParserAction.Collect, ParserState.CsiIntermediate); + table.Add (r (0x30, 0x40), ParserState.CsiIntermediate, ParserAction.Ignore, ParserState.CsiIgnore); + table.Add (r (0x40, 0x7f), ParserState.CsiIntermediate, ParserAction.CsiDispatch, ParserState.Ground); + table.Add (r (0x20, 0x30), ParserState.CsiParam, ParserAction.Collect, ParserState.CsiIntermediate); + // escIntermediate + table.Add (r (0x20, 0x30), ParserState.Escape, ParserAction.Collect, ParserState.EscapeIntermediate); + table.Add (r (0x20, 0x30), ParserState.EscapeIntermediate, ParserAction.Collect, ParserState.EscapeIntermediate); + table.Add (r (0x30, 0x7f), ParserState.EscapeIntermediate, ParserAction.EscDispatch, ParserState.Ground); + table.Add (r (0x30, 0x50), ParserState.Escape, ParserAction.EscDispatch, ParserState.Ground); + table.Add (r (0x51, 0x58), ParserState.Escape, ParserAction.EscDispatch, ParserState.Ground); + table.Add (new int [] { 0x59, 0x5a, 0x5c }, ParserState.Escape, ParserAction.EscDispatch, ParserState.Ground); + table.Add (r (0x60, 0x7f), ParserState.Escape, ParserAction.EscDispatch, ParserState.Ground); + // dcs entry + table.Add (0x50, ParserState.Escape, ParserAction.Clear, ParserState.DcsEntry); + table.Add (EXECUTABLES, ParserState.DcsEntry, ParserAction.Ignore, ParserState.DcsEntry); + table.Add (0x7f, ParserState.DcsEntry, ParserAction.Ignore, ParserState.DcsEntry); + table.Add (r (0x1c, 0x20), ParserState.DcsEntry, ParserAction.Ignore, ParserState.DcsEntry); + table.Add (r (0x20, 0x30), ParserState.DcsEntry, ParserAction.Collect, ParserState.DcsIntermediate); + table.Add (0x3a, ParserState.DcsEntry, ParserAction.Ignore, ParserState.DcsIgnore); + table.Add (r (0x30, 0x3a), ParserState.DcsEntry, ParserAction.Param, ParserState.DcsParam); + table.Add (0x3b, ParserState.DcsEntry, ParserAction.Param, ParserState.DcsParam); + table.Add (new int [] { 0x3c, 0x3d, 0x3e, 0x3f }, ParserState.DcsEntry, ParserAction.Collect, ParserState.DcsParam); + table.Add (EXECUTABLES, ParserState.DcsIgnore, ParserAction.Ignore, ParserState.DcsIgnore); + table.Add (r (0x20, 0x80), ParserState.DcsIgnore, ParserAction.Ignore, ParserState.DcsIgnore); + table.Add (r (0x1c, 0x20), ParserState.DcsIgnore, ParserAction.Ignore, ParserState.DcsIgnore); + table.Add (EXECUTABLES, ParserState.DcsParam, ParserAction.Ignore, ParserState.DcsParam); + table.Add (0x7f, ParserState.DcsParam, ParserAction.Ignore, ParserState.DcsParam); + table.Add (r (0x1c, 0x20), ParserState.DcsParam, ParserAction.Ignore, ParserState.DcsParam); + table.Add (r (0x30, 0x3a), ParserState.DcsParam, ParserAction.Param, ParserState.DcsParam); + table.Add (0x3b, ParserState.DcsParam, ParserAction.Param, ParserState.DcsParam); + table.Add (new int [] { 0x3a, 0x3c, 0x3d, 0x3e, 0x3f }, ParserState.DcsParam, ParserAction.Ignore, ParserState.DcsIgnore); + table.Add (r (0x20, 0x30), ParserState.DcsParam, ParserAction.Collect, ParserState.DcsIntermediate); + table.Add (EXECUTABLES, ParserState.DcsIntermediate, ParserAction.Ignore, ParserState.DcsIntermediate); + table.Add (0x7f, ParserState.DcsIntermediate, ParserAction.Ignore, ParserState.DcsIntermediate); + table.Add (r (0x1c, 0x20), ParserState.DcsIntermediate, ParserAction.Ignore, ParserState.DcsIntermediate); + table.Add (r (0x20, 0x30), ParserState.DcsIntermediate, ParserAction.Collect, ParserState.DcsIntermediate); + table.Add (r (0x30, 0x40), ParserState.DcsIntermediate, ParserAction.Ignore, ParserState.DcsIgnore); + table.Add (r (0x40, 0x7f), ParserState.DcsIntermediate, ParserAction.DcsHook, ParserState.DcsPassthrough); + table.Add (r (0x40, 0x7f), ParserState.DcsParam, ParserAction.DcsHook, ParserState.DcsPassthrough); + table.Add (r (0x40, 0x7f), ParserState.DcsEntry, ParserAction.DcsHook, ParserState.DcsPassthrough); + table.Add (EXECUTABLES, ParserState.DcsPassthrough, ParserAction.DcsPut, ParserState.DcsPassthrough); + table.Add (PRINTABLES, ParserState.DcsPassthrough, ParserAction.DcsPut, ParserState.DcsPassthrough); + table.Add (0x7f, ParserState.DcsPassthrough, ParserAction.Ignore, ParserState.DcsPassthrough); + table.Add (new int [] { 0x1b, 0x9c }, ParserState.DcsPassthrough, ParserAction.DcsUnhook, ParserState.Ground); + table.Add (NonAsciiPrintable, ParserState.OscString, ParserAction.OscPut, ParserState.OscString); + + return table; + } + + public delegate void CsiHandler (int [] parameters, string collect); + public delegate void OscHandler (string data); + public delegate void EscHandler (string collect, int flag); + public unsafe delegate void PrintHandler (byte * data, int start, int end); + public delegate void ExecuteHandler (); + + // Handler lookup container + public Dictionary> CsiHandlers; + public Dictionary> OscHandlers; + public Dictionary ExecuteHandlers; + public Dictionary EscHandlers; + public Dictionary DcsHandlers; + public IDcsHandler ActiveDcsHandler; + public Func ErrorHandler; + + public ParserState initialState, currentState; + + static void EmptyExecuteHandler (byte code) { } + static ParsingState EmptyErrorHandler (ParsingState state) => state; + + // Fallback handlers + public unsafe PrintHandler PrintHandlerFallback = (data, start, end) => { }; + public Action ExecuteHandlerFallback = EmptyExecuteHandler; + public Action CsiHandlerFallback = (collect, parameters, flag) => { Console.WriteLine ("Can not handle ESC-[" + flag); }; + public EscHandler EscHandlerFallback = (collect, flag) => { }; + public Action OscHandlerFallback = (identifier, data) => { }; + public IDcsHandler DcsHandlerFallback = new DcsDummy (); + public Func ErrorHandlerFallback = (state) => state; + + // buffers over several parser calls + string _osc; + List _pars; + string _collect; + unsafe PrintHandler printHandler = (data, start, end) => { }; + public Action PrintStateReset = () => { }; + + + TransitionTable table; + public EscapeSequenceParser () + { + table = BuildVt500TransitionTable (); + CsiHandlers = new Dictionary> (); + OscHandlers = new Dictionary> (); + ExecuteHandlers = new Dictionary (); + EscHandlers = new Dictionary (); + DcsHandlers = new Dictionary (); + + initialState = ParserState.Ground; + currentState = initialState; + _osc = ""; + _pars = new List { 0 } ; + _collect = ""; + SetEscHandler ("\\", EscHandlerFallback); + } + + public void Dispose () + { + CsiHandlers = null; + OscHandlers = null; + ExecuteHandlers = null; + EscHandlers = null; + DcsHandlers = null; + ActiveDcsHandler = null; + ErrorHandler = null; + PrintHandlerFallback = null; + ExecuteHandlerFallback = null; + CsiHandlerFallback = null; + EscHandlerFallback = null; + OscHandlerFallback = null; + DcsHandlerFallback = null; + ErrorHandlerFallback = null; + printHandler = null; + } + + public void SetPrintHandler (PrintHandler printHandler) => this.printHandler = printHandler; + public void ClearPrintHandler () => printHandler = PrintHandlerFallback; + + public void SetExecuteHandler (byte flag, ExecuteHandler handler) => ExecuteHandlers [flag] = handler; + public void ClearExecuteHandler (byte flag) => ExecuteHandlers.Remove (flag); + public void SetExecuteHandlerFallback (Action fallback) => ExecuteHandlerFallback = fallback; + + public void SetEscHandler (string flag, EscHandler callback) => EscHandlers [flag] = callback; + public void ClearEscHandler (string flag) => EscHandlers.Remove (flag); + public void SetEscHandlerFallback (EscHandler fallback) => EscHandlerFallback = fallback; + + class CsiHandlerRemover : IDisposable { + public List Container; + public CsiHandler ToRemove; + + void IDisposable.Dispose () + { + Container.Remove (ToRemove); + } + } + + IDisposable AddCsiHandler (byte flag, CsiHandler callback) + { + List list; + + if (!CsiHandlers.ContainsKey (flag)) { + list = new List (); + CsiHandlers [flag] = list; + } else + list = CsiHandlers [flag]; + + list.Add (callback); + return new CsiHandlerRemover () { + Container = list, + ToRemove = callback + }; + } + + public void SetCsiHandler (char flag, CsiHandler callback) => CsiHandlers [(byte)flag] = new List () { callback }; + public void ClearCsiHandler (byte flag) => CsiHandlers.Remove (flag); + public void SetCsiHandlerFallback (Action fallback) => CsiHandlerFallback = fallback; + + class OscHandlerRemover : IDisposable { + public List Container; + public OscHandler ToRemove; + + void IDisposable.Dispose () + { + Container.Remove (ToRemove); + } + } + + IDisposable AddOscHandler (int identifier, OscHandler callback) + { + List list; + + if (!OscHandlers.ContainsKey (identifier)) { + list = new List (); + OscHandlers [identifier] = list; + } else + list = OscHandlers [identifier]; + + list.Add (callback); + return new OscHandlerRemover () { + Container = list, + ToRemove = callback + }; + } + + public void SetOscHandler (int identifier, OscHandler callback) => OscHandlers [identifier] = new List () { callback }; + public void ClearOscHandler (int identifier) => OscHandlers.Remove (identifier); + public void SetOscHandlerFallback (Action fallback) => OscHandlerFallback = fallback; + + public void SetDcsHandler (string flag, IDcsHandler handler) => DcsHandlers [flag] = handler; + public void ClearDcsHandler (string flag) => DcsHandlers.Remove (flag); + public void SetDcsHandlerFallback (IDcsHandler fallback) => DcsHandlerFallback = fallback; + + public void SetErrorHandler (Func errorHandler) => ErrorHandler = errorHandler; + public void ClearErrorHandler () => ErrorHandler = EmptyErrorHandler; + + public void Reset () + { + currentState = initialState; + _osc = ""; + _pars.Clear (); + _pars.Add (0); + _collect = ""; + ActiveDcsHandler = null; + PrintStateReset (); + } + + unsafe public void Parse (byte *data, int len) + { + byte code = 0; + var transition = 0; + var error = false; + var currentState = this.currentState; + var print = -1; + var dcs = -1; + var osc = this._osc; + var collect = this._collect; + var pars = this._pars; + var dcsHandler = ActiveDcsHandler; + + // process input string + for (var i = 0; i < len; ++i) { + code = data [i]; + +#if false + // shortcut for most chars (print action) + if (currentState == ParserState.Ground && code > 0x1f && code < 0x80) { + print = (~print != 0) ? print : i; + do { i++; } while (i < len && data [i] > 0x1f && data [i] < 0x80); + i--; + continue; + } +#else + // This version eliminates the check for < 0x80, as we allow any UTF8 sequences. + if (currentState == ParserState.Ground && code > 0x1f) { + print = (~print != 0) ? print : i; + do { i++; } while (i < len && data [i] > 0x1f); + i--; + continue; + } + +#endif + + // shorcut for CSI params + if (currentState == ParserState.CsiParam && (code > 0x2f && code < 0x39)) { + pars [pars.Count - 1] = pars [pars.Count - 1] * 10 + code - 48; + continue; + } + + // Normal transition and action lookup + transition = table [(int)currentState << 8 | (code < 0xa0 ? code : NonAsciiPrintable)]; + var action = (ParserAction)(transition >> 4); + switch (action) { + case ParserAction.Print: + print = (~print != 0) ? print : i; + break; + case ParserAction.Execute: + if (~print != 0) { + printHandler (data, print, i); + print = -1; + } + if (ExecuteHandlers.TryGetValue (code, out var callback)) + callback (); + else + ExecuteHandlerFallback (code); + break; + case ParserAction.Ignore: + // handle leftover print or dcs chars + if (~print != 0) { + printHandler (data, print, i); + print = -1; + } else if (~dcs != 0) { + dcsHandler?.Put (data, dcs, i); + dcs = -1; + } + break; + case ParserAction.Error: + // chars higher than 0x9f are handled by this action + // to keep the transition table small + if (code > 0x9f) { + switch (currentState) { + case ParserState.Ground: + print = (~print != 0) ? print : i; + break; + case ParserState.CsiIgnore: + transition |= (int)ParserState.CsiIgnore; + break; + case ParserState.DcsIgnore: + transition |= (int)ParserState.DcsIgnore; + break; + case ParserState.DcsPassthrough: + dcs = (~dcs != 0) ? dcs : i; + transition |= (int)ParserState.DcsPassthrough; + break; + default: + error = true; + break; + } + } else { + error = true; + } + // if we end up here a real error happened + if (error) { + var inject = ErrorHandler (new ParsingState () { + Position = i, + Code = code, + CurrentState = currentState, + Print = print, + Dcs = dcs, + Osc = osc, + Collect = collect, + }); + if (inject.Abort) + return; + error = false; + } + break; + case ParserAction.CsiDispatch: + // Trigger CSI handler + if (CsiHandlers.TryGetValue (code, out var csiHandlers)) { + + var jj = csiHandlers.Count - 1; + for (; jj >= 0; jj--) { + csiHandlers [jj] (pars.ToArray (), collect); + } + } else + CsiHandlerFallback (collect, pars.ToArray (), code); + break; + case ParserAction.Param: + if (code == 0x3b) + pars.Add (0); + else + pars [pars.Count - 1] = pars [pars.Count - 1] * 10 + code - 48; + break; + case ParserAction.Collect: + // AUDIT: make collect a ustring + collect += (char)(code); + break; + case ParserAction.EscDispatch: + if (EscHandlers.TryGetValue (collect + (char)code, out var ehandler)) + ehandler (collect, code); + else + EscHandlerFallback (collect, code); + break; + case ParserAction.Clear: + if (~print != 0) { + printHandler (data, print, i); + print = -1; + } + osc = ""; + pars.Clear (); + pars.Add (0); + collect = ""; + dcs = -1; + PrintStateReset (); + break; + case ParserAction.DcsHook: + if (DcsHandlers.TryGetValue (collect + (char)code, out dcsHandler)) + dcsHandler.Hook (collect, pars.ToArray (), code); + else + DcsHandlerFallback.Hook (collect, pars.ToArray (), code); + break; + case ParserAction.DcsPut: + dcs = (~dcs != 0) ? dcs : i; + break; + case ParserAction.DcsUnhook: + if (dcsHandler != null) { + if (~dcs != 0) + dcsHandler.Put (data, dcs, i); + dcsHandler.Unhook (); + dcsHandler = null; + } + if (code == 0x1b) + transition |= (int)ParserState.Escape; + osc = ""; + pars.Clear (); + pars.Add (0); + collect = ""; + dcs = -1; + PrintStateReset (); + break; + case ParserAction.OscStart: + if (~print != 0) { + printHandler (data, print, i); + print = -1; + } + osc = ""; + break; + case ParserAction.OscPut: + for (var j = i; ; j++) { + if (j > len || (data [j] < 0x20) || (data [j] > 0x7f && data [j] < 0x9f)) { + var block = new byte [j - (i+1)]; + for (int k = i+1; k < j; k++) + block [k-i-1] = data [k]; + // TODO: Audit, the code below as I would not like the code below to abort on invalid UTF8 + // So we need a way of producing memory blocks. + osc += System.Text.Encoding.UTF8.GetString (block); + + i = j - 1; + break; + } + } + break; + case ParserAction.OscEnd: + if (osc != "" && code != 0x18 && code != 0x1a) { + // NOTE: OSC subparsing is not part of the original parser + // we do basic identifier parsing here to offer a jump table for OSC as well + int idx = osc.IndexOf (';'); + if (idx == -1) { + OscHandlerFallback (-1, osc); // this is an error mal-formed OSC + } else { + // Note: NaN is not handled here + // either catch it with the fallback handler + // or with an explicit NaN OSC handler + int identifier = 0; + Int32.TryParse (osc.Substring (0, idx), out identifier); + var content = osc.Substring (idx + 1); + // Trigger OSC handler + int c = -1; + if (OscHandlers.TryGetValue (identifier, out var ohandlers)) { + c = ohandlers.Count - 1; + for (; c >= 0;) { + ohandlers [c] (content); + break; + } + } + if (c < 0) + OscHandlerFallback (identifier, content); + } + } + if (code == 0x1b) + transition |= (int)ParserState.Escape; + osc = ""; + pars.Clear (); + pars.Add (0); + collect = ""; + dcs = -1; + PrintStateReset (); + break; + } + currentState = (ParserState)(transition & 15); + } + // push leftover pushable buffers to terminal + if (currentState == ParserState.Ground && (~print != 0)) { + printHandler (data, print, len); + } else if (currentState == ParserState.DcsPassthrough && (~dcs != 0) && dcsHandler != null) { + dcsHandler.Put (data, dcs, len); + } + + // save non pushable buffers + _osc = osc; + _collect = collect; + _pars = pars; + + // save active dcs handler reference + ActiveDcsHandler = dcsHandler; + + // save state + this.currentState = currentState; + } + } +} diff --git a/src/XtermSharp/EscapeSequences.cs b/src/XtermSharp/EscapeSequences.cs new file mode 100644 index 00000000..b8ca8410 --- /dev/null +++ b/src/XtermSharp/EscapeSequences.cs @@ -0,0 +1,41 @@ +using System; +namespace XtermSharp { + public static class EscapeSequences { + public static byte[] CmdNewline = { 10 }; + public static byte[] CmdRet = { 13 }; + public static byte[] CmdEsc = { 0x1b }; + public static byte[] CmdDel = { 0x7f }; + public static byte[] CmdDelKey = { 0x1b, (byte) '[', (byte) '3', (byte) '~' }; + public static byte[] MoveUpApp = { 0x1b, (byte)'O', (byte)'A' }; + public static byte[] MoveUpNormal = { 0x1b, (byte)'[', (byte)'A' }; + public static byte[] MoveDownApp = { 0x1b, (byte)'O', (byte)'B' }; + public static byte[] MoveDownNormal = { 0x1b, (byte)'[', (byte)'B' }; + public static byte[] MoveLeftApp = { 0x1b, (byte)'O', (byte)'D' }; + public static byte[] MoveLeftNormal = { 0x1b, (byte)'[', (byte)'D' }; + public static byte[] MoveRightApp = { 0x1b, (byte)'O', (byte)'C' }; + public static byte[] MoveRightNormal = { 0x1b, (byte)'[', (byte)'C' }; + public static byte[] MoveHomeApp = { 0x1b, (byte)'O', (byte)'H' }; + public static byte[] MoveHomeNormal = { 0x1b, (byte)'[', (byte)'H' }; + public static byte[] MoveEndApp = { 0x1b, (byte)'O', (byte)'F' }; + public static byte[] MoveEndNormal = { 0x1b, (byte)'[', (byte)'F' }; + public static byte[] CmdTab = { 9 }; + public static byte[] CmdBackTab = { 0x1b, (byte)'[', (byte)'Z' }; + public static byte[] CmdPageUp = { 0x1b, (byte)'[', (byte)'5', (byte)'~' }; + public static byte[] CmdPageDown = { 0x1b, (byte)'[', (byte)'6', (byte)'~' }; + + public static byte[][] CmdF = { + new byte [] { 0x1b, (byte) 'O', (byte) 'P' }, /* F1 */ + new byte [] { 0x1b, (byte) 'O', (byte) 'Q' }, /* F2 */ + new byte [] { 0x1b, (byte) 'O', (byte) 'R' }, /* F3 */ + new byte [] { 0x1b, (byte) 'O', (byte) 'S' }, /* F4 */ + new byte [] { 0x1b, (byte) '[', (byte) '1', (byte) '5', (byte) '~' }, /* F5 */ + new byte [] { 0x1b, (byte) '[', (byte) '1', (byte) '7', (byte) '~' }, /* F6 */ + new byte [] { 0x1b, (byte) '[', (byte) '1', (byte) '8', (byte) '~' }, /* F7 */ + new byte [] { 0x1b, (byte) '[', (byte) '1', (byte) '9', (byte) '~' }, /* F8 */ + new byte [] { 0x1b, (byte) '[', (byte) '2', (byte) '0', (byte) '~' }, /* F9 */ + new byte [] { 0x1b, (byte) '[', (byte) '2', (byte) '1', (byte) '~' }, /* F10 */ + new byte [] { 0x1b, (byte) '[', (byte) '2', (byte) '3', (byte) '~' }, /* F11 */ + new byte [] { 0x1b, (byte) '[', (byte) '2', (byte) '4', (byte) '~' }, /* F12 */ + }; + } +} diff --git a/src/XtermSharp/GlobalSuppressions.cs b/src/XtermSharp/GlobalSuppressions.cs new file mode 100644 index 00000000..577049d0 --- /dev/null +++ b/src/XtermSharp/GlobalSuppressions.cs @@ -0,0 +1,8 @@ + +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage ("Potential Code Quality Issues", "RECS0082:Parameter has the same name as a member and hides it", Justification = "", Scope = "member", Target = "~M:Application.EscapeSequenceParser.SetPrintHandler(Application.EscapeSequenceParser.PrintHandler)")] + diff --git a/src/XtermSharp/ITerminalDelegate.cs b/src/XtermSharp/ITerminalDelegate.cs new file mode 100644 index 00000000..b3aa79c2 --- /dev/null +++ b/src/XtermSharp/ITerminalDelegate.cs @@ -0,0 +1,84 @@ +namespace XtermSharp { + + /// + /// + /// + public interface ITerminalDelegate { + /// + /// + /// + void ShowCursor (Terminal source); + + /// + /// This event is raised when the title of the terminal has been changed + /// + void SetTerminalTitle (Terminal source, string title); + + /// + /// This event is raised when the icon title of the terminal has been changed + /// + void SetTerminalIconTitle (Terminal source, string title); + + /// + /// This event is triggered from the engine, when the request to resize the window is received from an escape sequence. + /// + void SizeChanged (Terminal source); + + /// + /// Used to respond to the client running on the other end. This information should be sent to the remote end or subshell. + /// + void Send (byte [] data); + + /// + /// These are various commands that are sent by the client. They are rare, + /// and if you do not know what to return, just return null, the terminal + /// will return a suitable value. + /// + /// The response string needs to be suitable for the Xterm CSI Ps; Ps ; Ps t command + /// see the WindowManipulationCommand enumeration for those that need to return values + /// + string WindowCommand (Terminal source, WindowManipulationCommand command, params int[] args); + + /// + /// This method should return `true` if operations that can read the buffer back should be allowed, + /// otherwise, return false. This is useful to run some applications that attempt to checksum the + /// contents of the screen (unit tests) + /// + bool IsProcessTrusted (); + } + + /// + /// A simple ITerminalDelegate that does nothing + /// + public class SimpleTerminalDelegate : ITerminalDelegate { + public virtual void Send (byte [] data) + { + } + + public virtual void SetTerminalTitle (Terminal source, string title) + { + } + + public virtual void SetTerminalIconTitle (Terminal source, string title) + { + } + + public virtual void ShowCursor (Terminal source) + { + } + + public virtual void SizeChanged (Terminal source) + { + } + + public virtual string WindowCommand (Terminal source, WindowManipulationCommand command, int [] args) + { + return null; + } + + public virtual bool IsProcessTrusted () + { + return true; + } + } +} diff --git a/src/XtermSharp/InputHandlers/CsiCommandExtensions.cs b/src/XtermSharp/InputHandlers/CsiCommandExtensions.cs new file mode 100644 index 00000000..9bc7a7d6 --- /dev/null +++ b/src/XtermSharp/InputHandlers/CsiCommandExtensions.cs @@ -0,0 +1,25 @@ +using System; +using XtermSharp.CommandExtensions; + +namespace XtermSharp.CsiCommandExtensions { + /// + /// Simple extensions to map CSI commands to terminal commands. Useful in porting esc tests + /// + internal static class CsiCommands { + public static void csiDECSET (this Terminal terminal, int mode) + { + terminal.csiDECSET (mode, "?"); + } + + public static void csiCUP (this Terminal terminal, (int col, int row) point) + { + // switch the params around + terminal.csiCUP (point.row, point.col); + } + + public static void csiDECRESET (this Terminal terminal, int mode) + { + terminal.csiDECRESET (mode, "?"); + } + } +} \ No newline at end of file diff --git a/src/XtermSharp/InputHandlers/DECRQSS.cs b/src/XtermSharp/InputHandlers/DECRQSS.cs new file mode 100644 index 00000000..baabead3 --- /dev/null +++ b/src/XtermSharp/InputHandlers/DECRQSS.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; + +// +// Not implemented (either in xterm.js): +// DECUDK (https://vt100.net/docs/vt510-rm/DECUDK.html) +// DCS + q Pt ST (xterm) * Request Terminfo String +// DCS + p Pt ST (xterm) * set terminfo data + +namespace XtermSharp { + /// + /// DCS Subparser implementations + /// + /// DCS $ q Pt ST + /// DECRQSS (https://vt100.net/docs/vt510-rm/DECRQSS.html) + /// Request Status String (DECRQSS), VT420 and up. + /// Response: DECRPSS (https://vt100.net/docs/vt510-rm/DECRPSS.html) + /// + class DECRQSS : IDcsHandler { + readonly Terminal terminal; + List data; + + public DECRQSS (Terminal terminal) + { + this.terminal = terminal; + } + + public void Hook (string collect, int [] parameters, int flag) + { + data = new List (); + } + + unsafe public void Put (byte* data, int start, int end) + { + for (int i = start; i < end; i++) + this.data.Add (data [i]); + } + + public void Unhook () + { + var newData = System.Text.Encoding.Default.GetString (data.ToArray ()); + int ok = 1; // 0 means the request is valid according to docs, but tests expect 0? + string result = null; + + switch (newData) { + case "\"q": // DECCSA - Set Character Attribute + result = "\"q"; + return; + case "\"p": // DECSCL - conformance level + result = "65;1\"p"; + break; + case "r": // DECSTBM - the top and bottom margins + result = $"{terminal.Buffer.ScrollTop + 1};{terminal.Buffer.ScrollBottom + 1}r"; + break; + case "m": // SGR- the set graphic rendition + // TODO: report real settings instead of 0m + result = CharacterAttribute.ToSGR (terminal.CurAttr); + break; + case "s": // DECSLRM - the current left and right margins + result = $"{terminal.Buffer.MarginLeft + 1};{terminal.Buffer.MarginRight + 1}s"; + break; + case " q": // DECSCUSR - the set cursor style + // TODO this should send a number for the current cursor style 2 for block, 4 for underline and 6 for bar + var style = "2"; // block + result = $"{style} q"; + break; + default: + ok = 0; // this means the request is not valid, report that to the host. + result = string.Empty; + // invalid: DCS 0 $ r Pt ST (xterm) + terminal.Error ($"Unknown DCS + {newData}"); + break; + } + + terminal.SendResponse ($"{terminal.ControlCodes.DCS}{ok}$r{result}{terminal.ControlCodes.ST}"); + } + } +} diff --git a/src/XtermSharp/InputHandlers/InputHandler.cs b/src/XtermSharp/InputHandlers/InputHandler.cs new file mode 100644 index 00000000..d3102a08 --- /dev/null +++ b/src/XtermSharp/InputHandlers/InputHandler.cs @@ -0,0 +1,1374 @@ +using System; +using System.Collections.Generic; +using System.Text; +using NStack; +using XtermSharp.CommandExtensions; + +// +// Not implemented (either in xterm.js): +// DECUDK (https://vt100.net/docs/vt510-rm/DECUDK.html) +// DCS + q Pt ST (xterm) * Request Terminfo String +// DCS + p Pt ST (xterm) * set terminfo data + +namespace XtermSharp { + // + // The terminal's standard implementation of IInputHandler, this handles all + // input from the Parser. + // + // Refer to http://invisible-island.net/xterm/ctlseqs/ctlseqs.html to understand + // each function's header comment. + // + class InputHandler { + readonly ReadingBuffer readingBuffer; + readonly Terminal terminal; + readonly EscapeSequenceParser parser; + + public InputHandler (Terminal terminal) + { + this.terminal = terminal; + readingBuffer = new ReadingBuffer (); + + parser = new EscapeSequenceParser (); + parser.SetCsiHandlerFallback ((string collect, int [] pars, int flag) => { + terminal.Error ("Unknown CSI code", collect, pars, flag); + }); + parser.SetEscHandlerFallback ((string collect, int flag) => { + terminal.Error ("Unknown ESC code", collect, flag); + }); + parser.SetExecuteHandlerFallback ((code) => { + terminal.Error ("Unknown EXECUTE code", code); + }); + parser.SetOscHandlerFallback ((int identifier, string data) => { + terminal.Error ("Unknown OSC code", identifier, data); + }); + + // Print handler + unsafe { parser.SetPrintHandler (Print); } + parser.PrintStateReset = PrintStateReset; + + // CSI handler + parser.SetCsiHandler ('@', (pars, collect) => InsertChars (pars)); + parser.SetCsiHandler ('A', (pars, collect) => terminal.csiCUU (pars)); + parser.SetCsiHandler ('B', (pars, collect) => terminal.csiCUD (pars)); + parser.SetCsiHandler ('C', (pars, collect) => terminal.csiCUF (pars)); + parser.SetCsiHandler ('D', (pars, collect) => terminal.csiCUB (pars)); + parser.SetCsiHandler ('E', (pars, collect) => CursorNextLine (pars)); + parser.SetCsiHandler ('F', (pars, collect) => CursorPrecedingLine (pars)); + parser.SetCsiHandler ('G', (pars, collect) => terminal.csiCHA (pars)); + parser.SetCsiHandler ('H', (pars, collect) => terminal.csiCUP (pars)); + parser.SetCsiHandler ('I', (pars, collect) => CursorForwardTab (pars)); + parser.SetCsiHandler ('J', (pars, collect) => EraseInDisplay (pars)); + parser.SetCsiHandler ('K', (pars, collect) => EraseInLine (pars)); + parser.SetCsiHandler ('L', (pars, collect) => InsertLines (pars)); + parser.SetCsiHandler ('M', (pars, collect) => terminal.csiDL (pars)); + parser.SetCsiHandler ('P', (pars, collect) => terminal.csiDCH (pars)); + parser.SetCsiHandler ('S', (pars, collect) => ScrollUp (pars)); + parser.SetCsiHandler ('T', (pars, collect) => ScrollDown (pars)); + parser.SetCsiHandler ('X', (pars, collect) => EraseChars (pars)); + parser.SetCsiHandler ('Z', (pars, collect) => terminal.csiCBT (pars)); + parser.SetCsiHandler ('`', (pars, collect) => CharPosAbsolute (pars)); + parser.SetCsiHandler ('a', (pars, collect) => HPositionRelative (pars)); + parser.SetCsiHandler ('b', (pars, collect) => RepeatPrecedingCharacter (pars)); + parser.SetCsiHandler ('c', (pars, collect) => terminal.csiDA1 (pars, collect)); + parser.SetCsiHandler ('d', (pars, collect) => LinePosAbsolute (pars)); + parser.SetCsiHandler ('e', (pars, collect) => VPositionRelative (pars)); + parser.SetCsiHandler ('f', (pars, collect) => HVPosition (pars)); + parser.SetCsiHandler ('g', (pars, collect) => TabClear (pars)); + parser.SetCsiHandler ('h', (pars, collect) => SetMode (pars, collect)); + parser.SetCsiHandler ('l', (pars, collect) => ResetMode (pars, collect)); + parser.SetCsiHandler ('m', (pars, collect) => CharAttributes (pars)); + parser.SetCsiHandler ('n', (pars, collect) => terminal.csiDSR (pars, collect)); + parser.SetCsiHandler ('p', (pars, collect) => { + switch (collect) { + case "!": + terminal.SoftReset (); + break; + case "\"": + //TODO: SetConformanceLevel (pars, collect); + break; + default: + terminal.Error ("Unknown CSI code", collect, pars, "p"); + break; + } + }); + parser.SetCsiHandler ('q', (pars, collect) => SetCursorStyle (pars, collect)); + parser.SetCsiHandler ('r', (pars, collect) => { + if (collect == "") { + terminal.csiDECSTBM (pars); + } + }); + parser.SetCsiHandler ('s', (pars, collect) => { + // "CSI s" is overloaded, can mean save cursor, but also set the margins with DECSLRM + if (terminal.MarginMode) { + this.terminal.csiDECSLRM (pars); + } else { + terminal.SaveCursor (); + } + }); + parser.SetCsiHandler ('t', (pars, collect) => terminal.csiDISPATCH (pars)); + parser.SetCsiHandler ('u', (pars, collect) => terminal.RestoreCursor ()); + parser.SetCsiHandler ('v', (pars, collect) => terminal.csiDECCRA (pars, collect)); + parser.SetCsiHandler ('y', (pars, collect) => terminal.csiDECRQCRA (pars)); + parser.SetCsiHandler ('x', (pars, collect) => { + switch (collect) { + case "$": + terminal.csiDECFRA (pars); + break; + default: + terminal.Error ("Unknown CSI code", collect, pars, "x"); + break; + } + }); + parser.SetCsiHandler ('z', (pars, collect) => { + switch (collect) { + case "$": + terminal.csiDECERA (pars); + break; + case "'": + // TODO: Enable Locator Reporting (DECELR) + // Enable Locator Reporting (DECELR). + // Valid values for the first parameter: + // Ps = 0 ⇒ Locator disabled (default). + // Ps = 1 ⇒ Locator enabled. + // Ps = 2 ⇒ Locator enabled for one report, then disabled. + // The second parameter specifies the coordinate unit for locator + // reports. + // Valid values for the second parameter: + // Pu = 0 or omitted ⇒ default to character cells. + // Pu = 1 ⇐ device physical pixels. + // Pu = 2 ⇐ character cells. + break; + default: + terminal.Error ("Unknown CSI code", collect, pars, "z"); + break; + } + }); + parser.SetCsiHandler ('{', (pars, collect) => { + switch (collect) { + case "$": + terminal.csiDECSERA (pars); + break; + default: + terminal.Error ("Unknown CSI code", collect, pars, "{"); + break; + } + }); + parser.SetCsiHandler ('}', (pars, collect) => { + switch (collect) { + case "'": + terminal.csiDECIC (pars); + break; + default: + terminal.Error ("Unknown CSI code", collect, pars, "}"); + break; + } + }); + parser.SetCsiHandler ('~', (pars, collect) => terminal.csiDECDC (pars)); + + + + + // Execute Handler + parser.SetExecuteHandler (7, terminal.Bell); + parser.SetExecuteHandler (10, terminal.LineFeed); + parser.SetExecuteHandler (11, terminal.LineFeedBasic); // VT Vertical Tab - ignores auto-new-line behavior in ConvertEOL + parser.SetExecuteHandler (12, terminal.LineFeedBasic); + parser.SetExecuteHandler (13, terminal.CarriageReturn); + parser.SetExecuteHandler (8, terminal.Backspace); + parser.SetExecuteHandler (9, Tab); + parser.SetExecuteHandler (14, ShiftOut); + parser.SetExecuteHandler (15, ShiftIn); + // Comment in original FIXME: What do to with missing? Old code just added those to print. + + // some C1 control codes - FIXME: should those be enabled by default? + parser.SetExecuteHandler (0x84 /* Index */, () => terminal.Index ()); + parser.SetExecuteHandler (0x85 /* Next Line */, terminal.NextLine); + parser.SetExecuteHandler (0x88 /* Horizontal Tabulation Set */, TabSet); + + // + // OSC handler + // + // 0 - icon name + title + parser.SetOscHandler (0, SetTitleAndIcon); + // 1 - icon name + parser.SetOscHandler (1, SetIconTitle); + // 2 - title + parser.SetOscHandler (2, SetTitle); + // 3 - set property X in the form "prop=value" + // 4 - Change Color Number() + // 5 - Change Special Color Number + // 6 - Enable/disable Special Color Number c + // 7 - current directory? (not in xterm spec, see https://gitlab.com/gnachman/iterm2/issues/3939) + // 10 - Change VT100 text foreground color to Pt. + // 11 - Change VT100 text background color to Pt. + // 12 - Change text cursor color to Pt. + // 13 - Change mouse foreground color to Pt. + // 14 - Change mouse background color to Pt. + // 15 - Change Tektronix foreground color to Pt. + // 16 - Change Tektronix background color to Pt. + // 17 - Change highlight background color to Pt. + // 18 - Change Tektronix cursor color to Pt. + // 19 - Change highlight foreground color to Pt. + // 46 - Change Log File to Pt. + // 50 - Set Font to Pt. + // 51 - reserved for Emacs shell. + // 52 - Manipulate Selection Data. + // 104 ; c - Reset Color Number c. + // 105 ; c - Reset Special Color Number c. + // 106 ; c; f - Enable/disable Special Color Number c. + // 110 - Reset VT100 text foreground color. + // 111 - Reset VT100 text background color. + // 112 - Reset text cursor color. + // 113 - Reset mouse foreground color. + // 114 - Reset mouse background color. + // 115 - Reset Tektronix foreground color. + // 116 - Reset Tektronix background color. + + // + // ESC handlers + // + parser.SetEscHandler ("7", (c, f) => terminal.SaveCursor()); + parser.SetEscHandler ("8", (c, f) => terminal.RestoreCursor()); + parser.SetEscHandler ("D", (c, f) => terminal.Index ()); + parser.SetEscHandler ("E", (c, b) => terminal.NextLine ()); + parser.SetEscHandler ("H", (c, f) => TabSet ()); + parser.SetEscHandler ("M", (c, f) => ReverseIndex ()); + parser.SetEscHandler ("=", (c, f) => KeypadApplicationMode ()); + parser.SetEscHandler (">", (c, f) => KeypadNumericMode ()); + parser.SetEscHandler ("c", (c, f) => Reset ()); + parser.SetEscHandler ("n", (c, f) => SetgLevel (2)); + parser.SetEscHandler ("o", (c, f) => SetgLevel (3)); + parser.SetEscHandler ("|", (c, f) => SetgLevel (3)); + parser.SetEscHandler ("}", (c, f) => SetgLevel (2)); + parser.SetEscHandler ("~", (c, f) => SetgLevel (1)); + parser.SetEscHandler ("%@", (c, f) => SelectDefaultCharset ()); + parser.SetEscHandler ("%G", (c, f) => SelectDefaultCharset ()); + parser.SetEscHandler ("#3", (c, f) => SetDoubleHeightTop ()); // dhtop + parser.SetEscHandler ("#4", (c, f) => SetDoubleHeightBottom ()); // dhbot + parser.SetEscHandler ("#5", (c, f) => SingleWidthSingleHeight ()); // swsh + parser.SetEscHandler ("#6", (c, f) => DoubleWidthSingleHeight ()); // dwsh + foreach (var bflag in CharSets.All.Keys) { + char flag = (char)bflag; + parser.SetEscHandler ("(" + flag, (code, f) => SelectCharset ("(" + flag)); + parser.SetEscHandler (")" + flag, (code, f) => SelectCharset (")" + flag)); + parser.SetEscHandler ("*" + flag, (code, f) => SelectCharset ("*" + flag)); + parser.SetEscHandler ("+" + flag, (code, f) => SelectCharset ("+" + flag)); + parser.SetEscHandler ("-" + flag, (code, f) => SelectCharset ("-" + flag)); + parser.SetEscHandler ("." + flag, (code, f) => SelectCharset ("." + flag)); + parser.SetEscHandler ("/" + flag, (code, f) => SelectCharset ("/" + flag)); // TODO: supported? + } + + // Error handler + parser.SetErrorHandler ((state) => { + terminal.Error ("Parsing error, state: ", state); + return state; + }); + + // DCS Handler + parser.SetDcsHandler ("$q", new DECRQSS (terminal)); + } + + public void Parse (byte [] data, int length = -1) + { + if (length == -1) + length = data.Length; + + var buffer = terminal.Buffer; + var cursorStartX = buffer.X; + var cursorStartY = buffer.Y; + + unsafe { + fixed (byte* p = &data [0]) { + parser.Parse (p, length); + } + } + + buffer = terminal.Buffer; + } + + public void Parse (IntPtr data, int length) + { + var buffer = terminal.Buffer; + var cursorStartX = buffer.X; + var cursorStartY = buffer.Y; + + unsafe { parser.Parse ((byte*)data, length); } + + buffer = terminal.Buffer; + } + + // + // CSI Ps L + // Insert Ps Line(s) (default = 1) (IL). + // + private void InsertLines (int [] pars) + { + var p = Math.Max (pars.Length == 0 ? 1 : pars [0], 1); + var buffer = terminal.Buffer; + var row = buffer.Y + buffer.YBase; + + var scrollBottomRowsOffset = terminal.Rows - 1 - buffer.ScrollBottom; + var scrollBottomAbsolute = terminal.Rows - 1 + buffer.YBase - scrollBottomRowsOffset + 1; + + var eraseAttr = terminal.EraseAttr (); + while (p-- != 0) { + // test: echo -e '\e[44m\e[1L\e[0m' + // blankLine(true) - xterm/linux behavior + buffer.Lines.Splice (scrollBottomAbsolute - 1, 1); + var newLine = buffer.GetBlankLine (eraseAttr); + buffer.Lines.Splice (row, 0, newLine); + } + + // this.maxRange(); + terminal.UpdateRange (buffer.Y); + terminal.UpdateRange (buffer.ScrollBottom); + } + + // + // ESC ( C + // Designate G0 Character Set, VT100, ISO 2022. + // ESC ) C + // Designate G1 Character Set (ISO 2022, VT100). + // ESC * C + // Designate G2 Character Set (ISO 2022, VT220). + // ESC + C + // Designate G3 Character Set (ISO 2022, VT220). + // ESC - C + // Designate G1 Character Set (VT300). + // ESC . C + // Designate G2 Character Set (VT300). + // ESC / C + // Designate G3 Character Set (VT300). C = A -> ISO Latin-1 Supplemental. - Supported? + // + void SelectCharset (string p) + { + if (p.Length != 2) + SelectDefaultCharset (); + byte ch; + + Dictionary charset; + if (!CharSets.All.TryGetValue ((byte)p [1], out charset)) + charset = null; + + switch (p [0]) { + case '(': + ch = 0; + break; + case ')': + case '-': + ch = 1; + break; + case '*': + case '.': + ch = 2; + break; + case '+': + ch = 3; + break; + default: + // includes '/' -> unsupported? (MIGUEL TODO) + return; + } + terminal.SetgCharset (ch, charset); + } + + // + // ESC # NUMBER + // + void DoubleWidthSingleHeight () + { + } + + // + // dhtop + // + void SetDoubleHeightTop () {} + + // dhbot + void SetDoubleHeightBottom () { } // dhbot + + // + // swsh + // + void SingleWidthSingleHeight () { } + + // + // ESC % @ + // ESC % G + // Select default character set. UTF-8 is not supported (string are unicode anyways) + // therefore ESC % G does the same. + // + void SelectDefaultCharset () + { + terminal.SetgLevel (0); + terminal.SetgCharset (0, CharSets.Default); + } + + // + // ESC n + // ESC o + // ESC | + // ESC } + // ESC ~ + // DEC mnemonic: LS (https://vt100.net/docs/vt510-rm/LS.html) + // When you use a locking shift, the character set remains in GL or GR until + // you use another locking shift. (partly supported) + // + void SetgLevel (int n) + { + terminal.SetgLevel (n); + } + + // + // ESC c + // DEC mnemonic: RIS (https://vt100.net/docs/vt510-rm/RIS.html) + // Reset to initial state. + // + void Reset () + { + parser.Reset (); + terminal.Reset (); + } + + // + // ESC > + // DEC mnemonic: DECKPNM (https://vt100.net/docs/vt510-rm/DECKPNM.html) + // Enables the keypad to send numeric characters to the host. + // + void KeypadNumericMode () + { + terminal.ApplicationKeypad = false; + terminal.SyncScrollArea (); + } + + // + // ESC = + // DEC mnemonic: DECKPAM (https://vt100.net/docs/vt510-rm/DECKPAM.html) + // Enables the numeric keypad to send application sequences to the host. + // + void KeypadApplicationMode () + { + terminal.ApplicationKeypad = true; + terminal.SyncScrollArea (); + } + + // + // ESC M + // C1.RI + // DEC mnemonic: HTS + // Moves the cursor up one line in the same column. If the cursor is at the top margin, + // the page scrolls down. + // + void ReverseIndex () + { + terminal.ReverseIndex (); + } + + /// + /// OSC 0; ST (set window and icon title) + /// Proxy to set window title. + /// + /// + void SetTitleAndIcon (string data) + { + terminal.SetTitle (data ?? string.Empty); + terminal.SetIconTitle (data ?? string.Empty); + } + + /// + /// OSC 2; ST (set window title) + /// Proxy to set window title. + /// + /// + void SetTitle (string data) + { + terminal.SetTitle (data ?? string.Empty); + } + + /// + /// OSC 1; ST (set window title) + /// Proxy to set icon title. + /// + void SetIconTitle (string data) + { + terminal.SetIconTitle (data ?? string.Empty); + } + + // + // ESC H + // C1.HTS + // DEC mnemonic: HTS (https://vt100.net/docs/vt510-rm/HTS.html) + // Sets a horizontal tab stop at the column position indicated by + // the value of the active column when the terminal receives an HTS. + // + void TabSet () + { + terminal.Buffer.TabSet (terminal.Buffer.X); + } + + // SI + // ShiftIn (Control-O) Switch to standard character set. This invokes the G0 character set + void ShiftIn () + { + terminal.SetgLevel (0); + } + + // SO + // ShiftOut (Control-N) Switch to alternate character set. This invokes the G1 character set + void ShiftOut () + { + terminal.SetgLevel (1); + } + + // + // Horizontal tab (Control-I) + // + void Tab () + { + var originalX = terminal.Buffer.X; + terminal.Buffer.X = terminal.Buffer.NextTabStop (); + if (terminal.Options.ScreenReaderMode) + terminal.EmitA11yTab (terminal.Buffer.X - originalX); + } + + // + // Helper method to erase cells in a terminal row. + // The cell gets replaced with the eraseChar of the terminal. + // @param y row index + // @param start first cell index to be erased + // @param end end - 1 is last erased cell + // + void EraseInBufferLine (int y, int start, int end, bool clearWrap = false) + { + var line = terminal.Buffer.Lines [terminal.Buffer.YBase + y]; + var cd = new CharData (terminal.EraseAttr ()); + line.ReplaceCells (start, end, cd); + if (clearWrap) + line.IsWrapped = false; + } + + // + // Helper method to reset cells in a terminal row. + // The cell gets replaced with the eraseChar of the terminal and the isWrapped property is set to false. + // @param y row index + // + void ResetBufferLine (int y) + { + EraseInBufferLine (y, 0, terminal.Cols, true); + } + + // + // CSI Ps SP q Set cursor style (DECSCUSR, VT520). + // Ps = 0 -> blinking block. + // Ps = 1 -> blinking block (default). + // Ps = 2 -> steady block. + // Ps = 3 -> blinking underline. + // Ps = 4 -> steady underline. + // Ps = 5 -> blinking bar (xterm). + // Ps = 6 -> steady bar (xterm). + // + void SetCursorStyle (int [] pars, string collect) + { + if (collect != " ") + return; + + var p = Math.Max (pars.Length == 0 ? 1 : pars [0], 1); + switch (p) { + case 1: + terminal.SetCursorStyle (CursorStyle.BlinkBlock); + break; + case 2: + terminal.SetCursorStyle (CursorStyle.SteadyBlock); + break; + case 3: + terminal.SetCursorStyle (CursorStyle.BlinkUnderline); + break; + case 4: + terminal.SetCursorStyle (CursorStyle.SteadyUnderline); + break; + case 5: + terminal.SetCursorStyle (CursorStyle.BlinkingBar); + break; + case 6: + terminal.SetCursorStyle (CursorStyle.SteadyBar); + break; + } + } + + + // + // CSI Pm m Character Attributes (SGR). + // Ps = 0 -> Normal (default). + // Ps = 1 -> Bold. + // Ps = 2 -> Faint, decreased intensity (ISO 6429). + // Ps = 4 -> Underlined. + // Ps = 5 -> Blink (appears as Bold). + // Ps = 7 -> Inverse. + // Ps = 8 -> Invisible, i.e., hidden (VT300). + // Ps = 2 2 -> Normal (neither bold nor faint). + // Ps = 2 4 -> Not underlined. + // Ps = 2 5 -> Steady (not blinking). + // Ps = 2 7 -> Positive (not inverse). + // Ps = 2 8 -> Visible, i.e., not hidden (VT300). + // Ps = 3 0 -> Set foreground color to Black. + // Ps = 3 1 -> Set foreground color to Red. + // Ps = 3 2 -> Set foreground color to Green. + // Ps = 3 3 -> Set foreground color to Yellow. + // Ps = 3 4 -> Set foreground color to Blue. + // Ps = 3 5 -> Set foreground color to Magenta. + // Ps = 3 6 -> Set foreground color to Cyan. + // Ps = 3 7 -> Set foreground color to White. + // Ps = 3 9 -> Set foreground color to default (original). + // Ps = 4 0 -> Set background color to Black. + // Ps = 4 1 -> Set background color to Red. + // Ps = 4 2 -> Set background color to Green. + // Ps = 4 3 -> Set background color to Yellow. + // Ps = 4 4 -> Set background color to Blue. + // Ps = 4 5 -> Set background color to Magenta. + // Ps = 4 6 -> Set background color to Cyan. + // Ps = 4 7 -> Set background color to White. + // Ps = 4 9 -> Set background color to default (original). + // + // If 16-color support is compiled, the following apply. Assume + // that xterm's resources are set so that the ISO color codes are + // the first 8 of a set of 16. Then the aixterm colors are the + // bright versions of the ISO colors: + // Ps = 9 0 -> Set foreground color to Black. + // Ps = 9 1 -> Set foreground color to Red. + // Ps = 9 2 -> Set foreground color to Green. + // Ps = 9 3 -> Set foreground color to Yellow. + // Ps = 9 4 -> Set foreground color to Blue. + // Ps = 9 5 -> Set foreground color to Magenta. + // Ps = 9 6 -> Set foreground color to Cyan. + // Ps = 9 7 -> Set foreground color to White. + // Ps = 1 0 0 -> Set background color to Black. + // Ps = 1 0 1 -> Set background color to Red. + // Ps = 1 0 2 -> Set background color to Green. + // Ps = 1 0 3 -> Set background color to Yellow. + // Ps = 1 0 4 -> Set background color to Blue. + // Ps = 1 0 5 -> Set background color to Magenta. + // Ps = 1 0 6 -> Set background color to Cyan. + // Ps = 1 0 7 -> Set background color to White. + // + // If xterm is compiled with the 16-color support disabled, it + // supports the following, from rxvt: + // Ps = 1 0 0 -> Set foreground and background color to + // default. + // + // If 88- or 256-color support is compiled, the following apply. + // Ps = 3 8 ; 5 ; Ps -> Set foreground color to the second + // Ps. + // Ps = 4 8 ; 5 ; Ps -> Set background color to the second + // Ps. + // + void CharAttributes (int [] pars) + { + // Optimize a single SGR0. + if (pars.Length == 1 && pars [0] == 0) { + terminal.CurAttr = CharData.DefaultAttr; + return; + } + + var parCount = pars.Length; + var flags = (FLAGS)(terminal.CurAttr >> 18); + var fg = (terminal.CurAttr >> 9) & 0x1ff; + var bg = terminal.CurAttr & 0x1ff; + var def = CharData.DefaultAttr; + + for (var i = 0; i < parCount; i++) { + int p = pars [i]; + if (p >= 30 && p <= 37) { + // fg color 8 + fg = p - 30; + } else if (p >= 40 && p <= 47) { + // bg color 8 + bg = p - 40; + } else if (p >= 90 && p <= 97) { + // fg color 16 + p += 8; + fg = p - 90; + } else if (p >= 100 && p <= 107) { + // bg color 16 + p += 8; + bg = p - 100; + } else if (p == 0) { + // default + + flags = (FLAGS)(def >> 18); + fg = (def >> 9) & 0x1ff; + bg = def & 0x1ff; + // flags = 0; + // fg = 0x1ff; + // bg = 0x1ff; + } else if (p == 1) { + // bold text + flags |= FLAGS.BOLD; + } else if (p == 3) { + // italic text + flags |= FLAGS.ITALIC; + } else if (p == 4) { + // underlined text + flags |= FLAGS.UNDERLINE; + } else if (p == 5) { + // blink + flags |= FLAGS.BLINK; + } else if (p == 7) { + // inverse and positive + // test with: echo -e '\e[31m\e[42mhello\e[7mworld\e[27mhi\e[m' + flags |= FLAGS.INVERSE; + } else if (p == 8) { + // invisible + flags |= FLAGS.INVISIBLE; + } else if (p == 2) { + // dimmed text + flags |= FLAGS.DIM; + } else if (p == 22) { + // not bold nor faint + flags &= ~FLAGS.BOLD; + flags &= ~FLAGS.DIM; + } else if (p == 23) { + // not italic + flags &= ~FLAGS.ITALIC; + } else if (p == 24) { + // not underlined + flags &= ~FLAGS.UNDERLINE; + } else if (p == 25) { + // not blink + flags &= ~FLAGS.BLINK; + } else if (p == 27) { + // not inverse + flags &= ~FLAGS.INVERSE; + } else if (p == 28) { + // not invisible + flags &= ~FLAGS.INVISIBLE; + } else if (p == 39) { + // reset fg + fg = (CharData.DefaultAttr >> 9) & 0x1ff; + } else if (p == 49) { + // reset bg + bg = CharData.DefaultAttr & 0x1ff; + } else if (p == 38) { + if (i + 1 < parCount) { + // fg color 256 + if (pars [i + 1] == 2) { + // Well this is a problem, if there are 3 arguments, expect R/G/B, if there are + // more than 3, skip the first that would be the colorspace + if (i + 5 < parCount) { + i += 1; + } + if (i + 4 < parCount) { + fg = terminal.MatchColor ( + pars [i + 2] & 0xff, + pars [i + 3] & 0xff, + pars [i + 4] & 0xff); + if (fg == -1) + fg = 0x1ff; + } + // Given the historical disagreement that was caused by an ambiguous spec, + // we eat all the remaining parameters. + i = parCount; + } else if (pars [i + 1] == 5) { + if (i + 2 < parCount) { + p = pars [i + 2] & 0xff; + fg = p; + i += 1; + } + i += 1; + } + } + } else if (p == 48) { + if (i + 1 < parCount) { + // bg color 256 + if (pars [i + 1] == 2) { + // Well this is a problem, if there are 3 arguments, expect R/G/B, if there are + // more than 3, skip the first that would be the colorspace + if (i + 5 < parCount) { + i += 1; + } + if (i + 4 < parCount) { + bg = terminal.MatchColor ( + pars [i + 2] & 0xff, + pars [i + 3] & 0xff, + pars [i + 4] & 0xff); + if (bg == -1) + bg = 0x1ff; + } + // Given the historical disagreement that was caused by an ambiguous spec, + // we eat all the remaining parameters. + i = parCount; + } else if (pars [i + 1] == 5) { + if (i + 2 < parCount) { + p = pars [i + 2] & 0xff; + bg = p; + i += 1; + } + i += 1; + } + } + } else if (p == 100) { + // reset fg/bg + fg = (def >> 9) & 0x1ff; + bg = def & 0x1ff; + } else { + terminal.Error ("Unknown SGR attribute: %d.", p); + } + } + terminal.CurAttr = ((int)flags << 18) | (fg << 9) | bg; + } + + void ResetMode (int [] pars, string collect) + { + if (pars.Length == 0) + return; + + if (pars.Length > 1) { + for (var i = 0; i < pars.Length; i++) + terminal.csiDECRESET (pars [i], ""); + + + return; + } + terminal.csiDECRESET (pars [0], collect); + } + + void SetMode (int [] pars, string collect) + { + if (pars.Length == 0) + return; + + if (pars.Length > 1) { + for (var i = 0; i < pars.Length; i++) + terminal.csiDECSET (pars [i], ""); + + + return; + } + terminal.csiDECSET (pars [0], collect); + } + + + // + // CSI Ps g Tab Clear (TBC). + // Ps = 0 -> Clear Current Column (default). + // Ps = 3 -> Clear All. + // Potentially: + // Ps = 2 -> Clear Stops on Line. + // http://vt100.net/annarbor/aaa-ug/section6.html + // + void TabClear (int [] pars) + { + var p = pars.Length == 0 ? 0 : pars [0]; + var buffer = terminal.Buffer; + if (p == 0) + buffer.ClearStop (buffer.X); + else if (p == 3) + buffer.ClearTabStops (); + } + + // + // CSI Ps ; Ps f + // Horizontal and Vertical Position [row;column] (default = + // [1,1]) (HVP). + // + void HVPosition (int [] pars) + { + int p = 1; + int q = 1; + if (pars.Length > 0) { + p = Math.Max (pars [0], 1); + if (pars.Length > 1) + q = Math.Max (pars [1], 1); + } + var buffer = terminal.Buffer; + buffer.Y = p - 1; + if (buffer.Y >= terminal.Rows) + buffer.Y = terminal.Rows - 1; + + buffer.X = q - 1; + if (buffer.X >= terminal.Cols) + buffer.X = terminal.Cols - 1; + } + + // + // CSI Pm e Vertical Position Relative (VPR) + // [rows] (default = [row+1,column]) + // reuse CSI Ps B ? + // + void VPositionRelative (int [] pars) + { + var p = Math.Max (pars.Length == 0 ? 1 : pars [0], 1); + var buffer = terminal.Buffer; + + var newY = buffer.Y + p; + + if (newY >= terminal.Rows) { + buffer.Y = terminal.Rows - 1; + } else + buffer.Y = newY; + + // If the end of the line is hit, prevent this action from wrapping around to the next line. + if (buffer.X >= terminal.Cols) + buffer.X--; + } + + // + // CSI Pm d Vertical Position Absolute (VPA) + // [row] (default = [1,column]) + // + void LinePosAbsolute (int [] pars) + { + var p = Math.Max (pars.Length == 0 ? 1 : pars [0], 1); + var buffer = terminal.Buffer; + + if (p - 1 >= terminal.Rows) + buffer.Y = terminal.Rows - 1; + else + buffer.Y = p - 1; + } + + // + // CSI Ps b Repeat the preceding graphic character Ps times (REP). + // + void RepeatPrecedingCharacter (int [] pars) + { + var p = Math.Max (pars.Length == 0 ? 1 : pars [0], 1); + + var buffer = terminal.Buffer; + var line = buffer.Lines [buffer.YBase + buffer.Y]; + CharData cd = buffer.X - 1 < 0 ? new CharData (CharData.DefaultAttr) : line [buffer.X - 1]; + line.ReplaceCells (buffer.X, + buffer.X + p, + cd); + // FIXME: no UpdateRange here? + } + + // + //CSI Pm a Character Position Relative + // [columns] (default = [row,col+1]) (HPR) + //reuse CSI Ps C ? + // + void HPositionRelative (int [] pars) + { + var p = Math.Max (pars.Length == 0 ? 1 : pars [0], 1); + var buffer = terminal.Buffer; + + buffer.X += p; + if (buffer.X >= terminal.Cols) + buffer.X = terminal.Cols - 1; + } + + // + // CSI Pm ` Character Position Absolute + // [column] (default = [row,1]) (HPA). + // + void CharPosAbsolute (int [] pars) + { + var p = Math.Max (pars.Length == 0 ? 1 : pars [0], 1); + var buffer = terminal.Buffer; + + buffer.X = p - 1; + if (buffer.X >= terminal.Cols) + buffer.X = terminal.Cols - 1; + } + + // + // CSI Ps X + // Erase Ps Character(s) (default = 1) (ECH). + // + void EraseChars (int [] pars) + { + var p = Math.Max (pars.Length == 0 ? 1 : pars [0], 1); + + var buffer = terminal.Buffer; + buffer.Lines [buffer.Y + buffer.YBase].ReplaceCells ( + buffer.X, + buffer.X + p, + new CharData (terminal.EraseAttr ())); + } + + // + // CSI Ps T Scroll down Ps lines (default = 1) (SD). + // + void ScrollDown (int [] pars) + { + var p = Math.Max (pars.Length == 0 ? 1 : pars [0], 1); + var buffer = terminal.Buffer; + + while (p-- != 0) { + buffer.Lines.Splice (buffer.YBase + buffer.ScrollBottom, 1); + buffer.Lines.Splice (buffer.YBase + buffer.ScrollBottom, 0, buffer.GetBlankLine (CharData.DefaultAttr)); + } + // this.maxRange(); + terminal.UpdateRange (buffer.ScrollTop); + terminal.UpdateRange (buffer.ScrollBottom); + + } + + // + // CSI Ps S Scroll up Ps lines (default = 1) (SU). + // + void ScrollUp (int [] pars) + { + var p = Math.Max (pars.Length == 0 ? 1 : pars [0], 1); + var buffer = terminal.Buffer; + + while (p-- != 0) { + buffer.Lines.Splice (buffer.YBase + buffer.ScrollTop, 1); + buffer.Lines.Splice (buffer.YBase + buffer.ScrollBottom, 0, buffer.GetBlankLine (CharData.DefaultAttr)); + } + // this.maxRange(); + terminal.UpdateRange (buffer.ScrollTop); + terminal.UpdateRange (buffer.ScrollBottom); + } + + // + // CSI Ps K Erase in Line (EL). + // Ps = 0 -> Erase to Right (default). + // Ps = 1 -> Erase to Left. + // Ps = 2 -> Erase All. + // CSI ? Ps K + // Erase in Line (DECSEL). + // Ps = 0 -> Selective Erase to Right (default). + // Ps = 1 -> Selective Erase to Left. + // Ps = 2 -> Selective Erase All. + // + void EraseInLine (int [] pars) + { + var p = pars.Length == 0 ? 0 : pars [0]; + var buffer = terminal.Buffer; + switch (p) { + case 0: + EraseInBufferLine (buffer.Y, buffer.X, terminal.Cols); + break; + case 1: + EraseInBufferLine (buffer.Y, 0, buffer.X + 1); + break; + case 2: + EraseInBufferLine (buffer.Y, 0, terminal.Cols); + break; + } + terminal.UpdateRange (buffer.Y); + } + + // + // CSI Ps J Erase in Display (ED). + // Ps = 0 -> Erase Below (default). + // Ps = 1 -> Erase Above. + // Ps = 2 -> Erase All. + // Ps = 3 -> Erase Saved Lines (xterm). + // CSI ? Ps J + // Erase in Display (DECSED). + // Ps = 0 -> Selective Erase Below (default). + // Ps = 1 -> Selective Erase Above. + // Ps = 2 -> Selective Erase All. + // + void EraseInDisplay (int [] pars) + { + var p = pars.Length == 0 ? 0 : pars [0]; + var buffer = terminal.Buffer; + int j; + switch (p) { + case 0: + j = buffer.Y; + terminal.UpdateRange (j); + EraseInBufferLine (j++, buffer.X, terminal.Cols, buffer.X == 0); + for (; j < terminal.Rows; j++) { + ResetBufferLine (j); + } + terminal.UpdateRange (j - 1); + break; + case 1: + j = buffer.Y; + terminal.UpdateRange (j); + // Deleted front part of line and everything before. This line will no longer be wrapped. + EraseInBufferLine (j, 0, buffer.X + 1, true); + if (buffer.X + 1 >= terminal.Cols) { + // Deleted entire previous line. This next line can no longer be wrapped. + buffer.Lines [j + 1].IsWrapped = false; + } + while (j-- != 0) { + ResetBufferLine (j); + } + terminal.UpdateRange (0); + break; + case 2: + j = terminal.Rows; + terminal.UpdateRange (j - 1); + while (j-- != 0) { + ResetBufferLine (j); + } + terminal.UpdateRange (0); + break; + case 3: + // Clear scrollback (everything not in viewport) + var scrollBackSize = buffer.Lines.Length - terminal.Rows; + if (scrollBackSize > 0) { + buffer.Lines.TrimStart (scrollBackSize); + buffer.YBase = Math.Max (buffer.YBase - scrollBackSize, 0); + buffer.YDisp = Math.Max (buffer.YDisp - scrollBackSize, 0); + } + break; + + } + } + + // + // CSI Ps I + // Cursor Forward Tabulation Ps tab stops (default = 1) (CHT). + // + void CursorForwardTab (int [] pars) + { + int param = Math.Max (pars.Length > 0 ? pars [0] : 1, 1); + var buffer = terminal.Buffer; + while (param-- != 0) + buffer.X = buffer.NextTabStop (); + } + + // + // CSI Ps F + // Cursor Preceding Line Ps Times (default = 1) (CNL). + // reuse CSI Ps A ? + // + void CursorPrecedingLine (int [] pars) + { + int param = Math.Max (pars.Length > 0 ? pars [0] : 1, 1); + var buffer = terminal.Buffer; + + buffer.Y -= param; + var newY = buffer.Y - param; + if (newY < 0) + buffer.Y = 0; + else + buffer.Y = newY; + buffer.X = 0; + } + + // + // CSI Ps E + // Cursor Next Line Ps Times (default = 1) (CNL). + // same as CSI Ps B? + // + void CursorNextLine (int [] pars) + { + int param = Math.Max (pars.Length > 0 ? pars [0] : 1, 1); + var buffer = terminal.Buffer; + + var newY = buffer.Y + param; + + if (newY >= terminal.Rows) + buffer.Y = terminal.Rows - 1; + else + buffer.Y = newY; + + buffer.X = 0; + } + + // + // CSI Ps @ + // Insert Ps (Blank) Character(s) (default = 1) (ICH). + // + void InsertChars (int [] pars) + { + terminal.RestrictCursor (); + var buffer = terminal.Buffer; + var cd = new CharData (terminal.EraseAttr ()); + + buffer.Lines [buffer.Y + buffer.YBase].InsertCells ( + buffer.X, + pars.Length > 0 ? Math.Max(pars [0], 1) : 1, + rightMargin: terminal.MarginMode ? buffer.MarginRight : buffer.Cols - 1, + cd); + + terminal.UpdateRange (buffer.Y); + } + + void PrintStateReset() + { + readingBuffer.Reset (); + } + + unsafe void Print (byte* data, int start, int end) + { + readingBuffer.Prepare (data, start, end - start); + + var buffer = terminal.Buffer; + var charset = terminal.Charset; + var screenReaderMode = terminal.Options.ScreenReaderMode; + var cols = terminal.Cols; + var wrapAroundMode = terminal.Wraparound; + var insertMode = terminal.InsertMode; + var curAttr = terminal.CurAttr; + var bufferRow = buffer.Lines [buffer.Y + buffer.YBase]; + + terminal.UpdateRange (buffer.Y); + + while (readingBuffer.HasNext()) { + var ch = ' '; + var chWidth = 0; + int code = 32; + byte bufferValue = readingBuffer.GetNext(); + var n = RuneExt.ExpectedSizeFromFirstByte(bufferValue); + if (n == -1 || n == 1) { + var chSet = false; + if (bufferValue < 127 && charset != null) { + var str = charset[bufferValue]; + if (!string.IsNullOrEmpty(str)) { + ch = str[0]; + // Every single mapping in the charset only takes one slot + chWidth = 1; + chSet = true; + } + } + + if (!chSet) { + (var rune, var size) = Rune.DecodeRune(new byte[] { bufferValue }); + chWidth = size; + ch = rune.ToString()[0]; + } + + } + // Invalid UTF-8 sequence, client sent us some junk, happens if we run with the wrong locale set + // for example if LANG=en + else if (readingBuffer.BytesLeft() >= (n - 1)) { + var bytes = new byte[n]; + bytes[0] = bufferValue; + for (int j = 1; j < n; j++) + bytes[j] = readingBuffer.GetNext(); + + string s = Encoding.UTF8.GetString(bytes, 0, bytes.Length); + if (s.Length > 0) { + ch = s[0]; + } + else { + ch = ' '; + } + + // Now the challenge is that we have a character, not a rune, and we want to compute + // the width of it. + if (s.Length == 1) { + chWidth = RuneHelper.ConsoleWidth(s[0]); + } + else { + chWidth = 0; + + foreach (var scalar in s) { + chWidth = Math.Max(chWidth, RuneHelper.ConsoleWidth(scalar)); + } + } + + } + else { + readingBuffer.Putback(bufferValue); + return; + } + + // get charset replacement character + // charset are only defined for ASCII, therefore we only + // search for an replacement char if code < 127 + if (bufferValue < 127 && charset != null) { + + // MIGUEL-FIXME - this is broken for dutch cahrset that returns two letters "ij", need to figure out what to do + if (charset.TryGetValue (bufferValue, out var str)) { + ch = str [0]; + code = ch; + } + } + if (screenReaderMode) + terminal.EmitChar (ch); + + // insert combining char at last cursor position + // FIXME: needs handling after cursor jumps + // buffer.x should never be 0 for a combining char + // since they always follow a cell consuming char + // therefore we can test for buffer.x to avoid overflow left + if (chWidth == 0 && buffer.X > 0) { + // MIGUEL TODO: in the original code the getter might return a null value + // does this mean that JS returns null for out of bounsd? + if (buffer.X >= 1 && buffer.X < bufferRow.Length) { + var chMinusOne = bufferRow [buffer.X - 1]; + if (chMinusOne.Width == 0) { + // found empty cell after fullwidth, need to go 2 cells back + // it is save to step 2 cells back here + // since an empty cell is only set by fullwidth chars + if (buffer.X >= 2) { + var chMinusTwo = bufferRow [buffer.X - 2]; + + chMinusTwo.Code += ch; + chMinusTwo.Rune = (uint)code; + bufferRow [buffer.X - 2] = chMinusTwo; // must be set explicitly now + } + } else { + chMinusOne.Code += ch; + chMinusOne.Rune = (uint)code; + bufferRow [buffer.X - 1] = chMinusOne; // must be set explicitly now + } + } + continue; + } + + // goto next line if ch would overflow + // TODO: needs a global min terminal width of 2 + // FIXME: additionally ensure chWidth fits into a line + // --> maybe forbid cols right) { + // autowrap - DECAWM + // automatically wraps to the beginning of the next line + if (wrapAroundMode) { + buffer.X = terminal.MarginMode ? buffer.MarginLeft : 0; + + if (buffer.Y >= buffer.ScrollBottom) { + terminal.Scroll (isWrapped: true); + } else { + // The line already exists (eg. the initial viewport), mark it as a + // wrapped line + buffer.Lines [++buffer.Y].IsWrapped = true; + } + + // row changed, get it again + bufferRow = buffer.Lines [buffer.Y + buffer.YBase]; + } else { + if (chWidth == 2) { + // FIXME: check for xterm behavior + // What to do here? We got a wide char that does not fit into last cell + continue; + } + + buffer.X = right; + } + } + + var empty = CharData.Null; + empty.Attribute = curAttr; + // insert mode: move characters to right + if (insertMode) { + // right shift cells according to the width + bufferRow.InsertCells (buffer.X, chWidth, terminal.MarginMode ? buffer.MarginRight : cols - 1, empty); + // test last cell - since the last cell has only room for + // a halfwidth char any fullwidth shifted there is lost + // and will be set to eraseChar + var lastCell = bufferRow [cols - 1]; + if (lastCell.Width == 2) + bufferRow [cols - 1] = empty; + + } + + // write current char to buffer and advance cursor + var charData = new CharData (curAttr, ch, chWidth); + bufferRow [buffer.X++] = charData; + + // fullwidth char - also set next cell to placeholder stub and advance cursor + // for graphemes bigger than fullwidth we can simply loop to zero + // we already made sure above, that buffer.x + chWidth will not overflow right + if (chWidth > 0) { + while (--chWidth != 0) { + bufferRow [buffer.X++] = empty; + } + } + } + terminal.UpdateRange (buffer.Y); + readingBuffer.Done (); + } + } +} diff --git a/src/XtermSharp/InputHandlers/TerminalBufferManipulationCommandExtensions.cs b/src/XtermSharp/InputHandlers/TerminalBufferManipulationCommandExtensions.cs new file mode 100644 index 00000000..4777223e --- /dev/null +++ b/src/XtermSharp/InputHandlers/TerminalBufferManipulationCommandExtensions.cs @@ -0,0 +1,233 @@ +using System; +using System.Collections.Generic; + +namespace XtermSharp.CommandExtensions { + /// + /// Commands that query the terminal for information about the current buffer + /// + internal static class TerminalBufferManipulationCommandExtensions { + + /// + /// DECERA - Erase Rectangular Area + /// CSI Pt ; Pl ; Pb ; Pr ; $ z + /// + public static void csiDECERA (this Terminal terminal, int [] pars) + { + var buffer = terminal.Buffer; + var rect = GetRectangleFromRequest (buffer, terminal.OriginMode, 0, pars); + + if (rect.valid) { + for (int row = rect.top; row <= rect.bottom; row++) { + var line = buffer.Lines [row + buffer.YBase]; + for (int col = rect.left; col <= rect.right; col++) { + line [col] = new CharData (terminal.CurAttr, ' ', 1, 32); + } + } + } + } + + /// + /// DECSERA - Selective Erase Rectangular Area + /// CSI Pt ; Pl ; Pb ; Pr ; $ { + /// + public static void csiDECSERA (this Terminal terminal, params int [] pars) + { + var buffer = terminal.Buffer; + var rect = GetRectangleFromRequest (buffer, terminal.OriginMode, 0, pars); + + if (rect.valid) { + for (int row = rect.top; row <= rect.bottom; row++) { + var line = buffer.Lines [row + buffer.YBase]; + for (int col = rect.left; col <= rect.right; col++) { + line [col] = new CharData (terminal.CurAttr, ' ', 1, 32); + } + } + } + } + + /// + /// CSI Pc ; Pt ; Pl ; Pb ; Pr $ x Fill Rectangular Area (DECFRA), VT420 and up. + /// + public static void csiDECFRA (this Terminal terminal, params int [] pars) + { + var buffer = terminal.Buffer; + var rect = GetRectangleFromRequest (buffer, terminal.OriginMode, 1, pars); + + if (rect.valid) { + char fillChar = ' '; + if (pars.Length > 0) { + fillChar = (char)pars [0]; + } + + for (int row = rect.top; row <= rect.bottom; row++) { + var line = buffer.Lines [row + buffer.YBase]; + for (int col = rect.left; col <= rect.right; col++) { + line [col] = new CharData (terminal.CurAttr, fillChar, 1, (int)fillChar); + } + } + } + } + + /// + /// Copy Rectangular Area (DECCRA), VT400 and up. + /// CSI Pts ; Pls ; Pbs ; Prs ; Pps ; Ptd ; Pld ; Ppd $ v + /// Pts ; Pls ; Pbs ; Prs denotes the source rectangle. + /// Pps denotes the source page. + /// Ptd ; Pld denotes the target location. + /// Ppd denotes the target page. + /// + public static void csiDECCRA (this Terminal terminal, int [] pars, string collect) + { + var buffer = terminal.Buffer; + if (collect == "$") { + var parArray = new int [8]; + parArray [0] = (pars.Length > 1 && pars [0] != 0 ? pars [0] : 1); // Pts default 1 + parArray [1] = (pars.Length > 2 && pars [1] != 0 ? pars [1] : 1); // Pls default 1 + parArray [2] = (pars.Length > 3 && pars [2] != 0 ? pars [2] : buffer.Rows - 1); // Pbs default to last line of page + parArray [3] = (pars.Length > 4 && pars [3] != 0 ? pars [3] : buffer.Cols - 1); // Prs defaults to last column + parArray [4] = (pars.Length > 5 && pars [4] != 0 ? pars [4] : 1); // Pps page source = 1 + parArray [5] = (pars.Length > 6 && pars [5] != 0 ? pars [5] : 1); // Ptd default is 1 + parArray [6] = (pars.Length > 7 && pars [6] != 0 ? pars [6] : 1); // Pld default is 1 + parArray [7] = (pars.Length > 8 && pars [7] != 0 ? pars [7] : 1); // Ppd default is 1 + + // We only support copying on the same page, and the page being 1 + if (parArray [4] == parArray [7] && parArray [4] == 1) { + var rect = GetRectangleFromRequest (buffer, terminal.OriginMode, 0, parArray); + if (rect.valid) { + var rowTarget = parArray [5] - 1; + var colTarget = parArray [6] - 1; + + // Block size + var columns = rect.right - rect.left + 1; + + var cright = Math.Min (buffer.Cols - 1, rect.left + Math.Min (columns, buffer.Cols - colTarget)); + + var lines = new List> (); + for (int row = rect.top; row <= rect.bottom; row++) { + var line = buffer.Lines [row + buffer.YBase]; + var lineCopy = new List (); + for (int col = rect.left; col <= cright; col++) { + lineCopy.Add (line [col]); + } + lines.Add (lineCopy); + } + + for (int row = 0; row <= rect.bottom - rect.top; row++) { + if (row + rowTarget >= buffer.Rows) { + break; + } + + var line = buffer.Lines [row + rowTarget + buffer.YBase]; + var lr = lines [row]; + for (int col = 0; col <= cright - rect.left; col++) { + if (col >= buffer.Cols) { + break; + } + + line [colTarget + col] = lr [col]; + } + } + } + } + } + } + + /// + /// Required by the test suite + /// CSI Pi ; Pg ; Pt ; Pl ; Pb ; Pr * y + /// Request Checksum of Rectangular Area (DECRQCRA), VT420 and up. + /// Response is + /// DCS Pi ! ~ x x x x ST + /// Pi is the request id. + /// Pg is the page number. + /// Pt ; Pl ; Pb ; Pr denotes the rectangle. + /// The x's are hexadecimal digits 0-9 and A-F. + /// + public static void csiDECRQCRA (this Terminal terminal, params int [] pars) + { + var buffer = terminal.Buffer; + + int checksum = 0; + var rid = pars.Length > 0 ? pars [0] : 1; + var _ = pars.Length > 1 ? pars [1] : 0; + var result = "0000"; + + // Still need to imeplemnt the checksum here + // Which is just the sum of the rune values + if (terminal.Delegate.IsProcessTrusted ()) { + var rect = GetRectangleFromRequest (buffer, terminal.OriginMode, 2, pars); + + var top = rect.top; + var left = rect.left; + var bottom = rect.bottom; + var right = rect.right; + + for (int row = top; row <= bottom; row++) { + var line = buffer.Lines [row + buffer.YBase]; + for (int col = left; col <= right; col++) { + var cd = line [col]; + + //var ch = cd.getCharacter (); + //for (scalar in ch.unicodeScalars) { + // checksum += scalar.value; + //} + checksum += cd.IsNullChar() ? 32 : cd.Code; + } + } + + result = String.Format ("{0,4:X}", checksum); + } + + terminal.SendResponse ($"{terminal.ControlCodes.DCS}{rid}!~{result}{terminal.ControlCodes.ST}"); + } + + /// + /// Validates optional arguments for top, left, bottom, right sent by various + /// escape sequences and returns validated top, left, bottom, right in our 0-based + /// internal coordinates + /// + static (bool valid, int top, int left, int bottom, int right) GetRectangleFromRequest (Buffer buffer, bool originMode, int start, int [] pars) + { + var top = Math.Max (1, pars.Length > start ? pars [start] : 1); + var left = Math.Max (pars.Length > start + 1 ? pars [start + 1] : 1, 1); + var bottom = pars.Length > start + 2 ? pars [start + 2] : -1; + var right = pars.Length > start + 3 ? pars [start + 3] : -1; + + var rect = GetRectangleFromRequest (buffer, originMode, top, left, bottom, right); + return rect; + } + + /// + /// Validates optional arguments for top, left, bottom, right sent by various + /// escape sequences and returns validated top, left, bottom, right in our 0-based + /// internal coordinates + /// + static (bool valid, int top, int left, int bottom, int right) GetRectangleFromRequest (Buffer buffer, bool originMode, int top, int left, int bottom, int right) + { + if (bottom < 0) { + bottom = buffer.Rows; + } + if (right < 0) { + right = buffer.Cols; + } + if (right > buffer.Cols) { + right = buffer.Cols; + } + if (bottom > buffer.Rows) { + bottom = buffer.Rows; + } + if (originMode) { + top += buffer.ScrollTop; + bottom += buffer.ScrollTop; + left += buffer.MarginLeft; + right += buffer.MarginLeft; + } + + if (top > bottom || left > right) { + return (false, 0, 0, 0, 0); + } + + return (true, top - 1, left - 1, bottom - 1, right - 1); + } + } +} \ No newline at end of file diff --git a/src/XtermSharp/InputHandlers/TerminalCommandExtensions.cs b/src/XtermSharp/InputHandlers/TerminalCommandExtensions.cs new file mode 100644 index 00000000..1926703a --- /dev/null +++ b/src/XtermSharp/InputHandlers/TerminalCommandExtensions.cs @@ -0,0 +1,337 @@ +using System; + +namespace XtermSharp.CommandExtensions { + /// + /// Commands that operate on a terminal from CSI params + /// + internal static class TerminalCommandExtensions { + /// + // CSI Ps A + // Cursor Up Ps Times (default = 1) (CUU). + /// + public static void csiCUU (this Terminal terminal, params int [] pars) + { + int param = Math.Max (pars.Length > 0 ? pars [0] : 1, 1); + terminal.CursorUp (param); + } + + /// + // CSI Ps B + // Cursor Down Ps Times (default = 1) (CUD). + /// + public static void csiCUD (this Terminal terminal, params int [] pars) + { + int param = Math.Max (pars.Length > 0 ? pars [0] : 1, 1); + terminal.CursorDown (param); + } + + /// + // CSI Ps C + // Cursor Forward Ps Times (default = 1) (CUF). + /// + public static void csiCUF (this Terminal terminal, params int [] pars) + { + int param = Math.Max (pars.Length > 0 ? pars [0] : 1, 1); + terminal.CursorForward (param); + } + + /// + /// CSI Ps D + /// Cursor Backward Ps Times (default = 1) (CUB). + /// + public static void csiCUB (this Terminal terminal, int [] pars) + { + int param = Math.Max (pars.Length > 0 ? pars [0] : 1, 1); + terminal.CursorBackward (param); + } + + /// + /// CSI Ps G + /// Cursor Character Absolute [column] (default = [row,1]) (CHA). + /// + public static void csiCHA (this Terminal terminal, int [] pars) + { + int param = Math.Max (pars.Length > 0 ? pars [0] : 1, 1); + terminal.CursorCharAbsolute (param); + } + + /// + /// Sets the cursor position from csi CUP + /// CSI Ps ; Ps H + /// Cursor Position [row;column] (default = [1,1]) (CUP). + /// + public static void csiCUP (this Terminal terminal, params int [] pars) + { + int col, row; + switch (pars.Length) { + case 1: + row = pars [0] - 1; + col = 0; + break; + case 2: + row = pars [0] - 1; + col = pars [1] - 1; + break; + default: + col = 0; + row = 0; + break; + } + + terminal.SetCursor (col, row); + } + + /// + /// Deletes lines + /// + /// + // CSI Ps M + // Delete Ps Line(s) (default = 1) (DL). + /// + public static void csiDL (this Terminal terminal, params int [] pars) + { + var p = Math.Max (pars.Length == 0 ? 1 : pars [0], 1); + terminal.DeleteLines (p); + } + + /// + /// CSI Ps P + /// Delete Ps Character(s) (default = 1) (DCH). + /// + public static void csiDCH (this Terminal terminal, params int [] pars) + { + var p = Math.Max (pars.Length == 0 ? 1 : pars [0], 1); + terminal.DeleteChars (p); + } + + /// + /// CSI Ps Z Cursor Backward Tabulation Ps tab stops (default = 1) (CBT). + /// + public static void csiCBT (this Terminal terminal, params int [] pars) + { + var p = Math.Max (pars.Length == 0 ? 1 : pars [0], 1); + terminal.CursorBackwardTab (p); + } + + /// + /// Sets the margins from csi DECSLRM + /// + public static void csiDECSLRM (this Terminal terminal, params int [] pars) + { + var buffer = terminal.Buffer; + var left = (pars.Length > 0 ? pars [0] : 1) - 1; + var right = (pars.Length > 1 ? pars [1] : buffer.Cols) - 1; + + buffer.SetMargins (left, right); + } + + /// + /// CSI Ps ; Ps r + /// Set Scrolling Region [top;bottom] (default = full size of win- + /// dow) (DECSTBM). + // CSI ? Pm r + /// + public static void csiDECSTBM (this Terminal terminal, params int [] pars) + { + var top = pars.Length > 0 ? Math.Max (pars [0] - 1, 0) : 0; + var bottom = pars.Length > 1 ? pars [1] : 0; + + terminal.SetScrollRegion (top, bottom); + } + + /// + /// CSI # } Pop video attributes from stack (XTPOPSGR), xterm. Popping + /// restores the video-attributes which were saved using XTPUSHSGR + /// to their previous state. + /// + /// CSI Pm ' } + /// Insert Ps Column(s) (default = 1) (DECIC), VT420 and up. + /// + public static void csiDECIC (this Terminal terminal, int[] pars) + { + var n = pars.Length > 0 ? Math.Max (pars [0], 1) : 1; + terminal.InsertColumn (n); + } + + /// + /// CSI Ps ' ~ + /// Delete Ps Column(s) (default = 1) (DECDC), VT420 and up. + /// + /// @vt: #Y CSI DECDC "Delete Columns" "CSI Ps ' ~" "Delete `Ps` columns at cursor position." + /// DECDC deletes `Ps` times columns at the cursor position for all lines with the scroll margins, + /// moving content to the left. Blank columns are added at the right margin. + /// DECDC has no effect outside the scrolling margins. + /// + public static void csiDECDC (this Terminal terminal, params int [] pars) + { + var n = pars.Length > 0 ? Math.Max (pars [0], 1) : 1; + terminal.DeleteColumn (n); + } + + /// + /// CSI Ps ; Ps ; Ps t - Various window manipulations and reports (xterm) + /// See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html for a full + /// list of commans for this escape sequence + /// + public static void csiDISPATCH (this Terminal terminal, int [] pars) + { + if (pars == null || pars.Length == 0) + return; + + if (pars.Length == 3 && pars [0] == 3) { + terminal.Delegate.WindowCommand (terminal, WindowManipulationCommand.MoveWindowTo, pars [1], pars [2]); + return; + } + if (pars.Length == 3 && pars [0] == 4) { + terminal.Delegate.WindowCommand (terminal, WindowManipulationCommand.MoveWindowTo, pars [1], pars [2]); + return; + } + + if (pars.Length == 3 && pars [0] == 8) { + terminal.Delegate.WindowCommand (terminal, WindowManipulationCommand.ResizeTo, pars [1], pars [2]); + return; + } + + if (pars.Length == 2 && pars [0] == 9) { + switch (pars [1]) { + case 0: + terminal.Delegate.WindowCommand (terminal, WindowManipulationCommand.RestoreMaximizedWindow); + return; + case 1: + terminal.Delegate.WindowCommand (terminal, WindowManipulationCommand.MaximizeWindow); + return; + case 2: + terminal.Delegate.WindowCommand (terminal, WindowManipulationCommand.MaximizeWindowVertically); + return; + case 3: + terminal.Delegate.WindowCommand (terminal, WindowManipulationCommand.MaximizeWindowHorizontally); + return; + default: + return; + } + } + + if (pars.Length == 2 && pars [0] == 10) { + switch (pars [1]) { + case 0: + terminal.Delegate.WindowCommand (terminal, WindowManipulationCommand.UndoFullScreen); + return; + case 1: + terminal.Delegate.WindowCommand (terminal, WindowManipulationCommand.SwitchToFullScreen); + return; + case 2: + terminal.Delegate.WindowCommand (terminal, WindowManipulationCommand.ToggleFullScreen); + return; + default: + return; + } + } + + if (pars.Length == 2 && pars [0] == 22) { + switch (pars [1]) { + case 0: + terminal.PushTitle (); + terminal.PushIconTitle (); + return; + case 1: + terminal.PushIconTitle (); + return; + case 2: + terminal.PushTitle (); + return; + default: + return; + } + } + + if (pars.Length == 2 && pars [0] == 23) { + switch (pars [1]) { + case 0: + terminal.PopTitle (); + terminal.PopIconTitle (); + return; + case 1: + terminal.PopTitle (); + return; + case 2: + terminal.PopIconTitle (); + return; + default: + return; + } + } + + if (pars.Length == 1) { + string response = null; + switch (pars [0]) { + case 0: + terminal.Delegate.WindowCommand (terminal, WindowManipulationCommand.DeiconifyWindow); + return; + case 1: + terminal.Delegate.WindowCommand (terminal, WindowManipulationCommand.IconifyWindow); + return; + case 2: + return; + case 3: + return; + case 4: + return; + case 5: + terminal.Delegate.WindowCommand (terminal, WindowManipulationCommand.BringToFront); + return; + case 6: + terminal.Delegate.WindowCommand (terminal, WindowManipulationCommand.SendToBack); + return; + case 7: + terminal.Delegate.WindowCommand (terminal, WindowManipulationCommand.RefreshWindow); + return; + case 15: + response = terminal.Delegate.WindowCommand (terminal, WindowManipulationCommand.ReportSizeOfScreenInPixels); + if (response == null) { + response = $"{terminal.ControlCodes.CSI}5;768;1024t"; + } + + terminal.SendResponse (response); + return; + case 16: + response = terminal.Delegate.WindowCommand (terminal, WindowManipulationCommand.ReportCellSizeInPixels); + if (response == null) { + response = $"{terminal.ControlCodes.CSI}6;16;10t"; + } + + terminal.SendResponse (response); + return; + case 17: + return; + case 18: + response = terminal.Delegate.WindowCommand (terminal, WindowManipulationCommand.ReportScreenSizeCharacters); + if (response == null) { + response = $"{terminal.ControlCodes.CSI}8;{terminal.Rows};{terminal.Cols}t"; + } + + terminal.SendResponse (response); + return; + case 19: + response = terminal.Delegate.WindowCommand (terminal, WindowManipulationCommand.ReportScreenSizeCharacters); + if (response == null) { + response = $"{terminal.ControlCodes.CSI}9;{terminal.Rows};{terminal.Cols}t"; + } + + terminal.SendResponse (response); + return; + case 20: + response = terminal.IconTitle.Replace ("\\", ""); + terminal.SendResponse ($"{terminal.ControlCodes.OSC}l{response}{terminal.ControlCodes.ST}"); + return; + case 21: + response = terminal.Title.Replace ("\\", ""); + terminal.SendResponse ($"{terminal.ControlCodes.OSC}l{response}{terminal.ControlCodes.ST}"); + return; + default: + return; + } + } + } + + } +} \ No newline at end of file diff --git a/src/XtermSharp/InputHandlers/TerminalModeSetExtensions.cs b/src/XtermSharp/InputHandlers/TerminalModeSetExtensions.cs new file mode 100644 index 00000000..56b7325c --- /dev/null +++ b/src/XtermSharp/InputHandlers/TerminalModeSetExtensions.cs @@ -0,0 +1,404 @@ +namespace XtermSharp.CommandExtensions { + internal static class TerminalModeSetExtensions { + /// + /// Sets the mode from csi DECSTR + /// + // CSI Pm h Set Mode (SM). + // Ps = 2 -> Keyboard Action Mode (AM). + // Ps = 4 -> Insert Mode (IRM). + // Ps = 1 2 -> Send/receive (SRM). + // Ps = 2 0 -> Automatic Newline (LNM). + // CSI ? Pm h + // DEC Private Mode Set (DECSET). + // Ps = 1 -> Application Cursor Keys (DECCKM). + // Ps = 2 -> Designate USASCII for character sets G0-G3 + // (DECANM), and set VT100 mode. + // Ps = 3 -> 132 Column Mode (DECCOLM). + // Ps = 4 -> Smooth (Slow) Scroll (DECSCLM). + // Ps = 5 -> Reverse Video (DECSCNM). + // Ps = 6 -> Origin Mode (DECOM). + // Ps = 7 -> Wraparound Mode (DECAWM). + // Ps = 8 -> Auto-repeat Keys (DECARM). + // Ps = 9 -> Send Mouse X & Y on button press. See the sec- + // tion Mouse Tracking. + // Ps = 1 0 -> Show toolbar (rxvt). + // Ps = 1 2 -> Start Blinking Cursor (att610). + // Ps = 1 8 -> Print form feed (DECPFF). + // Ps = 1 9 -> Set print extent to full screen (DECPEX). + // Ps = 2 5 -> Show Cursor (DECTCEM). + // Ps = 3 0 -> Show scrollbar (rxvt). + // Ps = 3 5 -> Enable font-shifting functions (rxvt). + // Ps = 3 8 -> Enter Tektronix Mode (DECTEK). + // Ps = 4 0 -> Allow 80 -> 132 Mode. + // Ps = 4 1 -> more(1) fix (see curses resource). + // Ps = 4 2 -> Enable Nation Replacement Character sets (DECN- + // RCM). + // Ps = 4 4 -> Turn On Margin Bell. + // Ps = 4 5 -> Reverse-wraparound Mode. + // Ps = 4 6 -> Start Logging. This is normally disabled by a + // compile-time option. + // Ps = 4 7 -> Use Alternate Screen Buffer. (This may be dis- + // abled by the titeInhibit resource). + // Ps = 6 6 -> Application keypad (DECNKM). + // Ps = 6 7 -> Backarrow key sends backspace (DECBKM). + // Ps = 1 0 0 0 -> Send Mouse X & Y on button press and + // release. See the section Mouse Tracking. + // Ps = 1 0 0 1 -> Use Hilite Mouse Tracking. + // Ps = 1 0 0 2 -> Use Cell Motion Mouse Tracking. + // Ps = 1 0 0 3 -> Use All Motion Mouse Tracking. + // Ps = 1 0 0 4 -> Send FocusIn/FocusOut events. + // Ps = 1 0 0 5 -> Enable Extended Mouse Mode. + // Ps = 1 0 1 0 -> Scroll to bottom on tty output (rxvt). + // Ps = 1 0 1 1 -> Scroll to bottom on key press (rxvt). + // Ps = 1 0 3 4 -> Interpret "meta" key, sets eighth bit. + // (enables the eightBitInput resource). + // Ps = 1 0 3 5 -> Enable special modifiers for Alt and Num- + // Lock keys. (This enables the numLock resource). + // Ps = 1 0 3 6 -> Send ESC when Meta modifies a key. (This + // enables the metaSendsEscape resource). + // Ps = 1 0 3 7 -> Send DEL from the editing-keypad Delete + // key. + // Ps = 1 0 3 9 -> Send ESC when Alt modifies a key. (This + // enables the altSendsEscape resource). + // Ps = 1 0 4 0 -> Keep selection even if not highlighted. + // (This enables the keepSelection resource). + // Ps = 1 0 4 1 -> Use the CLIPBOARD selection. (This enables + // the selectToClipboard resource). + // Ps = 1 0 4 2 -> Enable Urgency window manager hint when + // Control-G is received. (This enables the bellIsUrgent + // resource). + // Ps = 1 0 4 3 -> Enable raising of the window when Control-G + // is received. (enables the popOnBell resource). + // Ps = 1 0 4 7 -> Use Alternate Screen Buffer. (This may be + // disabled by the titeInhibit resource). + // Ps = 1 0 4 8 -> Save cursor as in DECSC. (This may be dis- + // abled by the titeInhibit resource). + // Ps = 1 0 4 9 -> Save cursor as in DECSC and use Alternate + // Screen Buffer, clearing it first. (This may be disabled by + // the titeInhibit resource). This combines the effects of the 1 + // 0 4 7 and 1 0 4 8 modes. Use this with terminfo-based + // applications rather than the 4 7 mode. + // Ps = 1 0 5 0 -> Set terminfo/termcap function-key mode. + // Ps = 1 0 5 1 -> Set Sun function-key mode. + // Ps = 1 0 5 2 -> Set HP function-key mode. + // Ps = 1 0 5 3 -> Set SCO function-key mode. + // Ps = 1 0 6 0 -> Set legacy keyboard emulation (X11R6). + // Ps = 1 0 6 1 -> Set VT220 keyboard emulation. + // Ps = 2 0 0 4 -> Set bracketed paste mode. + // Modes: + // http: *vt100.net/docs/vt220-rm/chapter4.html + public static void csiDECSET (this Terminal terminal, int par, string collect) + { + if (collect == "") { + switch (par) { + case 4: + //Console.WriteLine ("This needs to handle the replace mode as well"); + // https://vt100.net/docs/vt510-rm/IRM.html + terminal.InsertMode = true; + break; + case 20: + + // Automatic New Line (LNM) + // this._t.convertEol = true; + break; + } + } else if (collect == "?") { + switch (par) { + case 1: + terminal.ApplicationCursor = true; + break; + case 2: + terminal.SetgCharset (0, CharSets.Default); + terminal.SetgCharset (1, CharSets.Default); + terminal.SetgCharset (2, CharSets.Default); + terminal.SetgCharset (3, CharSets.Default); + // set VT100 mode here + break; + case 3: // 132 col mode + if (terminal.Allow80To132) { + terminal.Resize (132, terminal.Rows); + terminal.Reset (); + terminal.Delegate.SizeChanged (terminal); + } + break; + case 5: + // Inverted colors + terminal.CurAttr = CharData.InvertedAttr; + break; + case 6: + terminal.OriginMode = true; + break; + case 7: + terminal.Wraparound = true; + break; + case 9: // X10 Mouse + // no release, no motion, no wheel, no modifiers. + terminal.MouseMode = MouseMode.X10; + break; + case 12: + // this.cursorBlink = true; + break; + case 40: + terminal.Allow80To132 = true; + break; + case 45: + // Xterm Reverse Wrap-around + // reverse wraparound can only be enabled if Auto-wrap is enabled (DECAWM) + if (terminal.Wraparound) { + terminal.ReverseWraparound = true; + } + break; + case 66: + terminal.Log ("Serial port requested application keypad."); + terminal.ApplicationKeypad = true; + terminal.SyncScrollArea (); + break; + case 69: + // Enable left and right margin mode (DECLRMM), + terminal.MarginMode = true; + break; + case 1000: // vt200 mouse + // no motion. + // no modifiers, except control on the wheel. + terminal.MouseMode = MouseMode.VT200; + break; + case 1002: + // SET_BTN_EVENT_MOUSE + terminal.MouseMode = MouseMode.ButtonEventTracking; + break; + case 1003: + // SET_ANY_EVENT_MOUSE + terminal.MouseMode = MouseMode.AnyEvent; + break; + case 1004: // send focusin/focusout events + // focusin: ^[[I + // focusout: ^[[O + terminal.SendFocus = true; + break; + case 1005: // utf8 ext mode mouse + // for wide terminals + // simply encodes large values as utf8 characters + terminal.MouseProtocol = MouseProtocolEncoding.UTF8; + break; + case 1006: // sgr ext mode mouse + terminal.MouseProtocol = MouseProtocolEncoding.SGR; + // for wide terminals + // does not add 32 to fields + // press: ^[[ Keyboard Action Mode (AM). + // Ps = 4 -> Replace Mode (IRM). + // Ps = 1 2 -> Send/receive (SRM). + // Ps = 2 0 -> Normal Linefeed (LNM). + //CSI ? Pm l + // DEC Private Mode Reset (DECRST). + // Ps = 1 -> Normal Cursor Keys (DECCKM). + // Ps = 2 -> Designate VT52 mode (DECANM). + // Ps = 3 -> 80 Column Mode (DECCOLM). + // Ps = 4 -> Jump (Fast) Scroll (DECSCLM). + // Ps = 5 -> Normal Video (DECSCNM). + // Ps = 6 -> Normal Cursor Mode (DECOM). + // Ps = 7 -> No Wraparound Mode (DECAWM). + // Ps = 8 -> No Auto-repeat Keys (DECARM). + // Ps = 9 -> Don't send Mouse X & Y on button press. + // Ps = 1 0 -> Hide toolbar (rxvt). + // Ps = 1 2 -> Stop Blinking Cursor (att610). + // Ps = 1 8 -> Don't print form feed (DECPFF). + // Ps = 1 9 -> Limit print to scrolling region (DECPEX). + // Ps = 2 5 -> Hide Cursor (DECTCEM). + // Ps = 3 0 -> Don't show scrollbar (rxvt). + // Ps = 3 5 -> Disable font-shifting functions (rxvt). + // Ps = 4 0 -> Disallow 80 -> 132 Mode. + // Ps = 4 1 -> No more(1) fix (see curses resource). + // Ps = 4 2 -> Disable Nation Replacement Character sets (DEC- + // NRCM). + // Ps = 4 4 -> Turn Off Margin Bell. + // Ps = 4 5 -> No Reverse-wraparound Mode. + // Ps = 4 6 -> Stop Logging. (This is normally disabled by a + // compile-time option). + // Ps = 4 7 -> Use Normal Screen Buffer. + // Ps = 6 6 -> Numeric keypad (DECNKM). + // Ps = 6 7 -> Backarrow key sends delete (DECBKM). + // Ps = 1 0 0 0 -> Don't send Mouse X & Y on button press and + // release. See the section Mouse Tracking. + // Ps = 1 0 0 1 -> Don't use Hilite Mouse Tracking. + // Ps = 1 0 0 2 -> Don't use Cell Motion Mouse Tracking. + // Ps = 1 0 0 3 -> Don't use All Motion Mouse Tracking. + // Ps = 1 0 0 4 -> Don't send FocusIn/FocusOut events. + // Ps = 1 0 0 5 -> Disable Extended Mouse Mode. + // Ps = 1 0 1 0 -> Don't scroll to bottom on tty output + // (rxvt). + // Ps = 1 0 1 1 -> Don't scroll to bottom on key press (rxvt). + // Ps = 1 0 3 4 -> Don't interpret "meta" key. (This disables + // the eightBitInput resource). + // Ps = 1 0 3 5 -> Disable special modifiers for Alt and Num- + // Lock keys. (This disables the numLock resource). + // Ps = 1 0 3 6 -> Don't send ESC when Meta modifies a key. + // (This disables the metaSendsEscape resource). + // Ps = 1 0 3 7 -> Send VT220 Remove from the editing-keypad + // Delete key. + // Ps = 1 0 3 9 -> Don't send ESC when Alt modifies a key. + // (This disables the altSendsEscape resource). + // Ps = 1 0 4 0 -> Do not keep selection when not highlighted. + // (This disables the keepSelection resource). + // Ps = 1 0 4 1 -> Use the PRIMARY selection. (This disables + // the selectToClipboard resource). + // Ps = 1 0 4 2 -> Disable Urgency window manager hint when + // Control-G is received. (This disables the bellIsUrgent + // resource). + // Ps = 1 0 4 3 -> Disable raising of the window when Control- + // G is received. (This disables the popOnBell resource). + // Ps = 1 0 4 7 -> Use Normal Screen Buffer, clearing screen + // first if in the Alternate Screen. (This may be disabled by + // the titeInhibit resource). + // Ps = 1 0 4 8 -> Restore cursor as in DECRC. (This may be + // disabled by the titeInhibit resource). + // Ps = 1 0 4 9 -> Use Normal Screen Buffer and restore cursor + // as in DECRC. (This may be disabled by the titeInhibit + // resource). This combines the effects of the 1 0 4 7 and 1 0 + // 4 8 modes. Use this with terminfo-based applications rather + // than the 4 7 mode. + // Ps = 1 0 5 0 -> Reset terminfo/termcap function-key mode. + // Ps = 1 0 5 1 -> Reset Sun function-key mode. + // Ps = 1 0 5 2 -> Reset HP function-key mode. + // Ps = 1 0 5 3 -> Reset SCO function-key mode. + // Ps = 1 0 6 0 -> Reset legacy keyboard emulation (X11R6). + // Ps = 1 0 6 1 -> Reset keyboard emulation to Sun/PC style. + // Ps = 2 0 0 4 -> Reset bracketed paste mode. + // + public static void csiDECRESET (this Terminal terminal, int par, string collect) + { + if (collect == "") { + switch (par) { + case 4: + terminal.InsertMode = false; + break; + case 20: + // this._t.convertEol = false; + break; + } + } else if (collect == "?") { + switch (par) { + case 1: + terminal.ApplicationCursor = false; + break; + case 3: + if (terminal.Allow80To132) { + terminal.Resize (80, terminal.Rows); + terminal.Delegate.SizeChanged (terminal); + terminal.Reset (); + } + break; + case 5: + // Reset default color + terminal.CurAttr = CharData.DefaultAttr; + break; + case 6: + terminal.OriginMode = false; + break; + case 7: + terminal.Wraparound = false; + break; + case 12: + // this.cursorBlink = false; + break; + case 40: + terminal.Allow80To132 = false; + break; + case 45: + terminal.ReverseWraparound = false; + break; + case 66: + terminal.Log ("Switching back to normal keypad."); + terminal.ApplicationKeypad = false; + terminal.SyncScrollArea (); + break; + case 69: + // DECSLRM + terminal.MarginMode = false; + break; + case 9: // X10 Mouse + case 1000: // vt200 mouse + case 1002: // button event mouse + case 1003: // any event mouse + terminal.MouseMode = MouseMode.Off; + break; + case 1004: // send focusin/focusout events + terminal.SendFocus = false; + break; + case 1005: // utf8 ext mode mouse + if (terminal.MouseProtocol == MouseProtocolEncoding.UTF8) { + terminal.MouseProtocol = MouseProtocolEncoding.X10; + } + break; + case 1006: // sgr ext mode mouse + if (terminal.MouseProtocol == MouseProtocolEncoding.SGR) { + terminal.MouseProtocol = MouseProtocolEncoding.X10; + } + break; + case 1015: // urxvt ext mode mouse + if (terminal.MouseProtocol == MouseProtocolEncoding.URXVT) { + terminal.MouseProtocol = MouseProtocolEncoding.X10; + } + break; + case 25: // hide cursor + terminal.CursorHidden = true; + break; + case 1048: // alt screen cursor + terminal.RestoreCursor (); + break; + case 1049: // alt screen buffer cursor + // FALL-THROUGH + goto case 47; + case 47: // normal screen buffer + case 1047: // normal screen buffer - clearing it first + // Ensure the selection manager has the correct buffer + terminal.Buffers.ActivateNormalBuffer (par == 1047); + if (par == 1049) + terminal.RestoreCursor (); + terminal.Refresh (0, terminal.Rows - 1); + terminal.SyncScrollArea (); + terminal.ShowCursor (); + break; + case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste) + terminal.BracketedPasteMode = false; + break; + } + } + } + } +} \ No newline at end of file diff --git a/src/XtermSharp/InputHandlers/TerminalStatusCommandExtensions.cs b/src/XtermSharp/InputHandlers/TerminalStatusCommandExtensions.cs new file mode 100644 index 00000000..f5835e95 --- /dev/null +++ b/src/XtermSharp/InputHandlers/TerminalStatusCommandExtensions.cs @@ -0,0 +1,187 @@ +using System; + +namespace XtermSharp.CommandExtensions { + /// + /// Commands that report status back about the terminal + /// + internal static class TerminalStatusCommandExtensions { + + // CSI Ps c Send Device Attributes (Primary DA). + // Ps = 0 or omitted -> request attributes from terminal. The + // response depends on the decTerminalID resource setting. + // -> CSI ? 1 ; 2 c (``VT100 with Advanced Video Option'') + // -> CSI ? 1 ; 0 c (``VT101 with No Options'') + // -> CSI ? 6 c (``VT102'') + // -> CSI ? 6 0 ; 1 ; 2 ; 6 ; 8 ; 9 ; 1 5 ; c (``VT220'') + // The VT100-style response parameters do not mean anything by + // themselves. VT220 parameters do, telling the host what fea- + // tures the terminal supports: + // Ps = 1 -> 132-columns. + // Ps = 2 -> Printer. + // Ps = 6 -> Selective erase. + // Ps = 8 -> User-defined keys. + // Ps = 9 -> National replacement character sets. + // Ps = 1 5 -> Technical characters. + // Ps = 2 2 -> ANSI color, e.g., VT525. + // Ps = 2 9 -> ANSI text locator (i.e., DEC Locator mode). + // CSI > Ps c + // Send Device Attributes (Secondary DA). + // Ps = 0 or omitted -> request the terminal's identification + // code. The response depends on the decTerminalID resource set- + // ting. It should apply only to VT220 and up, but xterm extends + // this to VT100. + // -> CSI > Pp ; Pv ; Pc c + // where Pp denotes the terminal type + // Pp = 0 -> ``VT100''. + // Pp = 1 -> ``VT220''. + // and Pv is the firmware version (for xterm, this was originally + // the XFree86 patch number, starting with 95). In a DEC termi- + // nal, Pc indicates the ROM cartridge registration number and is + // always zero. + // More information: + // xterm/charproc.c - line 2012, for more information. + // vim responds with ^[[?0c or ^[[?1c after the terminal's response (?) + // + public static void csiDA1 (this Terminal terminal, int [] pars, string collect) + { + if (pars.Length > 0 && pars [0] > 0) + return; + + if (collect == ">" || collect == ">0") { + // DA2 Secondary Device Attributes + if (pars.Length == 0 || pars [0] == 0) { + var vt510 = 61; // we identified as a vt510 + var kbd = 1; // PC-style keyboard + terminal.SendResponse ($"{terminal.ControlCodes.CSI}>{vt510};20;{kbd}c"); + return; + } + + return; + } + + var name = terminal.Options.TermName; + if (collect == "") { + if (name.StartsWith ("xterm", StringComparison.Ordinal) || name.StartsWith ("rxvt-unicode", StringComparison.Ordinal) || name.StartsWith ("screen", StringComparison.Ordinal)) { + terminal.SendResponse ($"{terminal.ControlCodes.CSI}?1;2c"); + } else if (name.StartsWith ("linux", StringComparison.Ordinal)) { + terminal.SendResponse ($"{terminal.ControlCodes.CSI}?6c"); + } + } else if (collect == ">") { + // xterm and urxvt + // seem to spit this + // out around ~370 times (?). + if (name.StartsWith ("xterm", StringComparison.Ordinal)) { + terminal.SendResponse ("\x1b[>0;276;0c"); + } else if (name.StartsWith ("rxvt-unicode", StringComparison.Ordinal)) { + terminal.SendResponse ("\x1b[>85;95;0c"); + } else if (name.StartsWith ("linux", StringComparison.Ordinal)) { + // not supported by linux console. + // linux console echoes parameters. + terminal.SendResponse ("" + pars [0] + 'c'); + } else if (name.StartsWith ("screen", StringComparison.Ordinal)) { + terminal.SendResponse ("\x1b[>83;40003;0c"); + } + } + } + + /// + /// CSI Ps n Device Status Report (DSR). + /// Ps = 5 -> Status Report. Result (``OK'') is + /// CSI 0 n + /// Ps = 6 -> Report Cursor Position (CPR) [row;column]. + /// Result is + /// CSI r ; c R + /// CSI ? Ps n + /// Device Status Report (DSR, DEC-specific). + /// Ps = 6 -> Report Cursor Position (CPR) [row;column] as CSI + /// ? r ; c R (assumes page is zero). + /// Ps = 1 5 -> Report Printer status as CSI ? 1 0 n (ready). + /// or CSI ? 1 1 n (not ready). + /// Ps = 2 5 -> Report UDK status as CSI ? 2 0 n (unlocked) + /// or CSI ? 2 1 n (locked). + /// Ps = 2 6 -> Report Keyboard status as + /// CSI ? 2 7 ; 1 ; 0 ; 0 n (North American). + /// The last two parameters apply to VT400 & up, and denote key- + /// board ready and LK01 respectively. + /// Ps = 5 3 -> Report Locator status as + /// CSI ? 5 3 n Locator available, if compiled-in, or + /// CSI ? 5 0 n No Locator, if not. + /// + public static void csiDSR (this Terminal terminal, int [] pars, string collect) + { + var buffer = terminal.Buffer; + + if (collect == "") { + switch (pars [0]) { + case 5: + // status report + terminal.SendResponse ("\x1b[0n"); + break; + case 6: + // cursor position + var y = Math.Max (1, buffer.Y + 1 - (terminal.OriginMode ? buffer.ScrollTop : 0)); + // Need the max, because the cursor could be before the leftMargin + var x = Math.Max (1, buffer.X + 1 - (terminal.OriginMode ? buffer.MarginLeft : 0)); + terminal.SendResponse ($"\x1b[{y};{x}R"); + break; + } + } else if (collect == "?") { + // modern xterm doesnt seem to + // respond to any of these except ?6, 6, and 5 + switch (pars [0]) { + case 6: + // cursor position + var y = buffer.Y + 1 - (terminal.OriginMode ? buffer.ScrollTop : 0); + // Need the max, because the cursor could be before the leftMargin + var x = Math.Max (1, buffer.X + 1 - (terminal.IsUsingMargins () ? buffer.MarginLeft : 0)); + terminal.SendResponse ($"\x1b[?{y};{x};1R"); + break; + case 15: + // Request printer status report, we respond "We are ready" + terminal.SendResponse ($"{terminal.ControlCodes.CSI}?10n"); + break; + case 25: + // We respond "User defined keys are locked" + terminal.SendResponse ($"{terminal.ControlCodes.CSI}?21n"); + break; + case 26: + // Requests keyboard type + // We respond "American keyboard", TODO: worth plugging something else? Mac perhaps? + terminal.SendResponse ($"{terminal.ControlCodes.CSI}?27;1;0;0n"); + break; + case 53: + // no dec locator/mouse + // this.handler(C0.ESC + '[?50n'); + break; + case 55: + // Request locator status + terminal.SendResponse ($"{terminal.ControlCodes.CSI}?53n"); + break; + case 56: + // What kind of locator we have, we reply mouse, but perhaps on iOS we should respond something else + terminal.SendResponse ($"{terminal.ControlCodes.CSI}?57;1n"); + break; + case 62: + // Macro space report + terminal.SendResponse ($"{terminal.ControlCodes.CSI}0*{'{'}"); + break; + case 63: + // Requests checksum of macros, we return 0 + var id = pars.Length > 1 ? pars [1] : 0; + terminal.SendResponse ($"{terminal.ControlCodes.DCS}{id}!~0000{terminal.ControlCodes.ST}"); + break; + case 75: + // Data integrity report, no issues: + terminal.SendResponse ($"{terminal.ControlCodes.CSI}?70n"); + break; + case 85: + // Multiple session status, we reply single session + terminal.SendResponse ($"{terminal.ControlCodes.CSI}?83n"); + break; + } + } + } + + } + +} \ No newline at end of file diff --git a/src/XtermSharp/Line.cs b/src/XtermSharp/Line.cs new file mode 100644 index 00000000..c808c03c --- /dev/null +++ b/src/XtermSharp/Line.cs @@ -0,0 +1,120 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; + +namespace XtermSharp { + /// + /// Gets a line of text that consists of one or more fragments + /// + [DebuggerDisplay ("{DebuggerDisplay}")] + public class Line { + readonly List fragments; + + public Line () + { + fragments = new List (); + } + + /// + /// Gets the line number of the first fragment. + /// + public int StartLine { + get { + if (fragments.Count > 0) { + return fragments [0].Line; + } + + return 0; + } + } + + /// + /// Gets the location of the first fragment. + /// + public int StartLocation { + get { + if (fragments.Count > 0) { + return fragments [0].Location; + } + + return 0; + } + } + + /// + /// Gets the length of the line + /// + public int Length { get; private set; } + + string DebuggerDisplay { + get { + if (fragments.Count < 1) { + return "[]"; + } + + var sb = new StringBuilder (); + sb.Append ($"{fragments.Count}/{Length} : ["); + for (int i = 0; i < fragments.Count; i++) { + if (fragments [i].Text == "\n") { + sb.Append ("\\n"); + } else { + sb.Append (fragments [i].Text); + } + + if (i < fragments.Count - 1) + sb.Append ("]["); + } + sb.Append ("]"); + + return sb.ToString (); + } + } + + public void Add (LineFragment fragment) + { + fragments.Add (fragment); + + Length += fragment.Length; + } + + public void GetFragmentStrings (StringBuilder builder) + { + foreach (var fragment in fragments) { + builder.Append (fragment.Text); + } + } + + + /// + /// For a given line, find the fragment with the position + /// + public int GetFragmentIndexForPosition (int pos) + { + int count = 0; + for (int i = 0; i < fragments.Count; i++) { + count += fragments [i].Length; + if (count > pos) { + return i; + } + } + + return fragments.Count - 1; + } + + public LineFragment GetFragment (int index) + { + return fragments [index]; + } + + + public override string ToString () + { + var sb = new StringBuilder (); + foreach (var fragment in fragments) { + sb.Append (fragment.Text); + } + + return sb.ToString (); + } + } +} diff --git a/src/XtermSharp/LineFragment.cs b/src/XtermSharp/LineFragment.cs new file mode 100644 index 00000000..cf1d30c7 --- /dev/null +++ b/src/XtermSharp/LineFragment.cs @@ -0,0 +1,54 @@ +using System.Diagnostics; + +namespace XtermSharp { + /// + /// Represents a fragment of text from a line. A fragment is wholly enclosed + /// in a buffer line. + /// + [DebuggerDisplay ("{DebuggerDisplay}")] + public class LineFragment { + public LineFragment (string text, int line, int location) + { + Text = text ?? string.Empty; + Line = line; + Location = location; + Length = Text.Length; + } + + /// + /// Gets the line in the buffer in which this fragment exists + /// + public int Line { get; } + + /// + /// Gets the position in the buffer line where this fragment starts + /// + public int Location { get; } + + /// + /// Gets the text representation of this fragment + /// + public string Text { get; } + + /// + /// Gets the length of the text fragment + /// + public int Length { get; } + + string DebuggerDisplay { + get { + if (Text == "\n") { + return $"{Line}:{Location}:\\n"; + } + + return $"{Line}:{Location}:{Text}"; + } + } + + public static LineFragment NewLine (int line) + { + // TOOD: is this location correct or useful? + return new LineFragment ("\n", line, -1); + } + } +} diff --git a/src/XtermSharp/MouseMode.cs b/src/XtermSharp/MouseMode.cs new file mode 100644 index 00000000..e11aa0f7 --- /dev/null +++ b/src/XtermSharp/MouseMode.cs @@ -0,0 +1,37 @@ +namespace XtermSharp { + /// + /// Represents the mouse operation mode that the terminal is currently using and higher level + /// implementations should use the functions in this enumeration to determine what events to + /// send + /// + public enum MouseMode { + /// + /// + /// No mouse events are reported + Off, + + /// + /// X10 Compatibility mode - only sends events in button press + /// + X10, + + /// + /// VT200, also known as Normal Tracking Mode - sends both press and release events + /// + VT200, + + /// + /// ButtonEventTracking - In addition to sending button press and release events, it sends motion events when the button is pressed + /// + ButtonEventTracking, + + /// + /// Sends button presses, button releases, and motion events regardless of the button state + /// + AnyEvent + + // Unsupported modes: + // - vt200Highlight, this can deadlock the terminal + // - declocator, rarely used + } +} diff --git a/src/XtermSharp/MouseModeExensions.cs b/src/XtermSharp/MouseModeExensions.cs new file mode 100644 index 00000000..d425031b --- /dev/null +++ b/src/XtermSharp/MouseModeExensions.cs @@ -0,0 +1,43 @@ +namespace XtermSharp { + public static class MouseModeExensions { + /// + /// Returns true if you should send a button press event (separate from release) + /// + public static bool SendButtonPress (this MouseMode mode) + { + return mode == MouseMode.VT200 || mode == MouseMode.ButtonEventTracking || mode == MouseMode.AnyEvent; + } + + /// + /// Returns true if you should send the button release event + /// + public static bool SendButtonRelease (this MouseMode mode) + { + return mode != MouseMode.Off; + } + + /// + /// Returns true if you should send a motion event when a button is pressed + /// + public static bool SendButtonTracking (this MouseMode mode) + { + return mode == MouseMode.ButtonEventTracking || mode == MouseMode.AnyEvent; + } + + /// + /// Returns true if you should send a motion event, regardless of button state + /// + public static bool SendMotionEvent (this MouseMode mode) + { + return mode == MouseMode.AnyEvent; + } + + /// + /// Returns true if the modifiers should be encoded + /// + public static bool SendsModifiers (this MouseMode mode) + { + return mode == MouseMode.VT200 || mode == MouseMode.ButtonEventTracking || mode == MouseMode.AnyEvent; + } + } +} diff --git a/src/XtermSharp/MouseProtocolEncoding.cs b/src/XtermSharp/MouseProtocolEncoding.cs new file mode 100644 index 00000000..0f5b9c49 --- /dev/null +++ b/src/XtermSharp/MouseProtocolEncoding.cs @@ -0,0 +1,33 @@ +using System; + +namespace XtermSharp { + /// + /// The mouse coordinates can be encoded in a number of ways, and obey to historical + /// upgrades to the protocol, but also attempts at fixing limitations of the different + /// encodings. + /// + public enum MouseProtocolEncoding { + /// + /// The default x10 mode is limited to coordinates up to 223. + /// (255-32). The other modes solve this limitaion + /// + X10, + + /// + /// Extends the range of a coordinate to 2015 by using UTF-8 encoding of the + /// coordinate value. This encoding is troublesome for applications that + /// do not support utf8 input. + /// + UTF8, + + /// + /// The response uses CSI < ButtonValue ; Px ; Py [Mm] + /// + SGR, + + /// + // Different response style, with possible ambiguities, not recommended + /// + URXVT + } +} diff --git a/src/XtermSharp/Point.cs b/src/XtermSharp/Point.cs new file mode 100644 index 00000000..672fee7b --- /dev/null +++ b/src/XtermSharp/Point.cs @@ -0,0 +1,28 @@ +namespace XtermSharp +{ + public struct Point + { + public static readonly Point Empty; + + public Point(int x, int y) + { + X = x; + Y = y; + } + + public int X { get; set; } + public int Y { get; set; } + + public bool IsEmpty => X == 0 && Y == 0; + + public static bool operator ==(Point left, Point right) => left.X == right.X && left.Y == right.Y; + + public static bool operator !=(Point left, Point right) => !(left == right); + + public override bool Equals(object obj) => obj is Point point && point.X == X && point.Y == Y; + + public override int GetHashCode() => X.GetHashCode() * 327 + Y.GetHashCode(); + + public override string ToString() => "{X=" + X.ToString() + ",Y=" + Y.ToString() + "}"; + } +} diff --git a/src/XtermSharp/Pty.cs b/src/XtermSharp/Pty.cs new file mode 100644 index 00000000..680e13ad --- /dev/null +++ b/src/XtermSharp/Pty.cs @@ -0,0 +1,161 @@ +using System; +using System.Runtime.InteropServices; +using System.IO; +using System.Diagnostics; +using System.Text; + +namespace XtermSharp { + [StructLayout(LayoutKind.Sequential)] + public struct UnixWindowSize { + public short row, col, xpixel, ypixel; + } + + public class Pty { + [DllImport ("util")] + extern static int forkpty (ref int master, IntPtr dataReturn, IntPtr termios, ref UnixWindowSize WinSz); + + [DllImport ("libc")] + extern static int execv (string process, string [] args); + + [DllImport ("libc")] + extern static int execve (string process, string [] args, string [] env); + + [DllImport ("libpty.dylib", SetLastError = true, EntryPoint="fork_and_exec")] + extern static unsafe int HeavyFork (string process, byte** args, byte** env, ref int master, ref UnixWindowSize winSize); + + static bool HeavyDuty = true; + /// + /// Forks a process and returns a file handle that is connected to the standard output of the child process + /// + /// Name of the program to run + /// Argument to pass to the program + /// Desired environment variables for the program + /// The file descriptor connected to the input and output of the child process + /// Desired window size + /// + public static int ForkAndExec (string programName, string [] args, string [] env, ref int master, UnixWindowSize winSize) + { + if (HeavyDuty) { + return DoHeavyFork (programName, args, env, ref master, ref winSize); + } else { + var pid = forkpty (ref master, IntPtr.Zero, IntPtr.Zero, ref winSize); + if (pid < 0) + throw new Exception ("Could not create Pty"); + + if (pid == 0) { + execve (programName, args, env); + } + return pid; + } + } + + + static unsafe int DoHeavyFork (string programName, string [] args, string [] env, ref int master, ref UnixWindowSize winSize) + { + byte** argvPtr = null, envpPtr = null; + int result; + try { + AllocNullTerminatedArray (args, ref argvPtr); + AllocNullTerminatedArray (env, ref envpPtr); + result = HeavyFork (programName, argvPtr, envpPtr, ref master, ref winSize); + + if (result < 0) + { + throw new ArgumentException($"Invalid PID. Last error { Marshal.GetLastWin32Error() }"); + } + + return result; + } finally { + FreeArray (argvPtr, args.Length); + FreeArray (envpPtr, env.Length); + } + } + + private static unsafe void AllocNullTerminatedArray (string [] arr, ref byte** arrPtr) + { + int arrLength = arr.Length + 1; // +1 is for null termination + + // Allocate the unmanaged array to hold each string pointer. + // It needs to have an extra element to null terminate the array. + arrPtr = (byte**)Marshal.AllocHGlobal (sizeof (IntPtr) * arrLength); + Debug.Assert (arrPtr != null); + + // Zero the memory so that if any of the individual string allocations fails, + // we can loop through the array to free any that succeeded. + // The last element will remain null. + for (int i = 0; i < arrLength; i++) { + arrPtr [i] = null; + } + + // Now copy each string to unmanaged memory referenced from the array. + // We need the data to be an unmanaged, null-terminated array of UTF8-encoded bytes. + for (int i = 0; i < arr.Length; i++) { + byte [] byteArr = Encoding.UTF8.GetBytes (arr [i]); + + arrPtr [i] = (byte*)Marshal.AllocHGlobal (byteArr.Length + 1); //+1 for null termination + Debug.Assert (arrPtr [i] != null); + + Marshal.Copy (byteArr, 0, (IntPtr)arrPtr [i], byteArr.Length); // copy over the data from the managed byte array + arrPtr [i] [byteArr.Length] = (byte)'\0'; // null terminate + } + } + + private static unsafe void FreeArray (byte** arr, int length) + { + if (arr != null) { + // Free each element of the array + for (int i = 0; i < length; i++) { + if (arr [i] != null) { + Marshal.FreeHGlobal ((IntPtr)arr [i]); + arr [i] = null; + } + } + + // And then the array itself + Marshal.FreeHGlobal ((IntPtr)arr); + } + } + + [DllImport ("libc", SetLastError = true)] + extern static int ioctl (int fd, ulong cmd, ref UnixWindowSize WinSz); + + [DllImport ("libpty.dylib", EntryPoint = "set_window_size")] + extern static unsafe int set_window_size (int master, ref UnixWindowSize winSize); + + /// + /// Sends a request to the pseudo terminal to set the size to the specified one + /// + /// File descriptor returned by ForkPty + /// The desired window size + /// + public static int SetWinSize (int fd, ref UnixWindowSize winSize) + { + var r = set_window_size (fd, ref winSize); + if (r == -1) { + var lastErr = Marshal.GetLastWin32Error (); + Console.WriteLine (lastErr); + } + return r; + } + + [DllImport ("libc", SetLastError = true)] + extern static int ioctl (int fd, long cmd, ref long size); + + /// + /// Returns the number of bytes available for reading on a file descriptor + /// + /// + /// + /// + public static int AvailableBytes (int fd, ref long size) + { + const long MAC_FIONREAD = 0x4004667f; + var r = ioctl (fd, MAC_FIONREAD, ref size); + if (r == -1) { + var lastErr = Marshal.GetLastWin32Error (); + Console.WriteLine (lastErr); + } + return r; + } + } +} diff --git a/src/XtermSharp/ReadingBuffer.cs b/src/XtermSharp/ReadingBuffer.cs new file mode 100644 index 00000000..fd38dd1c --- /dev/null +++ b/src/XtermSharp/ReadingBuffer.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; + +namespace XtermSharp { + /// + /// Buffer for processing input + /// + /// + /// Because data might not be complete, we need to put back data that we read to process on + /// a future read. To prepare for reading, on every call to parse, the prepare method is + /// given the new buffer to read from. + /// + /// the `hasNext` describes whether there is more data left on the buffer, and `bytesLeft` + /// returnes the number of bytes left. The `getNext` method fetches either the next + /// value from the putback buffer, or when it is empty, it returns it from the buffer that + /// was passed during prepare. + /// + /// Additionally, the terminal parser needs to reset the parser state on demand, and + /// that is surfaced via reset + /// + class ReadingBuffer { + byte[] putbackBuffer = new byte [0]; + unsafe byte* buffer; + int bufferStart; + int totalCount; + int index; + + unsafe public void Prepare (byte* data, int start, int length) + { + buffer = data; + bufferStart = start; + + index = 0; + totalCount = putbackBuffer.Length + length; + } + + public int BytesLeft () + { + return totalCount - index; + } + + public bool HasNext () + { + return index < totalCount; + } + + unsafe public byte GetNext () + { + byte val; + if (index < putbackBuffer.Length) { + // grab from putback buffer + val = putbackBuffer [index]; + } else { + // grab from the prepared buffer + val = buffer [bufferStart + (index - putbackBuffer.Length)]; + } + + index++; + return val; + } + + /// + /// Puts back code and the remainder of the buffer + /// + public void Putback (byte code) + { + var left = BytesLeft (); + byte [] newPutback = new byte[left + 1]; + newPutback [0] = code; + + for (int i = 0; i < left; i++) { + newPutback [i + 1] = GetNext (); + } + + putbackBuffer = newPutback; + } + + unsafe public void Done () + { + if (index < putbackBuffer.Length) { + byte [] newPutback = new byte [putbackBuffer.Length - index]; + Array.Copy (putbackBuffer, index, newPutback, 0, newPutback.Length); + putbackBuffer = newPutback; + } else { + putbackBuffer = new byte [0]; + } + + buffer = null; + } + + public void Reset () + { + putbackBuffer = new byte [0]; + index = 0; + } + } +} diff --git a/src/XtermSharp/ReflowNarrower.cs b/src/XtermSharp/ReflowNarrower.cs new file mode 100644 index 00000000..423e1784 --- /dev/null +++ b/src/XtermSharp/ReflowNarrower.cs @@ -0,0 +1,267 @@ +using System; +using System.Collections.Generic; + +namespace XtermSharp { + class ReflowNarrower : ReflowStrategy { + public ReflowNarrower (Buffer buffer) : base (buffer) + { + } + + public override void Reflow (int newCols, int newRows, int oldCols, int oldRows) + { + // Gather all BufferLines that need to be inserted into the Buffer here so that they can be + // batched up and only committed once + List toInsert = new List (); + int countToInsert = 0; + + // Go backwards as many lines may be trimmed and this will avoid considering them + for (int y = Buffer.Lines.Length - 1; y >= 0; y--) { + // Check whether this line is a problem or not, if not skip it + BufferLine nextLine = Buffer.Lines [y]; + int lineLength = nextLine.GetTrimmedLength (); + if (!nextLine.IsWrapped && lineLength <= newCols) { + continue; + } + + // Gather wrapped lines and adjust y to be the starting line + List wrappedLines = new List (); + wrappedLines.Add (nextLine); + while (nextLine.IsWrapped && y > 0) { + nextLine = Buffer.Lines [--y]; + wrappedLines.Insert (0, nextLine); + } + + // If these lines contain the cursor don't touch them, the program will handle fixing up + // wrapped lines with the cursor + int absoluteY = Buffer.YBase + Buffer.Y; + + if (absoluteY >= y && absoluteY < y + wrappedLines.Count) { + continue; + } + + int lastLineLength = wrappedLines [wrappedLines.Count - 1].GetTrimmedLength (); + int [] destLineLengths = GetNewLineLengths (wrappedLines, oldCols, newCols); + int linesToAdd = destLineLengths.Length - wrappedLines.Count; + + int trimmedLines; + if (Buffer.YBase == 0 && Buffer.Y != Buffer.Lines.Length - 1) { + // If the top section of the buffer is not yet filled + trimmedLines = Math.Max (0, Buffer.Y - Buffer.Lines.MaxLength + linesToAdd); + } else { + trimmedLines = Math.Max (0, Buffer.Lines.Length - Buffer.Lines.MaxLength + linesToAdd); + } + + // Add the new lines + List newLines = new List (); + for (int i = 0; i < linesToAdd; i++) { + BufferLine newLine = Buffer.GetBlankLine (CharData.DefaultAttr, true); + newLines.Add (newLine); + } + + if (newLines.Count > 0) { + toInsert.Add (new InsertionSet { + Start = y + wrappedLines.Count + countToInsert, + Lines = newLines.ToArray () + }); + + countToInsert += newLines.Count; + } + + newLines.ForEach (l => wrappedLines.Add (l)); + + // Copy buffer data to new locations, this needs to happen backwards to do in-place + int destLineIndex = destLineLengths.Length - 1; // Math.floor(cellsNeeded / newCols); + int destCol = destLineLengths [destLineIndex]; // cellsNeeded % newCols; + if (destCol == 0) { + destLineIndex--; + destCol = destLineLengths [destLineIndex]; + } + + int srcLineIndex = wrappedLines.Count - linesToAdd - 1; + int srcCol = lastLineLength; + while (srcLineIndex >= 0) { + int cellsToCopy = Math.Min (srcCol, destCol); + wrappedLines [destLineIndex].CopyCellsFrom (wrappedLines [srcLineIndex], srcCol - cellsToCopy, destCol - cellsToCopy, cellsToCopy); + destCol -= cellsToCopy; + if (destCol == 0) { + destLineIndex--; + if (destLineIndex >= 0) + destCol = destLineLengths [destLineIndex]; + } + + srcCol -= cellsToCopy; + if (srcCol == 0) { + srcLineIndex--; + int wrappedLinesIndex = Math.Max (srcLineIndex, 0); + srcCol = GetWrappedLineTrimmedLength (wrappedLines, wrappedLinesIndex, oldCols); + } + } + + // Null out the end of the line ends if a wide character wrapped to the following line + for (int i = 0; i < wrappedLines.Count; i++) { + if (destLineLengths [i] < newCols) { + wrappedLines [i] [destLineLengths [i]] = CharData.Null; + } + } + + // Adjust viewport as needed + int viewportAdjustments = linesToAdd - trimmedLines; + while (viewportAdjustments-- > 0) { + if (Buffer.YBase == 0) { + if (Buffer.Y < newRows - 1) { + Buffer.Y++; + Buffer.Lines.Pop (); + } else { + Buffer.YBase++; + Buffer.YDisp++; + } + } else { + // Ensure ybase does not exceed its maximum value + if (Buffer.YBase < Math.Min (Buffer.Lines.MaxLength, Buffer.Lines.Length + countToInsert) - newRows) { + if (Buffer.YBase == Buffer.YDisp) { + Buffer.YDisp++; + } + + Buffer.YBase++; + } + } + } + + Buffer.SavedY = Math.Min (Buffer.SavedY + linesToAdd, Buffer.YBase + newRows - 1); + } + + Rearrange (toInsert, countToInsert); + + } + + void Rearrange (List toInsert, int countToInsert) + { + // Rearrange lines in the buffer if there are any insertions, this is done at the end rather + // than earlier so that it's a single O(n) pass through the buffer, instead of O(n^2) from many + // costly calls to CircularList.splice. + if (toInsert.Count > 0) { + // Record buffer insert events and then play them back backwards so that the indexes are + // correct + List insertEvents = new List (); + + // Record original lines so they don't get overridden when we rearrange the list + CircularList originalLines = new CircularList (Buffer.Lines.MaxLength); + for (int i = 0; i < Buffer.Lines.Length; i++) { + originalLines.Push (Buffer.Lines [i]); + } + + int originalLinesLength = Buffer.Lines.Length; + + int originalLineIndex = originalLinesLength - 1; + int nextToInsertIndex = 0; + InsertionSet nextToInsert = toInsert [nextToInsertIndex]; + Buffer.Lines.Length = Math.Min (Buffer.Lines.MaxLength, Buffer.Lines.Length + countToInsert); + + int countInsertedSoFar = 0; + for (int i = Math.Min (Buffer.Lines.MaxLength - 1, originalLinesLength + countToInsert - 1); i >= 0; i--) { + if (!nextToInsert.IsNull && nextToInsert.Start > originalLineIndex + countInsertedSoFar) { + // Insert extra lines here, adjusting i as needed + for (int nextI = nextToInsert.Lines.Length - 1; nextI >= 0; nextI--) { + if (i < 0) { + // if we reflow and the content has to be scrolled back past the beginning + // of the buffer then we end up loosing those lines + break; + } + + Buffer.Lines [i--] = nextToInsert.Lines [nextI]; + } + + i++; + + // Create insert events for later + //insertEvents.Add ({ + // index: originalLineIndex + 1, + // amount: nextToInsert.newLines.length + //}); + + countInsertedSoFar += nextToInsert.Lines.Length; + if (nextToInsertIndex < toInsert.Count - 1) { + nextToInsert = toInsert [++nextToInsertIndex]; + } else { + nextToInsert = InsertionSet.Null; + } + } else { + Buffer.Lines [i] = originalLines [originalLineIndex--]; + } + } + + /* + // Update markers + let insertCountEmitted = 0; + for (let i = insertEvents.length - 1; i >= 0; i--) { + insertEvents [i].index += insertCountEmitted; + this.lines.onInsertEmitter.fire (insertEvents [i]); + insertCountEmitted += insertEvents [i].amount; + } + + const amountToTrim = Math.max (0, originalLinesLength + countToInsert - this.lines.maxLength); + if (amountToTrim > 0) { + this.lines.onTrimEmitter.fire (amountToTrim); + } + */ + } + } + + /// + /// Gets the new line lengths for a given wrapped line. The purpose of this function it to pre- + /// compute the wrapping points since wide characters may need to be wrapped onto the following line. + /// This function will return an array of numbers of where each line wraps to, the resulting array + /// will only contain the values `newCols` (when the line does not end with a wide character) and + /// `newCols - 1` (when the line does end with a wide character), except for the last value which + /// will contain the remaining items to fill the line. + /// Calling this with a `newCols` value of `1` will lock up. + /// + int [] GetNewLineLengths (List wrappedLines, int oldCols, int newCols) + { + List newLineLengths = new List (); + + int cellsNeeded = 0; + for (int i = 0; i < wrappedLines.Count; i++) { + cellsNeeded += GetWrappedLineTrimmedLength (wrappedLines, i, oldCols); + } + + // Use srcCol and srcLine to find the new wrapping point, use that to get the cellsAvailable and + // linesNeeded + int srcCol = 0; + int srcLine = 0; + int cellsAvailable = 0; + while (cellsAvailable < cellsNeeded) { + if (cellsNeeded - cellsAvailable < newCols) { + // Add the final line and exit the loop + newLineLengths.Add (cellsNeeded - cellsAvailable); + break; + } + + srcCol += newCols; + int oldTrimmedLength = GetWrappedLineTrimmedLength (wrappedLines, srcLine, oldCols); + if (srcCol > oldTrimmedLength) { + srcCol -= oldTrimmedLength; + srcLine++; + } + + bool endsWithWide = wrappedLines [srcLine].GetWidth (srcCol - 1) == 2; + if (endsWithWide) { + srcCol--; + } + + int lineLength = endsWithWide ? newCols - 1 : newCols; + newLineLengths.Add (lineLength); + cellsAvailable += lineLength; + } + + return newLineLengths.ToArray (); + } + + struct InsertionSet { + public BufferLine [] Lines; + public int Start; + public bool IsNull; + public static InsertionSet Null => new InsertionSet { IsNull = true }; + } + } +} diff --git a/src/XtermSharp/ReflowStrategy.cs b/src/XtermSharp/ReflowStrategy.cs new file mode 100644 index 00000000..493efd3b --- /dev/null +++ b/src/XtermSharp/ReflowStrategy.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; + +namespace XtermSharp { + abstract class ReflowStrategy { + protected ReflowStrategy (Buffer buffer) + { + this.Buffer = buffer; + } + + public Buffer Buffer { get; } + + public abstract void Reflow (int newCols, int newRows, int oldCols, int oldRows); + + protected static int GetWrappedLineTrimmedLength (CircularList lines, int row, int cols) + { + return GetWrappedLineTrimmedLength (lines [row], row == lines.Length - 1 ? null : lines [row + 1], cols); + } + + protected static int GetWrappedLineTrimmedLength (List lines, int row, int cols) + { + return GetWrappedLineTrimmedLength (lines [row], row == lines.Count - 1 ? null : lines [row+1], cols); + } + + protected static int GetWrappedLineTrimmedLength (BufferLine line, BufferLine nextLine, int cols) + { + // If this is the last row in the wrapped line, get the actual trimmed length + if (nextLine == null) { + return line.GetTrimmedLength (); + } + + // Detect whether the following line starts with a wide character and the end of the current line + // is null, if so then we can be pretty sure the null character should be excluded from the line + // length] + bool endsInNull = !(line.HasContent (cols - 1)) && line.GetWidth (cols - 1) == 1; + bool followingLineStartsWithWide = nextLine.GetWidth (0) == 2; + + if (endsInNull && followingLineStartsWithWide) { + return cols - 1; + } + + return cols; + } + } +} diff --git a/src/XtermSharp/ReflowWider.cs b/src/XtermSharp/ReflowWider.cs new file mode 100644 index 00000000..37effd07 --- /dev/null +++ b/src/XtermSharp/ReflowWider.cs @@ -0,0 +1,200 @@ +using System; +using System.Collections.Generic; + +namespace XtermSharp { + class ReflowWider : ReflowStrategy { + public ReflowWider (Buffer buffer) : base (buffer) + { + } + + public override void Reflow (int newCols, int newRows, int oldCols, int oldRows) + { + int [] toRemove = GetLinesToRemove (Buffer.Lines, oldCols, newCols, Buffer.YBase + Buffer.Y, CharData.Null); + if (toRemove.Length > 0) { + LayoutResult newLayoutResult = CreateNewLayout (Buffer.Lines, toRemove); + ApplyNewLayout (Buffer.Lines, newLayoutResult.Layout); + AdjustViewport (newCols, newRows, newLayoutResult.RemovedCount); + } + + } + + /// + /// Evaluates and returns indexes to be removed after a reflow larger occurs. Lines will be removed + /// when a wrapped line unwraps. + /// + /// The buffer lines + /// The columns before resize + /// The columns after resize + /// + /// + int [] GetLinesToRemove (CircularList lines, int oldCols, int newCols, int bufferAbsoluteY, CharData nullCharacter) + { + // Gather all BufferLines that need to be removed from the Buffer here so that they can be + // batched up and only committed once + List toRemove = new List (); + + for (int y = 0; y < lines.Length - 1; y++) { + // Check if this row is wrapped + int i = y; + BufferLine nextLine = lines [++i]; + if (!nextLine.IsWrapped) { + continue; + } + + // Check how many lines it's wrapped for + List wrappedLines = new List (lines.Length - y); + wrappedLines.Add (lines [y]); + while (i < lines.Length && nextLine.IsWrapped) { + wrappedLines.Add (nextLine); + nextLine = lines [++i]; + } + + // If these lines contain the cursor don't touch them, the program will handle fixing up wrapped + // lines with the cursor + if (bufferAbsoluteY >= y && bufferAbsoluteY < i) { + y += wrappedLines.Count - 1; + continue; + } + + // Copy buffer data to new locations + int destLineIndex = 0; + int destCol = GetWrappedLineTrimmedLength (Buffer.Lines, destLineIndex, oldCols); + int srcLineIndex = 1; + int srcCol = 0; + while (srcLineIndex < wrappedLines.Count) { + int srcTrimmedTineLength = GetWrappedLineTrimmedLength (wrappedLines, srcLineIndex, oldCols); + int srcRemainingCells = srcTrimmedTineLength - srcCol; + int destRemainingCells = newCols - destCol; + int cellsToCopy = Math.Min (srcRemainingCells, destRemainingCells); + + wrappedLines [destLineIndex].CopyCellsFrom (wrappedLines [srcLineIndex], srcCol, destCol, cellsToCopy); + + destCol += cellsToCopy; + if (destCol == newCols) { + destLineIndex++; + destCol = 0; + } + + srcCol += cellsToCopy; + if (srcCol == srcTrimmedTineLength) { + srcLineIndex++; + srcCol = 0; + } + + // Make sure the last cell isn't wide, if it is copy it to the current dest + if (destCol == 0 && destLineIndex != 0) { + if (wrappedLines [destLineIndex - 1].GetWidth (newCols - 1) == 2) { + wrappedLines [destLineIndex].CopyCellsFrom (wrappedLines [destLineIndex - 1], newCols - 1, destCol++, 1); + // Null out the end of the last row + wrappedLines [destLineIndex - 1].ReplaceCells (newCols - 1, 1, nullCharacter); + } + } + } + + // Clear out remaining cells or fragments could remain; + wrappedLines [destLineIndex].ReplaceCells (destCol, newCols, nullCharacter); + + // Work backwards and remove any rows at the end that only contain null cells + int countToRemove = 0; + for (int ix = wrappedLines.Count - 1; ix > 0; ix--) { + if (ix > destLineIndex || wrappedLines [ix].GetTrimmedLength () == 0) { + countToRemove++; + } else { + break; + } + } + + if (countToRemove > 0) { + toRemove.Add (y + wrappedLines.Count - countToRemove); // index + toRemove.Add (countToRemove); + } + + y += wrappedLines.Count - 1; + } + + return toRemove.ToArray (); + } + + LayoutResult CreateNewLayout (CircularList lines, int [] toRemove) + { + var layout = new CircularList (lines.Length); + + // First iterate through the list and get the actual indexes to use for rows + int nextToRemoveIndex = 0; + int nextToRemoveStart = toRemove [nextToRemoveIndex]; + int countRemovedSoFar = 0; + + for (int i = 0; i < lines.Length; i++) { + if (nextToRemoveStart == i) { + int countToRemove = toRemove [++nextToRemoveIndex]; + + // Tell markers that there was a deletion + //lines.onDeleteEmitter.fire ({ + // index: i - countRemovedSoFar, + // amount: countToRemove + //}); + + i += countToRemove - 1; + countRemovedSoFar += countToRemove; + + nextToRemoveStart = int.MaxValue; + if (nextToRemoveIndex < toRemove.Length - 1) + nextToRemoveStart = toRemove [++nextToRemoveIndex]; + } else { + layout.Push (i); + } + } + + return new LayoutResult () { + Layout = layout.ToArray (), + RemovedCount = countRemovedSoFar, + }; + } + + void ApplyNewLayout (CircularList lines, int [] newLayout) + { + var newLayoutLines = new CircularList (lines.Length); + + for (int i = 0; i < newLayout.Length; i++) { + newLayoutLines.Push (lines [newLayout [i]]); + } + + // Rearrange the list + for (int i = 0; i < newLayoutLines.Length; i++) { + lines [i] = newLayoutLines [i]; + } + + lines.Length = newLayout.Length; + } + + void AdjustViewport (int newCols, int newRows, int countRemoved) + { + int viewportAdjustments = countRemoved; + while (viewportAdjustments-- > 0) { + if (Buffer.YBase == 0) { + if (Buffer.Y > 0) { + Buffer.Y--; + } + + if (Buffer.Lines.Length < newRows) { + // Add an extra row at the bottom of the viewport + Buffer.Lines.Push (new BufferLine (newCols, CharData.Null)); + } + } else { + if (Buffer.YDisp == Buffer.YBase) { + Buffer.YDisp--; + } + + Buffer.YBase--; + } + } + + Buffer.SavedY = Math.Max (Buffer.SavedY - countRemoved, 0); + } + + struct LayoutResult { + public int [] Layout; + public int RemovedCount; + } + } +} diff --git a/src/XtermSharp/Renderer/Renderer.cs b/src/XtermSharp/Renderer/Renderer.cs new file mode 100644 index 00000000..115ca9d4 --- /dev/null +++ b/src/XtermSharp/Renderer/Renderer.cs @@ -0,0 +1,11 @@ +using System; +namespace XtermSharp { + public class Renderer { + public const int DefaultColor = 256; + public const int InvertedDefaultColor = 257; + + public Renderer () + { + } + } +} diff --git a/src/XtermSharp/SearchService.cs b/src/XtermSharp/SearchService.cs new file mode 100644 index 00000000..3fff1227 --- /dev/null +++ b/src/XtermSharp/SearchService.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; + +namespace XtermSharp { + /// + /// Handles searching in a terminal + /// + public class SearchService { + readonly SelectionService selection; + SearchSnapshot cache; + + public SearchService (Terminal terminal) + { + selection = new SelectionService (terminal); + } + + /// + /// This event is triggered when the current search snapshot has been invalidated + /// + public event Action Invalidated; + + /// + /// Gets a snapshot that can be used to perform searches. A snapshot is only useful if the buffer content or dimensions have not changed. + /// + public SearchSnapshot GetSnapshot () + { + if (cache == null) { + var result = CalculateSnapshot (); + cache = result; + } + + return cache; + } + + /// + /// Invalidates the current search snapshot due to content or size changes. + /// The cache should be invalidated when either the content of the buffer or the buffer dimensions change + /// because the snapshot has direct mappings to buffer line and locations. + /// + public void Invalidate () + { + // TODO: ideally this would be private and handled completely by the search service and consumers don't have to call this + Invalidated?.Invoke (this, cache?.LastSearch); + cache = null; + } + + private SearchSnapshot CalculateSnapshot () + { + selection.SelectAll (); + + var lines = selection.GetSelectedLines (); + + return CalculateSnapshot (lines); + } + + SearchSnapshot CalculateSnapshot (Line [] lines) + { + var result = new SearchSnapshot (lines); + + return result; + } + } + + public class SearchSnapshot { + readonly Line [] lines; + + public SearchSnapshot (Line [] lines) + { + this.lines = lines; + Text = GetTextFromLines (lines); + } + + public string Text { get; } + + [DebuggerDisplay ("{Start}, {End}")] + public class SearchResult { + public Point Start; + public Point End; + } + + /// + /// Gets the last used search term + /// + public string LastSearch { get; private set; } + + /// + /// Gets the last search results + /// + public SearchResult [] LastSearchResults { get; private set; } + + /// + /// Gets the index of the current search result + /// + public int CurrentSearchResult; + + /// + /// Given a string, returns start and end points in the buffer that contain that text + /// + public int FindText (string txt) + { + LastSearch = txt; + CurrentSearchResult = -1; + + if (string.IsNullOrEmpty(txt)) { + LastSearchResults = Array.Empty (); + return 0; + } + + // simple search for now, we might be able to just do a single scan of the buffer + // but if we want to support regex then this might be the better way + // a quick look at xterm.js and they still get a copy of the buffer and translate it to string + // so this is similar, maybe(?) caching more than we ultimately need. + var results = new List (); + + int baseLineIndex = 0; + int baseCount = 0; + var index = Text.IndexOf (txt, 0, StringComparison.CurrentCultureIgnoreCase); + while (index >= 0) { + // found a result + var result = new SearchResult (); + // whats the start and end pos of this text + // we can assume that it's on the same line, unless we are doing a regex because + // the user can't enter a \n as part of the seach term without regex + + // count the lines up to index + int count = baseCount; + for (int i = baseLineIndex; i < lines.Length; i++) { + count += lines [i].Length; + if (count > index) { + // found text is on line i + // the x position is the delta between the line start and index + // we can assume for now that the end is on the same line, since we do not yet support regex + + int lineStartCount = count - lines [i].Length; + + // we need to offset the points depending on whether the line fragment is wrapped or not + int startFragmentIndex = lines [i].GetFragmentIndexForPosition (index - lineStartCount); + LineFragment startFragment = lines [i].GetFragment (startFragmentIndex); + + // number of chars before this fragment, but on this line + int startOffset = 0; + for (int fi = 0; fi < startFragmentIndex; fi++) { + startOffset += lines [i].GetFragment (fi).Length; + } + + + result.Start = new Point (index - lineStartCount - startOffset, startFragment.Line); + + int endFragmentIndex = lines [i].GetFragmentIndexForPosition (index - lineStartCount + txt.Length - 1); + LineFragment endFragment = lines [i].GetFragment (endFragmentIndex); + + int endOffset = 0; + for (int fi = 0; fi < endFragmentIndex; fi++) { + endOffset += lines [i].GetFragment (fi).Length; + } + + result.End = new Point (index - lineStartCount + txt.Length - endOffset, endFragment.Line); + + // now, we need to fix up the end points because we might be on wrapped line + // which line fragment is the text on + + results.Add (result); + + break; + } + + // update base counts so that next time we loop we don't have to count these lines again + baseCount += lines [i].Length; + baseLineIndex++; + } + + // search again + index = Text.IndexOf (txt, index + txt.Length, StringComparison.CurrentCultureIgnoreCase); + } + + LastSearchResults = results.ToArray (); + CurrentSearchResult = -1; + + return LastSearchResults.Length; + } + + public SearchResult FindNext() + { + if (LastSearchResults == null || LastSearchResults.Length == 0) { + return null; + } + + CurrentSearchResult++; + if (CurrentSearchResult > LastSearchResults.Length - 1) + CurrentSearchResult = 0; + + return LastSearchResults [CurrentSearchResult]; + } + + public SearchResult FindPrevious() + { + if (LastSearchResults == null || LastSearchResults.Length == 0) { + return null; + } + + CurrentSearchResult--; + if (CurrentSearchResult < 0) + CurrentSearchResult = LastSearchResults.Length - 1; + + return LastSearchResults [CurrentSearchResult]; + } + + string GetTextFromLines (Line [] lines) + { + if (lines.Length == 0) + return string.Empty; + + var builder = new StringBuilder (); + foreach (var line in lines) { + line.GetFragmentStrings (builder); + } + + return builder.ToString (); + } + } +} diff --git a/src/XtermSharp/SelectionService.cs b/src/XtermSharp/SelectionService.cs new file mode 100644 index 00000000..05360abb --- /dev/null +++ b/src/XtermSharp/SelectionService.cs @@ -0,0 +1,499 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace XtermSharp { + /// + /// Provides a service for working with selections + /// + public class SelectionService { + readonly PointComparer comparer; + readonly Terminal terminal; + readonly NStack.ustring nullString; + readonly NStack.ustring spaceString; + private bool active; + + public SelectionService (Terminal terminal) + { + this.terminal = terminal; + comparer = new PointComparer (); + nullString = NStack.ustring.Make (CharData.Null.Rune); + spaceString = NStack.ustring.Make (" "); + } + + /// + /// Gets or sets a value indicating whether the selection is active or not + /// + public bool Active { + get => active; + set { + var oldState = active; + active = value; + if (oldState != value) { + SelectionChanged?.Invoke (); + } + } + } + + /// + /// Gets the selection start point in buffer coordinates + /// + public Point Start { get; private set; } + + /// + /// Gets the selection end point in buffer coordinates + /// + public Point End { get; private set; } + + /// + /// Raised when the selection range or active state has been changed + /// + public event Action SelectionChanged; + + /// + /// Starts selection from the given point in the buffer + /// + public void StartSelection (int row, int col) + { + Start = End = new Point (col, row + terminal.Buffer.YDisp); + // set the field to bypass sending this event twice + active = true; + SelectionChanged?.Invoke (); + } + + /// + /// Starts selection from the last start position + /// + public void StartSelection () + { + End = Start; + // set the field to bypass sending this event twice + active = true; + SelectionChanged?.Invoke (); + } + + /// + /// Sets the start and end positions but does not start selection + /// this lets us record the last position of mouse clicks so that + /// drag and shift+click operations know from where to start selection + /// from + /// + public void SetSoftStart(int row, int col) + { + Start = End = new Point (col, row + terminal.Buffer.YDisp); + } + + /// + /// Extends the selection based on the user "shift" clicking. This has + /// slightly different semantics than a "drag" extension because we can + /// shift the start to be the last prior end point if the new extension + /// is before the current start point. + /// + public void ShiftExtend (int row, int col) + { + active = true; + var newEnd = new Point (col, row + terminal.Buffer.YDisp); + + var shouldSwapStart = false; + if (comparer.Compare (Start, End) < 0) { + // start is before end, is the new end before Start + if (comparer.Compare (newEnd, Start) < 0) { + // yes, swap Start and End + shouldSwapStart = true; + } + } else if (comparer.Compare (Start, End) > 0) { + if (comparer.Compare (newEnd, Start) > 0) { + // yes, swap Start and End + shouldSwapStart = true; + } + } + + if (shouldSwapStart) { + Start = End; + } + + End = newEnd; + SelectionChanged?.Invoke (); + } + + /// + /// Extends the selection by moving the end point to the new point. + /// + public void DragExtend (int row, int col) + { + End = new Point (col, row + terminal.Buffer.YDisp); + SelectionChanged?.Invoke (); + } + + /// + /// Selects the entire buffer + /// + public void SelectAll() + { + Start = new Point (0, 0); + End = new Point (terminal.Cols - 1, terminal.Buffer.Lines.MaxLength - 1); + + // set the field to bypass sending this event twice + active = true; + SelectionChanged?.Invoke (); + } + + /// + /// Clears the selection + /// + public void SelectNone () + { + active = false; + SelectionChanged?.Invoke (); + } + + /// + /// Selects a row in the terminal + /// + public void SelectRow (int row) + { + Start = new Point (0, row + terminal.Buffer.YDisp); + End = new Point (terminal.Cols - 1, row + terminal.Buffer.YDisp); + // set the field to bypass sending this event twice + active = true; + SelectionChanged?.Invoke (); + } + + /// + /// Selects a word or expression based on the col and row that the user sees on screen + /// An expression is a balanced set parenthesis, braces or brackets + /// + public void SelectWordOrExpression(int col, int row) + { + var buffer = terminal.Buffer; + + // ensure the bounds are inside the terminal. + row = Math.Max (row, 0); + col = Math.Max (Math.Min (col, terminal.Buffer.Cols - 1), 0); + + row += buffer.YDisp; + + Func isLetterOrChar = (cd) => { + if (cd.IsNullChar ()) + return false; + return Rune.IsLetterOrDigit (cd.Rune); + }; + + var chr = buffer.GetChar (col, row); + if (chr.IsNullChar()) { + SimpleScanSelection (col, row, (ch) => { + return ch.IsNullChar (); + }); + } else { + if (isLetterOrChar(chr)) { + SimpleScanSelection (col, row, (ch) => { + return isLetterOrChar (ch) || ch.MatchesRune(CharData.Period); + }); + } else { + if (chr.MatchesRune(CharData.WhiteSpace)) { + SimpleScanSelection (col, row, (ch) => { + return ch.MatchesRune (CharData.WhiteSpace); + }); + } else if (chr.MatchesRune (CharData.LeftBrace) || chr.MatchesRune (CharData.LeftBracket) || chr.MatchesRune (CharData.LeftParenthesis)) { + BalancedSearchForward (col, row); + } else if (chr.MatchesRune (CharData.RightBrace) || chr.MatchesRune (CharData.RightBracket) || chr.MatchesRune (CharData.RightParenthesis)) { + BalancedSearchBackward (col, row); + } else { + // For other characters, we just stop there + Start = End = new Point (col, row + terminal.Buffer.YDisp); + } + } + } + + active = true; + SelectionChanged?.Invoke (); + } + + /// + /// Gets the selected range as text + /// + public string GetSelectedText () + { + var lines = GetSelectedLines (); + if (lines.Length == 0) + return string.Empty; + + var builder = new StringBuilder (); + foreach (var line in lines) { + line.GetFragmentStrings (builder); + } + + return builder.ToString (); + } + + /// + /// Gets the selected range as an array of Line + /// + public Line [] GetSelectedLines() + { + var start = Start; + var end = End; + + switch (comparer.Compare (start, End)) { + case 0: + return Array.Empty(); + case 1: + start = End; + end = Start; + break; + } + + if (start.Y < 0 || start.Y > terminal.Buffer.Lines.Length) { + return Array.Empty (); + } + + if (end.Y >= terminal.Buffer.Lines.Length) { + end.Y = terminal.Buffer.Lines.Length - 1; + } + + return GetSelectedLines (start, end); + } + + Line [] GetSelectedLines (Point start, Point end) + { + var lines = new List (); + var buffer = terminal.Buffer; + string str; + Line currentLine = new Line (); + lines.Add (currentLine); + + // keep a list of blank lines that we see. if we see content after a group + // of blanks, add those blanks but skip all remaining / trailing blanks + // these will be blank lines in the selected text output + var blanks = new List (); + + Action addBlanks = () => { + int lastLine = -1; + foreach (var b in blanks) { + if (lastLine != -1 && b.Line != lastLine) { + currentLine = new Line (); + lines.Add (currentLine); + } + + lastLine = b.Line; + currentLine.Add (b); + } + blanks.Clear (); + }; + + // get the first line + BufferLine bufferLine = buffer.Lines [start.Y]; + if (bufferLine != null && bufferLine.HasAnyContent ()) { + str = TranslateBufferLineToString (buffer, start.Y, start.X, start.Y < end.Y ? -1 : end.X); + + var fragment = new LineFragment (str, start.Y, start.X); + currentLine.Add (fragment); + } + + // get the middle rows + var line = start.Y + 1; + var isWrapped = false; + while (line < end.Y) { + bufferLine = buffer.Lines [line]; + isWrapped = bufferLine?.IsWrapped ?? false; + + str = TranslateBufferLineToString (buffer, line, 0, -1); + + if (bufferLine.HasAnyContent ()) { + // add previously gathered blank fragments + addBlanks (); + + if (!isWrapped) { + // this line is not a wrapped line, so the + // prior line has a hard linefeed + // add a fragment to that line + currentLine.Add (LineFragment.NewLine (line - 1)); + + // start a new line + currentLine = new Line (); + lines.Add (currentLine); + } + + // add the text we found to the current line + currentLine.Add (new LineFragment (str, line, 0)); + } else { + // this line has no content, which means that it's a blank line inserted + // somehow, or one of the trailing blank lines after the last actual content + // make a note of the line + // check that this line is a wrapped line, if so, add a line feed fragment + if (!isWrapped) { + blanks.Add (LineFragment.NewLine (line - 1)); + } + + blanks.Add (new LineFragment (str, line, 0)); + } + + line++; + } + + // get the last row + if (end.Y != start.Y) { + bufferLine = buffer.Lines [end.Y]; + if (bufferLine != null && bufferLine.HasAnyContent ()) { + addBlanks (); + + isWrapped = bufferLine?.IsWrapped ?? false; + str = TranslateBufferLineToString (buffer, end.Y, 0, end.X); + if (!isWrapped) { + currentLine.Add (LineFragment.NewLine (line - 1)); + currentLine = new Line (); + lines.Add (currentLine); + } + + currentLine.Add (new LineFragment (str, line, 0)); + } + } + + return lines.ToArray (); + } + + string TranslateBufferLineToString(Buffer buffer, int line, int start, int end) + { + return buffer.TranslateBufferLineToString (line, true, start, end).Replace (nullString, spaceString).ToString(); + } + + /// + /// Performs a simple "word" selection based on a function that determines inclussion into the group + /// + void SimpleScanSelection (int col, int row, Func includeFunc) + { + var buffer = terminal.Buffer; + + // Look backward + var colScan = col; + var left = colScan; + while (colScan >= 0) { + var ch = buffer.GetChar(colScan, row); + if (!includeFunc (ch)) { + break; + } + + left = colScan; + colScan -= 1; + } + + // Look forward + colScan = col; + var right = colScan; + var limit = terminal.Cols; + while (colScan < limit) { + var ch = buffer.GetChar (colScan, row); + + if (!includeFunc (ch)) { + break; + } + + colScan += 1; + right = colScan; + } + + Start = new Point (left, row); + End = new Point (right, row); + } + + /// + /// Performs a forward search for the `end` character, but this can extend across matching subexpressions + /// made of pairs of parenthesis, braces and brackets. + /// + void BalancedSearchForward (int col, int row) + { + var buffer = terminal.Buffer; + var startCol = col; + var wait = new List (); + + Start = new Point(col, row); + + for (int line = row; line < terminal.Rows; line++) { + for (int colIndex = startCol; colIndex < terminal.Cols; colIndex++) { + var p = new Point (colIndex, line); + var ch = buffer.GetChar (colIndex, line); + + if (ch.MatchesRune(CharData.LeftParenthesis)) { + wait.Insert (0, CharData.RightParenthesis); + } else if (ch.MatchesRune(CharData.LeftBracket)) { + wait.Insert (0, CharData.RightBracket); + } else if (ch.MatchesRune(CharData.LeftBrace)) { + wait.Insert (0, CharData.RightBrace); + } else { + var v = wait.Count > 0 ? wait [0] : CharData.Null; + if (!v.MatchesRune(CharData.Null) && v.MatchesRune(ch)) { + wait.RemoveAt (0); + if (wait.Count == 0) { + End = new Point (p.X + 1, p.Y); + return; + } + } + } + } + + startCol = 0; + } + + Start = End = new Point (col, row); + } + + /// + /// Performs a backward search for the `end` character, but this can extend across matching subexpressions + /// made of pairs of parenthesis, braces and brackets. + /// + void BalancedSearchBackward (int col, int row) + { + var buffer = terminal.Buffer; + var startCol = col; + var wait = new List (); + + End = new Point (col, row); + + for (int line = row; line > 0; line--) { + for (int colIndex = startCol; colIndex > 0; colIndex--) { + var p = new Point (colIndex, line); + var ch = buffer.GetChar (colIndex, line); + + if (ch.MatchesRune(CharData.RightParenthesis)) { + wait.Insert (0, CharData.LeftParenthesis); + } else if (ch.MatchesRune(CharData.RightBracket)) { + wait.Insert (0,CharData.LeftBracket); + } else if (ch.MatchesRune(CharData.RightBrace)) { + wait.Insert (0, CharData.LeftBrace); + } else { + var v = wait.Count > 0 ? wait [0] : CharData.Null; + if (!v.MatchesRune (CharData.Null) && v.MatchesRune (ch)) { + wait.RemoveAt (0); + if (wait.Count == 0) { + End = new Point (End.X + 1, End.Y); + Start = p; + return; + } + } + } + } + + startCol = terminal.Cols - 1; + } + + Start = End = new Point (col, row); + } + + class PointComparer : IComparer { + public int Compare (Point x, Point y) + { + if (x.Y < y.Y) + return -1; + if (x.Y > y.Y) + return 1; + // x and y are on the same row, compare columns + if (x.X < y.X) + return -1; + if (x.X > y.X) + return 1; + // they are the same + return 0; + } + } + } +} diff --git a/src/XtermSharp/Terminal.cs b/src/XtermSharp/Terminal.cs new file mode 100644 index 00000000..623032db --- /dev/null +++ b/src/XtermSharp/Terminal.cs @@ -0,0 +1,1215 @@ +using System; +using System.Collections.Generic; +using System.Text; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace XtermSharp { + + public partial class Terminal: ObservableObject + { + const int MINIMUM_COLS = 2; + const int MINIMUM_ROWS = 1; + + //static Dictionary matchColorCache = new Dictionary (); + readonly ITerminalDelegate terminalDelegate; + readonly ControlCodes controlCodes; + readonly List titleStack; + readonly List iconTitleStack; + readonly BufferSet buffers; + readonly InputHandler input; + + BufferLine blankLine; + + // modes + bool insertMode; + bool bracketedPasteMode; + + // saved modes + bool savedMarginMode; + bool savedOriginMode; + bool savedWraparound; + bool savedReverseWraparound; + + // unsorted + bool applicationKeypad, applicationCursor; + bool cursorHidden; + Dictionary charset; + int gcharset; + int gLevel; + int refreshStart = Int32.MaxValue; + int refreshEnd = -1; + bool userScrolling; + + public Terminal (ITerminalDelegate terminalDelegate = null, TerminalOptions options = null) + { + this.terminalDelegate = terminalDelegate ?? new SimpleTerminalDelegate (); + controlCodes = new ControlCodes () { Send8bit = false }; + titleStack = new List (); + iconTitleStack = new List (); + input = new InputHandler (this); + + Options = options ?? new TerminalOptions (); + Cols = Math.Max (Options.Cols, MINIMUM_COLS); + Rows = Math.Max (Options.Rows, MINIMUM_ROWS); + + buffers = new BufferSet (this); + Setup (); + } + + /// + /// Gets the delegate for the terminal + /// + public ITerminalDelegate Delegate => terminalDelegate; + + /// + /// Gets the control codes for the terminal + /// + public ControlCodes ControlCodes => controlCodes; + + /// + /// Gets the current title of the terminal + /// + public string Title { get; private set; } + + /// + /// Gets the current icon title of the terminal + /// + public string IconTitle { get; private set; } + + /// + /// Gets the currently active buffer + /// + public Buffer Buffer => buffers.Active; + + /// + /// Gets the BufferSet for the terminal + /// + public BufferSet Buffers => buffers; + + /// + /// Gets the margin mode of the terminal + /// + public bool MarginMode { get; internal set; } + + /// + /// Gets the origin mode of the terminal + /// + public bool OriginMode { get; internal set; } + + /// + /// Gets the Wraparound mode of the terminal + /// + public bool Wraparound { get; internal set; } + + /// + /// Gets the ReverseWraparound mode of the terminal + /// + public bool ReverseWraparound { get; internal set; } + + /// + /// Gets the current mouse mode + /// + public MouseMode MouseMode { get; internal set; } + + /// + /// Gets the current mouse protocol + /// + public MouseProtocolEncoding MouseProtocol { get; internal set; } + + /// + /// Gets a value indicating whether the terminal can be resized to 132 + /// + public bool Allow80To132 { get; internal set; } + + public Dictionary Charset { + get => charset; + set { + charset = value; + } + } + + public bool ApplicationCursor { get; internal set; } + public int SavedCols { get; internal set; } + public bool ApplicationKeypad { get; internal set; } + + public bool SendFocus { get; internal set; } + + public bool CursorHidden { get; internal set; } + public bool BracketedPasteMode { get; internal set; } + + public TerminalOptions Options { get; private set; } + + [ObservableProperty] + public partial int Cols { get; private set; } + + [ObservableProperty] + public partial int Rows { get; private set; } + + public bool InsertMode; + public int CurAttr; + + /// + /// Provides a baseline set of environment variables that would be useful to run the terminal, + /// you can customzie these accordingly. + /// + public static string [] GetEnvironmentVariables (string termName = null) + { + var l = new List (); + if (termName == null) + termName = "xterm-256color"; + + l.Add ("TERM=" + termName); + + // Without this, tools like "vi" produce sequences that are not UTF-8 friendly + l.Add ("LANG=en_US.UTF-8"); + var env = Environment.GetEnvironmentVariables (); + foreach (var x in new [] { "LOGNAME", "DISPLAY", "LC_TYPE", "USER", "HOME", "PATH" }) + if (env.Contains (x)) + l.Add ($"{x}={env [x]}"); + return l.ToArray (); + } + + /// + /// Called by input handlers to set the title + /// + internal void SetTitle (string text) + { + Title = text; + terminalDelegate.SetTerminalTitle (this, text); + } + + /// + /// Called by input handlers to push the current title onto the stack + /// + internal void PushTitle () + { + titleStack.Insert (0, Title); + } + + /// + /// Called by input handlers to pop and set the title to the last one on the stack + /// + internal void PopTitle () + { + if (titleStack.Count > 0) { + Title = titleStack[0]; + titleStack.RemoveAt (0); + } + + terminalDelegate.SetTerminalTitle (this, Title); + } + + /// + /// Called by input handlers to set the icon title + /// + internal void SetIconTitle (string text) + { + IconTitle = text; + terminalDelegate.SetTerminalIconTitle (this, text); + } + + /// + /// Called by input handlers to push the current icon title onto the stack + /// + internal void PushIconTitle () + { + iconTitleStack.Insert (0, IconTitle); + } + + /// + /// Called by input handlers to pop and set the icon title to the last one on the stack + /// + internal void PopIconTitle () + { + if (iconTitleStack.Count > 0) { + IconTitle = iconTitleStack [0]; + iconTitleStack.RemoveAt (0); + } + + terminalDelegate.SetTerminalIconTitle (this, IconTitle); + } + + /// + /// Sends a response to a command + /// + public void SendResponse (string txt) + { + terminalDelegate.Send (Encoding.UTF8.GetBytes (txt)); + } + + /// + /// Sends a response to a command + /// + public void SendResponse (params object[] args) + { + if (args == null) { + return; + } + + int len = args.Length; + for (int i = 0; i < args.Length; i++) { + if (args [i] is string s) { + len += s != null ? s.Length - 1: 0; + } else if (args [i] is byte [] ba) { + len += ba.Length - 1; + } + } + + var buffer = new byte [len]; + + int bufferIndex = 0; + for (int i = 0; i < args.Length; i++) { + if (args[i] == null) { + buffer [bufferIndex] = 0; + } else if (args[i] is byte b) { + buffer [bufferIndex] = b; + } else if (args[i] is string s) { + if (s == null) { + buffer [bufferIndex] = 0; + } else { + foreach (var sb in Encoding.UTF8.GetBytes (s)) { + buffer [bufferIndex++] = sb; + } + } + bufferIndex--; + } else if (args[i] is byte[] ba) { + foreach (var bab in ba) { + buffer [bufferIndex++] = bab; + } + bufferIndex--; + } else { + Error ("Unsupported type in SendResponse", args[i].GetType()); + } + + bufferIndex++; + } + + terminalDelegate.Send (buffer); + } + + /// + /// Reports an error to the system log + /// + public void Error (string txt, params object [] args) + { + Report ("ERROR", txt, args); + } + + /// + /// Logs a message to the system log + /// + public void Log (string text, params object [] args) + { + Report ("LOG", text, args); + } + + public void Feed (byte [] data, int len = -1) + { + input.Parse (data, len); + } + + public void Feed (IntPtr data, int len = -1) + { + input.Parse (data, len); + } + + public void Feed (string text) + { + var bytes = Encoding.UTF8.GetBytes (text); + Feed (bytes, bytes.Length); + } + + internal void UpdateRange (int y) + { + if (y < 0) + throw new ArgumentException (); + + if (y < refreshStart) + refreshStart = y; + if (y > refreshEnd) + refreshEnd = y; + } + + public void GetUpdateRange (out int startY, out int endY) + { + startY = refreshStart; + endY = refreshEnd; + } + + public void ClearUpdateRange () + { + refreshStart = Int32.MaxValue; + refreshEnd = -1; + } + + internal void EmitChar (int ch) + { + // For accessibility purposes 'a11y.char' in the original source. + } + + // + // ESC c Full Reset (RIS) + // + internal void Reset () + { + Options.Rows = Rows; + Options.Cols = Cols; + + var savedCursorHidden = cursorHidden; + Setup (); + cursorHidden = savedCursorHidden; + Refresh (0, Rows - 1); + SyncScrollArea (); + } + + // + // ESC D Index (Index is 0x84) + // + internal void Index () + { + var buffer = Buffer; + var newY = buffer.Y + 1; + if (newY > buffer.ScrollBottom) { + Scroll (); + } else { + buffer.Y = newY; + } + // If the end of the line is hit, prevent this action from wrapping around to the next line. + if (buffer.X > Cols) + buffer.X--; + } + + internal void Scroll (bool isWrapped = false) + { + var buffer = Buffer; + BufferLine newLine = blankLine; + if (newLine == null || newLine.Length != Cols || newLine [0].Attribute != EraseAttr ()) { + newLine = buffer.GetBlankLine (EraseAttr (), isWrapped); + blankLine = newLine; + } + newLine.IsWrapped = isWrapped; + + var topRow = buffer.YBase + buffer.ScrollTop; + var bottomRow = buffer.YBase + buffer.ScrollBottom; + + if (buffer.ScrollTop == 0) { + // Determine whether the buffer is going to be trimmed after insertion. + var willBufferBeTrimmed = buffer.Lines.IsFull; + + // Insert the line using the fastest method + if (bottomRow == buffer.Lines.Length - 1) { + if (willBufferBeTrimmed) { + buffer.Lines.Recycle ().CopyFrom (newLine); + } else { + buffer.Lines.Push (new BufferLine (newLine)); + } + } else { + buffer.Lines.Splice (bottomRow + 1, 0, new BufferLine (newLine)); + } + + // Only adjust ybase and ydisp when the buffer is not trimmed + if (!willBufferBeTrimmed) { + buffer.YBase++; + // Only scroll the ydisp with ybase if the user has not scrolled up + if (!userScrolling) { + buffer.YDisp++; + } + } else { + // When the buffer is full and the user has scrolled up, keep the text + // stable unless ydisp is right at the top + if (userScrolling) { + buffer.YDisp = Math.Max (buffer.YDisp - 1, 0); + } + } + } else { + // scrollTop is non-zero which means no line will be going to the + // scrollback, instead we can just shift them in-place. + var scrollRegionHeight = bottomRow - topRow + 1/*as it's zero-based*/; + + if (scrollRegionHeight > 1) { + buffer.Lines.ShiftElements (topRow + 1, scrollRegionHeight - 1, -1); + } + + buffer.Lines [bottomRow] = new BufferLine (newLine); + } + + // Move the viewport to the bottom of the buffer unless the user is + // scrolling. + if (!userScrolling) { + buffer.YDisp = buffer.YBase; + } + + // Flag rows that need updating + UpdateRange (buffer.ScrollTop); + UpdateRange (buffer.ScrollBottom); + + /** + * This event is emitted whenever the terminal is scrolled. + * The one parameter passed is the new y display position. + * + * @event scroll + */ + Scrolled?.Invoke (this, buffer.YDisp); + } + + /// + /// Scroll the display of the terminal + /// + /// The number of lines to scroll down (negative scroll up) + /// Don't emit the scroll event as scrollLines. This is use to avoid unwanted + /// events being handled by the viewport when the event was triggered from the viewport originally. + public void ScrollLines(int disp, bool suppressScrollEvent = false) + { + if (disp < 0) { + if (Buffer.YDisp == 0) { + return; + } + + this.userScrolling = true; + } else if (disp + Buffer.YDisp >= Buffer.YBase) { + this.userScrolling = false; + } + + int oldYdisp = Buffer.YDisp; + Buffer.YDisp = Math.Max (Math.Min (Buffer.YDisp + disp, Buffer.YBase), 0); + + // No change occurred, don't trigger scroll/refresh + if (oldYdisp == Buffer.YDisp) { + return; + } + + if (!suppressScrollEvent) { + Scrolled?.Invoke (this, Buffer.YDisp); + } + + Refresh (0, this.Rows - 1); + } + + public event Action Scrolled; + + public event Action DataEmitted; + + internal void Bell () + { + //Console.WriteLine ("beep"); + } + + public void EmitLineFeed () + { + if (LineFeedEvent != null) + LineFeedEvent (this); + } + + public event Action LineFeedEvent; + + internal void EmitA11yTab (object p) + { + throw new NotImplementedException (); + } + + internal void SetgLevel (int v) + { + gLevel = v; + if (CharSets.All.TryGetValue ((byte)v, out var cs)) + Charset = cs; + else + Charset = null; + } + + internal int EraseAttr () + { + return (CharData.DefaultAttr & ~0x1ff) | CurAttr & 0x1ff; + } + + internal void EmitScroll (int v) + { + return; + throw new NotImplementedException (); + } + + internal void SetgCharset (byte v, Dictionary charset) + { + CharSets.All [v] = charset; + if (gLevel == v) + this.charset = charset; + } + + public void Resize (int cols, int rows) + { + if (cols < MINIMUM_COLS) + cols = MINIMUM_COLS; + if (rows < MINIMUM_ROWS) + rows = MINIMUM_ROWS; + if (cols == Cols && rows == Rows) + return; + + var oldCols = Cols; + Cols = cols; + Rows = rows; + Buffers.Resize (cols, rows); + buffers.SetupTabStops (oldCols); + Refresh (0, Rows - 1); + } + + internal void SyncScrollArea () + { + // This should call the viewport syncscrollarea + //throw new NotImplementedException (); + } + + /// + /// Implemented by subclasses - must refresh the display from the starting to the end row. + /// + /// Initial row to update, offset starts at zero. + /// Last row to update. + public void Refresh (int startRow, int endRow) + { + // TO BE HONEST - This probably should not be called directly, + // instead the view shoudl after feeding data, determine if there is a need + // to refresh based on the parameters provided for refresh ranges, and then + // update, to avoid the backend rtiggering this multiple times. + + UpdateRange (startRow); + UpdateRange (endRow); + } + + public void ShowCursor () + { + if (cursorHidden == false) + return; + cursorHidden = false; + Refresh (Buffer.Y, Buffer.Y); + terminalDelegate.ShowCursor (this); + } + + /// + /// Encodes button and position to characters + /// + void EncodeMouseUtf (List data, int ch) + { + if (ch == 2047) { + data.Add (0); + return; + } + if (ch < 127) { + data.Add ((byte)ch); + } else { + if (ch > 2047) + ch = 2047; + data.Add ((byte)(0xC0 | (ch >> 6))); + data.Add ((byte)(0x80 | (ch & 0x3F))); + } + } + + /// + /// Encodes the mouse button. + /// + /// The mouse button. + /// Button (0, 1, 2 for left, middle, right) and 4 for wheel up, and 5 for wheel down. + /// If set to true release. + /// If set to true wheel up. + /// If set to true shift. + /// If set to true meta. + /// If set to true control. + public int EncodeMouseButton (int button, bool release, bool shift, bool meta, bool control) + { + int value; + + if (release) + value = 3; + else { + switch (button) { + case 0: + value = 0; + break; + case 1: + value = 1; + break; + case 2: + value = 2; + break; + case 4: + value = 64; + break; + case 5: + value = 65; + break; + default: + value = 0; + break; + } + } + + if (MouseMode.SendsModifiers()) { + if (shift) + value |= 4; + if (meta) + value |= 8; + if (control) + value |= 16; + } + return value; + } + + /// + /// Sends a mouse event for a specific button at the specific location + /// + /// Button flags encoded in Cb mode. + /// The x coordinate. + /// The y coordinate. + public void SendEvent (int buttonFlags, int x, int y) + { + switch (MouseProtocol) { + case MouseProtocolEncoding.X10: + SendResponse (ControlCodes.CSI, "M", (byte)(buttonFlags + 32), (byte)Math.Min (255, (32 + x + 1)), (byte)Math.Min (255, (32 + y + 1))); + break; + case MouseProtocolEncoding.SGR: + var bflags = ((buttonFlags & 3) == 3) ? (buttonFlags & ~3) : buttonFlags; + var m = ((buttonFlags & 3) == 3) ? "m" : "M"; + SendResponse (ControlCodes.CSI, $"<{bflags};{x+1};{y+1}{m}"); + break; + case MouseProtocolEncoding.URXVT: + SendResponse (ControlCodes.CSI, $"{buttonFlags+32};{x+1};{y+1}M"); + break; + case MouseProtocolEncoding.UTF8: + var utf8 = new List () { 0x4d /* M */ }; + EncodeMouseUtf (utf8, ch: buttonFlags+32); + EncodeMouseUtf (utf8, ch: x+33); + EncodeMouseUtf (utf8, ch: y+33); + SendResponse (ControlCodes.CSI, utf8.ToArray()); + break; + } + } + + public void SendMouseMotion (int buttonFlags, int x, int y) + { + SendEvent (buttonFlags + 32, x, y); + + } + + public int MatchColor (int r1, int g1, int b1) + { + throw new NotImplementedException (); + } + + internal void EmitData (string txt) + { + DataEmitted?.Invoke (this, txt); + } + + /// + /// Implement to change the cursor style, call the base implementation. + /// + /// + public void SetCursorStyle (CursorStyle style) + { + } + + internal void ReverseIndex () + { + var buffer = Buffer; + + if (buffer.Y == buffer.ScrollTop) { + // possibly move the code below to term.reverseScroll(); + // test: echo -ne '\e[1;1H\e[44m\eM\e[0m' + // blankLine(true) is xterm/linux behavior + var scrollRegionHeight = buffer.ScrollBottom - buffer.ScrollTop; + buffer.Lines.ShiftElements (buffer.Y + buffer.YBase, scrollRegionHeight, 1); + buffer.Lines [buffer.Y + buffer.YBase] = buffer.GetBlankLine (EraseAttr ()); + UpdateRange (buffer.ScrollTop); + UpdateRange (buffer.ScrollBottom); + } else { + buffer.Y--; + } + } + + + #region Cursor Commands + /// + /// Sets the location of the cursor (zero based) + /// + public void SetCursor (int col, int row) + { + var buffer = Buffer; + + // make sure we stay within the boundaries + col = Math.Min (Math.Max (col, 0), buffer.Cols - 1); + row = Math.Min (Math.Max (row, 0), buffer.Rows - 1); + + if (OriginMode) { + buffer.X = col + (IsUsingMargins () ? buffer.MarginLeft : 0); + buffer.Y = buffer.ScrollTop + row; + } else { + buffer.X = col; + buffer.Y = row; + } + } + /// + // Moves the cursor up by rows + /// + public void CursorUp (int rows) + { + var buffer = Buffer; + var top = buffer.ScrollTop; + + if (buffer.Y < top) { + top = 0; + } + + if (buffer.Y - rows < top) + buffer.Y = top; + else + buffer.Y -= rows; + } + + /// + // Moves the cursor down by rows + /// + public void CursorDown (int rows) + { + var buffer = Buffer; + var bottom = buffer.ScrollBottom; + + // When the cursor starts below the scroll region, CUD moves it down to the + // bottom of the screen. + if (buffer.Y > bottom) { + bottom = buffer.Rows - 1; + } + + var newY = buffer.Y + rows; + + if (newY >= bottom) + buffer.Y = bottom; + else + buffer.Y = newY; + + // If the end of the line is hit, prevent this action from wrapping around to the next line. + if (buffer.X >= Cols) + buffer.X--; + } + + /// + // Moves the cursor forward by cols + /// + public void CursorForward (int cols) + { + var buffer = Buffer; + var right = MarginMode ? buffer.MarginRight : buffer.Cols - 1; + + if (buffer.X > right) { + right = buffer.Cols - 1; + } + + buffer.X += cols; + if (buffer.X > right) { + buffer.X = right; + } + } + + /// + // Moves the cursor forward by cols + /// + public void CursorBackward (int cols) + { + var buffer = Buffer; + + // What is our left margin - depending on the settings. + var left = MarginMode ? buffer.MarginLeft : 0; + + // If the cursor is positioned before the margin, we can go backwards to the first column + if (buffer.X < left) { + left = 0; + } + buffer.X -= cols; + + if (buffer.X < left) { + buffer.X = left; + } + } + + /// + /// Performs a backwards tab + /// + public void CursorBackwardTab (int tabs) + { + var buffer = Buffer; + while (tabs-- != 0) { + buffer.X = buffer.PreviousTabStop (); + } + } + + /// + /// Moves the cursor to the given column + /// + public void CursorCharAbsolute (int col) + { + var buffer = Buffer; + buffer.X = (IsUsingMargins () ? buffer.MarginLeft : 0) + Math.Min (col - 1, buffer.Cols - 1); + } + + /// + /// Performs a linefeed + /// + public void LineFeed () + { + var buffer = Buffer; + if (Options.ConvertEol) { + buffer.X = MarginMode ? buffer.MarginLeft : 0; + } + + LineFeedBasic (); + } + + /// + /// Performs a basic linefeed + /// + public void LineFeedBasic () + { + var buffer = Buffer; + var by = buffer.Y; + + if (by == buffer.ScrollBottom) { + Scroll (isWrapped: false); + } else if (by == buffer.Rows - 1) { + } else { + buffer.Y = by + 1; + } + + // If the end of the line is hit, prevent this action from wrapping around to the next line. + if (buffer.X >= buffer.Cols) { + buffer.X -= 1; + } + + // This event is emitted whenever the terminal outputs a LF or NL. + EmitLineFeed (); + } + + /// + /// Moves cursor to first position on next line. + /// + public void NextLine () + { + var buffer = Buffer; + buffer.X = IsUsingMargins () ? buffer.MarginLeft : 0; + Index (); + } + + /// + /// Save cursor (ANSI.SYS). + /// + public void SaveCursor () + { + var buffer = Buffer; + buffer.SaveCursor (CurAttr); + } + + /// + /// Restores the cursor and modes + /// + public void RestoreCursor () + { + var buffer = Buffer; + CurAttr = buffer.RestoreCursor(); + MarginMode = savedMarginMode; + OriginMode = savedOriginMode; + Wraparound = savedWraparound; + ReverseWraparound = savedReverseWraparound; + } + + /// + /// Restrict cursor to viewport size / scroll margin (origin mode) + /// - Parameter limitCols: by default it is true, but the reverseWraparound mechanism in Backspace needs `x` to go beyond. + /// + public void RestrictCursor (bool limitCols = true) + { + var buffer = Buffer; + buffer.X = Math.Min (buffer.Cols - (limitCols ? 1 : 0), Math.Max (0, buffer.X)); + buffer.Y = OriginMode + ? Math.Min (buffer.ScrollBottom, Math.Max (buffer.ScrollTop, buffer.Y)) + : Math.Min (buffer.Rows - 1, Math.Max (0, buffer.Y)); + + UpdateRange (buffer.Y); + } + + /// + /// Returns true if the terminal is using margins in origin mode + /// + internal bool IsUsingMargins () + { + return OriginMode && MarginMode; + } + + #endregion + + /// + /// Performs a carriage return + /// + public void CarriageReturn () + { + var buffer = Buffer; + if (MarginMode) { + if (buffer.X < buffer.MarginLeft) { + buffer.X = 0; + } else { + buffer.X = buffer.MarginLeft; + } + } else { + buffer.X = 0; + } + } + + #region Text Manupulation + /// + /// Backspace handler (Control-h) + /// + public void Backspace () + { + var buffer = Buffer; + + RestrictCursor (!ReverseWraparound); + + int left = MarginMode ? buffer.MarginLeft : 0; + int right = MarginMode ? buffer.MarginRight : buffer.Cols - 1; + + if (buffer.X > left) { + buffer.X--; + } else if (ReverseWraparound) { + if (buffer.X <= left) { + if (buffer.Y > buffer.ScrollTop && buffer.Y <= buffer.ScrollBottom && (buffer.Lines [buffer.Y + buffer.YBase].IsWrapped || MarginMode)) { + if (!MarginMode) { + buffer.Lines [buffer.Y + buffer.YBase].IsWrapped = false; + } + + buffer.Y--; + buffer.X = right; + // TODO: find actual last cell based on width used + } else if (buffer.Y == buffer.ScrollTop) { + buffer.X = right; + buffer.Y = buffer.ScrollBottom; + } else if (buffer.Y > 0) { + buffer.X = right; + buffer.Y--; + } + } + } else { + if (buffer.X < left && buffer.X > 0) { + // This compensates for the scenario where backspace is supposed to move one step + // backwards if the "x" position is behind the left margin. + // Test BS_MovesLeftWhenLeftOfLeftMargin + buffer.X--; + } else if (buffer.X > left) { + // If we have not reached the limit, we can go back, otherwise stop at the margin + // Test BS_StopsAtLeftMargin + buffer.X--; + + } + } + } + + /// + /// Deletes charstoDelete chars from the cursor position to the right margin + /// + public void DeleteChars (int charsToDelete) + { + var buffer = Buffer; + + if (MarginMode) { + if (buffer.X + charsToDelete > buffer.MarginRight) { + charsToDelete = buffer.MarginRight - buffer.X; + } + } + + buffer.Lines [buffer.Y + buffer.YBase].DeleteCells (buffer.X, charsToDelete, MarginMode ? buffer.MarginRight : buffer.Cols - 1, new CharData (EraseAttr ())); + + UpdateRange (buffer.Y); + } + + /// + /// Deletes lines + /// + public void DeleteLines (int rowsToDelete) + { + RestrictCursor (); + var buffer = Buffer; + var row = buffer.Y + buffer.YBase; + + int j; + j = buffer.Rows - 1 - buffer.ScrollBottom; + j = buffer.Rows - 1 + buffer.YBase - j; + + var eraseAttr = EraseAttr (); + + if (MarginMode) { + if (buffer.X >= buffer.MarginLeft && buffer.X <= buffer.MarginRight) { + var columnCount = buffer.MarginRight - buffer.MarginLeft + 1; + var rowCount = buffer.ScrollBottom - buffer.ScrollTop; + while (rowsToDelete-- > 0) { + for (int i = 0; i < rowCount; i++) { + var src = buffer.Lines [row + i + 1]; + var dst = buffer.Lines [row + i]; + + if (src != null) { + dst.CopyFrom (src, buffer.MarginLeft, buffer.MarginLeft, columnCount); + } + } + + var last = buffer.Lines [row + rowCount]; + last?.Fill (new CharData (eraseAttr), atCol: buffer.MarginLeft, len: columnCount); + } + } + } else { + if (buffer.Y >= buffer.ScrollTop && buffer.Y <= buffer.ScrollBottom) { + while (rowsToDelete-- > 0) { + buffer.Lines.Splice (row, 1); + buffer.Lines.Splice (j, 0, buffer.GetBlankLine (eraseAttr)); + } + } + } + + UpdateRange (buffer.Y); + UpdateRange (buffer.ScrollBottom); + } + + /// + /// Inserts columns + /// + public void InsertColumn (int columns) + { + var buffer = Buffer; + + for (int row = buffer.ScrollTop; row < buffer.ScrollBottom; row++) { + var line = buffer.Lines [row + buffer.YBase]; + // TODO:is this the right filldata? + line.InsertCells (buffer.X, columns, MarginMode ? buffer.MarginRight : buffer.Cols - 1, CharData.WhiteSpace); + line.IsWrapped = false; + } + + UpdateRange (buffer.ScrollTop); + UpdateRange (buffer.ScrollBottom); + } + + /// + /// Deletes columns + /// + public void DeleteColumn (int columns) + { + var buffer = Buffer; + + if (buffer.Y > buffer.ScrollBottom || buffer.Y < buffer.ScrollTop) + return; + + for (int row = buffer.ScrollTop; row < buffer.ScrollBottom; row++) { + var line = buffer.Lines [row + buffer.YBase]; + line.DeleteCells (buffer.X, columns, MarginMode ? buffer.MarginRight : buffer.Cols - 1, CharData.Null); + line.IsWrapped = false; + } + + UpdateRange (buffer.ScrollTop); + UpdateRange (buffer.ScrollBottom); + } + + + + #endregion + + /// + /// Sets the scroll region + /// + public void SetScrollRegion (int top, int bottom) + { + var buffer = Buffer; + + if (bottom == 0) + bottom = buffer.Rows; + bottom = Math.Min (bottom, buffer.Rows); + + // normalize (make zero based) + bottom--; + + // only set the scroll region if top < bottom + if (top < bottom) { + buffer.ScrollBottom = bottom; + buffer.ScrollTop = top; + } + + SetCursor (0, 0); + } + + + /// + /// Performs a soft reset + /// + public void SoftReset () + { + var buffer = Buffer; + + CursorHidden = false; + InsertMode = false; + OriginMode = false; + + Wraparound = true; // defaults: xterm - true, vt100 - false + ReverseWraparound = false; + ApplicationKeypad = false; + SyncScrollArea (); + ApplicationCursor = false; + CurAttr = CharData.DefaultAttr; + + Charset = null; + SetgLevel (0); + + savedOriginMode = false; + savedMarginMode = false; + savedWraparound = false; + savedReverseWraparound = false; + + buffer.ScrollTop = 0; + buffer.ScrollBottom = buffer.Rows - 1; + buffer.SavedAttr = CharData.DefaultAttr; + buffer.SavedY = 0; + buffer.SavedX = 0; + buffer.SetMargins (0, buffer.Cols - 1); + //conformance = .vt500 + } + + + /// + /// Reports a message to the system log + /// + void Report (string prefix, string text, object [] args) + { + Console.WriteLine ($"{prefix}: {text}"); + for (int i = 0; i < args.Length; i++) + Console.WriteLine (" {0}: {1}", i, args [i]); + } + + /// + /// Sets up the terminals initial state + /// + void Setup () + { + cursorHidden = false; + + // modes + applicationKeypad = false; + applicationCursor = false; + OriginMode = false; + MarginMode = false; + InsertMode = false; + Wraparound = true; + bracketedPasteMode = false; + + // charset + charset = null; + gcharset = 0; + gLevel = 0; + + CurAttr = CharData.DefaultAttr; + + MouseMode = MouseMode.Off; + MouseProtocol = MouseProtocolEncoding.X10; + + Allow80To132 = false; + // TODO REST + } + } +} diff --git a/src/XtermSharp/TerminalOptions.cs b/src/XtermSharp/TerminalOptions.cs new file mode 100644 index 00000000..dbe08a8a --- /dev/null +++ b/src/XtermSharp/TerminalOptions.cs @@ -0,0 +1,25 @@ +using System; +namespace XtermSharp { + public enum CursorStyle { + BlinkBlock, SteadyBlock, BlinkUnderline, SteadyUnderline, BlinkingBar, SteadyBar + } + + public class TerminalOptions { + public int Cols, Rows; + public bool ConvertEol = true, CursorBlink; + public string TermName; + public CursorStyle CursorStyle; + public bool ScreenReaderMode; + public int? Scrollback { get; set; } + public int? TabStopWidth { get; set; } + + public TerminalOptions () + { + Cols = 80; + Rows = 25; + TermName = "xterm"; + Scrollback = 1000; + TabStopWidth = 8; + } + } +} diff --git a/src/XtermSharp/Utils/CircularList.cs b/src/XtermSharp/Utils/CircularList.cs new file mode 100644 index 00000000..f3bacbc7 --- /dev/null +++ b/src/XtermSharp/Utils/CircularList.cs @@ -0,0 +1,226 @@ +using System; +namespace XtermSharp { + /// + /// Represents a circular list; a list with a maximum size that wraps around when push is called overriding values at the start of the list. + /// + public class CircularList { + int length; + T [] array; + int startIndex; + + /// + /// Initializes a new instance of the class with the specified number of elements. + /// + /// Max length. + public CircularList (int maxLength) + { + array = new T [maxLength]; + length = 0; + } + + // Gets the cyclic index for the specified regular index. The cyclic index can then be used on the + // backing array to get the element associated with the regular index. + int GetCyclicIndex (int index) + { + return (startIndex + index) % array.Length; + } + + /// + /// Gets or sets the maximum length of the circular list + /// + /// The length of the max. + public int MaxLength { + get => array.Length; + + set { + if (value <= 0) + throw new ArgumentException (nameof (value)); + + if (value == array.Length) + return; + + // Reconstruct array, starting at index 0. Only transfer values from the + // indexes 0 to length. + var newArray = new T [value]; + var top = Math.Min (value, array.Length); + for (int i = 0; i < top; i++) + newArray [i] = array [GetCyclicIndex (i)]; + startIndex = 0; + array = newArray; + } + } + + /// + /// The current length of the circular buffer + /// + /// The length. + public int Length { + get => length; + set { + if (value > length) { + for (int i = length; i < value; i++) + array [i] = default (T); + } + length = value; + } + } + + /// + /// Invokes the specificied callback for each items of the circular list, the first parameter is the value, the second is the ith-index. + /// + /// Callback. + public void ForEach (Action callback) + { + var top = length; + for (int i = 0; i < top; i++) + callback (this [i], i); + } + + /// + /// Gets or sets the at the specified index. + /// + /// Index. + public T this [int index] { + get => array [GetCyclicIndex (index)]; + set => array [GetCyclicIndex (index)] = value; + } + + /// + /// Event raised when an item is removed from the circular array, the parameter is the number of items removed. + /// + public Action Trimmed; + + /// + /// Pushes a new value onto the list, wrapping around to the start of the array, overriding index 0 if the maximum length is reached + /// + /// The push. + /// Value to push. + public void Push (T value) + { + array [GetCyclicIndex (length)] = value; + if (length == array.Length) { + startIndex++; + if (startIndex == array.Length) + startIndex = 0; + + Trimmed?.Invoke (1); + } else { + length++; + } + } + + public T Recycle () + { + if (Length != MaxLength) { + throw new Exception ("Can only recycle when the buffer is full"); + } + startIndex = ++startIndex % MaxLength; + + return array [GetCyclicIndex (Length - 1)]; + } + + /// + /// Removes and returns the last value on the list. + /// + /// The popped value. + public T Pop () + { + return array [GetCyclicIndex (length-- - 1)]; + } + + /// + /// Deletes and/or inserts items at a particular index (in that order). + /// + /// The splice. + /// The index to delete and/or insert. + /// The number of elements to delete. + /// The items to insert. + public void Splice (int start, int deleteCount, params T [] items) + { + // delete items + if (deleteCount > 0) { + for (int i = start; i < length - deleteCount; i++) + array [GetCyclicIndex (i)] = array [GetCyclicIndex (i + deleteCount)]; + length -= deleteCount; + } + if (items.Length != 0) { + // add items + for (int i = length - 1; i >= start; i--) + array [GetCyclicIndex (i + items.Length)] = array [GetCyclicIndex (i)]; + for (int i = 0; i < items.Length; i++) + array [GetCyclicIndex (start + i)] = items [i]; + } + + // Adjust length as needed + if (length + items.Length > array.Length) { + int countToTrim = length + items.Length - array.Length; + startIndex += countToTrim; + length = array.Length; + Trimmed?.Invoke (countToTrim); + } else { + length += items.Length; + } + } + + /// + /// Trims a number of items from the start of the list. + /// + /// The number of items to remove.. + public void TrimStart (int count) + { + if (count > length) + count = length; + + // TODO: perhaps bug in original code, this does not clamp the value of startIndex + startIndex += count; + length -= count; + Trimmed?.Invoke (count); + } + + /// + /// Shifts the elements. + /// + /// Start. + /// Count. + /// Offset. + public void ShiftElements (int start, int count, int offset) + { + if (count < 0) + return; + if (start < 0 || start >= length) + throw new ArgumentException ("Start argument is out of range"); + if (start + offset < 0) + throw new ArgumentException ("Can not shift elements in list beyond index 0"); + if (offset > 0) { + for (var i = count - 1; i >= 0; i--) { + this [start + i + offset] = this [start + i]; + } + var expandListBy = (start + count + offset) - length; + if (expandListBy > 0) { + length += expandListBy; + while (length > array.Length) { + length--; + startIndex++; + Trimmed.Invoke (1); + } + } + } else { + for (var i = 0; i < count; i++) { + this [start + i + offset] = this [start + i]; + } + } + } + + public bool IsFull => Length == MaxLength; + + public T[] ToArray() + { + var result = new T [Length]; + for (int i = 0; i < Length; i++) { + result [i] = array [GetCyclicIndex (i)]; + } + + return result; + } + } +} diff --git a/src/XtermSharp/Utils/RuneExt.cs b/src/XtermSharp/Utils/RuneExt.cs new file mode 100644 index 00000000..319e5819 --- /dev/null +++ b/src/XtermSharp/Utils/RuneExt.cs @@ -0,0 +1,94 @@ +using System; +namespace XtermSharp { + public class RuneExt { + public static int ExpectedSizeFromFirstByte (byte b) + { + var x = first [b]; + + // Invalid runes, just return 1 for byte, and let higher level pass to print + if (x == xx) + return -1; + if (x == a1) + return 1; + return x & 0xf; + } + + const byte xx = 0xF1; // invalid: size 1 + const byte a1 = 0xF0; // a1CII: size 1 + const byte s1 = 0x02; // accept 0, size 2 + const byte s2 = 0x13; // accept 1, size 3 + const byte s3 = 0x03; // accept 0, size 3 + const byte s4 = 0x23; // accept 2, size 3 + const byte s5 = 0x34; // accept 3, size 4 + const byte s6 = 0x04; // accept 0, size 4 + const byte s7 = 0x44; // accept 4, size 4 + + static byte [] first = new byte [256]{ + // 1 2 3 4 5 6 7 8 9 A B C D E F + a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, // 0x00-0x0F + a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1,a1, a1, a1, a1, a1, // 0x10-0x1F + a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, // 0x20-0x2F + a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, // 0x30-0x3F + a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, // 0x40-0x4F + a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, // 0x50-0x5F + a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, // 0x60-0x6F + a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, a1, // 0x70-0x7F + + // 1 2 3 4 5 6 7 8 9 A B C D E F + xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0x80-0x8F + xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0x90-0x9F + xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0xA0-0xAF + xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0xB0-0xBF + xx, xx, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, // 0xC0-0xCF + s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, // 0xD0-0xDF + s2, s3, s3, s3, s3, s3, s3, s3, s3, s3, s3, s3, s3, s4, s3, s3, // 0xE0-0xEF + s5, s6, s6, s6, s7, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0xF0-0xFF + }; + + // The default lowest and highest continuation byte. + const byte locb = 0x80; // 1000 0000 + const byte hicb = 0xBF; // 1011 1111 + + struct AcceptRange { + public byte Lo, Hi; + public AcceptRange (byte lo, byte hi) + { + Lo = lo; + Hi = hi; + } + } + + static AcceptRange [] AcceptRanges = new AcceptRange [] { + new AcceptRange (locb, hicb), + new AcceptRange (0xa0, hicb), + new AcceptRange (locb, 0x9f), + new AcceptRange (0x90, hicb), + new AcceptRange (locb, 0x8f), + }; + + public unsafe static bool FullRune (byte * p, int n) + { + if (p == null) + throw new ArgumentNullException (nameof (p)); + + if (n == 0) + return false; + var x = first [p [0]]; + if (n >= (x & 7)) { + // ascii, invalid or valid + return true; + } + // must be short or invalid + if (n > 1) { + var accept = AcceptRanges [x >> 4]; + var c = p [1]; + if (c < accept.Lo || accept.Hi < c) + return true; + else if (n > 2 && (p [2] < locb || hicb < p [2])) + return true; + } + return false; + } + + } +} diff --git a/src/XtermSharp/WindowManipulationCommand.cs b/src/XtermSharp/WindowManipulationCommand.cs new file mode 100644 index 00000000..42231d93 --- /dev/null +++ b/src/XtermSharp/WindowManipulationCommand.cs @@ -0,0 +1,89 @@ +using System; +namespace XtermSharp { + /// + /// Commands send to the `windowCommand` delegate for the front-end to implement capabilities + /// on behalf of the client.The expected return strings in some of these enumeration values is documented + /// below. Returns are only expected for the enum values that start with the prefix `report` + /// + public enum WindowManipulationCommand { + /// + /// Raised when the backend should deiconify a window, no return expected + /// + DeiconifyWindow, + + /// + /// Raised when the backend should iconify a window, no return expected + /// + IconifyWindow, + + /// + /// Raised when the client would like the window to be moved to the x,y position int he screen, no return expected + /// + /// (x: Int, y: Int) + MoveWindowTo, + + /// + /// Raised when the client would like the window to be resized to the specified widht and heigh in pixels, no return expected + /// + /// (width: Int, height: Int) + ResizeWindowTo, + + /// + /// Raised to bring the terminal to the front + /// + BringToFront, + + /// + /// Send the terminal to the back if possible + /// + SendToBack, + + /// + /// Trigger a terminal refresh + /// + RefreshWindow, + + /// + /// Request that the size of the terminal be changed to the specified cols and rows + /// + /// (cols: Int, rows: Int) + ResizeTo, + + RestoreMaximizedWindow, + + /// + /// Attempt to maximize the window + /// + MaximizeWindow, + + /// + /// Attempt to maximize the window vertically + /// + MaximizeWindowVertically, + + /// + /// Attempt to maximize the window horizontally + /// + MaximizeWindowHorizontally, + + UndoFullScreen, + SwitchToFullScreen, + ToggleFullScreen, + ReportTerminalState, + ReportTerminalPosition, + ReportTextAreaPosition, + ReporttextAreaPixelDimension, + ReportSizeOfScreenInPixels, + ReportCellSizeInPixels, + ReportTextAreaCharacters, + ReportScreenSizeCharacters, + ReportIconLabel, + ReportWindowTitle, + + /// + /// Request that the size of the terminal be changed to the specified lines + /// + /// (lines: Int) + ResizeToLines, + } +} diff --git a/src/XtermSharp/XtermSharp.csproj b/src/XtermSharp/XtermSharp.csproj new file mode 100644 index 00000000..85fbc5a8 --- /dev/null +++ b/src/XtermSharp/XtermSharp.csproj @@ -0,0 +1,27 @@ + + + + netstandard2.1 + preview + db2a43a5-c230-49a5-b71b-59eae23db090 + + + + true + + + true + + + + + + + + + + + + + +