From fed1827f776ac4ecc2d3bcb1cd136f2bbc26cbe8 Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 5 Oct 2024 19:50:04 +0100 Subject: [PATCH] Merge #3768 into sixel branch This work by @BDisp enables us to detect sixel support on demand --- .../AnsiEscapeSequenceRequest.cs | 186 ++++++++++++++++++ .../AnsiEscapeSequenceResponse.cs | 53 +++++ .../CursesDriver/CursesDriver.cs | 6 + .../ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs | 30 +-- Terminal.Gui/ConsoleDrivers/NetDriver.cs | 127 ++++++------ .../Views/AutocompleteFilepathContext.cs | 2 +- Terminal.Gui/Views/TextField.cs | 18 +- Terminal.Gui/Views/TextView.cs | 45 +++-- .../Scenarios/AnsiEscapeSequenceRequests.cs | 118 +++++++++++ UnitTests/Text/AutocompleteTests.cs | 2 +- UnitTests/View/Mouse/MouseTests.cs | 1 + UnitTests/Views/TextFieldTests.cs | 14 ++ UnitTests/Views/TextViewTests.cs | 14 ++ 13 files changed, 504 insertions(+), 112 deletions(-) create mode 100644 Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceRequest.cs create mode 100644 Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceResponse.cs create mode 100644 UICatalog/Scenarios/AnsiEscapeSequenceRequests.cs diff --git a/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceRequest.cs b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceRequest.cs new file mode 100644 index 0000000000..d6a5d3470b --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceRequest.cs @@ -0,0 +1,186 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// Describes an ongoing ANSI request sent to the console. +/// Use to handle the response +/// when console answers the request. +/// +public class AnsiEscapeSequenceRequest +{ + /// + /// Execute an ANSI escape sequence escape which may return a response or error. + /// + /// The ANSI escape sequence to request. + /// A with the response, error, terminator and value. + public static AnsiEscapeSequenceResponse ExecuteAnsiRequest (AnsiEscapeSequenceRequest ansiRequest) + { + var response = new StringBuilder (); + var error = new StringBuilder (); + var savedIsReportingMouseMoves = false; + NetDriver? netDriver = null; + + try + { + switch (Application.Driver) + { + case NetDriver: + netDriver = Application.Driver as NetDriver; + savedIsReportingMouseMoves = netDriver!.IsReportingMouseMoves; + + if (savedIsReportingMouseMoves) + { + netDriver.StopReportingMouseMoves (); + } + + while (Console.KeyAvailable) + { + netDriver._mainLoopDriver._netEvents._waitForStart.Set (); + netDriver._mainLoopDriver._netEvents._waitForStart.Reset (); + + netDriver._mainLoopDriver._netEvents._forceRead = true; + } + + netDriver._mainLoopDriver._netEvents._forceRead = false; + + break; + case CursesDriver cursesDriver: + savedIsReportingMouseMoves = cursesDriver.IsReportingMouseMoves; + + if (savedIsReportingMouseMoves) + { + cursesDriver.StopReportingMouseMoves (); + } + + break; + } + + if (netDriver is { }) + { + NetEvents._suspendRead = true; + } + else + { + Thread.Sleep (100); // Allow time for mouse stopping and to flush the input buffer + + // Flush the input buffer to avoid reading stale input + while (Console.KeyAvailable) + { + Console.ReadKey (true); + } + } + + // Send the ANSI escape sequence + Console.Write (ansiRequest.Request); + Console.Out.Flush (); // Ensure the request is sent + + // Read the response from stdin (response should come back as input) + Thread.Sleep (100); // Allow time for the terminal to respond + + // Read input until no more characters are available or the terminator is encountered + while (Console.KeyAvailable) + { + // Peek the next key + ConsoleKeyInfo keyInfo = Console.ReadKey (true); // true to not display on the console + + // Append the current key to the response + response.Append (keyInfo.KeyChar); + + if (keyInfo.KeyChar == ansiRequest.Terminator [^1]) // Check if the key is terminator (ANSI escape sequence ends) + { + // Break out of the loop when terminator is found + break; + } + } + + if (!response.ToString ().EndsWith (ansiRequest.Terminator [^1])) + { + throw new InvalidOperationException ($"Terminator doesn't ends with: '{ansiRequest.Terminator [^1]}'"); + } + } + catch (Exception ex) + { + error.AppendLine ($"Error executing ANSI request: {ex.Message}"); + } + finally + { + if (savedIsReportingMouseMoves) + { + switch (Application.Driver) + { + case NetDriver: + NetEvents._suspendRead = false; + netDriver!.StartReportingMouseMoves (); + + break; + case CursesDriver cursesDriver: + cursesDriver.StartReportingMouseMoves (); + + break; + } + } + } + + var values = new string? [] { null }; + + if (string.IsNullOrEmpty (error.ToString ())) + { + (string? c1Control, string? code, values, string? terminator) = EscSeqUtils.GetEscapeResult (response.ToString ().ToCharArray ()); + } + + AnsiEscapeSequenceResponse ansiResponse = new () + { + Response = response.ToString (), Error = error.ToString (), + Terminator = string.IsNullOrEmpty (response.ToString ()) ? "" : response.ToString () [^1].ToString (), Value = values [0] + }; + + // Invoke the event if it's subscribed + ansiRequest.ResponseReceived?.Invoke (ansiRequest, ansiResponse); + + return ansiResponse; + } + + /// + /// Request to send e.g. see + /// + /// EscSeqUtils.CSI_SendDeviceAttributes.Request + /// + /// + public required string Request { get; init; } + + /// + /// Invoked when the console responds with an ANSI response code that matches the + /// + /// + public event EventHandler? ResponseReceived; + + /// + /// + /// The terminator that uniquely identifies the type of response as responded + /// by the console. e.g. for + /// + /// EscSeqUtils.CSI_SendDeviceAttributes.Request + /// + /// the terminator is + /// + /// EscSeqUtils.CSI_SendDeviceAttributes.Terminator + /// + /// . + /// + /// + /// After sending a request, the first response with matching terminator will be matched + /// to the oldest outstanding request. + /// + /// + public required string Terminator { get; init; } + + /// + /// The value expected in the response e.g. + /// + /// EscSeqUtils.CSI_ReportTerminalSizeInChars.Value + /// + /// which will have a 't' as terminator but also other different request may return the same terminator with a + /// different value. + /// + public string? Value { get; init; } +} diff --git a/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceResponse.cs b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceResponse.cs new file mode 100644 index 0000000000..df98511558 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceResponse.cs @@ -0,0 +1,53 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// Describes a finished ANSI received from the console. +/// +public class AnsiEscapeSequenceResponse +{ + /// + /// Error received from e.g. see + /// + /// EscSeqUtils.CSI_SendDeviceAttributes.Request + /// + /// + public required string Error { get; init; } + + /// + /// Response received from e.g. see + /// + /// EscSeqUtils.CSI_SendDeviceAttributes.Request + /// + /// . + /// + public required string Response { get; init; } + + /// + /// + /// The terminator that uniquely identifies the type of response as responded + /// by the console. e.g. for + /// + /// EscSeqUtils.CSI_SendDeviceAttributes.Request + /// + /// the terminator is + /// + /// EscSeqUtils.CSI_SendDeviceAttributes.Terminator + /// + /// + /// + /// The received terminator must match to the terminator sent by the request. + /// + /// + public required string Terminator { get; init; } + + /// + /// The value expected in the response e.g. + /// + /// EscSeqUtils.CSI_ReportTerminalSizeInChars.Value + /// + /// which will have a 't' as terminator but also other different request may return the same terminator with a + /// different value. + /// + public string? Value { get; init; } +} diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs index 99e560044d..a48881d4b1 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs @@ -177,11 +177,15 @@ public override bool SetCursorVisibility (CursorVisibility visibility) return true; } + public bool IsReportingMouseMoves { get; private set; } + public void StartReportingMouseMoves () { if (!RunningUnitTests) { Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents); + + IsReportingMouseMoves = true; } } @@ -190,6 +194,8 @@ public void StopReportingMouseMoves () if (!RunningUnitTests) { Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents); + + IsReportingMouseMoves = false; } } diff --git a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs index 1eb63e34aa..d32a8e01fe 100644 --- a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs +++ b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs @@ -1316,13 +1316,9 @@ public enum DECSCUSR_Style /// /// ESC [ ? 6 n - Request Cursor Position Report (?) (DECXCPR) /// https://terminalguide.namepad.de/seq/csi_sn__p-6/ + /// The terminal reply to . ESC [ ? (y) ; (x) ; 1 R /// - public static readonly string CSI_RequestCursorPositionReport = CSI + "?6n"; - - /// - /// The terminal reply to . ESC [ ? (y) ; (x) R - /// - public const string CSI_RequestCursorPositionReport_Terminator = "R"; + public static readonly AnsiEscapeSequenceRequest CSI_RequestCursorPositionReport = new () { Request = CSI + "?6n", Terminator = "R" }; /// /// ESC [ 0 c - Send Device Attributes (Primary DA) @@ -1341,37 +1337,25 @@ public enum DECSCUSR_Style /// 28 = Rectangular area operations /// 32 = Text macros /// 42 = ISO Latin-2 character set + /// The terminator indicating a reply to or + /// /// - public static readonly string CSI_SendDeviceAttributes = CSI + "0c"; + public static readonly AnsiEscapeSequenceRequest CSI_SendDeviceAttributes = new () { Request = CSI + "0c", Terminator = "c" }; /// /// ESC [ > 0 c - Send Device Attributes (Secondary DA) /// Windows Terminal v1.18+ emits: "\x1b[>0;10;1c" (vt100, firmware version 1.0, vt220) - /// - public static readonly string CSI_SendDeviceAttributes2 = CSI + ">0c"; - - /// /// The terminator indicating a reply to or /// /// - public const string CSI_ReportDeviceAttributes_Terminator = "c"; + public static readonly AnsiEscapeSequenceRequest CSI_SendDeviceAttributes2 = new () { Request = CSI + ">0c", Terminator = "c" }; /// /// CSI 1 8 t | yes | yes | yes | report window size in chars /// https://terminalguide.namepad.de/seq/csi_st-18/ - /// - public static readonly string CSI_ReportTerminalSizeInChars = CSI + "18t"; - - /// /// The terminator indicating a reply to : ESC [ 8 ; height ; width t /// - public const string CSI_ReportTerminalSizeInChars_Terminator = "t"; - - /// - /// The value of the response to indicating value 1 and 2 are the terminal - /// size in chars. - /// - public const string CSI_ReportTerminalSizeInChars_ResponseValue = "8"; + public static readonly AnsiEscapeSequenceRequest CSI_ReportTerminalSizeInChars = new () { Request = CSI + "18t", Terminator = "t", Value = "8" }; #endregion } diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver.cs index b5729406ce..9097b37309 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver.cs @@ -136,7 +136,7 @@ internal class NetEvents : IDisposable { private readonly ManualResetEventSlim _inputReady = new (false); private CancellationTokenSource _inputReadyCancellationTokenSource; - private readonly ManualResetEventSlim _waitForStart = new (false); + internal readonly ManualResetEventSlim _waitForStart = new (false); //CancellationTokenSource _waitForStartCancellationTokenSource; private readonly ManualResetEventSlim _winChange = new (false); @@ -202,7 +202,7 @@ private static ConsoleKeyInfo ReadConsoleKeyInfo (CancellationToken cancellation { // if there is a key available, return it without waiting // (or dispatching work to the thread queue) - if (Console.KeyAvailable) + if (Console.KeyAvailable && !_suspendRead) { return Console.ReadKey (intercept); } @@ -211,7 +211,7 @@ private static ConsoleKeyInfo ReadConsoleKeyInfo (CancellationToken cancellation { Task.Delay (100, cancellationToken).Wait (cancellationToken); - if (Console.KeyAvailable) + if (Console.KeyAvailable && !_suspendRead) { return Console.ReadKey (intercept); } @@ -222,6 +222,9 @@ private static ConsoleKeyInfo ReadConsoleKeyInfo (CancellationToken cancellation return default (ConsoleKeyInfo); } + internal bool _forceRead; + internal static bool _suspendRead; + private void ProcessInputQueue () { while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false }) @@ -237,7 +240,7 @@ private void ProcessInputQueue () _waitForStart.Reset (); - if (_inputQueue.Count == 0) + if (_inputQueue.Count == 0 || _forceRead) { ConsoleKey key = 0; ConsoleModifiers mod = 0; @@ -591,55 +594,48 @@ private MouseButtonState MapMouseFlags (MouseFlags mouseFlags) private void HandleRequestResponseEvent (string c1Control, string code, string [] values, string terminating) { - switch (terminating) - { - // BUGBUG: I can't find where we send a request for cursor position (ESC[?6n), so I'm not sure if this is needed. - case EscSeqUtils.CSI_RequestCursorPositionReport_Terminator: - var point = new Point { X = int.Parse (values [1]) - 1, Y = int.Parse (values [0]) - 1 }; - - if (_lastCursorPosition.Y != point.Y) - { - _lastCursorPosition = point; - var eventType = EventType.WindowPosition; - var winPositionEv = new WindowPositionEvent { CursorPosition = point }; - - _inputQueue.Enqueue ( - new InputResult { EventType = eventType, WindowPositionEvent = winPositionEv } - ); - } - else - { - return; - } - - break; + if (terminating == - case EscSeqUtils.CSI_ReportTerminalSizeInChars_Terminator: - switch (values [0]) - { - case EscSeqUtils.CSI_ReportTerminalSizeInChars_ResponseValue: - EnqueueWindowSizeEvent ( - Math.Max (int.Parse (values [1]), 0), - Math.Max (int.Parse (values [2]), 0), - Math.Max (int.Parse (values [1]), 0), - Math.Max (int.Parse (values [2]), 0) - ); - - break; - default: - EnqueueRequestResponseEvent (c1Control, code, values, terminating); + // BUGBUG: I can't find where we send a request for cursor position (ESC[?6n), so I'm not sure if this is needed. + // The observation is correct because the response isn't immediate and this is useless + EscSeqUtils.CSI_RequestCursorPositionReport.Terminator) + { + var point = new Point { X = int.Parse (values [1]) - 1, Y = int.Parse (values [0]) - 1 }; - break; - } + if (_lastCursorPosition.Y != point.Y) + { + _lastCursorPosition = point; + var eventType = EventType.WindowPosition; + var winPositionEv = new WindowPositionEvent { CursorPosition = point }; - break; - case EscSeqUtils.CSI_ReportDeviceAttributes_Terminator: - ConsoleDriver.SupportsSixel = values.Any (v => v == "4"); - break; - default: + _inputQueue.Enqueue ( + new InputResult { EventType = eventType, WindowPositionEvent = winPositionEv } + ); + } + else + { + return; + } + } + else if (terminating == EscSeqUtils.CSI_ReportTerminalSizeInChars.Terminator) + { + if (values [0] == EscSeqUtils.CSI_ReportTerminalSizeInChars.Value) + { + EnqueueWindowSizeEvent ( + Math.Max (int.Parse (values [1]), 0), + Math.Max (int.Parse (values [2]), 0), + Math.Max (int.Parse (values [1]), 0), + Math.Max (int.Parse (values [2]), 0) + ); + } + else + { EnqueueRequestResponseEvent (c1Control, code, values, terminating); - - break; + } + } + else + { + EnqueueRequestResponseEvent (c1Control, code, values, terminating); } _inputReady.Set (); @@ -819,7 +815,7 @@ internal class NetDriver : ConsoleDriver private const int COLOR_RED = 31; private const int COLOR_WHITE = 37; private const int COLOR_YELLOW = 33; - private NetMainLoop _mainLoopDriver; + internal NetMainLoop _mainLoopDriver; public bool IsWinPlatform { get; private set; } public NetWinVTConsole NetWinConsole { get; private set; } @@ -1141,13 +1137,9 @@ internal override MainLoop Init () _mainLoopDriver = new NetMainLoop (this); _mainLoopDriver.ProcessInput = ProcessInput; - _mainLoopDriver._netEvents.EscSeqRequests.Add ("c"); - // Determine if sixel is supported - Console.Out.Write (EscSeqUtils.CSI_SendDeviceAttributes); - return new MainLoop (_mainLoopDriver); } - + private void ProcessInput (InputResult inputEvent) { switch (inputEvent.EventType) @@ -1348,7 +1340,6 @@ private bool SetCursorPosition (int col, int row) } private CursorVisibility? _cachedCursorVisibility; - private static bool _supportsSixel; public override void UpdateCursor () { @@ -1397,11 +1388,15 @@ public override bool EnsureCursorVisibility () #region Mouse Handling + public bool IsReportingMouseMoves { get; private set; } + public void StartReportingMouseMoves () { if (!RunningUnitTests) { Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents); + + IsReportingMouseMoves = true; } } @@ -1410,6 +1405,8 @@ public void StopReportingMouseMoves () if (!RunningUnitTests) { Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents); + + IsReportingMouseMoves = false; } } @@ -1769,7 +1766,13 @@ void IMainLoopDriver.Iteration () { while (_resultQueue.Count > 0) { - ProcessInput?.Invoke (_resultQueue.Dequeue ().Value); + // Always dequeue even if it's null and invoke if isn't null + InputResult? dequeueResult = _resultQueue.Dequeue (); + + if (dequeueResult is { }) + { + ProcessInput?.Invoke (dequeueResult.Value); + } } } @@ -1825,10 +1828,16 @@ private void NetInputHandler () _resultQueue.Enqueue (_netEvents.DequeueInput ()); } - while (_resultQueue.Count > 0 && _resultQueue.Peek () is null) + try { - _resultQueue.Dequeue (); + while (_resultQueue.Count > 0 && _resultQueue.Peek () is null) + { + // Dequeue null values + _resultQueue.Dequeue (); + } } + catch (InvalidOperationException) // Peek can raise an exception + { } if (_resultQueue.Count > 0) { diff --git a/Terminal.Gui/Views/AutocompleteFilepathContext.cs b/Terminal.Gui/Views/AutocompleteFilepathContext.cs index f577e554fe..96ce1dfaef 100644 --- a/Terminal.Gui/Views/AutocompleteFilepathContext.cs +++ b/Terminal.Gui/Views/AutocompleteFilepathContext.cs @@ -6,7 +6,7 @@ namespace Terminal.Gui; internal class AutocompleteFilepathContext : AutocompleteContext { public AutocompleteFilepathContext (string currentLine, int cursorPosition, FileDialogState state) - : base (TextModel.ToRuneCellList (currentLine), cursorPosition) + : base (RuneCell.ToRuneCellList (currentLine), cursorPosition) { State = state; } diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs index 64e3d0482e..aa4322e2e0 100644 --- a/Terminal.Gui/Views/TextField.cs +++ b/Terminal.Gui/Views/TextField.cs @@ -546,7 +546,7 @@ public string SelectedText if (!Secret && !_historyText.IsFromHistory) { _historyText.Add ( - new List> { TextModel.ToRuneCellList (oldText) }, + new List> { RuneCell.ToRuneCellList (oldText) }, new Point (_cursorPosition, 0) ); @@ -952,7 +952,7 @@ public override void OnDrawContent (Rectangle viewport) int p = ScrollOffset; var col = 0; - int width = Frame.Width + OffSetBackground (); + int width = Viewport.Width + OffSetBackground (); int tcount = _text.Count; Attribute roc = GetReadOnlyColor (); @@ -1140,10 +1140,10 @@ public virtual void Paste () } int cols = _text [idx].GetColumns (); - TextModel.SetCol (ref col, Frame.Width - 1, cols); + TextModel.SetCol (ref col, Viewport.Width - 1, cols); } - int pos = _cursorPosition - ScrollOffset + Math.Min (Frame.X, 0); + int pos = _cursorPosition - ScrollOffset + Math.Min (Viewport.X, 0); Move (pos, 0); return new Point (pos, 0); } @@ -1225,16 +1225,16 @@ private void Adjust () ScrollOffset = _cursorPosition; need = true; } - else if (Frame.Width > 0 - && (ScrollOffset + _cursorPosition - (Frame.Width + offB) == 0 - || TextModel.DisplaySize (_text, ScrollOffset, _cursorPosition).size >= Frame.Width + offB)) + else if (Viewport.Width > 0 + && (ScrollOffset + _cursorPosition - (Viewport.Width + offB) == 0 + || TextModel.DisplaySize (_text, ScrollOffset, _cursorPosition).size >= Viewport.Width + offB)) { ScrollOffset = Math.Max ( TextModel.CalculateLeftColumn ( _text, ScrollOffset, _cursorPosition, - Frame.Width + offB + Viewport.Width + offB ), 0 ); @@ -1342,7 +1342,7 @@ private List DeleteSelectedText () private void GenerateSuggestions () { - List currentLine = TextModel.ToRuneCellList (Text); + List currentLine = RuneCell.ToRuneCellList (Text); int cursorPosition = Math.Min (CursorPosition, currentLine.Count); Autocomplete.Context = new AutocompleteContext ( diff --git a/Terminal.Gui/Views/TextView.cs b/Terminal.Gui/Views/TextView.cs index b81877b531..5c4a82fd8d 100644 --- a/Terminal.Gui/Views/TextView.cs +++ b/Terminal.Gui/Views/TextView.cs @@ -42,6 +42,22 @@ public override string ToString () return $"U+{Rune.Value:X4} '{Rune.ToString ()}'; {colorSchemeStr}"; } + + /// Converts the string into a . + /// The string to convert. + /// The to use. + /// + public static List ToRuneCellList (string str, ColorScheme? colorScheme = null) + { + List cells = new (); + + foreach (Rune rune in str.EnumerateRunes ()) + { + cells.Add (new () { Rune = rune, ColorScheme = colorScheme }); + } + + return cells; + } } internal class TextModel @@ -233,22 +249,6 @@ public static List> StringToLinesOfRuneCells (string content, Col return SplitNewLines (cells); } - /// Converts the string into a . - /// The string to convert. - /// The to use. - /// - public static List ToRuneCellList (string str, ColorScheme? colorScheme = null) - { - List cells = new (); - - foreach (Rune rune in str.EnumerateRunes ()) - { - cells.Add (new () { Rune = rune, ColorScheme = colorScheme }); - } - - return cells; - } - public override string ToString () { var sb = new StringBuilder (); @@ -855,7 +855,7 @@ internal static int GetColFromX (List t, int start, int x, int tabWidth = found = true; } - _lines [i] = ToRuneCellList (ReplaceText (x, textToReplace!, matchText, col)); + _lines [i] = RuneCell.ToRuneCellList (ReplaceText (x, textToReplace!, matchText, col)); x = _lines [i]; txt = GetText (x); pos = new (col, i); @@ -1706,7 +1706,7 @@ public List> ToListRune (List textList) foreach (string text in textList) { - runesList.Add (TextModel.ToRuneCellList (text)); + runesList.Add (RuneCell.ToRuneCellList (text)); } return runesList; @@ -3591,6 +3591,8 @@ public override void OnDrawContent (Rectangle viewport) else { AddRune (col, row, rune); + // Ensures that cols less than 0 to be 1 because it will be converted to a printable rune + cols = Math.Max (cols, 1); } if (!TextModel.SetCol (ref col, viewport.Right, cols)) @@ -3713,7 +3715,7 @@ public void Paste () if (_copyWithoutSelection && contents.FirstOrDefault (x => x == '\n' || x == '\r') == 0) { - List runeList = contents is null ? new () : TextModel.ToRuneCellList (contents); + List runeList = contents is null ? new () : RuneCell.ToRuneCellList (contents); List currentLine = GetCurrentLine (); _historyText.Add (new () { new (currentLine) }, CursorPosition); @@ -3802,6 +3804,11 @@ public void Paste () { cols += TabWidth + 1; } + else + { + // Ensures that cols less than 0 to be 1 because it will be converted to a printable rune + cols = Math.Max (cols, 1); + } if (!TextModel.SetCol (ref col, Viewport.Width, cols)) { diff --git a/UICatalog/Scenarios/AnsiEscapeSequenceRequests.cs b/UICatalog/Scenarios/AnsiEscapeSequenceRequests.cs new file mode 100644 index 0000000000..10f152c17c --- /dev/null +++ b/UICatalog/Scenarios/AnsiEscapeSequenceRequests.cs @@ -0,0 +1,118 @@ +using System.Collections.Generic; +using Terminal.Gui; + +namespace UICatalog.Scenarios; + +[ScenarioMetadata ("AnsiEscapeSequenceRequest", "Ansi Escape Sequence Request")] +[ScenarioCategory ("Ansi Escape Sequence")] +public sealed class AnsiEscapeSequenceRequests : Scenario +{ + public override void Main () + { + // Init + Application.Init (); + + // Setup - Create a top-level application window and configure it. + Window appWindow = new () + { + Title = GetQuitKeyAndName (), + }; + appWindow.Padding.Thickness = new (1); + + var scrRequests = new List + { + "CSI_SendDeviceAttributes", + "CSI_ReportTerminalSizeInChars", + "CSI_RequestCursorPositionReport", + "CSI_SendDeviceAttributes2" + }; + + var cbRequests = new ComboBox () { Width = 40, Height = 5, ReadOnly = true, Source = new ListWrapper (new (scrRequests)) }; + appWindow.Add (cbRequests); + + var label = new Label { Y = Pos.Bottom (cbRequests) + 1, Text = "Request:" }; + var tfRequest = new TextField { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 20 }; + appWindow.Add (label, tfRequest); + + label = new Label { X = Pos.Right (tfRequest) + 1, Y = Pos.Top (tfRequest) - 1, Text = "Value:" }; + var tfValue = new TextField { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 6 }; + appWindow.Add (label, tfValue); + + label = new Label { X = Pos.Right (tfValue) + 1, Y = Pos.Top (tfValue) - 1, Text = "Terminator:" }; + var tfTerminator = new TextField { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 4 }; + appWindow.Add (label, tfTerminator); + + cbRequests.SelectedItemChanged += (s, e) => + { + var selAnsiEscapeSequenceRequestName = scrRequests [cbRequests.SelectedItem]; + AnsiEscapeSequenceRequest selAnsiEscapeSequenceRequest = null; + switch (selAnsiEscapeSequenceRequestName) + { + case "CSI_SendDeviceAttributes": + selAnsiEscapeSequenceRequest = EscSeqUtils.CSI_SendDeviceAttributes; + break; + case "CSI_ReportTerminalSizeInChars": + selAnsiEscapeSequenceRequest = EscSeqUtils.CSI_ReportTerminalSizeInChars; + break; + case "CSI_RequestCursorPositionReport": + selAnsiEscapeSequenceRequest = EscSeqUtils.CSI_RequestCursorPositionReport; + break; + case "CSI_SendDeviceAttributes2": + selAnsiEscapeSequenceRequest = EscSeqUtils.CSI_SendDeviceAttributes2; + break; + } + + tfRequest.Text = selAnsiEscapeSequenceRequest is { } ? selAnsiEscapeSequenceRequest.Request : ""; + tfValue.Text = selAnsiEscapeSequenceRequest is { } ? selAnsiEscapeSequenceRequest.Value ?? "" : ""; + tfTerminator.Text = selAnsiEscapeSequenceRequest is { } ? selAnsiEscapeSequenceRequest.Terminator : ""; + }; + // Forces raise cbRequests.SelectedItemChanged to update TextFields + cbRequests.SelectedItem = 0; + + label = new Label { Y = Pos.Bottom (tfRequest) + 2, Text = "Response:" }; + var tvResponse = new TextView { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 40, Height = 4, ReadOnly = true }; + appWindow.Add (label, tvResponse); + + label = new Label { X = Pos.Right (tvResponse) + 1, Y = Pos.Top (tvResponse) - 1, Text = "Error:" }; + var tvError = new TextView { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 40, Height = 4, ReadOnly = true }; + appWindow.Add (label, tvError); + + label = new Label { X = Pos.Right (tvError) + 1, Y = Pos.Top (tvError) - 1, Text = "Value:" }; + var tvValue = new TextView { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 6, Height = 4, ReadOnly = true }; + appWindow.Add (label, tvValue); + + label = new Label { X = Pos.Right (tvValue) + 1, Y = Pos.Top (tvValue) - 1, Text = "Terminator:" }; + var tvTerminator = new TextView { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 4, Height = 4, ReadOnly = true }; + appWindow.Add (label, tvTerminator); + + var btnResponse = new Button { X = Pos.Center (), Y = Pos.Bottom (tvResponse) + 2, Text = "Send Request" }; + btnResponse.Accept += (s, e) => + { + var ansiEscapeSequenceRequest = new AnsiEscapeSequenceRequest + { + Request = tfRequest.Text, + Terminator = tfTerminator.Text, + Value = string.IsNullOrEmpty (tfValue.Text) ? null : tfValue.Text + }; + + var ansiEscapeSequenceResponse = AnsiEscapeSequenceRequest.ExecuteAnsiRequest ( + ansiEscapeSequenceRequest + ); + + tvResponse.Text = ansiEscapeSequenceResponse.Response; + tvError.Text = ansiEscapeSequenceResponse.Error; + tvValue.Text = ansiEscapeSequenceResponse.Value ?? ""; + tvTerminator.Text = ansiEscapeSequenceResponse.Terminator; + }; + appWindow.Add (btnResponse); + + appWindow.Add (new Label { Y = Pos.Bottom (btnResponse) + 2, Text = "You can send other requests by editing the TextFields." }); + + // Run - Start the application. + Application.Run (appWindow); + appWindow.Dispose (); + + // Shutdown - Calling Application.Shutdown is required. + Application.Shutdown (); + } +} diff --git a/UnitTests/Text/AutocompleteTests.cs b/UnitTests/Text/AutocompleteTests.cs index c7d907e1a9..1689c70c8d 100644 --- a/UnitTests/Text/AutocompleteTests.cs +++ b/UnitTests/Text/AutocompleteTests.cs @@ -254,7 +254,7 @@ public void Test_GenerateSuggestions_Simple () ac.GenerateSuggestions ( new ( - TextModel.ToRuneCellList (tv.Text), + RuneCell.ToRuneCellList (tv.Text), 2 ) ); diff --git a/UnitTests/View/Mouse/MouseTests.cs b/UnitTests/View/Mouse/MouseTests.cs index 28613dadfa..8eadb5bbe6 100644 --- a/UnitTests/View/Mouse/MouseTests.cs +++ b/UnitTests/View/Mouse/MouseTests.cs @@ -344,6 +344,7 @@ public void WantContinuousButtonPressed_True_And_WantMousePositionReports_True_M me.Handled = false; view.Dispose (); + Application.ResetState (ignoreDisposed: true); } [Theory] diff --git a/UnitTests/Views/TextFieldTests.cs b/UnitTests/Views/TextFieldTests.cs index a5c6fe4e1e..60a999bff9 100644 --- a/UnitTests/Views/TextFieldTests.cs +++ b/UnitTests/Views/TextFieldTests.cs @@ -2047,4 +2047,18 @@ public void Autocomplete_Visible_False_By_Default () Assert.True (t.Visible); Assert.False (t.Autocomplete.Visible); } + + [Fact] + [AutoInitShutdown] + public void Draw_Esc_Rune () + { + var tf = new TextField { Width = 5, Text = "\u001b" }; + tf.BeginInit (); + tf.EndInit (); + tf.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ("\u241b", output); + + tf.Dispose (); + } } diff --git a/UnitTests/Views/TextViewTests.cs b/UnitTests/Views/TextViewTests.cs index 2ab55e7aea..cdbde04480 100644 --- a/UnitTests/Views/TextViewTests.cs +++ b/UnitTests/Views/TextViewTests.cs @@ -8660,4 +8660,18 @@ public void Autocomplete_Visible_False_By_Default () Assert.True (t.Visible); Assert.False (t.Autocomplete.Visible); } + + [Fact] + [AutoInitShutdown] + public void Draw_Esc_Rune () + { + var tv = new TextView { Width = 5, Height = 1, Text = "\u001b" }; + tv.BeginInit (); + tv.EndInit (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ("\u241b", _output); + + tv.Dispose (); + } }