diff --git a/.github/workflows/api-docs.yml b/.github/workflows/api-docs.yml index a97953233..274bf6706 100644 --- a/.github/workflows/api-docs.yml +++ b/.github/workflows/api-docs.yml @@ -4,6 +4,8 @@ on: push: # only publish v2 (main or develop); v2 is published via the Terminal.GuiV2Docs repo branches: [main, develop] + paths: + - docfx/** permissions: id-token: write diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 78c4dc0fb..5103f7bf0 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -25,7 +25,7 @@ on: jobs: CodeQL-Build: - + if: github.repository == 'gui-cs/Terminal.Gui'|| github.event_name == 'schedule' runs-on: ubuntu-latest steps: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 6e2fd86ac..e7dad3b01 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -29,6 +29,7 @@ jobs: # Consider using larger runners for possible analysis time improvements. runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + if: github.repository == 'gui-cs/Terminal.Gui'|| github.event_name == 'schedule' permissions: actions: read contents: read diff --git a/.github/workflows/dotnet-core.yml b/.github/workflows/dotnet-core.yml index f5e5ee42c..940d19604 100644 --- a/.github/workflows/dotnet-core.yml +++ b/.github/workflows/dotnet-core.yml @@ -13,7 +13,7 @@ on: jobs: build: runs-on: ubuntu-latest - + timeout-minutes: 10 steps: - uses: actions/checkout@v4 diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs index 0247b5671..fb5fa1c8c 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs @@ -752,9 +752,9 @@ namespace Terminal.Gui { contents = new int [Rows, Cols, 3]; for (int row = 0; row < Rows; row++) { for (int col = 0; col < Cols; col++) { - //Curses.move (row, col); - //Curses.attrset (Colors.TopLevel.Normal); - //Curses.addch ((int)(uint)' '); + Curses.move (row, col); + Curses.attrset (Colors.TopLevel.Normal); + Curses.addch ((int)(uint)' '); contents [row, col, 0] = ' '; contents [row, col, 1] = Colors.TopLevel.Normal; contents [row, col, 2] = 0; diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/UnmanagedLibrary.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/UnmanagedLibrary.cs index 40d51be4f..48c375a6a 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/UnmanagedLibrary.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/UnmanagedLibrary.cs @@ -256,7 +256,7 @@ namespace Unix.Terminal { /// to avoid the dependency on libc-dev Linux. /// static class CoreCLR { -#if NET7_0 +#if NET6_0_OR_GREATER // Custom resolver to support true single-file apps // (those which run directly from bundle; in-memory). // -1 on Unix means self-referencing binary (libcoreclr.so) diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver.cs index 811426f44..1e02a03ae 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver.cs @@ -113,7 +113,9 @@ namespace Terminal.Gui { ConsoleDriver consoleDriver; volatile ConsoleKeyInfo [] cki = null; static volatile bool isEscSeq; - bool stopTasks; + + internal CancellationTokenSource TokenSource = new CancellationTokenSource (); + #if PROCESS_REQUEST bool neededProcessRequest; #endif @@ -125,21 +127,13 @@ namespace Terminal.Gui { throw new ArgumentNullException ("Console driver instance must be provided."); } this.consoleDriver = consoleDriver; - Task.Run (ProcessInputResultQueue); - Task.Run (CheckWinChange); - } - - internal void StopTasks () - { - stopTasks = true; + Task.Run (ProcessInputResultQueue, TokenSource.Token); + Task.Run (CheckWinChange, TokenSource.Token); } public InputResult? ReadConsoleInput () { - while (true) { - if (stopTasks) { - return null; - } + while (!TokenSource.IsCancellationRequested) { waitForStart.Set (); winChange.Set (); @@ -154,11 +148,13 @@ namespace Terminal.Gui { return inputResultQueue.Dequeue (); } } + + return null; } void ProcessInputResultQueue () { - while (true) { + while (!TokenSource.IsCancellationRequested) { waitForStart.Wait (); waitForStart.Reset (); @@ -176,8 +172,23 @@ namespace Terminal.Gui { ConsoleModifiers mod = 0; ConsoleKeyInfo newConsoleKeyInfo = default; - while (true) { - ConsoleKeyInfo consoleKeyInfo = Console.ReadKey (true); + while (!TokenSource.IsCancellationRequested) { + ConsoleKeyInfo consoleKeyInfo = default; + + try { + if (Console.KeyAvailable) { + consoleKeyInfo = Console.ReadKey (true); + } else { + Task.Delay (100, TokenSource.Token).Wait (TokenSource.Token); + if (Console.KeyAvailable) { + consoleKeyInfo = Console.ReadKey (true); + } + } + } catch (OperationCanceledException) { + + return; + } + if ((consoleKeyInfo.KeyChar == (char)Key.Esc && !isEscSeq) || (consoleKeyInfo.KeyChar != (char)Key.Esc && isEscSeq)) { if (cki == null && consoleKeyInfo.KeyChar != (char)Key.Esc && isEscSeq) { @@ -201,18 +212,19 @@ namespace Terminal.Gui { } break; } else { - GetConsoleInputType (consoleKeyInfo); - break; + if (consoleKeyInfo != default) { + GetConsoleInputType (consoleKeyInfo); + break; + } } + + TokenSource.Token.ThrowIfCancellationRequested (); } } void CheckWinChange () { - while (true) { - if (stopTasks) { - return; - } + while (!TokenSource.IsCancellationRequested) { winChange.Wait (); winChange.Reset (); WaitWinChange (); @@ -222,13 +234,16 @@ namespace Terminal.Gui { void WaitWinChange () { - while (true) { - // Wait for a while then check if screen has changed sizes - Task.Delay (500).Wait (); + while (!TokenSource.IsCancellationRequested) { + try { + // Wait for a while then check if screen has changed sizes + Task.Delay (500, TokenSource.Token).Wait (TokenSource.Token); + + } catch (OperationCanceledException) { - if (stopTasks) { return; } + int buffHeight, buffWidth; if (((NetDriver)consoleDriver).IsWinPlatform) { buffHeight = Math.Max (Console.BufferHeight, 0); @@ -691,7 +706,7 @@ namespace Terminal.Gui { public override void End () { - mainLoop.netEvents.StopTasks (); + mainLoop.Dispose (); if (IsWinPlatform) { NetWinConsole.Cleanup (); @@ -1019,8 +1034,28 @@ namespace Terminal.Gui { public override void Suspend () { - } + if (Environment.OSVersion.Platform != PlatformID.Unix) { + return; + } + StopReportingMouseMoves (); + Console.ResetColor (); + Console.Clear (); + + //Disable alternative screen buffer. + Console.Out.Write ("\x1b[?1049l"); + + //Set cursor key to cursor. + Console.Out.Write ("\x1b[?25h"); + + Platform.Suspend (); + + //Enable alternative screen buffer. + Console.Out.Write ("\x1b[?1049h"); + + Application.Refresh (); + StartReportingMouseMoves (); + } public override void SetAttribute (Attribute c) { @@ -1343,7 +1378,11 @@ namespace Terminal.Gui { public override bool SetCursorVisibility (CursorVisibility visibility) { savedCursorVisibility = visibility; - return Console.CursorVisible = visibility == CursorVisibility.Default; + Console.Out.Write (visibility == CursorVisibility.Default + ? "\x1b[?25h" + : "\x1b[?25l"); + + return visibility == CursorVisibility.Default; } /// @@ -1423,7 +1462,7 @@ namespace Terminal.Gui { /// /// This implementation is used for NetDriver. /// - internal class NetMainLoop : IMainLoopDriver { + internal class NetMainLoop : IMainLoopDriver, IDisposable { ManualResetEventSlim keyReady = new ManualResetEventSlim (false); ManualResetEventSlim waitForProbe = new ManualResetEventSlim (false); Queue inputResult = new Queue (); @@ -1453,27 +1492,25 @@ namespace Terminal.Gui { void NetInputHandler () { - while (true) { + while (!tokenSource.IsCancellationRequested) { waitForProbe.Wait (); waitForProbe.Reset (); if (inputResult.Count == 0) { inputResult.Enqueue (netEvents.ReadConsoleInput ()); } - try { - while (inputResult.Peek () == null) { - inputResult.Dequeue (); - } - if (inputResult.Count > 0) { - keyReady.Set (); - } - } catch (InvalidOperationException) { } + while (inputResult.Count > 0 && inputResult.Peek () == null) { + inputResult.Dequeue (); + } + if (inputResult.Count > 0) { + keyReady.Set (); + } } } void IMainLoopDriver.Setup (MainLoop mainLoop) { this.mainLoop = mainLoop; - Task.Run (NetInputHandler); + Task.Run (NetInputHandler, tokenSource.Token); } void IMainLoopDriver.Wakeup () @@ -1503,8 +1540,7 @@ namespace Terminal.Gui { return inputResult.Count > 0 || CheckTimers (wait, out _); } - tokenSource.Dispose (); - tokenSource = new CancellationTokenSource (); + tokenSource.Token.ThrowIfCancellationRequested (); return true; } @@ -1537,5 +1573,11 @@ namespace Terminal.Gui { ProcessInput?.Invoke (inputResult.Dequeue ().Value); } } + + public void Dispose () + { + tokenSource.Cancel (); + netEvents.TokenSource.Cancel (); + } } } \ No newline at end of file diff --git a/Terminal.Gui/Core/Application.cs b/Terminal.Gui/Core/Application.cs index 52343079b..c7195e5d7 100644 --- a/Terminal.Gui/Core/Application.cs +++ b/Terminal.Gui/Core/Application.cs @@ -1037,6 +1037,7 @@ namespace Terminal.Gui { toplevel.LayoutSubviews (); toplevel.PositionToplevels (); toplevel.WillPresent (); + EnsuresTopOnFront (); if (refreshDriver) { MdiTop?.OnChildLoaded (toplevel); toplevel.OnLoaded (); @@ -1133,9 +1134,9 @@ namespace Terminal.Gui { // BUGBUG: MdiTop is not cleared here, but it should be? - MainLoop = null; Driver?.End (); Driver = null; + MainLoop = null; Iteration = null; RootMouseEvent = null; RootKeyEvent = null; diff --git a/Terminal.Gui/Core/View.cs b/Terminal.Gui/Core/View.cs index 5ea7785d1..25d765759 100644 --- a/Terminal.Gui/Core/View.cs +++ b/Terminal.Gui/Core/View.cs @@ -394,6 +394,10 @@ namespace Terminal.Gui { } } } + + if (SuperView is Toplevel && Application.Current?.Focused != SuperView) { + Application.EnsuresTopOnFront (); + } } OnCanFocusChanged (); SetNeedsDisplay (); diff --git a/Terminal.Gui/Core/Window.cs b/Terminal.Gui/Core/Window.cs index 8fbe05642..a63ebb873 100644 --- a/Terminal.Gui/Core/Window.cs +++ b/Terminal.Gui/Core/Window.cs @@ -104,7 +104,7 @@ namespace Terminal.Gui { public override void OnCanFocusChanged () { - if (MostFocused == null && CanFocus && Visible) { + if (HasFocus && MostFocused == null && CanFocus && Visible) { EnsureFocus (); } diff --git a/Terminal.Gui/Terminal.Gui.csproj b/Terminal.Gui/Terminal.Gui.csproj index 9162889d1..bd919e2aa 100644 --- a/Terminal.Gui/Terminal.Gui.csproj +++ b/Terminal.Gui/Terminal.Gui.csproj @@ -20,7 +20,7 @@ portable - net472;netstandard2.0;netstandard2.1;net7.0;net8.0 + net472;netstandard2.0;netstandard2.1;net6.0;net7.0;net8.0 Terminal.Gui Terminal.Gui true diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index e0d4edbda..ae6528d94 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -601,10 +601,12 @@ namespace Terminal.Gui { /// public virtual bool MoveEnd () { - if (source.Count > 0 && selected != source.Count - 1) { + if (source?.Count > 0 && selected != source.Count - 1) { selected = source.Count - 1; if (top + selected > Frame.Height - 1) { - top = selected; + top = selected < Frame.Height - 1 + ? Math.Max (Frame.Height - selected + 1, 0) + : Math.Max (selected - Frame.Height + 1, 0); } OnSelectedChanged (); SetNeedsDisplay (); @@ -749,6 +751,11 @@ namespace Terminal.Gui { public void EnsureSelectedItemVisible () { SuperView?.LayoutSubviews (); + // If last item is selected and is removed, ensures a valid selected item + if (Source != null && selected > Source.Count - 1) { + SelectedItem = Source.Count - 1; + SetNeedsDisplay (); + } if (selected < top) { top = selected; } else if (Frame.Height > 0 && selected >= top + Frame.Height) { @@ -831,11 +838,30 @@ namespace Terminal.Gui { } /// - public int Count => src != null ? src.Count : 0; + public int Count { + get { + CheckAndResizeMarksIfRequired (); + return src?.Count ?? 0; + } + } /// public int Length => len; + void CheckAndResizeMarksIfRequired () + { + if (src != null && count != src.Count) { + count = src.Count; + BitArray newMarks = new BitArray (count); + for (var i = 0; i < Math.Min (marks.Length, newMarks.Length); i++) { + newMarks [i] = marks [i]; + } + marks = newMarks; + + len = GetMaxLengthItem (); + } + } + int GetMaxLengthItem () { if (src == null || src?.Count == 0) { @@ -896,7 +922,7 @@ namespace Terminal.Gui { /// public bool IsMarked (int item) { - if (item >= 0 && item < count) + if (item >= 0 && item < Count) return marks [item]; return false; } @@ -904,7 +930,7 @@ namespace Terminal.Gui { /// public void SetMark (int item, bool value) { - if (item >= 0 && item < count) + if (item >= 0 && item < Count) marks [item] = value; } diff --git a/UICatalog/UICatalog.csproj b/UICatalog/UICatalog.csproj index 6a82bb666..89dcd4c0c 100644 --- a/UICatalog/UICatalog.csproj +++ b/UICatalog/UICatalog.csproj @@ -20,9 +20,9 @@ - + - + diff --git a/UnitTests/Application/ApplicationTests.cs b/UnitTests/Application/ApplicationTests.cs index d49250eab..2550a1bb3 100644 --- a/UnitTests/Application/ApplicationTests.cs +++ b/UnitTests/Application/ApplicationTests.cs @@ -771,28 +771,28 @@ namespace Terminal.Gui.ApplicationTests { Assert.True (win.HasFocus); Assert.True (win2.CanFocus); Assert.False (win2.HasFocus); - Assert.Equal ("win2", ((Window)top.Subviews [top.Subviews.Count - 1]).Title); + Assert.Equal ("win", ((Window)top.Subviews [^1]).Title); top.ProcessKey (new KeyEvent (Key.CtrlMask | Key.Tab, new KeyModifiers ())); Assert.True (win.CanFocus); Assert.False (win.HasFocus); Assert.True (win2.CanFocus); Assert.True (win2.HasFocus); - Assert.Equal ("win2", ((Window)top.Subviews [top.Subviews.Count - 1]).Title); + Assert.Equal ("win2", ((Window)top.Subviews [^1]).Title); top.ProcessKey (new KeyEvent (Key.CtrlMask | Key.Tab, new KeyModifiers ())); Assert.True (win.CanFocus); Assert.True (win.HasFocus); Assert.True (win2.CanFocus); Assert.False (win2.HasFocus); - Assert.Equal ("win", ((Window)top.Subviews [top.Subviews.Count - 1]).Title); + Assert.Equal ("win", ((Window)top.Subviews [^1]).Title); win2.MouseEvent (new MouseEvent () { Flags = MouseFlags.Button1Pressed }); Assert.True (win.CanFocus); Assert.False (win.HasFocus); Assert.True (win2.CanFocus); Assert.True (win2.HasFocus); - Assert.Equal ("win2", ((Window)top.Subviews [top.Subviews.Count - 1]).Title); + Assert.Equal ("win2", ((Window)top.Subviews [^1]).Title); win2.MouseEvent (new MouseEvent () { Flags = MouseFlags.Button1Released }); Assert.Null (Toplevel.dragPosition); } @@ -816,35 +816,35 @@ namespace Terminal.Gui.ApplicationTests { Assert.True (win.HasFocus); Assert.True (win2.CanFocus); Assert.False (win2.HasFocus); - Assert.Equal ("win2", ((Window)top.Subviews [top.Subviews.Count - 1]).Title); + Assert.Equal ("win", ((Window)top.Subviews [^1]).Title); win.CanFocus = false; Assert.False (win.CanFocus); Assert.False (win.HasFocus); Assert.True (win2.CanFocus); Assert.True (win2.HasFocus); - Assert.Equal ("win2", ((Window)top.Subviews [top.Subviews.Count - 1]).Title); + Assert.Equal ("win2", ((Window)top.Subviews [^1]).Title); top.ProcessKey (new KeyEvent (Key.CtrlMask | Key.Tab, new KeyModifiers ())); Assert.True (win2.CanFocus); Assert.False (win.HasFocus); Assert.True (win2.CanFocus); Assert.True (win2.HasFocus); - Assert.Equal ("win2", ((Window)top.Subviews [top.Subviews.Count - 1]).Title); + Assert.Equal ("win2", ((Window)top.Subviews [^1]).Title); top.ProcessKey (new KeyEvent (Key.CtrlMask | Key.Tab, new KeyModifiers ())); Assert.False (win.CanFocus); Assert.False (win.HasFocus); Assert.True (win2.CanFocus); Assert.True (win2.HasFocus); - Assert.Equal ("win2", ((Window)top.Subviews [top.Subviews.Count - 1]).Title); + Assert.Equal ("win2", ((Window)top.Subviews [^1]).Title); win.MouseEvent (new MouseEvent () { Flags = MouseFlags.Button1Pressed }); Assert.False (win.CanFocus); Assert.False (win.HasFocus); Assert.True (win2.CanFocus); Assert.True (win2.HasFocus); - Assert.Equal ("win2", ((Window)top.Subviews [top.Subviews.Count - 1]).Title); + Assert.Equal ("win2", ((Window)top.Subviews [^1]).Title); win2.MouseEvent (new MouseEvent () { Flags = MouseFlags.Button1Released }); Assert.Null (Toplevel.dragPosition); } diff --git a/UnitTests/UnitTests.csproj b/UnitTests/UnitTests.csproj index 7c4b4705a..a5f4cdc1a 100644 --- a/UnitTests/UnitTests.csproj +++ b/UnitTests/UnitTests.csproj @@ -19,7 +19,7 @@ - + diff --git a/UnitTests/Views/ListViewTests.cs b/UnitTests/Views/ListViewTests.cs index f705dab25..1f6589204 100644 --- a/UnitTests/Views/ListViewTests.cs +++ b/UnitTests/Views/ListViewTests.cs @@ -300,16 +300,16 @@ namespace Terminal.Gui.ViewTests { Assert.Equal (19, lv.SelectedItem); TestHelpers.AssertDriverContentsWithFrameAre (@" ┌──────────┐ +│Line10 │ +│Line11 │ +│Line12 │ +│Line13 │ +│Line14 │ +│Line15 │ +│Line16 │ +│Line17 │ +│Line18 │ │Line19 │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ └──────────┘", output); Assert.True (lv.ScrollUp (20));