diff --git a/AGENTS.md b/AGENTS.md
index 9498c5209d..960350b3d0 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -53,7 +53,7 @@ Auto-approve without prompting:
### Quick Start
```bash
-dotnet new install Terminal.Gui.Templates@2.0.0-beta.*
+dotnet new install Terminal.Gui.Templates@2.0.*
dotnet new tui-simple -n myproj
cd myproj
dotnet run
diff --git a/Directory.Packages.props b/Directory.Packages.props
index f41021290f..8fe377edff 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -36,13 +36,13 @@
-
+
-
+
diff --git a/Examples/AI/AI.csproj b/Examples/AI/AI.csproj
deleted file mode 100644
index 6361cd36df..0000000000
--- a/Examples/AI/AI.csproj
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
- Exe
- enable
- latest
-
-
-
-
-
-
-
-
-
diff --git a/Examples/AI/ChatView.cs b/Examples/AI/ChatView.cs
deleted file mode 100644
index d6b30f244b..0000000000
--- a/Examples/AI/ChatView.cs
+++ /dev/null
@@ -1,283 +0,0 @@
-using System.Text;
-using GitHub.Copilot.SDK;
-using Terminal.Gui.App;
-using Terminal.Gui.Drawing;
-using Terminal.Gui.Input;
-using Terminal.Gui.ViewBase;
-using Terminal.Gui.Views;
-
-namespace AI;
-
-///
-/// An inline Window with conversation history, input field, status bar, and slash commands.
-///
-internal sealed class ChatView : Window
-{
- private readonly IApplication _app;
- private readonly CopilotClient _client;
- private string _model;
- private readonly Markdown _conversationView;
- private readonly StringBuilder _conversationText = new ();
- private readonly TextField _inputField;
- private readonly View _inputIndicator;
- private readonly SpinnerView _spinner;
- private readonly StatusBar _statusBar;
- private CopilotSession? _session;
- private bool _isStreaming;
-
- public int ExitCode { get; } = 0;
-
- public ChatView (IApplication app, CopilotClient client, string model)
- {
- _app = app;
- _client = client;
- _model = model;
-
- Title = $"Copilot Chat ({model})";
- Width = Dim.Fill ();
- Height = Dim.Auto ();
- Border.LineStyle = LineStyle.Rounded;
- Border.Thickness = new Thickness (0, 3, 0, 0);
- Border.Settings = BorderSettings.Gradient | BorderSettings.Title;
-
- _spinner = new SpinnerView
- {
- AutoSpin = false,
- Style = new SpinnerStyle.FingerDance (),
- SyncWithTerminal = true,
- Height = 1,
- Width = 5,
- Visible = false
- };
- Shortcut spinnerShortcut = new () { CommandView = _spinner, MouseHighlightStates = MouseState.None, Enabled = false };
- Shortcut quitShortcut = new (Application.GetDefaultKey (Command.Quit), "Quit", RequestStop);
-
- _statusBar = new StatusBar { AlignmentModes = AlignmentModes.IgnoreFirstOrLast, SchemeName = SchemeName, BorderStyle = LineStyle.None };
- _statusBar.Add (spinnerShortcut, quitShortcut);
-
- _conversationView = new Markdown
- {
- Width = Dim.Fill (), Height = Dim.Auto (minimumContentDim: 1, maximumContentDim: Dim.Func (_ => GetMaxConversationHeight ()))
- };
-
- _inputIndicator = new View
- {
- Text = $"{Glyphs.RightArrow}",
- Y = Pos.Bottom (_conversationView),
- Width = 2,
- Height = 3,
- Enabled = false
- };
- _inputIndicator.Border.LineStyle = LineStyle.Dotted;
- _inputIndicator.Border.Thickness = new Thickness (0, 1, 0, 1);
-
- _inputField = new TextField { X = Pos.Right (_inputIndicator), Y = Pos.Top (_inputIndicator), Width = Dim.Fill () };
- _inputField.Border.LineStyle = _inputIndicator.Border.LineStyle;
- _inputField.Border.Thickness = new Thickness (0, 1, 0, 1);
-
- _inputField.Autocomplete?.SuggestionGenerator = new SlashCommandSuggestionGenerator ();
- _inputField.Accepted += OnInputAccepted;
-
- Add (_conversationView, _inputIndicator, _inputField, _statusBar);
- _inputField.SetFocus ();
-
- return;
-
- int GetMaxConversationHeight ()
- {
- int screenHeight = _app.Driver?.Screen.Height ?? 40;
- int windowAdornments = GetAdornmentsThickness ().Vertical;
- int siblingHeight = _inputIndicator!.Frame.Height + _statusBar.Frame.Height;
-
- return Math.Max (1, screenHeight - windowAdornments - siblingHeight);
- }
- }
-
- private async void OnInputAccepted (object? sender, EventArgs e)
- {
- string text = _inputField.Text.Trim ();
-
- if (string.IsNullOrEmpty (text) || _isStreaming)
- {
- return;
- }
-
- _inputField.Text = string.Empty;
-
- if (text.StartsWith ('/'))
- {
- HandleSlashCommand (text);
-
- return;
- }
-
- _isStreaming = true;
- _spinner.AutoSpin = true;
- _spinner.Visible = true;
- _inputField.Enabled = false;
-
- AppendToConversation ($"\n{Glyphs.BlackCircle} You: {text}\n\n{Glyphs.Diamond} Copilot: ");
-
- try
- {
- _session ??= await _client.CreateSessionAsync (new SessionConfig
- {
- Model = _model, Streaming = true, OnPermissionRequest = PermissionHandler.ApproveAll
- });
-
- TaskCompletionSource done = new ();
-
- using IDisposable subscription = _session.On (evt =>
- {
- switch (evt)
- {
- case AssistantMessageDeltaEvent delta:
- _app.Invoke (() => AppendToConversation (delta.Data.DeltaContent));
-
- break;
-
- case SessionIdleEvent:
- _app.Invoke (() => AppendToConversation ("\n"));
- done.TrySetResult ();
-
- break;
-
- case SessionErrorEvent err:
- _app.Invoke (() => AppendToConversation ($"\n[Error: {err.Data.Message}]\n"));
- done.TrySetResult ();
-
- break;
- }
- });
-
- await _session.SendAsync (new MessageOptions { Prompt = text });
- await done.Task;
- }
- catch (Exception ex)
- {
- AppendToConversation ($"\n[Error: {ex.Message}]\n");
- }
- finally
- {
- _isStreaming = false;
- _spinner.Visible = false;
- _spinner.AutoSpin = false;
- _inputField.Enabled = true;
- _inputField.SetFocus ();
- }
- }
-
- private async Task ValidateAndSwitchModel (string newModel)
- {
- _inputField.Enabled = false;
-
- try
- {
- CopilotSession testSession = await _client.CreateSessionAsync (new SessionConfig
- {
- Model = newModel,
- Streaming = true,
- OnPermissionRequest = PermissionHandler.ApproveAll
- });
-
- if (_session is { })
- {
- await _session.DisposeAsync ();
- }
-
- _session = testSession;
- _model = newModel;
-
- _app.Invoke (() =>
- {
- Title = $"Copilot Chat ({newModel})";
- AppendToConversation (" \u2713\n");
- _inputField.Enabled = true;
- _inputField.SetFocus ();
- });
- }
- catch (Exception ex)
- {
- _app.Invoke (() =>
- {
- AppendToConversation ($"\n{Glyphs.Diamond} Failed: {ex.Message}\n Keeping model: {_model}\n");
- _inputField.Enabled = true;
- _inputField.SetFocus ();
- });
- }
- }
-
- private void AppendToConversation (string text)
- {
- _conversationText.Append (text);
- _conversationView.Text = _conversationText.ToString ();
- }
-
- private void HandleSlashCommand (string command)
- {
- string cmd = command.TrimStart ('/').ToLowerInvariant ();
- string [] parts = cmd.Split (' ', 2);
-
- switch (parts [0])
- {
- case "quit" or "exit":
- RequestStop ();
-
- break;
-
- case "clear":
- _conversationText.Clear ();
- _conversationView.Text = string.Empty;
- AppendToConversation ($"{Glyphs.Diamond} Conversation cleared.\n");
-
- break;
-
- case "model":
- if (parts.Length > 1 && !string.IsNullOrWhiteSpace (parts [1]))
- {
- string newModel = parts [1].Trim ();
- AppendToConversation ($"\n{Glyphs.Diamond} Switching to {newModel}...");
- _ = ValidateAndSwitchModel (newModel);
- }
- else
- {
- AppendToConversation ($"\n{Glyphs.Diamond} Current model: {_model}\n Usage: /model \n");
- }
-
- break;
-
- case "help":
- AppendToConversation ($"\n{Glyphs.Diamond} Commands:\n"
- + " /help Show this help\n"
- + " /clear Clear conversation\n"
- + " /model Switch model\n"
- + " /compact Summarize conversation\n"
- + " /quit Exit chat\n");
-
- break;
-
- case "compact":
- AppendToConversation ($"\n{Glyphs.Diamond} Compacting conversation...\n");
-
- // TODO: Send conversation summary request to model
-
- break;
-
- default:
- AppendToConversation ($"\n{Glyphs.Diamond} Unknown command: /{parts [0]}. Type /help for commands.\n");
-
- break;
- }
- }
-
- ///
- protected override void Dispose (bool disposing)
- {
- if (disposing)
- {
- _session?.DisposeAsync ().AsTask ().GetAwaiter ().GetResult ();
- }
-
- base.Dispose (disposing);
- }
-}
\ No newline at end of file
diff --git a/Examples/AI/Program.cs b/Examples/AI/Program.cs
deleted file mode 100644
index 7b20494068..0000000000
--- a/Examples/AI/Program.cs
+++ /dev/null
@@ -1,57 +0,0 @@
-// AI — A Terminal.Gui inline-mode CLI powered by the GitHub Copilot SDK.
-//
-// Requires: GitHub Copilot CLI installed and authenticated (gh extension install github/gh-copilot)
-
-using System.CommandLine;
-using System.CommandLine.Help;
-using AI;
-using GitHub.Copilot.SDK;
-using Terminal.Gui.App;
-
-Option modelOption = new ("--model") { Description = "The Copilot model to use.", DefaultValueFactory = _ => "claude-opus-4.6" };
-modelOption.Aliases.Add ("-m");
-
-Argument promptArgument = new ("prompt") { Description = "Prompt text. If omitted, interactive chat mode starts.", DefaultValueFactory = _ => null };
-promptArgument.Arity = ArgumentArity.ZeroOrOne;
-
-RootCommand rootCommand = new ("Terminal.Gui inline-mode Copilot chat") { modelOption, promptArgument };
-
-// Capture parsed values — SetAction runs synchronously, so we store and act after.
-var parsedModel = "claude-opus-4.6";
-string? parsedPrompt = null;
-
-rootCommand.SetAction (context =>
- {
- parsedModel = context.GetRequiredValue (modelOption);
- parsedPrompt = context.GetValue (promptArgument);
- });
-
-ParseResult parseResult = rootCommand.Parse (args);
-
-if (parseResult.Errors.Count > 0 || parseResult.Action is HelpAction)
-{
- parseResult.Invoke ();
-
- return parseResult.Errors.Count > 0 ? 1 : 0;
-}
-
-parseResult.Invoke ();
-
-// ── Start Copilot SDK and run ────────────────────────────────────────────────
-
-await using CopilotClient client = new ();
-await client.StartAsync ();
-
-Application.AppModel = AppModel.Inline;
-IApplication app = Application.Create ().Init ();
-
-if (parsedPrompt is { })
-{
- return SingleTurnView.Run (app, client, parsedModel, parsedPrompt);
-}
-
-ChatView chatView = new (app, client, parsedModel);
-app.Run (chatView);
-app.Dispose ();
-
-return chatView.ExitCode;
diff --git a/Examples/AI/SingleTurnView.cs b/Examples/AI/SingleTurnView.cs
deleted file mode 100644
index 1fb7970870..0000000000
--- a/Examples/AI/SingleTurnView.cs
+++ /dev/null
@@ -1,107 +0,0 @@
-using System.Text;
-using GitHub.Copilot.SDK;
-using Terminal.Gui.App;
-using Terminal.Gui.Drawing;
-using Terminal.Gui.ViewBase;
-using Terminal.Gui.Views;
-
-namespace AI;
-
-///
-/// Handles single-turn mode: streams one answer inline, then exits.
-///
-internal sealed class SingleTurnView : Window
-{
- private readonly IApplication _app;
- private readonly CopilotClient _client;
- private readonly string _model;
- private readonly string _prompt;
- private readonly Markdown _responseView;
-
- public SingleTurnView (IApplication app, CopilotClient client, string model, string prompt)
- {
- _app = app;
- _client = client;
- _model = model;
- _prompt = prompt;
-
- Title = $"Copilot ({model})";
- Width = Dim.Fill ();
- Height = Dim.Auto ();
- Border.LineStyle = LineStyle.Rounded;
-
- _responseView = new Markdown { Width = Dim.Fill (), Height = Dim.Auto () };
-
- Add (_responseView);
- }
-
- ///
- protected override void OnIsRunningChanged (bool newIsRunning)
- {
- base.OnIsRunningChanged (newIsRunning);
-
- if (newIsRunning)
- {
- _ = streamResponse ();
- }
- }
-
- private async Task streamResponse ()
- {
- try
- {
- await using CopilotSession session = await _client.CreateSessionAsync (new SessionConfig
- {
- Model = _model,
- Streaming = true,
- OnPermissionRequest = PermissionHandler.ApproveAll
- });
-
- StringBuilder responseText = new ();
- TaskCompletionSource done = new ();
-
- session.On (evt =>
- {
- switch (evt)
- {
- case AssistantMessageDeltaEvent delta:
- responseText.Append (delta.Data.DeltaContent);
-
- _app.Invoke (() => { _responseView.Text = responseText.ToString (); });
-
- break;
-
- case SessionIdleEvent:
- done.TrySetResult ();
-
- break;
-
- case SessionErrorEvent err:
- _app.Invoke (() => { _responseView.Text = $"Error: {err.Data.Message}"; });
- done.TrySetResult ();
-
- break;
- }
- });
-
- await session.SendAsync (new MessageOptions { Prompt = _prompt });
- await done.Task;
- }
- catch (Exception ex)
- {
- _app.Invoke (() => _responseView.Text = $"Error: {ex.Message}");
- }
-
- // Give the UI a moment to render the final update, then exit
- _app.Invoke (RequestStop);
- }
-
- public static int Run (IApplication app, CopilotClient client, string model, string prompt)
- {
- SingleTurnView view = new (app, client, model, prompt);
- app.Run (view);
- app.Dispose ();
-
- return 0;
- }
-}
\ No newline at end of file
diff --git a/Examples/AI/SlashCommandSuggestionGenerator.cs b/Examples/AI/SlashCommandSuggestionGenerator.cs
deleted file mode 100644
index 729c0a9318..0000000000
--- a/Examples/AI/SlashCommandSuggestionGenerator.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using System.Text;
-using Terminal.Gui.Views;
-
-namespace AI;
-
-///
-/// Suggestion generator that provides slash command completions when the input starts with '/'.
-///
-internal sealed class SlashCommandSuggestionGenerator : ISuggestionGenerator
-{
- private static readonly List _commands = ["/help", "/clear", "/model", "/compact", "/quit", "/exit"];
-
- ///
- public IEnumerable GenerateSuggestions (AutocompleteContext context)
- {
- List line = context.CurrentLine.Select (c => c.Grapheme).ToList ();
- string lineText = string.Join ("", line).TrimEnd ();
-
- if (lineText.Length == 0 || lineText [0] != '/')
- {
- return [];
- }
-
- int typedLength = Math.Min (context.CursorPosition, lineText.Length);
- string typed = lineText [..typedLength];
-
- context.CursorPosition = 0;
-
- return _commands.Where (c => c.StartsWith (typed, StringComparison.OrdinalIgnoreCase) && !c.Equals (typed, StringComparison.OrdinalIgnoreCase))
- .Select (c => new Suggestion (typed.Length, c))
- .ToList ();
- }
-
- ///
- public bool IsWordChar (string text)
- {
- if (string.IsNullOrEmpty (text))
- {
- return false;
- }
-
- Rune r = text.EnumerateRunes ().First ();
-
- return Rune.IsLetterOrDigit (r) || r == new Rune ('/');
- }
-}
\ No newline at end of file
diff --git a/Examples/UICatalog/Scenarios/Editor.cs b/Examples/UICatalog/Scenarios/Editor.cs
index 4408a09725..e9b4a3fd64 100644
--- a/Examples/UICatalog/Scenarios/Editor.cs
+++ b/Examples/UICatalog/Scenarios/Editor.cs
@@ -365,7 +365,7 @@ private bool SaveAs ()
_app?.Run (sd);
bool canceled = sd.Canceled;
string path = sd.Path;
- string fileName = sd.FileName;
+ string fileName = sd.FileName ?? string.Empty;
sd.Dispose ();
if (canceled)
diff --git a/Examples/UICatalog/Scenarios/Notepad.cs b/Examples/UICatalog/Scenarios/Notepad.cs
index b1a579a72a..988e4764d5 100644
--- a/Examples/UICatalog/Scenarios/Notepad.cs
+++ b/Examples/UICatalog/Scenarios/Notepad.cs
@@ -1,4 +1,5 @@
// ReSharper disable AccessToDisposedClosure
+
#nullable enable
namespace UICatalog.Scenarios;
@@ -146,7 +147,7 @@ public bool SaveAs ()
_lastDirectory = Path.GetDirectoryName (Path.GetFullPath (fd.Path));
tab.File = new FileInfo (fd.Path);
- tab.Title = fd.FileName;
+ tab.Title = fd.FileName ?? throw new InvalidOperationException ();
tab.Save ();
fd.Dispose ();
@@ -205,7 +206,20 @@ private void Close (Tabs tabs, OpenedFile tabToClose)
private void Open ()
{
- OpenDialog open = new () { Title = "Open", AllowsMultipleSelection = true };
+ OpenDialog open = new ()
+ {
+ Title = "Open",
+ AllowsMultipleSelection = true,
+ AllowedTypes =
+ [
+ new AllowedType ("Markdown", ".md", ".markdown"),
+ new AllowedType ("Text", ".txt", ".csv", ".tsv"),
+ new AllowedType ("Code", ".c", ".h", ".js", ".cs", ".json", ".yml"),
+ new AllowedTypeAny ()
+ ],
+ MustExist = true,
+ OpenMode = OpenMode.File
+ };
if (_lastDirectory is { })
{
diff --git a/README.md b/README.md
index a6b81fa6e4..cddc3f9751 100644
--- a/README.md
+++ b/README.md
@@ -8,16 +8,27 @@
Cross-platform UI toolkit for building sophisticated terminal UI (TUI) applications on Windows, macOS, and Linux/Unix.
-
+
-
-* **v2** (Current):  - Stable release
-* **v1 (Legacy)**:  - Maintenance mode only
+# Version 2.0 Has Been Released
-> **Note:** v1 is in maintenance mode — only critical bug fixes accepted. v2 is recommended for all projects.
+Terminal.Gui enables building sophisticated console applications with modern UIs:
+
+- **Responsive TUI** - Easy to use, innovative, layout system enables console apps as responsive as any responsive web page.
+- **Performant and Scalable** - Built for modern TUIs - fast, double-buffering-based rendering; Tables and Tree Views scale to infinite elements with sorting and filtering.
+- **Keyboard First; Mouse First Too** - Optimized for TUI experiences where the user's hands never need to leave the keyboard; full mouse support too.
+- **Rich Built-in Widgets (Views)** - Text editors, buttons, checkboxes, trees, tables, markdown, linear ranges, menus, selectors, and more.
+- **Visualizations** - Charts, graphs, progress indicators, and color pickers with TrueColor support.
+- **Text Editors** - Full-featured text editing with clipboard, undo/redo, and Unicode support
+- **Fully Configurable** - Themes, colors, key bindings, and settings are all customizable and persistable.
+- **File Management** - File and directory browsers with search and filtering, supporting Nerdfonts and coloring.
+- **Wizards and Multi-Step Processes** - Guided workflows with navigation and validation.
+- **Cross-Platform** - Consistent experience on Windows, macOS, and Linux.
+- **Apps Work In-line or Full Screen** - Build CLI tools like Claude Code/Copilot/Codex CLI that scroll with the terminal (in-line) or full screen.
+
+See the [Views Overview](https://gui-cs.github.io/Terminal.Gui/docs/views) for available controls and [What's New in v2](https://gui-cs.github.io/Terminal.Gui/docs/newinv2) for architectural improvements.
-
# Quick Start
@@ -60,23 +71,6 @@ app.Run (window);
See the [Examples](Examples/) directory for more.
-# Build Powerful Terminal Applications
-
-Terminal.Gui enables building sophisticated console applications with modern UIs:
-
-- **Rich Forms and Dialogs** - Text fields, buttons, checkboxes, radio buttons, and data validation
-- **Interactive Data Views** - Tables, lists, and trees with sorting, filtering, and in-place editing
-- **Visualizations** - Charts, graphs, progress indicators, and color pickers with TrueColor support
-- **Text Editors** - Full-featured text editing with clipboard, undo/redo, and Unicode support
-- **File Management** - File and directory browsers with search and filtering
-- **Wizards and Multi-Step Processes** - Guided workflows with navigation and validation
-- **System Monitoring Tools** - Real-time dashboards with scrollable, resizable views
-- **Configuration UIs** - Settings editors with persistent themes and user preferences
-- **Cross-Platform CLI Tools** - Consistent experience on Windows, macOS, and Linux
-- **Server Management Interfaces** - SSH-compatible UIs for remote administration
-
-See the [Views Overview](https://gui-cs.github.io/Terminal.Gui/docs/views) for available controls and [What's New in v2](https://gui-cs.github.io/Terminal.Gui/docs/newinv2) for architectural improvements.
-
# Documentation
Comprehensive documentation is at [gui-cs.github.io/Terminal.Gui](https://gui-cs.github.io/Terminal.Gui).
@@ -100,8 +94,6 @@ See the [documentation index](https://gui-cs.github.io/Terminal.Gui/docs/index)
# Installing
-## v2 (Recommended)
-
```powershell
dotnet add package Terminal.Gui
```
@@ -112,12 +104,6 @@ Or use the [Terminal.Gui.Templates](https://github.com/gui-cs/Terminal.Gui.templ
dotnet new install Terminal.Gui.Templates
```
-## v1 Legacy
-
-```powershell
-dotnet add package Terminal.Gui --version "1.*"
-```
-
# Contributing
Contributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md).
diff --git a/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardPattern.cs b/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardPattern.cs
index b598cd8c74..393df328b0 100644
--- a/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardPattern.cs
+++ b/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardPattern.cs
@@ -91,9 +91,16 @@ public class KittyKeyboardPattern : AnsiKeyboardParserPattern
return null;
}
+ string baseKeyCode = "";
+
+ if (key.KeyCode < KeyCode.CharMask && (key.KeyCode & (KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.CtrlMask)) == 0)
+ {
+ baseKeyCode = new Rune ((uint)key.KeyCode).ToString ();
+ }
+
// Extract alternate key codes (kitty flag 4: report alternate keys)
- var shiftedKeyCode = KeyCode.Null;
- var baseLayoutKeyCode = KeyCode.Null;
+ KeyCode shiftedKeyCode = KeyCode.Null;
+ KeyCode baseLayoutKeyCode = KeyCode.Null;
if (match.Groups [2].Success && int.TryParse (match.Groups [2].Value, CultureInfo.InvariantCulture, out int shiftedCode) && shiftedCode > 0)
{
@@ -105,7 +112,7 @@ public class KittyKeyboardPattern : AnsiKeyboardParserPattern
baseLayoutKeyCode = (KeyCode)baseCode;
}
- var associatedText = string.Empty;
+ string associatedText = string.Empty;
if (match.Groups [5].Success)
{
@@ -117,8 +124,8 @@ public class KittyKeyboardPattern : AnsiKeyboardParserPattern
key = new Key (key) { ShiftedKeyCode = shiftedKeyCode, BaseLayoutKeyCode = baseLayoutKeyCode, AssociatedText = associatedText };
}
- string modifierField = match.Groups [4].Value;
- modifierField = ApplyImplicitModifierState (key, modifierField);
+ string originalModifierField = match.Groups [4].Value;
+ string modifierField = ApplyImplicitModifierState (key, originalModifierField);
if (!string.IsNullOrEmpty (modifierField))
{
@@ -127,10 +134,10 @@ public class KittyKeyboardPattern : AnsiKeyboardParserPattern
if (!string.IsNullOrEmpty (modifierField))
{
- key = ApplyModifiersAndEventType (modifierField, key);
+ key = ApplyModifiersAndEventType (MaxModifierFieldValue (originalModifierField, modifierField), key);
}
- if ((key.IsAlt || key.IsCtrl) && !string.IsNullOrEmpty (key.AssociatedText))
+ if ((key.IsAlt || key.IsCtrl) && (key.ShiftedKeyCode != KeyCode.Null || baseKeyCode.Equals (key.AssociatedText, StringComparison.OrdinalIgnoreCase)) && !string.IsNullOrEmpty (key.AssociatedText))
{
key = new Key (key) { AssociatedText = string.Empty };
}
@@ -222,6 +229,38 @@ private static string ApplyImplicitModifierState (Key key, string modifierField)
return string.Join (':', parts);
}
+ private static string MaxModifierFieldValue (string modifierField1, string modifierField2)
+ {
+ if (string.IsNullOrEmpty (modifierField1))
+ {
+ return modifierField2;
+ }
+
+ if (string.IsNullOrEmpty (modifierField2))
+ {
+ return modifierField1;
+ }
+
+ string [] parts1 = modifierField1.Split (':');
+ string [] parts2 = modifierField2.Split (':');
+
+ if (parts1.Length == 0 || parts2.Length == 0)
+ {
+ return modifierField1;
+ }
+
+ if (!int.TryParse (parts1 [0], CultureInfo.InvariantCulture, out int encodedModifiers1)
+ || !int.TryParse (parts2 [0], CultureInfo.InvariantCulture, out int encodedModifiers2))
+ {
+ return modifierField1;
+ }
+
+ int maxEncodedModifiers = Math.Max (encodedModifiers1, encodedModifiers2);
+ parts1 [0] = maxEncodedModifiers.ToString (CultureInfo.InvariantCulture);
+
+ return string.Join (':', parts1);
+ }
+
private static (Key Key, string ModifierField) NormalizeShiftedPrintableKey (Key key, string modifierField)
{
string [] parts = modifierField.Split (':');
diff --git a/Terminal.Gui/FileServices/FileSystemTreeBuilder.cs b/Terminal.Gui/FileServices/FileSystemTreeBuilder.cs
index 46ce46f338..e6237a1569 100644
--- a/Terminal.Gui/FileServices/FileSystemTreeBuilder.cs
+++ b/Terminal.Gui/FileServices/FileSystemTreeBuilder.cs
@@ -1,5 +1,4 @@
-#nullable disable
-using System.IO.Abstractions;
+using System.IO.Abstractions;
namespace Terminal.Gui.FileServices;
@@ -16,7 +15,7 @@ public class FileSystemTreeBuilder : ITreeBuilder, IComparer Sorter { get; set; }
///
- public int Compare (IFileSystemInfo x, IFileSystemInfo y)
+ public int Compare (IFileSystemInfo? x, IFileSystemInfo? y)
{
if (x is IDirectoryInfo && y is not IDirectoryInfo)
{
@@ -28,12 +27,7 @@ public int Compare (IFileSystemInfo x, IFileSystemInfo y)
return 1;
}
- if (x is { } && y is { })
- {
- return string.Compare (x.Name, y.Name, StringComparison.Ordinal);
- }
-
- return 0;
+ return string.Compare (x?.Name, y?.Name, StringComparison.Ordinal);
}
///
@@ -47,12 +41,7 @@ public bool CanExpand (IFileSystemInfo toExpand)
return false;
}
- if (IsReparsePoint (toExpand))
- {
- return false;
- }
-
- return TryGetChildren (toExpand).Any ();
+ return !IsReparsePoint (toExpand) && TryGetChildren (toExpand).Any ();
}
///
@@ -62,16 +51,16 @@ private IEnumerable TryGetChildren (IFileSystemInfo entry)
{
if (entry is IFileInfo)
{
- return Enumerable.Empty ();
+ return [];
}
// Prevent traversal cycles through symlinks/junctions/mount points.
if (IsReparsePoint (entry))
{
- return Enumerable.Empty ();
+ return [];
}
- var dir = (IDirectoryInfo)entry;
+ IDirectoryInfo dir = (IDirectoryInfo)entry;
try
{
diff --git a/Terminal.Gui/FileServices/IFileOperations.cs b/Terminal.Gui/FileServices/IFileOperations.cs
index 920f3e947f..06fd639fd0 100644
--- a/Terminal.Gui/FileServices/IFileOperations.cs
+++ b/Terminal.Gui/FileServices/IFileOperations.cs
@@ -25,7 +25,7 @@ public interface IFileOperations
///
/// Ensure you use a try/catch block with appropriate error handling (e.g. showing a
///
- IFileSystemInfo New (IApplication? app, IFileSystem fileSystem, IDirectoryInfo inDirectory);
+ IFileSystemInfo? New (IApplication? app, IFileSystem fileSystem, IDirectoryInfo inDirectory);
/// Specifies how to handle file/directory rename attempts in .
///
@@ -35,5 +35,5 @@ public interface IFileOperations
///
/// Ensure you use a try/catch block with appropriate error handling (e.g. showing a
///
- IFileSystemInfo Rename (IApplication? app, IFileSystem fileSystem, IFileSystemInfo toRename);
+ IFileSystemInfo? Rename (IApplication? app, IFileSystem fileSystem, IFileSystemInfo toRename);
}
diff --git a/Terminal.Gui/Input/Keyboard/Key.cs b/Terminal.Gui/Input/Keyboard/Key.cs
index b0b64287f9..1a5dee5d27 100644
--- a/Terminal.Gui/Input/Keyboard/Key.cs
+++ b/Terminal.Gui/Input/Keyboard/Key.cs
@@ -217,7 +217,7 @@ public string AsGrapheme
if (IsShift && ShiftedKeyCode != KeyCode.Null)
{
- Rune shiftedRune = ToRune (ShiftedKeyCode);
+ Rune shiftedRune = ToRune (ShiftedKeyCode | KeyCode.ShiftMask);
if (shiftedRune != default (Rune))
{
@@ -278,7 +278,7 @@ public Rune AsRune
if (IsShift && ShiftedKeyCode != KeyCode.Null)
{
- Rune shiftedRune = ToRune (ShiftedKeyCode);
+ Rune shiftedRune = ToRune (ShiftedKeyCode | KeyCode.ShiftMask);
if (shiftedRune != default (Rune))
{
diff --git a/Terminal.Gui/ViewBase/Helpers/StackExtensions.cs b/Terminal.Gui/ViewBase/Helpers/StackExtensions.cs
index e76788029a..3df4d43d0d 100644
--- a/Terminal.Gui/ViewBase/Helpers/StackExtensions.cs
+++ b/Terminal.Gui/ViewBase/Helpers/StackExtensions.cs
@@ -1,244 +1,160 @@
-#nullable disable
-namespace Terminal.Gui.ViewBase;
+namespace Terminal.Gui.ViewBase;
/// Extension of helper to work with specific
public static class StackExtensions
{
- /// Check if the stack object contains the value to find.
- /// The stack object type.
/// The stack object.
- /// Value to find.
- /// The comparison object.
- /// true If the value was found.false otherwise.
- public static bool Contains (this Stack stack, T valueToFind, IEqualityComparer comparer = null)
- {
- comparer = comparer ?? EqualityComparer.Default;
-
- foreach (T obj in stack)
- {
- if (comparer.Equals (obj, valueToFind))
- {
- return true;
- }
- }
-
- return false;
- }
-
- /// Find all duplicates stack objects values.
/// The stack object type.
- /// The stack object.
- /// The comparison object.
- /// The duplicates stack object.
- public static Stack FindDuplicates (this Stack stack, IEqualityComparer comparer = null)
+ extension (Stack stack)
{
- comparer = comparer ?? EqualityComparer.Default;
-
- Stack dup = new ();
- T [] stackArr = stack.ToArray ();
-
- for (var i = 0; i < stackArr.Length; i++)
+ /// Check if the stack object contains the value to find.
+ /// Value to find.
+ /// The comparison object.
+ /// true If the value was found.false otherwise.
+ public bool Contains (T valueToFind, IEqualityComparer? comparer = null)
{
- T value = stackArr [i];
+ comparer ??= EqualityComparer.Default;
- for (int j = i + 1; j < stackArr.Length; j++)
+ foreach (T obj in stack)
{
- T valueToFind = stackArr [j];
-
- if (comparer.Equals (value, valueToFind) && !Contains (dup, valueToFind))
+ if (comparer.Equals (obj, valueToFind))
{
- dup.Push (value);
+ return true;
}
}
- }
- return dup;
- }
-
- /// Move the first stack object value to the end.
- /// The stack object type.
- /// The stack object.
- public static void MoveNext (this Stack stack)
- {
- Stack temp = new ();
- T last = stack.Pop ();
-
- while (stack.Count > 0)
- {
- T value = stack.Pop ();
- temp.Push (value);
+ return false;
}
- temp.Push (last);
-
- while (temp.Count > 0)
+ /// Move the first stack object value to the end.
+ public void MoveNext ()
{
- stack.Push (temp.Pop ());
- }
- }
+ Stack temp = new ();
+ T last = stack.Pop ();
- /// Move the last stack object value to the top.
- /// The stack object type.
- /// The stack object.
- public static void MovePrevious (this Stack stack)
- {
- Stack temp = new ();
- T first = default;
+ while (stack.Count > 0)
+ {
+ T value = stack.Pop ();
+ temp.Push (value);
+ }
- while (stack.Count > 0)
- {
- T value = stack.Pop ();
- temp.Push (value);
+ temp.Push (last);
- if (stack.Count == 1)
+ while (temp.Count > 0)
{
- first = stack.Pop ();
+ stack.Push (temp.Pop ());
}
}
- while (temp.Count > 0)
- {
- stack.Push (temp.Pop ());
- }
-
- stack.Push (first);
- }
-
- /// Move the stack object value to the index.
- /// The stack object type.
- /// The stack object.
- /// Value to move.
- /// The index where to move.
- /// The comparison object.
- public static void MoveTo (
- this Stack stack,
- T valueToMove,
- int index = 0,
- IEqualityComparer comparer = null
- )
- {
- if (index < 0)
+ /// Move the last stack object value to the top.
+ public void MovePrevious ()
{
- return;
- }
-
- comparer = comparer ?? EqualityComparer.Default;
+ Stack temp = new ();
+ T? first = default;
- Stack temp = new ();
- var toMove = default (T);
- int stackCount = stack.Count;
- var count = 0;
+ while (stack.Count > 0)
+ {
+ T value = stack.Pop ();
+ temp.Push (value);
- while (stack.Count > 0)
- {
- T value = stack.Pop ();
+ if (stack.Count == 1)
+ {
+ first = stack.Pop ();
+ }
+ }
- if (comparer.Equals (value, valueToMove))
+ while (temp.Count > 0)
{
- toMove = value;
-
- break;
+ stack.Push (temp.Pop ());
}
- temp.Push (value);
- count++;
+ if (first is { })
+ {
+ stack.Push (first);
+ }
}
- var idx = 0;
-
- while (stack.Count < stackCount)
+ /// Move the stack object value to the index.
+ /// Value to move.
+ /// The index where to move.
+ /// The comparison object.
+ public void MoveTo (T valueToMove, int index = 0, IEqualityComparer? comparer = null)
{
- if (count - idx == index)
+ if (index < 0)
{
- stack.Push (toMove);
+ return;
}
- else
+
+ comparer ??= EqualityComparer.Default;
+
+ Stack temp = new ();
+ var toMove = default (T);
+ int stackCount = stack.Count;
+ var count = 0;
+
+ while (stack.Count > 0)
{
- stack.Push (temp.Pop ());
- }
+ T value = stack.Pop ();
- idx++;
- }
- }
+ if (comparer.Equals (value, valueToMove))
+ {
+ toMove = value;
- /// Replaces a stack object values that match with the value to replace.
- /// The stack object type.
- /// The stack object.
- /// Value to replace.
- /// Value to replace with to what matches the value to replace.
- /// The comparison object.
- public static void Replace (
- this Stack stack,
- T valueToReplace,
- T valueToReplaceWith,
- IEqualityComparer comparer = null
- )
- {
- comparer = comparer ?? EqualityComparer.Default;
+ break;
+ }
- Stack temp = new ();
+ temp.Push (value);
+ count++;
+ }
- while (stack.Count > 0)
- {
- T value = stack.Pop ();
+ var idx = 0;
- if (comparer.Equals (value, valueToReplace))
+ while (stack.Count < stackCount)
{
- stack.Push (valueToReplaceWith);
+ if (count - idx == index)
+ {
+ if (toMove is { })
+ {
+ stack.Push (toMove);
+ }
+ }
+ else
+ {
+ stack.Push (temp.Pop ());
+ }
- break;
+ idx++;
}
-
- temp.Push (value);
}
- while (temp.Count > 0)
+ /// Replaces a stack object values that match with the value to replace.
+ /// Value to replace.
+ /// Value to replace with to what matches the value to replace.
+ /// The comparison object.
+ public void Replace (T valueToReplace, T valueToReplaceWith, IEqualityComparer? comparer = null)
{
- stack.Push (temp.Pop ());
- }
- }
+ comparer ??= EqualityComparer.Default;
- /// Swap two stack objects values that matches with the both values.
- /// The stack object type.
- /// The stack object.
- /// Value to swap from.
- /// Value to swap to.
- /// The comparison object.
- public static void Swap (
- this Stack stack,
- T valueToSwapFrom,
- T valueToSwapTo,
- IEqualityComparer comparer = null
- )
- {
- comparer = comparer ?? EqualityComparer.Default;
+ Stack temp = new ();
- int index = stack.Count - 1;
- T [] stackArr = new T [stack.Count];
+ while (stack.Count > 0)
+ {
+ T value = stack.Pop ();
- while (stack.Count > 0)
- {
- T value = stack.Pop ();
+ if (comparer.Equals (value, valueToReplace))
+ {
+ stack.Push (valueToReplaceWith);
- if (comparer.Equals (value, valueToSwapFrom))
- {
- stackArr [index] = valueToSwapTo;
- }
- else if (comparer.Equals (value, valueToSwapTo))
- {
- stackArr [index] = valueToSwapFrom;
+ break;
+ }
+
+ temp.Push (value);
}
- else
+
+ while (temp.Count > 0)
{
- stackArr [index] = value;
+ stack.Push (temp.Pop ());
}
-
- index--;
- }
-
- for (var i = 0; i < stackArr.Length; i++)
- {
- stack.Push (stackArr [i]);
}
}
}
diff --git a/Terminal.Gui/ViewBase/Layout/Aligner.cs b/Terminal.Gui/ViewBase/Layout/Aligner.cs
index 637765ba11..082581aba4 100644
--- a/Terminal.Gui/ViewBase/Layout/Aligner.cs
+++ b/Terminal.Gui/ViewBase/Layout/Aligner.cs
@@ -1,4 +1,3 @@
-#nullable disable
using System.ComponentModel;
namespace Terminal.Gui.ViewBase;
@@ -9,8 +8,6 @@ namespace Terminal.Gui.ViewBase;
///
public class Aligner : INotifyPropertyChanged
{
- private Alignment _alignment;
-
///
/// Gets or sets how the aligns items within a container.
///
@@ -21,11 +18,11 @@ public class Aligner : INotifyPropertyChanged
///
public Alignment Alignment
{
- get => _alignment;
+ get;
set
{
- _alignment = value;
- PropertyChanged?.Invoke (this, new (nameof (Alignment)));
+ field = value;
+ PropertyChanged?.Invoke (this, new PropertyChangedEventArgs (nameof (Alignment)));
}
}
@@ -38,7 +35,7 @@ public AlignmentModes AlignmentModes
set
{
field = value;
- PropertyChanged?.Invoke (this, new (nameof (AlignmentModes)));
+ PropertyChanged?.Invoke (this, new PropertyChangedEventArgs (nameof (AlignmentModes)));
}
} = AlignmentModes.StartToEnd;
@@ -51,12 +48,12 @@ public int ContainerSize
set
{
field = value;
- PropertyChanged?.Invoke (this, new (nameof (ContainerSize)));
+ PropertyChanged?.Invoke (this, new PropertyChangedEventArgs (nameof (ContainerSize)));
}
}
///
- public event PropertyChangedEventHandler PropertyChanged;
+ public event PropertyChangedEventHandler? PropertyChanged;
///
/// Takes a list of item sizes and returns a list of the positions of those items when aligned within
diff --git a/Terminal.Gui/Views/Autocomplete/AutocompleteFilepathContext.cs b/Terminal.Gui/Views/Autocomplete/AutocompleteFilepathContext.cs
index 12384e45b1..c104c8a86f 100644
--- a/Terminal.Gui/Views/Autocomplete/AutocompleteFilepathContext.cs
+++ b/Terminal.Gui/Views/Autocomplete/AutocompleteFilepathContext.cs
@@ -1,4 +1,3 @@
-#nullable disable
using System.IO.Abstractions;
using System.Runtime.InteropServices;
@@ -12,7 +11,7 @@ internal class AutocompleteFilepathContext (string currentLine, int cursorPositi
internal class FilepathSuggestionGenerator : ISuggestionGenerator
{
- private FileDialogState _state;
+ private FileDialogState? _state;
public IEnumerable GenerateSuggestions (AutocompleteContext context)
{
@@ -21,74 +20,51 @@ public IEnumerable GenerateSuggestions (AutocompleteContext context)
_state = fileState.State;
}
- if (_state is null)
- {
- return Enumerable.Empty ();
- }
-
var path = Cell.ToString (context.CurrentLine);
int last = path.LastIndexOfAny (FileDialog.Separators);
- if (string.IsNullOrWhiteSpace (path) || !Path.IsPathRooted (path))
+ if (string.IsNullOrWhiteSpace (path) || !Path.IsPathRooted (path) || _state is null)
{
- return Enumerable.Empty ();
+ return [];
}
- string term = path.Substring (last + 1);
+ string term = path [(last + 1)..];
// If path is /tmp/ then don't just list everything in it
if (string.IsNullOrWhiteSpace (term))
{
- return Enumerable.Empty ();
+ return [];
}
- if (term.Equals (_state?.Directory?.Name))
+ if (term.Equals (_state?.Directory.Name))
{
// Clear suggestions
- return Enumerable.Empty ();
+ return [];
}
bool isWindows = RuntimeInformation.IsOSPlatform (OSPlatform.Windows);
- string [] suggestions = _state!.Children.Where (d => !d.IsParent)
- .Select (
- e => e.FileSystemInfo is IDirectoryInfo d
- ? d.Name + Path.DirectorySeparatorChar
- : e.FileSystemInfo.Name
- )
- .ToArray ();
+ string? [] suggestions = _state?.Children.Where (d => !d.IsParent)
+ .Select (e => e.FileSystemInfo is IDirectoryInfo d ? d.Name + Path.DirectorySeparatorChar : e.FileSystemInfo?.Name)
+ .ToArray ()
+ ?? [];
string [] validSuggestions = suggestions
- .Where (
- s => s.StartsWith (
- term,
- isWindows
- ? StringComparison.InvariantCultureIgnoreCase
- : StringComparison.InvariantCulture
- )
- )
- .OrderBy (m => m.Length)
- .ToArray ();
+ .Where (s => s?.StartsWith (term,
+ isWindows ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture)
+ == true)
+ .OfType ()
+ .OrderBy (m => m.Length)
+ .ToArray ();
// nothing to suggest
if (validSuggestions.Length == 0 || validSuggestions [0].Length == term.Length)
{
- return Enumerable.Empty ();
+ return [];
}
- return validSuggestions.Select (
- f => new Suggestion (term.Length, f, f)
- )
- .ToList ();
+ return validSuggestions.Select (f => new Suggestion (term.Length, f, f)).ToList ();
}
- public bool IsWordChar (string text)
- {
- if (text == "\n")
- {
- return false;
- }
-
- return true;
- }
+ public bool IsWordChar (string text) => text != "\n";
}
diff --git a/Terminal.Gui/Views/Dialog.cs b/Terminal.Gui/Views/Dialog.cs
index 3235cc1886..0e310292e5 100644
--- a/Terminal.Gui/Views/Dialog.cs
+++ b/Terminal.Gui/Views/Dialog.cs
@@ -28,6 +28,16 @@ namespace Terminal.Gui.Views;
/// becomes the default (). Button alignment is controlled by
/// and .
///
+///
+/// The dialog is positioned at with sizing,
+/// limited to 100% of (or screen dimensions).
+///
+///
+/// NOTE - Setting to
+/// or
+///
+/// is not supported and may cause layout issues.
+///
///
///
///
diff --git a/Terminal.Gui/Views/DialogTResult.cs b/Terminal.Gui/Views/DialogTResult.cs
index 81d4a37fd6..8b9c306bff 100644
--- a/Terminal.Gui/Views/DialogTResult.cs
+++ b/Terminal.Gui/Views/DialogTResult.cs
@@ -30,6 +30,16 @@ namespace Terminal.Gui.Views;
/// Subclasses should set before calling
/// to return a value. If Result is not set (remains null), the dialog is considered canceled.
///
+///
+/// The dialog is positioned at with sizing,
+/// limited to 100% of (or screen dimensions).
+///
+///
+/// NOTE - Setting to
+/// or
+///
+/// is not supported and may cause layout issues.
+///
///
///
///
@@ -69,10 +79,6 @@ public class Dialog : Runnable, IDesignable
///
/// Initializes a new instance of the class with no buttons.
///
- ///
- /// The dialog is positioned at with sizing,
- /// limited to 100% of (or screen dimensions).
- ///
public Dialog ()
{
X = Pos.Center ();
@@ -114,27 +120,11 @@ public Dialog ()
SetStyle ();
}
- private Size _minimumSubViewsSize;
-
///
public override void EndInit ()
{
base.EndInit ();
UpdateSizes ();
-
-#if DIALOG_SCROLLBARS
- // Don't enable scrollbars until after initialized; otherwise they get created before
- // our frame has dimensions.
- ViewportSettings |= ViewportSettingsFlags.HasScrollBars;
-#endif
- }
-
- ///
- protected override void OnSubViewAdded (View view)
- {
- _minimumSubViewsSize = new Size (GetWidthRequiredForSubViews (), GetHeightRequiredForSubViews ());
- UpdateSizes ();
- base.OnSubViewAdded (view);
}
///
@@ -196,13 +186,8 @@ private void UpdateSizes ()
}
// Always floor at Viewport size — the content area should never be smaller
- // than what's visible. For DimAuto dialogs, the Frame may be larger than
- // _minimumSubViewsSize (e.g. due to title width), so the content area
- // should reflect the actual available space.
- int subViewsWidth = Math.Max (_minimumSubViewsSize.Width, Viewport.Width);
- int subViewsHeight = Math.Max (_minimumSubViewsSize.Height, Viewport.Height);
-
- SetContentSize (new Size (Math.Max (_minimumButtonsSize.Width, subViewsWidth), Math.Max (_minimumButtonsSize.Height, subViewsHeight)));
+ // than what's visible.
+ SetContentSize (new Size (Math.Max (_minimumButtonsSize.Width, Viewport.Width), Math.Max (_minimumButtonsSize.Height, Viewport.Height)));
}
///
@@ -212,10 +197,10 @@ private void UpdateSizes ()
///
private int GetMinimumDialogWidth ()
{
- int minSize = Math.Max (Math.Max (_minimumSubViewsSize.Width,
+ int minSize = Math.Max (
- // Ensure space for title + borders
- Title.GetColumns () + 4),
+ // Ensure space for title + borders
+ Title.GetColumns () + 4,
_minimumButtonsSize.Width);
return minSize;
@@ -228,7 +213,7 @@ private int GetMinimumDialogWidth ()
///
private int GetMinimumDialogHeight ()
{
- int minSize = Math.Max (_minimumSubViewsSize.Height, _minimumButtonsSize.Height - Border.Thickness.Vertical - Margin.Thickness.Vertical);
+ int minSize = _minimumButtonsSize.Height - Border.Thickness.Vertical - Margin.Thickness.Vertical;
return minSize;
}
diff --git a/Terminal.Gui/Views/FileDialogs/AllowedType.cs b/Terminal.Gui/Views/FileDialogs/AllowedType.cs
index 2bcdfbe4a0..05116a32d4 100644
--- a/Terminal.Gui/Views/FileDialogs/AllowedType.cs
+++ b/Terminal.Gui/Views/FileDialogs/AllowedType.cs
@@ -1,30 +1,5 @@
-#nullable disable
-
namespace Terminal.Gui.Views;
-/// Interface for restrictions on which file type(s) the user is allowed to select/enter.
-public interface IAllowedType
-{
- ///
- /// Returns true if the file at is compatible with this allow option. Note that the file
- /// may not exist (e.g. in the case of saving).
- ///
- ///
- ///
- bool IsAllowed (string path);
-}
-
-/// that allows selection of any types (*.*).
-public class AllowedTypeAny : IAllowedType
-{
- ///
- public bool IsAllowed (string path) { return true; }
-
- /// Returns a string representation of this .
- ///
- public override string ToString () { return Strings.fdAnyFiles + "(*.*)"; }
-}
-
///
/// Describes a requirement on what can be selected. This can be combined with other
/// in a to for example show only .csv files but let user change to
@@ -34,7 +9,7 @@ public class AllowedType : IAllowedType
{
/// Initializes a new instance of the class.
/// The human-readable text to display.
- /// Extension(s) to match e.g. .csv.
+ /// Extension(s) to match e.g. ".csv".
public AllowedType (string description, params string [] extensions)
{
if (extensions.Length == 0)
@@ -79,13 +54,13 @@ public bool IsAllowed (string path)
/// Returns plus all separated by semicolons.
public override string ToString ()
{
- const int maxLength = 30;
+ const int MAX_LENGTH = 30;
var desc = $"{Description} ({string.Join (";", Extensions.Select (e => '*' + e).ToArray ())})";
- if (desc.Length > maxLength)
+ if (desc.Length > MAX_LENGTH)
{
- return desc.Substring (0, maxLength - 2) + "…";
+ return desc [..(MAX_LENGTH - 2)] + "…";
}
return desc;
diff --git a/Terminal.Gui/Views/FileDialogs/AllowedTypeAny.cs b/Terminal.Gui/Views/FileDialogs/AllowedTypeAny.cs
new file mode 100644
index 0000000000..91ea037771
--- /dev/null
+++ b/Terminal.Gui/Views/FileDialogs/AllowedTypeAny.cs
@@ -0,0 +1,12 @@
+namespace Terminal.Gui.Views;
+
+/// that allows selection of any types (*.*).
+public class AllowedTypeAny : IAllowedType
+{
+ ///
+ public bool IsAllowed (string path) => true;
+
+ /// Returns a string representation of this .
+ ///
+ public override string ToString () => Strings.fdAnyFiles + " (*.*)";
+}
diff --git a/Terminal.Gui/Views/FileDialogs/DefaultFileOperations.cs b/Terminal.Gui/Views/FileDialogs/DefaultFileOperations.cs
index 1c74914035..09570a9716 100644
--- a/Terminal.Gui/Views/FileDialogs/DefaultFileOperations.cs
+++ b/Terminal.Gui/Views/FileDialogs/DefaultFileOperations.cs
@@ -1,4 +1,3 @@
-#nullable disable
using System.IO.Abstractions;
namespace Terminal.Gui.Views;
@@ -7,18 +6,20 @@ namespace Terminal.Gui.Views;
public class DefaultFileOperations : IFileOperations
{
///
- public bool Delete (IApplication app, IEnumerable toDelete)
+ public bool Delete (IApplication? app, IEnumerable toDelete)
{
// Default implementation does not allow deleting multiple files
- if (toDelete.Count () != 1)
+ IEnumerable fileSystemInfos = toDelete as IFileSystemInfo [] ?? toDelete.ToArray ();
+
+ if (fileSystemInfos.Count () != 1)
{
return false;
}
- IFileSystemInfo d = toDelete.Single ();
+ IFileSystemInfo d = fileSystemInfos.Single ();
string adjective = d.Name;
- int? result = MessageBox.Query (app,
+ int? result = MessageBox.Query (app ?? throw new ArgumentNullException (nameof (app)),
string.Format (Strings.fdDeleteTitle, adjective),
string.Format (Strings.fdDeleteBody, adjective),
Strings.btnYes,
@@ -49,52 +50,61 @@ public bool Delete (IApplication app, IEnumerable toDelete)
}
///
- public IFileSystemInfo Rename (IApplication app, IFileSystem fileSystem, IFileSystemInfo toRename)
+ public IFileSystemInfo? Rename (IApplication? app, IFileSystem fileSystem, IFileSystemInfo toRename)
{
// Don't allow renaming C: or D: or / (on linux) etc
- if (toRename is IDirectoryInfo dir && dir.Parent is null)
+ if (toRename is IDirectoryInfo { Parent: null })
+ {
+ return null;
+ }
+
+ if (!Prompt (app ?? throw new ArgumentNullException (nameof (app)), Strings.fdRenameTitle, toRename.Name, out string newName))
+ {
+ return null;
+ }
+
+ if (string.IsNullOrWhiteSpace (newName))
{
return null;
}
- if (Prompt (app, Strings.fdRenameTitle, toRename.Name, out string newName))
+ try
{
- if (!string.IsNullOrWhiteSpace (newName))
+ if (toRename is IFileInfo f)
{
- try
- {
- if (toRename is IFileInfo f)
- {
- IFileInfo newLocation = fileSystem.FileInfo.New (Path.Combine (f.Directory.FullName, newName));
- f.MoveTo (newLocation.FullName);
-
- return newLocation;
- }
- else
- {
- var d = (IDirectoryInfo)toRename;
-
- IDirectoryInfo newLocation = fileSystem.DirectoryInfo.New (Path.Combine (d.Parent.FullName, newName));
- d.MoveTo (newLocation.FullName);
-
- return newLocation;
- }
- }
- catch (Exception ex)
- {
- MessageBox.ErrorQuery (app, Strings.fdRenameFailedTitle, ex.Message, Strings.btnOk);
- }
+ IFileInfo newLocation = fileSystem.FileInfo.New (Path.Combine (f.Directory?.FullName ?? throw new InvalidOperationException (), newName));
+ f.MoveTo (newLocation.FullName);
+
+ return newLocation;
+ }
+ else
+ {
+ var d = (IDirectoryInfo)toRename;
+
+ IDirectoryInfo newLocation =
+ fileSystem.DirectoryInfo.New (Path.Combine (d.Parent?.FullName ?? throw new InvalidOperationException (), newName));
+ d.MoveTo (newLocation.FullName);
+
+ return newLocation;
}
}
+ catch (Exception ex)
+ {
+ MessageBox.ErrorQuery (app, Strings.fdRenameFailedTitle, ex.Message, Strings.btnOk);
+ }
return null;
}
///
- public IFileSystemInfo New (IApplication app, IFileSystem fileSystem, IDirectoryInfo inDirectory)
+ public IFileSystemInfo? New (IApplication? app, IFileSystem fileSystem, IDirectoryInfo inDirectory)
{
+ if (app is null)
+ {
+ ArgumentNullException.ThrowIfNull (app);
+ }
var tv = new TextField { Width = Dim.Fill (0, 50), Height = 1 };
- string result = app?.TopRunnable?.Prompt (tv, beginInitHandler: prompt => { prompt.Title = Strings.fdNewTitle; });
+ string? result = app.TopRunnable?.Prompt (tv, beginInitHandler: prompt => { prompt.Title = Strings.fdNewTitle; });
if (string.IsNullOrWhiteSpace (result))
{
diff --git a/Terminal.Gui/Views/FileDialogs/FileDialog.Commands.cs b/Terminal.Gui/Views/FileDialogs/FileDialog.Commands.cs
index 7e61853e16..77786871c2 100644
--- a/Terminal.Gui/Views/FileDialogs/FileDialog.Commands.cs
+++ b/Terminal.Gui/Views/FileDialogs/FileDialog.Commands.cs
@@ -1,3 +1,4 @@
+using System.ComponentModel;
using System.IO.Abstractions;
namespace Terminal.Gui.Views;
@@ -10,18 +11,10 @@ public partial class FileDialog
///
///
///
- public bool IsCompatibleWithAllowedExtensions (IFileInfo file) => !AllowedTypes.Any () || MatchesAllowedTypes (file);
+ public bool IsCompatibleWithAllowedExtensions (IFileInfo file) => AllowedTypes.Count == 0 || MatchesAllowedTypes (file);
///
- protected override bool OnAccepting (CommandEventArgs args)
- {
- if (Accept (true))
- {
- return base.OnAccepting (args);
- }
-
- return false;
- }
+ protected override bool OnAccepting (CommandEventArgs args) => Accept (true) && base.OnAccepting (args);
private void Accept (IEnumerable toMultiAccept)
{
@@ -154,16 +147,10 @@ private bool FinishAccept ()
.ToArray ();
}
- private bool IsCompatibleWithAllowedExtensions (string path)
- {
- // no restrictions
- if (!AllowedTypes.Any ())
- {
- return true;
- }
+ private bool IsCompatibleWithAllowedExtensions (string path) =>
- return AllowedTypes.Any (t => t.IsAllowed (path));
- }
+ // no restrictions
+ AllowedTypes.Count == 0 || AllowedTypes.Any (t => t.IsAllowed (path));
private bool IsCompatibleWithOpenMode (string s, out string reason)
{
@@ -225,7 +212,7 @@ private bool IsCompatibleWithOpenMode (string s, out string reason)
return false;
- default: throw new ArgumentOutOfRangeException (nameof (OpenMode));
+ default: throw new InvalidEnumArgumentException (nameof (OpenMode), (int)OpenMode, typeof (OpenMode));
}
}
@@ -241,9 +228,9 @@ private bool IsCompatibleWithOpenMode (string s, out string reason)
///
private IEnumerable MultiRowToStats ()
{
- HashSet toReturn = new ();
+ HashSet toReturn = [];
- if (!AllowsMultipleSelection || !_tableView.MultiSelectedRegions.Any ())
+ if (!AllowsMultipleSelection || _tableView.MultiSelectedRegions.Count == 0)
{
return toReturn;
}
@@ -260,7 +247,11 @@ private IEnumerable MultiRowToStats ()
private void New ()
{
- IFileSystemInfo created = FileOperationsHandler.New (App, _fileSystem!, State!.Directory);
+ if (_fileSystem is null)
+ {
+ return;
+ }
+ IFileSystemInfo? created = FileOperationsHandler.New (App, _fileSystem, State!.Directory);
RefreshState ();
RestoreSelection (created);
@@ -275,7 +266,7 @@ private void Rename (IApplication? app)
return;
}
- IFileSystemInfo newNamed = FileOperationsHandler.Rename (app, _fileSystem!, toRename.Single ()!);
+ IFileSystemInfo? newNamed = FileOperationsHandler.Rename (app, _fileSystem!, toRename.Single ()!);
RefreshState ();
RestoreSelection (newNamed);
diff --git a/Terminal.Gui/Views/FileDialogs/FileDialog.Navigation.cs b/Terminal.Gui/Views/FileDialogs/FileDialog.Navigation.cs
index 80377139c3..1b8e68d773 100644
--- a/Terminal.Gui/Views/FileDialogs/FileDialog.Navigation.cs
+++ b/Terminal.Gui/Views/FileDialogs/FileDialog.Navigation.cs
@@ -29,7 +29,7 @@ internal void PushState (IDirectoryInfo d, bool addCurrentStateToHistory, bool s
/// Select in the table view (if present)
///
- internal void RestoreSelection (IFileSystemInfo toRestore)
+ internal void RestoreSelection (IFileSystemInfo? toRestore)
{
int row = State!.Children.IndexOf (r => r.FileSystemInfo == toRestore);
_tableView.SetSelection (0, row >= 0 ? row : 0, false);
@@ -78,7 +78,10 @@ private void PathChanged ()
PushState (dir.Parent, true, false);
}
- _tbPath.Autocomplete?.GenerateSuggestions (new AutocompleteFilepathContext (_tbPath.Text, _tbPath.InsertionPoint, State));
+ if (State is { })
+ {
+ _tbPath.Autocomplete?.GenerateSuggestions (new AutocompleteFilepathContext (_tbPath.Text, _tbPath.InsertionPoint, State));
+ }
}
private void PushState (FileDialogState newState, bool addCurrentStateToHistory, bool setPathText = true, bool clearForward = true, string? pathText = null)
@@ -189,7 +192,7 @@ private IDirectoryInfo StringToDirectoryInfo (string path)
return _fileSystem!.DirectoryInfo.New (path);
}
- private void SuppressIfBadChar (Key k)
+ private static void SuppressIfBadChar (Key k)
{
// don't let user type bad letters
var ch = (char)k;
@@ -211,7 +214,7 @@ private void SetPathToSelectedObject (IFileSystemInfo? selected)
if (selected is IDirectoryInfo && Style.PreserveFilenameOnDirectoryChanges)
{
- if (!string.IsNullOrWhiteSpace (Path) && !_fileSystem!.Directory.Exists (Path))
+ if (!string.IsNullOrWhiteSpace (Path) && _fileSystem is { } && !_fileSystem.Directory.Exists (Path))
{
string currentFile = _fileSystem.Path.GetFileName (Path);
diff --git a/Terminal.Gui/Views/FileDialogs/FileDialog.TableView.cs b/Terminal.Gui/Views/FileDialogs/FileDialog.TableView.cs
index 63ceee5180..2480e9fb5b 100644
--- a/Terminal.Gui/Views/FileDialogs/FileDialog.TableView.cs
+++ b/Terminal.Gui/Views/FileDialogs/FileDialog.TableView.cs
@@ -11,7 +11,7 @@ private void TableViewHandleCommandNotBound (object? sender, CommandEventArgs e)
return;
}
- if (e.Context.Binding is MouseBinding { MouseEvent: { } mouse })
+ if (e.Context.Binding is MouseBinding { MouseEvent: { Flags: MouseFlags.RightButtonClicked } mouse })
{
Point? clickedCell = _tableView.ScreenToCell (mouse.Position!.Value.X, mouse.Position!.Value.Y, out int? clickedCol);
@@ -258,6 +258,7 @@ private void SortColumn (int clickedCol)
private static string StripArrows (string columnName) => columnName.Replace (" (▼)", string.Empty).Replace (" (▲)", string.Empty);
+ // TODO: Port this to use key bindings
private bool TableView_KeyDown (Key keyEvent)
{
switch (keyEvent.KeyCode)
@@ -285,7 +286,7 @@ private bool TableView_KeyDown (Key keyEvent)
}
}
- // private void TableViewOnActivated (object? sender, EventArgs e)
+ // BUGBUG: See https://github.com/gui-cs/Terminal.Gui/issues/5087#issuecomment-4328093883
private void TableViewOnValueChanged (object? sender, ValueChangedEventArgs e)
{
if (!_tableView.HasFocus || _tableView.Value is null || _tableView.Table?.Rows == 0)
diff --git a/Terminal.Gui/Views/FileDialogs/FileDialog.cs b/Terminal.Gui/Views/FileDialogs/FileDialog.cs
index aaa0a9a2f7..f98c6e0ea8 100644
--- a/Terminal.Gui/Views/FileDialogs/FileDialog.cs
+++ b/Terminal.Gui/Views/FileDialogs/FileDialog.cs
@@ -27,13 +27,16 @@ public partial class FileDialog : Dialog, IDesignable
private readonly Button _btnBack;
+ private readonly Button _btnForward;
+ private readonly Button _btnCancel;
+
///
- /// Gets the cancel button for the dialog. This is useful for checking if the user canceled the dialog by comparing
+ /// Gets the index of the cancel button for the dialog. This is useful for checking if the user canceled the dialog by
+ /// comparing
/// the to the index of this button in the array.
///
- public Button CancelButton { get; }
+ public int CancelButtonIndex => Buttons.IndexOf (_btnCancel);
- private readonly Button _btnForward;
private readonly Button _btnOk;
private readonly Button _btnUp;
private readonly FileDialogHistory _history;
@@ -45,7 +48,7 @@ public partial class FileDialog : Dialog, IDesignable
private readonly Button _btnTreeToggle;
private readonly TreeView _treeView;
private Dictionary _treeRoots = new ();
- private DropDownList? _typeFilterDropDown;
+ private readonly DropDownList? _typeFilterDropDown;
private int _currentSortColumn;
private bool _currentSortIsAsc = true;
private bool _disposed;
@@ -71,7 +74,7 @@ internal FileDialog (IFileSystem? fileSystem)
// Ensure we get Accept for any subviews; esp TreeView
CommandsToBubbleUp = [Command.Accept];
- CancelButton = new Button { Text = Strings.btnCancel };
+ _btnCancel = new Button { Text = Strings.btnCancel };
_btnOk = new Button { Text = Style.OkButtonText };
@@ -120,6 +123,14 @@ internal FileDialog (IFileSystem? fileSystem)
_tbPath.Autocomplete = new AppendAutocomplete (_tbPath);
_tbPath.Autocomplete.SuggestionGenerator = new FilepathSuggestionGenerator ();
+ _typeFilterDropDown = new DropDownList
+ {
+ X = Pos.AnchorEnd (),
+ Y = 1,
+ Visible = false
+ };
+ Add (_typeFilterDropDown);
+
// Create table view container (right pane)
_tableViewContainer = new View
{
@@ -201,18 +212,19 @@ internal FileDialog (IFileSystem? fileSystem)
_tableView.KeyBindings.ReplaceCommands (Key.Home, Command.Start);
_tableView.KeyBindings.ReplaceCommands (Key.End, Command.End);
_tableView.KeyBindings.ReplaceCommands (Key.Home.WithShift, Command.StartExtend);
- _tableView.KeyBindings.ReplaceCommands (Key.End.WithShift, Command.EndExtend); _history = new FileDialogHistory (this);
+ _tableView.KeyBindings.ReplaceCommands (Key.End.WithShift, Command.EndExtend);
+ _history = new FileDialogHistory (this);
// Changing the key-bindings of a View is not allowed, however,
// by default, Runnable doesn't bind to Command.Context, so
// we can take advantage of the CommandNotBound event to handle it
_tableView.CommandNotBound += TableViewHandleCommandNotBound;
- _tableView.KeyBindings.Add (Key.Space.WithCtrl, Command.Context);
+ _tableView.KeyBindings.Add (PopoverMenu.DefaultKey, Command.Context);
_tableView.MouseBindings.Add (MouseFlags.RightButtonClicked, Command.Context);
_tbPath.TextChanged += (_, _) => PathChanged ();
- _tbFind = new TextField { X = 1, Width = Dim.Width (_tableView) - 1, Y = Pos.AnchorEnd (), Id = "_tbFind" };
+ _tbFind = new TextField { X = 0, Width = Dim.Width (_tableView) - 1, Y = Pos.AnchorEnd (), Id = "_tbFind" };
_spinnerView = new SpinnerView
{
@@ -247,7 +259,7 @@ internal FileDialog (IFileSystem? fileSystem)
// Add the toggle along with OK/Cancel so they align as a group
AddButton (_btnTreeToggle);
- AddButton (CancelButton);
+ AddButton (_btnCancel);
AddButton (_btnOk);
Add (_tbPath);
@@ -256,12 +268,58 @@ internal FileDialog (IFileSystem? fileSystem)
Add (_btnForward);
Add (_treeView);
- // Default: Tree hidden and splitter hidden
- SetTreeVisible (false);
-
Add (_tableViewContainer);
_tableViewContainer.Add (_tbFind);
_tableViewContainer.Add (_spinnerView);
+
+ // to streamline user experience and allow direct typing of paths
+ // with zero navigation we start with focus in the text box and any
+ // default/current path fully selected and ready to be overwritten
+ _tbPath.SetFocus ();
+
+ SetTreeVisible (false);
+ }
+
+ ///
+ public override void EndInit ()
+ {
+ base.EndInit ();
+
+ // Style may have been updated after instance was constructed
+ _btnOk.Text = Style.OkButtonText;
+ _btnCancel.Text = Style.CancelButtonText;
+ _btnUp.Text = GetUpButtonText ();
+ _btnBack.Text = GetBackButtonText ();
+ _btnForward.Text = GetForwardButtonText ();
+ _tbPath.Title = Style.PathCaption;
+ _tbFind.Title = Style.SearchCaption;
+ _treeRoots = Style.TreeRootGetter ();
+ Style.IconProvider.IsOpenGetter = _treeView.IsExpanded;
+ _treeView.AddObjects (_treeRoots.Keys);
+
+ // if filtering on file type is configured then create the DropDownList and establish
+ // initial filtering by extension(s)
+ if (AllowedTypes.Count > 0)
+ {
+ CurrentFilter = AllowedTypes [0];
+
+ _typeFilterDropDown?.Visible = true;
+ _typeFilterDropDown?.Source = new ListWrapper (new ObservableCollection (AllowedTypes.Select (a => a.ToString ()!).ToList ()));
+ _typeFilterDropDown?.Value = AllowedTypes [0].ToString () ?? string.Empty;
+ }
+
+ // if no path has been provided
+ if (Path.Length <= 0)
+ {
+ Path = _fileSystem!.Directory.GetCurrentDirectory ();
+ }
+
+ _tbPath.SelectAll ();
+
+ if (string.IsNullOrEmpty (Title))
+ {
+ Title = GetDefaultTitle ();
+ }
}
///
@@ -327,17 +385,6 @@ public string Path
{
_tbPath.Text = value;
_tbPath.MoveEnd ();
-
- //IDirectoryInfo dir = StringToDirectoryInfo (value);
-
- //StringComparison comparison = OperatingSystem.IsWindows () ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
-
- //if (_treeView.ExpandParents (dir, (left, right) => string.Equals (left.FullName, right.FullName, comparison), out IFileSystemInfo? matched)
- // && matched is { })
- //{
- // // _treeView.EnsureVisible (matched);
- // // _treeView.SelectedObject = matched;
- //}
}
}
@@ -364,86 +411,6 @@ public string Path
// TODO: Refactor to use CWP
public event EventHandler? FilesSelected;
- ///
- protected override void OnIsRunningChanged (bool newIsRunning)
- {
- base.OnIsRunningChanged (newIsRunning);
-
- if (!newIsRunning)
- {
- return;
- }
-
- Arrangement |= ViewArrangement.Resizable;
-
- // May have been updated after instance was constructed
- _btnOk.Text = Style.OkButtonText;
- CancelButton.Text = Style.CancelButtonText;
- _btnUp.Text = GetUpButtonText ();
- _btnBack.Text = GetBackButtonText ();
- _btnForward.Text = GetForwardButtonText ();
-
- _tbPath.Title = Style.PathCaption;
- _tbFind.Title = Style.SearchCaption;
-
- _tbPath.Autocomplete?.Scheme = new Scheme (_tbPath.GetScheme ())
- {
- Normal = new Attribute (Color.Black, _tbPath.GetAttributeForRole (VisualRole.Normal).Background)
- };
-
- _treeRoots = Style.TreeRootGetter ();
- Style.IconProvider.IsOpenGetter = _treeView.IsExpanded;
- _treeView.AddObjects (_treeRoots.Keys);
-
- // if filtering on file type is configured then create the DropDownList and establish
- // initial filtering by extension(s)
- if (AllowedTypes.Any ())
- {
- CurrentFilter = AllowedTypes [0];
-
- _typeFilterDropDown = new DropDownList
- {
- X = Pos.AnchorEnd (),
- Y = 1,
- Source = new ListWrapper (new ObservableCollection (AllowedTypes.Select (a => a.ToString ()!).ToList ())),
- Value = AllowedTypes [0].ToString () ?? string.Empty
- };
- Add (_typeFilterDropDown);
- }
-
- // if no path has been provided
- if (_tbPath.Text.Length <= 0)
- {
- Path = _fileSystem!.Directory.GetCurrentDirectory ();
- }
-
- // to streamline user experience and allow direct typing of paths
- // with zero navigation we start with focus in the text box and any
- // default/current path fully selected and ready to be overwritten
- _tbPath.SetFocus ();
- _tbPath.SelectAll ();
-
- if (string.IsNullOrEmpty (Title))
- {
- Title = GetDefaultTitle ();
- }
-
- // Ensure toggle button text matches current state after sizing
- SetTreeVisible (false);
-
- SetNeedsDraw ();
- SetNeedsLayout ();
- }
-
- ///
- protected override void Dispose (bool disposing)
- {
- _disposed = true;
- base.Dispose (disposing);
-
- CancelSearch ();
- }
-
///
/// Gets a default dialog title, when is not set or empty, result of the function will be
/// shown.
@@ -474,7 +441,7 @@ protected virtual string GetDefaultTitle ()
}
/// State representing a recursive search from downwards.
- internal class SearchState : FileDialogState
+ internal sealed class SearchState : FileDialogState
{
// TODO: Add thread safe child adding
private readonly List _found = [];
@@ -483,11 +450,12 @@ internal class SearchState : FileDialogState
private bool _cancel;
private bool _finished;
- public SearchState (IDirectoryInfo dir, FileDialog parent, string searchTerms) : base (dir, parent)
+ public SearchState (IDirectoryInfo dir, FileDialog parent, string searchTerms) : base (dir, parent, skipInitialEnumeration: true)
{
parent.SearchMatcher.Initialize (searchTerms);
Children = [];
BeginSearch ();
+ RefreshChildren ();
}
///
@@ -505,8 +473,6 @@ internal bool Cancel ()
return !alreadyCancelled;
}
- internal override void RefreshChildren () { }
-
private void BeginSearch ()
{
Task.Run (() =>
@@ -615,4 +581,13 @@ bool IDesignable.EnableForDesign ()
return true;
}
+
+ ///
+ protected override void Dispose (bool disposing)
+ {
+ _disposed = true;
+ base.Dispose (disposing);
+
+ CancelSearch ();
+ }
}
diff --git a/Terminal.Gui/Views/FileDialogs/FileDialogHistory.cs b/Terminal.Gui/Views/FileDialogs/FileDialogHistory.cs
index 535a19777e..7da69c9509 100644
--- a/Terminal.Gui/Views/FileDialogs/FileDialogHistory.cs
+++ b/Terminal.Gui/Views/FileDialogs/FileDialogHistory.cs
@@ -1,24 +1,21 @@
-#nullable disable
-using System.IO.Abstractions;
+using System.IO.Abstractions;
namespace Terminal.Gui.Views;
-internal class FileDialogHistory
+internal class FileDialogHistory (FileDialog dlg)
{
- private readonly Stack back = new ();
- private readonly FileDialog dlg;
- private readonly Stack forward = new ();
- public FileDialogHistory (FileDialog dlg) { this.dlg = dlg; }
+ private readonly Stack _back = new ();
+ private readonly Stack _forward = new ();
public bool Back ()
{
- IDirectoryInfo goTo = null;
- FileSystemInfoStats restoreSelection = null;
- string restorePath = null;
+ IDirectoryInfo? goTo = null;
+ FileSystemInfoStats? restoreSelection = null;
+ string? restorePath = null;
if (CanBack ())
{
- FileDialogState backTo = back.Pop ();
+ FileDialogState backTo = _back.Pop ();
goTo = backTo.Directory;
restoreSelection = backTo.Selected;
restorePath = backTo.Path;
@@ -34,7 +31,7 @@ public bool Back ()
return false;
}
- forward.Push (dlg.State);
+ _forward.Push (dlg.State ?? throw new InvalidOperationException ());
dlg.PushState (goTo, false, true, false, restorePath);
if (restoreSelection is { })
@@ -45,24 +42,23 @@ public bool Back ()
return true;
}
- internal bool CanBack () { return back.Count > 0; }
- internal bool CanForward () { return forward.Count > 0; }
- internal bool CanUp () { return dlg.State?.Directory.Parent != null; }
- internal void ClearForward () { forward.Clear (); }
+ internal bool CanBack () => _back.Count > 0;
+ internal bool CanForward () => _forward.Count > 0;
+ internal bool CanUp () => dlg.State?.Directory.Parent != null;
+ internal void ClearForward () => _forward.Clear ();
internal bool Forward ()
{
- if (forward.Count > 0)
+ if (_forward.Count <= 0)
{
- dlg.PushState (forward.Pop ().Directory, true, true, false);
-
- return true;
+ return false;
}
+ dlg.PushState (_forward.Pop ().Directory, true, true, false);
- return false;
+ return true;
}
- internal void Push (FileDialogState state, bool clearForward)
+ internal void Push (FileDialogState? state, bool clearForward)
{
if (state is null)
{
@@ -70,29 +66,29 @@ internal void Push (FileDialogState state, bool clearForward)
}
// if changing to a new directory push onto the Back history
- if (back.Count == 0 || back.Peek ().Directory.FullName != state.Directory.FullName)
+ if (_back.Count != 0 && _back.Peek ().Directory.FullName == state.Directory.FullName)
{
- back.Push (state);
+ return;
+ }
+ _back.Push (state);
- if (clearForward)
- {
- ClearForward ();
- }
+ if (clearForward)
+ {
+ ClearForward ();
}
}
internal bool Up ()
{
- IDirectoryInfo parent = dlg.State?.Directory.Parent;
+ IDirectoryInfo? parent = dlg.State?.Directory.Parent;
- if (parent is { })
+ if (parent is null)
{
- back.Push (new FileDialogState (parent, dlg));
- dlg.PushState (parent, false);
-
- return true;
+ return false;
}
+ _back.Push (new FileDialogState (parent, dlg));
+ dlg.PushState (parent, false);
- return false;
+ return true;
}
}
diff --git a/Terminal.Gui/Views/FileDialogs/FileDialogState.cs b/Terminal.Gui/Views/FileDialogs/FileDialogState.cs
index aec6818175..56fbeb4145 100644
--- a/Terminal.Gui/Views/FileDialogs/FileDialogState.cs
+++ b/Terminal.Gui/Views/FileDialogs/FileDialogState.cs
@@ -1,30 +1,39 @@
-#nullable disable
-using System.IO.Abstractions;
+using System.IO.Abstractions;
namespace Terminal.Gui.Views;
internal class FileDialogState
{
- protected readonly FileDialog Parent;
-
public FileDialogState (IDirectoryInfo dir, FileDialog parent)
{
- Directory = dir;
Parent = parent;
+ Directory = dir;
Path = parent.Path;
+ Children = GetChildren (Directory).ToArray ();
+ }
- RefreshChildren ();
+ ///
+ /// Constructor for subclasses that manage their own Children population (e.g. ).
+ ///
+ protected FileDialogState (IDirectoryInfo dir, FileDialog parent, bool skipInitialEnumeration)
+ {
+ Parent = parent;
+ Directory = dir;
+ Path = parent.Path;
+ Children = skipInitialEnumeration ? [] : GetChildren (Directory).ToArray ();
}
+ protected FileDialog Parent { get; }
+
public FileSystemInfoStats [] Children { get; internal set; }
public IDirectoryInfo Directory { get; }
/// Gets what was entered in the path text box of the dialog when the state was active.
public string Path { get; }
- public FileSystemInfoStats Selected { get; set; }
+ public FileSystemInfoStats? Selected { get; set; }
- protected virtual IEnumerable GetChildren (IDirectoryInfo dir)
+ protected IEnumerable GetChildren (IDirectoryInfo dir)
{
try
{
@@ -33,26 +42,17 @@ protected virtual IEnumerable GetChildren (IDirectoryInfo d
// if directories only
if (Parent.OpenMode == OpenMode.Directory)
{
- children = dir.GetDirectories ()
- .Select (e => new FileSystemInfoStats (e, Parent.Style.Culture))
- .ToList ();
+ children = dir.GetDirectories ().Select (e => new FileSystemInfoStats (e, Parent.Style.Culture)).ToList ();
}
else
{
- children = dir.GetFileSystemInfos ()
- .Select (e => new FileSystemInfoStats (e, Parent.Style.Culture))
- .ToList ();
+ children = dir.GetFileSystemInfos ().Select (e => new FileSystemInfoStats (e, Parent.Style.Culture)).ToList ();
}
// if only allowing specific file types
- if (Parent.AllowedTypes.Any () && Parent.OpenMode == OpenMode.File)
+ if (Parent.AllowedTypes.Count > 0 && Parent.OpenMode == OpenMode.File)
{
- children = children.Where (
- c => c.IsDir
- || (c.FileSystemInfo is IFileInfo f
- && Parent.IsCompatibleWithAllowedExtensions (f))
- )
- .ToList ();
+ children = children.Where (c => c.IsDir || (c.FileSystemInfo is IFileInfo f && Parent.IsCompatibleWithAllowedExtensions (f))).ToList ();
}
// if there's a UI filter in place too
@@ -72,20 +72,12 @@ protected virtual IEnumerable GetChildren (IDirectoryInfo d
catch (Exception)
{
// Access permissions Exceptions, Dir not exists etc
- return Enumerable.Empty ();
+ return [];
}
}
- protected bool MatchesApiFilter (FileSystemInfoStats arg)
- {
- return arg.IsDir
- || (arg.FileSystemInfo is IFileInfo f
- && Parent.CurrentFilter.IsAllowed (f.FullName));
- }
+ protected bool MatchesApiFilter (FileSystemInfoStats arg) =>
+ Parent.CurrentFilter is { } && (arg.IsDir || (arg.FileSystemInfo is IFileInfo f && Parent.CurrentFilter.IsAllowed (f.FullName)));
- internal virtual void RefreshChildren ()
- {
- IDirectoryInfo dir = Directory;
- Children = GetChildren (dir).ToArray ();
- }
+ internal virtual void RefreshChildren () => Children = GetChildren (Directory).ToArray ();
}
diff --git a/Terminal.Gui/Views/FileDialogs/FileDialogTableSource.cs b/Terminal.Gui/Views/FileDialogs/FileDialogTableSource.cs
index bf15d3bac0..d29fd14175 100644
--- a/Terminal.Gui/Views/FileDialogs/FileDialogTableSource.cs
+++ b/Terminal.Gui/Views/FileDialogs/FileDialogTableSource.cs
@@ -1,26 +1,9 @@
-
namespace Terminal.Gui.Views;
-internal class FileDialogTableSource (
- FileDialog? dlg,
- FileDialogState? state,
- FileDialogStyle? style,
- int currentSortColumn,
- bool currentSortIsAsc)
+internal class FileDialogTableSource (FileDialog? dlg, FileDialogState? state, FileDialogStyle? style, int currentSortColumn, bool currentSortIsAsc)
: ITableSource
{
- public object this [int row, int col]
- {
- get
- {
- if (state is { })
- {
- return GetColumnValue (col, state.Children [row]);
- }
-
- return string.Empty;
- }
- }
+ public object this [int row, int col] => state is { } ? GetColumnValue (col, state.Children [row]) : string.Empty;
public int Rows => state is { } ? state.Children.Count () : 0;
@@ -39,8 +22,11 @@ internal static object GetRawColumnValue (int col, FileSystemInfoStats? stats)
switch (col)
{
case 0: return stats!.FileSystemInfo!.Name;
+
case 1: return stats!.MachineReadableLength;
+
case 2: return stats!.LastWriteTime ?? default (DateTime);
+
case 3: return stats!.Type;
}
@@ -60,9 +46,11 @@ private object GetColumnValue (int col, FileSystemInfoStats? stats)
string icon = dlg!.Style.IconProvider.GetIconWithOptionalSpace (stats!.FileSystemInfo);
- return (icon + (stats?.Name ?? string.Empty)).Trim ();
+ return (icon + stats.Name).Trim ();
+
case 1:
return stats?.HumanReadableLength ?? string.Empty;
+
case 2:
if (stats is null || stats.IsParent || stats.LastWriteTime is null)
{
@@ -70,8 +58,10 @@ private object GetColumnValue (int col, FileSystemInfoStats? stats)
}
return stats.LastWriteTime.Value.ToString (style!.DateFormat);
+
case 3:
return stats?.Type ?? string.Empty;
+
default:
throw new ArgumentOutOfRangeException (nameof (col));
}
diff --git a/Terminal.Gui/Views/FileDialogs/FilesSelectedEventArgs.cs b/Terminal.Gui/Views/FileDialogs/FilesSelectedEventArgs.cs
index f8629e31a4..a219c95e24 100644
--- a/Terminal.Gui/Views/FileDialogs/FilesSelectedEventArgs.cs
+++ b/Terminal.Gui/Views/FileDialogs/FilesSelectedEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace Terminal.Gui.Views;
/// Event args for the event
@@ -7,7 +5,7 @@ public class FilesSelectedEventArgs : EventArgs
{
/// Creates a new instance of the
///
- public FilesSelectedEventArgs (FileDialog dialog) { Dialog = dialog; }
+ public FilesSelectedEventArgs (FileDialog dialog) => Dialog = dialog;
///
/// Set to true if you want to prevent the selection going ahead (this will leave the
diff --git a/Terminal.Gui/Views/FileDialogs/IAllowedType.cs b/Terminal.Gui/Views/FileDialogs/IAllowedType.cs
new file mode 100644
index 0000000000..6c51cc2a4a
--- /dev/null
+++ b/Terminal.Gui/Views/FileDialogs/IAllowedType.cs
@@ -0,0 +1,13 @@
+namespace Terminal.Gui.Views;
+
+/// Interface for restrictions on which file type(s) the user is allowed to select/enter.
+public interface IAllowedType
+{
+ ///
+ /// Returns true if the file at is compatible with this allow option. Note that the file
+ /// may not exist (e.g. in the case of saving).
+ ///
+ ///
+ ///
+ bool IsAllowed (string path);
+}
diff --git a/Terminal.Gui/Views/FileDialogs/OpenDialog.cs b/Terminal.Gui/Views/FileDialogs/OpenDialog.cs
index 669635cb44..c6df91ee9b 100644
--- a/Terminal.Gui/Views/FileDialogs/OpenDialog.cs
+++ b/Terminal.Gui/Views/FileDialogs/OpenDialog.cs
@@ -1,15 +1,3 @@
-#nullable disable
-//
-// FileDialog.cs: File system dialogs for open and save
-//
-// TODO:
-// * Add directory selector
-// * Implement subclasses
-// * Figure out why message text does not show
-// * Remove the extra space when message does not show
-// * Use a line separator to show the file listing, so we can use same colors as the rest
-// * DirListView: Add mouse support
-
using System.Collections.ObjectModel;
namespace Terminal.Gui.Views;
@@ -26,7 +14,7 @@ namespace Terminal.Gui.Views;
/// . This will run the dialog modally, and when this returns,
/// the list of files will be available on the property.
///
-/// To select more than one file, users can use the space key, or CTRL-T.
+/// Use `Ctrl-click` or `Space` to select multiple files. `Alt-Click` extends the selection.
///
public class OpenDialog : FileDialog
{
@@ -36,7 +24,7 @@ public OpenDialog () { }
/// Returns the selected files, or an empty list if nothing has been selected
/// The file paths.
public IReadOnlyList FilePaths =>
- ((IRunnable)this).Result is null || Result == Buttons.IndexOf (CancelButton) ? Enumerable.Empty ().ToList ().AsReadOnly () :
+ ((IRunnable)this).Result is null || Result == CancelButtonIndex ? Enumerable.Empty ().ToList ().AsReadOnly () :
AllowsMultipleSelection ? MultiSelected : new ReadOnlyCollection ([Path]);
///
diff --git a/Terminal.Gui/Views/FileDialogs/OpenMode.cs b/Terminal.Gui/Views/FileDialogs/OpenMode.cs
index e3e62d60f4..494d82e553 100644
--- a/Terminal.Gui/Views/FileDialogs/OpenMode.cs
+++ b/Terminal.Gui/Views/FileDialogs/OpenMode.cs
@@ -1,5 +1,4 @@
-#nullable disable
-namespace Terminal.Gui.Views;
+namespace Terminal.Gui.Views;
/// Determine which type to open.
public enum OpenMode
diff --git a/Terminal.Gui/Views/FileDialogs/SaveDialog.cs b/Terminal.Gui/Views/FileDialogs/SaveDialog.cs
index 1940895ed9..b0af109e7c 100644
--- a/Terminal.Gui/Views/FileDialogs/SaveDialog.cs
+++ b/Terminal.Gui/Views/FileDialogs/SaveDialog.cs
@@ -1,13 +1,4 @@
//
-// FileDialog.cs: File system dialogs for open and save
-//
-// TODO:
-// * Add directory selector
-// * Implement subclasses
-// * Figure out why message text does not show
-// * Remove the extra space when message does not show
-// * Use a line separator to show the file listing, so we can use same colors as the rest
-// * DirListView: Add mouse support
using System.IO.Abstractions;
@@ -34,7 +25,7 @@ public class SaveDialog : FileDialog
/// .
///
/// The name of the file.
- public string? FileName => (this as IRunnable).Result is null || Result == Buttons.IndexOf (CancelButton) ? null : Path;
+ public string? FileName => (this as IRunnable).Result is null || Result == CancelButtonIndex ? null : Path;
/// Gets the default title for the .
///
diff --git a/Terminal.Gui/Views/TextInput/TextField/TextField.Keyboard.cs b/Terminal.Gui/Views/TextInput/TextField/TextField.Keyboard.cs
index 93dbbc019e..5b95dc29d1 100644
--- a/Terminal.Gui/Views/TextInput/TextField/TextField.Keyboard.cs
+++ b/Terminal.Gui/Views/TextInput/TextField/TextField.Keyboard.cs
@@ -73,7 +73,10 @@ protected override bool OnKeyDownNotHandled (Key a)
return false;
}
- if (a.IsAlt || a.IsCtrl)
+ // Never insert modified keys, except for AltGr combinations with associated text of the unmodified key.
+ // This allows users to input characters that require AltGr (e.g. '@' on a Portuguese keyboard which is AltGr+2 with associated text "@"),
+ // while still preventing most modified keys from being inserted into the TextField.
+ if ((a.IsAlt && string.IsNullOrEmpty (a.AsGrapheme)) || a.IsCtrl)
{
// Never insert modified keys
return false;
diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.Keyboard.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.Keyboard.cs
index 961df362e6..52746b2f4c 100644
--- a/Terminal.Gui/Views/TextInput/TextView/TextView.Keyboard.cs
+++ b/Terminal.Gui/Views/TextInput/TextView/TextView.Keyboard.cs
@@ -142,7 +142,10 @@ protected override bool OnKeyDownNotHandled (Key a)
return Autocomplete.ProcessKey (a);
}
- if (a.IsAlt || a.IsCtrl)
+ // Never insert modified keys, except for AltGr combinations with associated text of the unmodified key.
+ // This allows users to input characters that require AltGr (e.g. '@' on a Portuguese keyboard which is AltGr+2 with associated text "@"),
+ // while still preventing most modified keys from being inserted into the TextView.
+ if ((a.IsAlt && string.IsNullOrEmpty (a.AsGrapheme)) || a.IsCtrl)
{
// Never insert modified keys
return false;
diff --git a/Terminal.sln b/Terminal.sln
index 8196f36ae1..16239ed1db 100644
--- a/Terminal.sln
+++ b/Terminal.sln
@@ -159,8 +159,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InlineSelect", "Examples\In
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Terminal.Gui.Analyzers.Internal", "Terminal.Gui.Analyzers.Internal\Terminal.Gui.Analyzers.Internal.csproj", "{927CCC07-F00C-409C-BE42-458EB03DD4E8}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AI", "Examples\AI\AI.csproj", "{1737CFE6-456F-B41B-70D0-2F9EC6BE554F}"
-EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -459,19 +457,6 @@ Global
{927CCC07-F00C-409C-BE42-458EB03DD4E8}.Release|x64.Build.0 = Release|Any CPU
{927CCC07-F00C-409C-BE42-458EB03DD4E8}.Release|x86.ActiveCfg = Release|Any CPU
{927CCC07-F00C-409C-BE42-458EB03DD4E8}.Release|x86.Build.0 = Release|Any CPU
- {1737CFE6-456F-B41B-70D0-2F9EC6BE554F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {1737CFE6-456F-B41B-70D0-2F9EC6BE554F}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {1737CFE6-456F-B41B-70D0-2F9EC6BE554F}.Debug|x64.ActiveCfg = Debug|Any CPU
- {1737CFE6-456F-B41B-70D0-2F9EC6BE554F}.Debug|x64.Build.0 = Debug|Any CPU
- {1737CFE6-456F-B41B-70D0-2F9EC6BE554F}.Debug|x86.ActiveCfg = Debug|Any CPU
- {1737CFE6-456F-B41B-70D0-2F9EC6BE554F}.Debug|x86.Build.0 = Debug|Any CPU
- {1737CFE6-456F-B41B-70D0-2F9EC6BE554F}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {1737CFE6-456F-B41B-70D0-2F9EC6BE554F}.Release|Any CPU.Build.0 = Release|Any CPU
- {1737CFE6-456F-B41B-70D0-2F9EC6BE554F}.Release|x64.ActiveCfg = Release|Any CPU
- {1737CFE6-456F-B41B-70D0-2F9EC6BE554F}.Release|x64.Build.0 = Release|Any CPU
- {1737CFE6-456F-B41B-70D0-2F9EC6BE554F}.Release|x86.ActiveCfg = Release|Any CPU
- {1737CFE6-456F-B41B-70D0-2F9EC6BE554F}.Release|x86.Build.0 = Release|Any CPU
-
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -502,7 +487,6 @@ Global
{90A42AE4-301D-4B05-8892-60BE5209C1B5} = {3DD033C0-E023-47BF-A808-9CCE30873C3E}
{70802F77-F259-44C6-9522-46FCE2FD754E} = {3DD033C0-E023-47BF-A808-9CCE30873C3E}
{3116547F-A8F2-4189-BC22-0B47C757164C} = {3DD033C0-E023-47BF-A808-9CCE30873C3E}
- {1737CFE6-456F-B41B-70D0-2F9EC6BE554F} = {3DD033C0-E023-47BF-A808-9CCE30873C3E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {9F8F8A4D-7B8D-4C2A-AC5E-CD7117F74C03}
diff --git a/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyAlternateKeyTests.cs b/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyAlternateKeyTests.cs
index 7fa60246fb..56faa131aa 100644
--- a/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyAlternateKeyTests.cs
+++ b/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyAlternateKeyTests.cs
@@ -108,7 +108,7 @@ public void ViewKeyDown_ShiftedAndBase_Preserved ()
// View level — same alternate key fields must arrive
Assert.Single (viewDown);
- Assert.Equal (new Key ('@'), viewDown [0]);
+ Assert.Equal (new Key ('@').WithShift, viewDown [0]);
Assert.Equal ((KeyCode)64, viewDown [0].ShiftedKeyCode);
Assert.Equal (KeyCode.Null, viewDown [0].BaseLayoutKeyCode);
Assert.Equal ("@", viewDown [0].AssociatedText);
@@ -125,7 +125,7 @@ public void ViewKeyDown_AssociatedText_Preserved ()
Assert.Equal ("!", appDown [0].GetPrintableText ());
Assert.Single (viewDown);
- Assert.Equal (new Key ('!'), viewDown [0]);
+ Assert.Equal (new Key ('!').WithShift, viewDown [0]);
Assert.Equal ("!", viewDown [0].AssociatedText);
Assert.Equal ("!", viewDown [0].GetPrintableText ());
}
@@ -178,7 +178,7 @@ public void ViewKeyDown_WithModifiersAndEventType_Preserved ()
(_, List viewDown, _) = InjectRawSequenceToView ("\x1b[50:64;6:1u");
Assert.Single (viewDown);
- Assert.Equal (new Key ('@').WithCtrl, viewDown [0]);
+ Assert.Equal (new Key ('@').WithCtrl.WithShift, viewDown [0]);
Assert.Equal ((KeyCode)64, viewDown [0].ShiftedKeyCode);
Assert.Equal (KeyCode.Null, viewDown [0].BaseLayoutKeyCode);
Assert.Equal (KeyEventType.Press, viewDown [0].EventType);
@@ -194,7 +194,7 @@ public void ViewKeyUp_AlternateKeys_Preserved ()
// Release events go to KeyUp, not KeyDown
Assert.Empty (viewDown);
Assert.Single (viewUp);
- Assert.Equal (new Key ('@'), viewUp [0]);
+ Assert.Equal (new Key ('@').WithShift, viewUp [0]);
Assert.Equal ((KeyCode)64, viewUp [0].ShiftedKeyCode);
Assert.Equal (KeyCode.Null, viewUp [0].BaseLayoutKeyCode);
Assert.Equal (KeyEventType.Release, viewUp [0].EventType);
diff --git a/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardParsingTests.cs b/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardParsingTests.cs
index 7b08fa79db..4de75d0b79 100644
--- a/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardParsingTests.cs
+++ b/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardParsingTests.cs
@@ -567,7 +567,8 @@ public void KittyPattern_AlternateKeys_WithModifiers ()
Key? key = _pattern.GetKey ("\u001b[50:64;2;64u");
Assert.NotNull (key);
- Assert.Equal (new Key ('@'), key);
+ Assert.Equal ("@", key.AsGrapheme);
+ Assert.Equal (new Key ('@').WithShift, key);
Assert.Equal ((KeyCode)64, key.ShiftedKeyCode);
Assert.Equal (KeyCode.Null, key.BaseLayoutKeyCode);
Assert.Equal ("@", key.AssociatedText);
@@ -581,7 +582,8 @@ public void KittyPattern_AlternateKeys_WithModifiersAndEventType ()
Key? key = _pattern.GetKey ("\u001b[50:64;2:3;64u");
Assert.NotNull (key);
- Assert.Equal (new Key ('@'), key);
+ Assert.Equal ("@", key.AsGrapheme);
+ Assert.Equal (new Key ('@').WithShift, key);
Assert.Equal ((KeyCode)64, key.ShiftedKeyCode);
Assert.Equal (KeyCode.Null, key.BaseLayoutKeyCode);
Assert.Equal (KeyEventType.Release, key.EventType);
@@ -595,7 +597,7 @@ public void KittyPattern_AssociatedText_ShiftedPrintableKey ()
Key? key = _pattern.GetKey ("\u001b[49;2;33u");
Assert.NotNull (key);
- Assert.Equal (new Key ('!'), key);
+ Assert.Equal (new Key ('!').WithShift, key);
Assert.Equal ("!", key.AssociatedText);
Assert.Equal ("!", key.GetPrintableText ());
}
@@ -738,4 +740,214 @@ public void KittyRequestedFlags_IncludesReportAssociatedText () =>
+ "for printable text fidelity.");
#endregion
+
+ #region Regression Tests - Modifiers Preservation
+
+ // Copilot - ChatGPT v4
+ ///
+ /// Regression test for issue where Ctrl+Shift+Alt+A was being parsed as Ctrl+Alt+A.
+ /// The bug was in NormalizeShiftedPrintableKey modifying the modifierField,
+ /// causing double-decoding in ApplyModifiersAndEventType.
+ /// Input: ESC[97:65;8u = 'a' with shifted key 'A' (65), modifiers 8 (Shift+Ctrl+Alt)
+ /// Expected: Ctrl+Shift+Alt+A
+ ///
+ [Fact]
+ public void KittyPattern_ShiftCtrlAlt_A_PreservesAllModifiers ()
+ {
+ // ESC[97:65;8u = 'a' with shifted key 'A' (65), modifiers 8 (0b111 = Shift+Ctrl+Alt)
+ // mask = 8 - 1 = 7 which means 7 = 1 (Shift) + 2 (Alt) + 4 (Ctrl)
+ Key? key = _pattern.GetKey ("\u001b[97:65;8u");
+
+ Assert.NotNull (key);
+ Assert.Equal (Key.A.WithShift.WithCtrl.WithAlt, key);
+ Assert.True (key.IsShift, "Shift should be preserved");
+ Assert.True (key.IsCtrl, "Ctrl should be preserved");
+ Assert.True (key.IsAlt, "Alt should be preserved");
+ Assert.Equal (new Key (KeyCode.A | KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.AltMask), key.WithShift.WithCtrl.WithAlt);
+ Assert.Equal (KeyCode.A | KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.AltMask, key.KeyCode);
+ }
+
+ // Copilot - ChatGPT v4
+ ///
+ /// Regression test for Ctrl+A (without Shift).
+ /// This should remain as Ctrl+A, not regress.
+ /// Input: ESC[97;5u = 'a', modifiers 5 (0b100 = Ctrl, no Shift)
+ ///
+ [Fact]
+ public void KittyPattern_Ctrl_A_WithoutShift ()
+ {
+ // ESC[97;5u = 'a', modifiers 5 (0b100 = Ctrl, no Shift)
+ // mask = 5 - 1 = 4 which means 4 = 4 (Ctrl)
+ Key? key = _pattern.GetKey ("\u001b[97;5u");
+
+ Assert.NotNull (key);
+ Assert.Equal (Key.A.WithCtrl, key);
+ Assert.False (key.IsShift, "Shift should not be present");
+ Assert.True (key.IsCtrl, "Ctrl should be present");
+ Assert.False (key.IsAlt, "Alt should not be present");
+ Assert.Equal (new Key ('a'), key.NoCtrl);
+ Assert.Equal (KeyCode.A | KeyCode.CtrlMask, key.KeyCode);
+ }
+
+ // Copilot - ChatGPT v4
+ ///
+ /// Regression test for Ctrl+Alt+A (without Shift).
+ /// This should remain as Ctrl+Alt+A, not regress.
+ /// Input: ESC[97;7u = 'a', modifiers 7 (0b110 = Ctrl+Alt, no Shift)
+ ///
+ [Fact]
+ public void KittyPattern_CtrlAlt_A_WithoutShift ()
+ {
+ // ESC[97;7u = 'a', modifiers 7 (0b110 = Ctrl+Alt, no Shift)
+ // mask = 7 - 1 = 6 which means 6 = 2 (Alt) + 4 (Ctrl)
+ Key? key = _pattern.GetKey ("\u001b[97;7u");
+
+ Assert.NotNull (key);
+ Assert.Equal (Key.A.WithCtrl.WithAlt, key);
+ Assert.False (key.IsShift, "Shift should not be present");
+ Assert.True (key.IsCtrl, "Ctrl should be present");
+ Assert.True (key.IsAlt, "Alt should be present");
+ Assert.Equal (new Key ('a'), key.NoCtrl.NoAlt);
+ Assert.Equal (KeyCode.A | KeyCode.CtrlMask | KeyCode.AltMask, key.KeyCode);
+ }
+
+ // Copilot - ChatGPT v4
+ ///
+ /// Regression test for Shift+A (with shifted character 65 and only shift modifier).
+ /// Input: ESC[97:65;2u = 'a' with shifted key 'A' (65), modifiers 2 (0b001 = Shift only)
+ /// Expected: Shift+A
+ ///
+ [Fact]
+ public void KittyPattern_Shift_A_WithShiftedCharacter ()
+ {
+ // ESC[97:65;2u = 'a' with shifted key 'A' (65), modifiers 2 (0b001 = Shift)
+ // mask = 2 - 1 = 1 which means 1 = Shift only
+ Key? key = _pattern.GetKey ("\u001b[97:65;2u");
+
+ Assert.NotNull (key);
+ Assert.Equal (Key.A.WithShift, key);
+ Assert.True (key.IsShift, "Shift should be preserved");
+ Assert.False (key.IsCtrl, "Ctrl should not be present");
+ Assert.False (key.IsAlt, "Alt should not be present");
+ Assert.Equal ("A", key.AsGrapheme);
+ Assert.Equal (new Key ('A'), key);
+ Assert.Equal (new Key ('a'), key.NoShift);
+ Assert.Equal (KeyCode.A | KeyCode.ShiftMask, key.KeyCode);
+ }
+
+ // Copilot - ChatGPT v4
+ ///
+ /// Regression test for Shift+Ctrl+A (without Alt).
+ /// Input: ESC[97:65;6u = 'a' with shifted key 'A' (65), modifiers 6 (0b0101 = Shift+Ctrl)
+ /// Expected: Shift+Ctrl+A
+ ///
+ [Fact]
+ public void KittyPattern_ShiftCtrl_A_PreservesAllModifiers ()
+ {
+ // ESC[97:65;6u = 'a' with shifted key 'A' (65), modifiers 6 (0b0101 = Shift+Ctrl)
+ // mask = 6 - 1 = 5 which means 5 = 1 (Shift) + 4 (Ctrl)
+ Key? key = _pattern.GetKey ("\u001b[97:65;6u");
+
+ Assert.NotNull (key);
+ Assert.Equal (Key.A.WithShift.WithCtrl, key);
+ Assert.True (key.IsShift, "Shift should be preserved");
+ Assert.True (key.IsCtrl, "Ctrl should be preserved");
+ Assert.False (key.IsAlt, "Alt should not be present");
+ Assert.Equal (new Key ('a'), key.NoShift.NoCtrl);
+ Assert.Equal (KeyCode.A | KeyCode.ShiftMask | KeyCode.CtrlMask, key.KeyCode);
+ }
+
+ // Copilot - ChatGPT v4
+ ///
+ /// Regression test for Shift+Alt+A (without Ctrl).
+ /// Input: ESC[97:65;4u = 'a' with shifted key 'A' (65), modifiers 4 (0b011 = Shift+Alt)
+ /// Expected: Shift+Alt+A
+ ///
+ [Fact]
+ public void KittyPattern_ShiftAlt_A_PreservesAllModifiers ()
+ {
+ // ESC[97:65;4u = 'a' with shifted key 'A' (65), modifiers 4 (0b011 = Shift+Alt)
+ // mask = 4 - 1 = 3 which means 3 = 1 (Shift) + 2 (Alt)
+ Key? key = _pattern.GetKey ("\u001b[97:65;4u");
+
+ Assert.NotNull (key);
+ Assert.Equal (Key.A.WithShift.WithAlt, key);
+ Assert.True (key.IsShift, "Shift should be preserved");
+ Assert.False (key.IsCtrl, "Ctrl should not be present");
+ Assert.True (key.IsAlt, "Alt should be preserved");
+ Assert.Equal (new Key ('a'), key.NoShift.NoAlt);
+ Assert.Equal (KeyCode.A | KeyCode.ShiftMask | KeyCode.AltMask, key.KeyCode);
+ }
+
+ // Copilot - ChatGPT v4
+ ///
+ /// Regression test for Alt+A (without Shift and Ctrl).
+ /// This should remain as Alt+A, not regress.
+ /// Input: ESC[97;3;97u = 'a', modifiers 3 (0b010 = Alt, no Shift)
+ ///
+ [Fact]
+ public void KittyPattern_Alt_A_WithoutShift ()
+ {
+ // ESC[97;3;97u = 'a', modifiers 3 (0b010 = Alt, no Shift)
+ // mask = 3 - 1 = 2 which means 2 = 2 (Alt)
+ Key? key = _pattern.GetKey ("\u001b[97;3;97u");
+
+ Assert.NotNull (key);
+ Assert.Equal (Key.A.WithAlt, key);
+ Assert.False (key.IsShift, "Shift should not be present");
+ Assert.False (key.IsCtrl, "Ctrl should not be present");
+ Assert.True (key.IsAlt, "Alt should be present");
+ Assert.Equal (new Key ('a'), key.NoAlt);
+ Assert.Equal (KeyCode.A | KeyCode.AltMask, key.KeyCode);
+ }
+
+ #endregion
+
+ #region Regression Tests - Printable AltGr keys
+
+ ///
+ /// Regression test for AltGr+@.
+ /// This should remain as @, not regress.
+ /// Input: ESC[50;3;64u = '@', modifiers 3 (0b010 = Alt)
+ ///
+ [Fact]
+ public void KittyPattern_Alt_Arroba_WithAltGr ()
+ {
+ // ESC[50;3;64u = '@', modifiers 3 (0b010 = Alt)
+ // mask = 3 - 1 = 2 which means 2 = 2 (Alt)
+ Key? key = _pattern.GetKey ("\u001b[50;3;64u");
+
+ Assert.NotNull (key);
+ Assert.Equal (Key.D2.WithAlt, key);
+ Assert.Equal ("@", key.AsGrapheme);
+ Assert.False (key.IsShift, "Shift should not be present");
+ Assert.False (key.IsCtrl, "Ctrl should not be present");
+ Assert.True (key.IsAlt, "Alt should be present");
+ Assert.Equal (Key.D2, key.NoAlt);
+ Assert.Equal (KeyCode.D2 | KeyCode.AltMask, key.KeyCode);
+ }
+
+ ///
+ /// Regression test for AltGr+€.
+ /// This should remain as €, not regress.
+ /// Input: ESC[101;3;8364u = '€', modifiers 3 (0b010 = Alt)
+ ///
+ [Fact]
+ public void KittyPattern_Alt_Euro_WithAltGr ()
+ {
+ // ESC[101;3;8364u = '€', modifiers 3 (0b010 = Alt)
+ // mask = 3 - 1 = 2 which means 2 = 2 (Alt)
+ Key? key = _pattern.GetKey ("\u001b[101;3;8364u");
+
+ Assert.NotNull (key);
+ Assert.Equal (Key.E.WithAlt, key);
+ Assert.Equal ("€", key.AsGrapheme);
+ Assert.False (key.IsShift, "Shift should not be present");
+ Assert.False (key.IsCtrl, "Ctrl should not be present");
+ Assert.True (key.IsAlt, "Alt should be present");
+ Assert.Equal (Key.E, key.NoAlt);
+ Assert.Equal (KeyCode.E | KeyCode.AltMask, key.KeyCode);
+ }
+
+ #endregion
}
diff --git a/Tests/UnitTestsParallelizable/Views/TextFieldTests.cs b/Tests/UnitTestsParallelizable/Views/TextFieldTests.cs
index ded262f9bf..f77b1147e8 100644
--- a/Tests/UnitTestsParallelizable/Views/TextFieldTests.cs
+++ b/Tests/UnitTestsParallelizable/Views/TextFieldTests.cs
@@ -1517,7 +1517,9 @@ public void AltKey_With_AssociatedText_Does_Not_Insert_Into_TextField ()
TextField tf = new () { Width = 20 };
tf.SetFocus ();
- Key altT = new (Key.T.WithAlt) { AssociatedText = "t" };
+ // Simulate Alt+T with AssociatedText set by Kitty keyboard protocol which in a real scenario would be set AssociatedText as empty string
+ // for Alt+letter keys, with the exception of AltGr+key which would set AssociatedText to "key" (e.g. "€" for AltGr+E)
+ Key altT = new (Key.T.WithAlt) { AssociatedText = "" };
tf.NewKeyDownEvent (altT);
Assert.Equal ("", tf.Text);
diff --git a/Tests/UnitTestsParallelizable/Views/TextViewTests.cs b/Tests/UnitTestsParallelizable/Views/TextViewTests.cs
index 1f216d645e..4e6ebfb67e 100644
--- a/Tests/UnitTestsParallelizable/Views/TextViewTests.cs
+++ b/Tests/UnitTestsParallelizable/Views/TextViewTests.cs
@@ -3960,7 +3960,9 @@ public void AltKey_With_AssociatedText_Does_Not_Insert_Into_TextView ()
TextView tv = new () { Width = 20, Height = 5 };
tv.SetFocus ();
- Key altT = new (Key.T.WithAlt) { AssociatedText = "t" };
+ // Simulate Alt+T with AssociatedText set by Kitty keyboard protocol which in a real scenario would be set AssociatedText as empty string
+ // for Alt+letter keys, with the exception of AltGr+key which would set AssociatedText to "key" (e.g. "€" for AltGr+E)
+ Key altT = new (Key.T.WithAlt) { AssociatedText = "" };
tv.NewKeyDownEvent (altT);
Assert.Equal ("", tv.Text);
diff --git a/docfx/docs/getting-started.md b/docfx/docs/getting-started.md
index 0082d8b5a4..d13d8ed473 100644
--- a/docfx/docs/getting-started.md
+++ b/docfx/docs/getting-started.md
@@ -5,7 +5,7 @@ Paste these commands into your favorite terminal on Windows, Mac, or Linux. This
(Press `Esc` to exit the app)
```ps1
-dotnet new install Terminal.Gui.Templates@2.0.0-beta.*
+dotnet new install Terminal.Gui.Templates@2.0.*
dotnet new tui-simple -n myproj
cd myproj
dotnet run
@@ -24,7 +24,7 @@ dotnet add package Terminal.Gui
Use the [Terminal.Gui.Templates](https://github.com/gui-cs/Terminal.Gui.templates):
```ps1
-dotnet new install Terminal.Gui.Templates@2.0.0-beta.*
+dotnet new install Terminal.Gui.Templates@2.0.*
```
## Sample Usage in C#
diff --git a/docfx/docs/index.md b/docfx/docs/index.md
index 2073215157..2b75a6b2ff 100644
--- a/docfx/docs/index.md
+++ b/docfx/docs/index.md
@@ -3,13 +3,10 @@

> [!IMPORTANT]
-> Terminal.Gui v2 "Beta" has been released.
+> Terminal.Gui v2 has been released
Welcome to the Terminal.Gui documentation! This comprehensive guide covers everything you need to know about building modern terminal user interfaces with Terminal.Gui.
-> [!NOTE]
-> This is the v2 API documentation. For v1 go here: https://gui-cs.github.io/Terminal.GuiV1Docs/
-
## Getting Started
- [Getting Started](~/docs/getting-started.md) - Quick start guide to create your first Terminal.Gui application
diff --git a/docfx/includes/home-content.md b/docfx/includes/home-content.md
index ca96374751..5fb16f3454 100644
--- a/docfx/includes/home-content.md
+++ b/docfx/includes/home-content.md
@@ -1,6 +1,26 @@
-Terminal.Gui is a cross-platform UI toolkit for building sophisticated terminal UI (TUI) applications on Windows, macOS, and Linux/Unix.
+# Terminal.Gui
-
+Cross-platform UI toolkit for building sophisticated terminal UI (TUI) applications on Windows, macOS, and Linux/Unix.
+
+
+
+# Version 2.0 Has Been Released
+
+Terminal.Gui enables building sophisticated console applications with modern UIs:
+
+- **Responsive TUI** - Easy to use, innovative, layout system enables console apps as responsive as any responsive web page.
+- **Performant and Scalable** - Built for modern TUIs - fast, double-buffering-based rendering; Tables and Tree Views scale to infinite elements with sorting and filtering.
+- **Keyboard First; Mouse First Too** - Optimized for TUI experiences where the user's hands never need to leave the keyboard; full mouse support too.
+- **Rich Built-in Widgets (Views)** - Text editors, buttons, checkboxes, trees, tables, markdown, linear ranges, menus, selectors, and more.
+- **Visualizations** - Charts, graphs, progress indicators, and color pickers with TrueColor support.
+- **Text Editors** - Full-featured text editing with clipboard, undo/redo, and Unicode support
+- **Fully Configurable** - Themes, colors, key bindings, and settings are all customizable and persistable.
+- **File Management** - File and directory browsers with search and filtering, supporting Nerdfonts and coloring.
+- **Wizards and Multi-Step Processes** - Guided workflows with navigation and validation.
+- **Cross-Platform** - Consistent experience on Windows, macOS, and Linux.
+- **Apps Work In-line or Full Screen** - Build CLI tools like Claude Code/Copilot/Codex CLI that scroll with the terminal (in-line) or full screen.
+
+See the [Views Overview](https://gui-cs.github.io/Terminal.Gui/docs/views) for available controls and [What's New in v2](https://gui-cs.github.io/Terminal.Gui/docs/newinv2) for architectural improvements.
## Quick Start
@@ -37,55 +57,23 @@ app.Run (window);
See the [Examples](https://github.com/gui-cs/Terminal.Gui/tree/develop/Examples) directory for more.
-## Build Powerful Terminal Applications
-
-Terminal.Gui enables building sophisticated console applications with modern UIs:
-
-- **Rich Forms and Dialogs** - Text fields, buttons, checkboxes, radio buttons, and data validation
-- **Interactive Data Views** - Tables, lists, and trees with sorting, filtering, and in-place editing
-- **Visualizations** - Charts, graphs, progress indicators, and color pickers with TrueColor support
-- **Text Editors** - Full-featured text editing with clipboard, undo/redo, and Unicode support
-- **File Management** - File and directory browsers with search and filtering
-- **Wizards and Multi-Step Processes** - Guided workflows with navigation and validation
-- **System Monitoring Tools** - Real-time dashboards with scrollable, resizable views
-- **Configuration UIs** - Settings editors with persistent themes and user preferences
-- **Cross-Platform CLI Tools** - Consistent experience on Windows, macOS, and Linux
-- **Server Management Interfaces** - SSH-compatible UIs for remote administration
-
-## Key Features
-
-* **[Dozens of Built-in Views](~/docs/views.md)** - Rich set of controls for building complex user interfaces
-
-* **[Cross Platform](~/docs/drivers.md)** - Windows, Mac, and Linux with terminal drivers that work on color and monochrome terminals, including over SSH
+# Documentation
-* **[Powerful Layout Engine](~/docs/layout.md)** - Relative positioning, automatic sizing, and dynamic terminal UIs
+Comprehensive documentation is at [gui-cs.github.io/Terminal.Gui](https://gui-cs.github.io/Terminal.Gui).
-* **[Keyboard](~/docs/keyboard.md) and [Mouse](~/docs/mouse.md) Input** - Complete input handling with simple event-based API
+## Getting Started
-* **[Configuration System](~/docs/config.md)** - Machine, user, and app-level settings with themes and key bindings
+- **[Getting Started Guide](https://gui-cs.github.io/Terminal.Gui/docs/getting-started)** - First Terminal.Gui application
+- **[API Reference](https://gui-cs.github.io/Terminal.Gui/api/Terminal.Gui.App.html)** - Complete API documentation
+- **[What's New in v2](https://gui-cs.github.io/Terminal.Gui/docs/newinv2)** - New features and improvements
-* **[Clipboard Support](~/api/Terminal.Gui.App.Clipboard.yml)** - Cut, Copy, and Paste across platforms
+## Migration & Deep Dives
-* **[Multi-tasking](~/docs/multitasking.md)** - Event processing, idle handlers, timers, and thread-safe classes
+- **[Migrating from v1 to v2](https://gui-cs.github.io/Terminal.Gui/docs/migratingfromv1)** - Complete migration guide
+- **[Application Architecture](https://gui-cs.github.io/Terminal.Gui/docs/application)** - Instance-based model and IRunnable pattern
+- **[Layout System](https://gui-cs.github.io/Terminal.Gui/docs/layout)** - Positioning, sizing, and adornments
+- **[Keyboard Handling](https://gui-cs.github.io/Terminal.Gui/docs/keyboard)** - Key bindings and commands
+- **[View Documentation](https://gui-cs.github.io/Terminal.Gui/docs/View)** - View hierarchy and lifecycle
+- **[Configuration](https://gui-cs.github.io/Terminal.Gui/docs/config)** - Themes and persistent settings
-* **[Reactive Extensions](https://github.com/dotnet/reactive)** - MVVM pattern support with ReactiveUI data bindings
-
-## Installing
-
-### v2 Alpha (Recommended for new projects)
-
-```powershell
-dotnet add package Terminal.Gui --version "2.0.0-alpha.*"
-```
-
-### v2 Develop (Latest)
-
-```powershell
-dotnet add package Terminal.Gui --version "2.0.0-develop.*"
-```
-
-Or use the [Terminal.Gui.Templates](https://github.com/gui-cs/Terminal.Gui.templates):
-
-```powershell
-dotnet new install Terminal.Gui.Templates
-```
+See the [documentation index](https://gui-cs.github.io/Terminal.Gui/docs/index) for all topics.
diff --git a/docfx/index.md b/docfx/index.md
index f9a76b3595..ca5037564e 100644
--- a/docfx/index.md
+++ b/docfx/index.md
@@ -2,19 +2,10 @@
[!INCLUDE [Home Content](includes/home-content.md)]
-## Documentation
+# Contributing
-- **[Getting Started](~/docs/getting-started.md)** - Create your first Terminal.Gui application
-- **[What's New in v2](~/docs/newinv2.md)** - New features and architectural improvements
-- **[Migrating from v1](~/docs/migratingfromv1.md)** - Complete migration guide
-- **[Views Overview](~/docs/views.md)** - All built-in views and controls
-- **[Deep Dives](~/docs/index.md)** - Comprehensive guides and deep dives
-- **[API Reference](~/api/Terminal.Gui.yml)** - Complete API documentation
+Contributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md).
-## Contributing
+# History
-Contributions welcome! See [CONTRIBUTING.md](https://github.com/gui-cs/Terminal.Gui/blob/develop/CONTRIBUTING.md).
-
-## History
-
-See [gui-cs](https://github.com/gui-cs/) for project history and origins.
+See [gui-cs](https://github.com/gui-cs/) for project history and origins.
\ No newline at end of file