From 37eb926295f10903a7eeb3b6e8268178188d26fd Mon Sep 17 00:00:00 2001 From: Tig Date: Sat, 22 Aug 2026 17:29:22 -0600 Subject: [PATCH 1/2] Update all examples to Terminal.Gui 2.5.x (MEC configuration, post-#5416). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports every example off the legacy ConfigurationManager API removed by tui-cs/Terminal.Gui#5416 and onto the 2.5 MEC-based model: - Remove ConfigurationManager.Enable (ConfigLocations.All) everywhere — configuration is applied automatically at assembly load in 2.5. - Example: RuntimeConfig theme override now uses TuiConfigurationBuilder.Shared.RuntimeConfig + ApplyToStaticFacades. - ReactiveExample: the ObservableEvents source generator cannot wrap 2.5's TextField (it hides View.TextChanging with a different delegate type); subscribe to TextField.TextChanged via Observable.FromEventPattern instead. - Migrate Config/*.json and Themes/code-dark.config.json to the nested MEC shape (dotted keys and Themes/Schemes arrays no longer apply). Key bindings stay supported via nested Application:DefaultKeyBindings, View:DefaultKeyBindings, and View:ViewKeyBindings with per-command overlay. - Drop the removed ConfigurationManager.ThrowOnJsonErrors key and fix a mojibake check-mark glyph in example_config.json. - Port FSharpExample to the v2 API (v2 namespaces, IApplication model, F# 9 nullness) and add it to the solution. - Rewrite the CommunityToolkit/Reactive/SelfContained/Config READMEs (updated snippets; the old files had no line endings and mojibake). - Bump Terminal.Gui to 2.5.0-develop.* (floats once 2.5 dev packages publish after tui-cs/Terminal.Gui#5416 merges). Verified against a locally packed 2.5.0-develop.1 from the #5416 branch: solution + FSharpExample build with 0 warnings/0 errors, and all 9 smoke tests pass. dotnet format verify reports only the 46 violations already present on main under SDK 10.0.400. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01NGq38GbuZPAEa5wZHkmPiZ --- CommunityToolkitExample/Program.cs | 3 +- CommunityToolkitExample/README.md | 164 +++++++++++++++++++++- Config/README.md | 98 +++++++++++++- Config/example_config.json | 157 ++++++++++++++++++++- Config/macos.json | 210 +++++++++++++++++++++++++++- Config/windows.json | 211 ++++++++++++++++++++++++++++- Directory.Packages.props | 2 +- Example/Example.cs | 8 +- Examples.sln | 114 ++++++++++++++++ FSharpExample/Program.fs | 69 ++++++---- FSharpExample/README.md | 19 ++- PromptExample/Program.cs | 4 +- ReactiveExample/LoginView.cs | 12 +- ReactiveExample/Program.cs | 3 +- ReactiveExample/README.md | 54 +++++++- SelfContained/Program.cs | 4 +- SelfContained/README.md | 35 ++++- ShortcutTest/ShortcutTest.cs | 4 +- Themes/code-dark.config.json | 22 ++- 19 files changed, 1130 insertions(+), 63 deletions(-) diff --git a/CommunityToolkitExample/Program.cs b/CommunityToolkitExample/Program.cs index 9129cd3..0d687f3 100644 --- a/CommunityToolkitExample/Program.cs +++ b/CommunityToolkitExample/Program.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.DependencyInjection; using Terminal.Gui.App; -using Terminal.Gui.Configuration; namespace CommunityToolkitExample; @@ -12,7 +11,7 @@ private static async Task Main (string[] args) { var smokeTest = args.Length > 0 && args [0] == "--smoke-test"; - ConfigurationManager.Enable (ConfigLocations.All); + // Configuration (themes, schemes, settings) is applied automatically at assembly load. Services = ConfigureServices (); using IApplication app = Application.Create (); app.Init (); diff --git a/CommunityToolkitExample/README.md b/CommunityToolkitExample/README.md index dbedba3..51caff6 100644 --- a/CommunityToolkitExample/README.md +++ b/CommunityToolkitExample/README.md @@ -1 +1,163 @@ -# CommunityToolkit.MVVM ExampleThis small demo gives an example of using the `CommunityToolkit.MVVM` framework's `ObservableObject`, `ObservableProperty`, and `IRecipient` in conjunction with `Microsoft.Extensions.DependencyInjection`. Right away we use IoC to load our views and view models.``` csharp// As a public property for access further in the application if needed. public static IServiceProvider? Services { get; private set; }...// In MainConfigurationManager.Enable (ConfigLocations.All);Services = ConfigureServices ();...private static IServiceProvider ConfigureServices (){ var services = new ServiceCollection (); services.AddTransient (); services.AddTransient (); return services.BuildServiceProvider ();}```Now, we start the app using the modern Terminal.Gui model and get our main view.``` csharpusing IApplication app = Application.Create ();app.Init ();using var loginView = Services.GetRequiredService ();app.Run (loginView);```Our view implements `IRecipient` to demonstrate the use of the `WeakReferenceMessenger`. The binding of the view events is then created.``` csharpinternal partial class LoginView : IRecipient>{ public LoginView (LoginViewModel viewModel) { // Initialize our Receive method WeakReferenceMessenger.Default.Register (this); ... ViewModel = viewModel; ... passwordInput.TextChanged += (_, _) => { ViewModel.Password = passwordInput.Text; }; loginButton.Accepting += (_, e) => { if (!ViewModel.CanLogin) { return; } ViewModel.LoginCommand.Execute (null); // When Accepting is handled, set e.Handled to true to prevent further processing. e.Handled = true; }; ... // Let the view model know the view is initialized. Initialized += (_, _) => { ViewModel.Initialized (); }; } ...}```Momentarily slipping over to the view model, all bindable properties use some form of `ObservableProperty` with the class deriving from `ObservableObject`. Commands are of the `RelayCommand` type. The use of `ObservableProperty` generates the code for handling `INotifyPropertyChanged` and `INotifyPropertyChanging`.``` csharpinternal partial class LoginViewModel : ObservableObject{ ... [ObservableProperty] private bool _canLogin; private string _password; ... public LoginViewModel () { ... Password = string.Empty; ... LoginCommand = new (Execute); Clear (); return; async void Execute () { await Login (); } } ... public RelayCommand LoginCommand { get; } public string Password { get => _password; set { SetProperty (ref _password, value); PasswordLengthMessage = $"_Password ({_password.Length} characters):"; ValidateLogin (); } }```The use of `WeakReferenceMessenger` provides one method of signaling the view from the view model. It's just one way to handle cross-thread messaging in this framework.``` csharp...private async Task Login (){ SendMessage (LoginActions.LoginProgress, LOGGING_IN_PROGRESS_MESSAGE); await Task.Delay (TimeSpan.FromSeconds (1)); Clear ();}private void SendMessage (LoginActions loginAction, string message = ""){ switch (loginAction) { case LoginActions.LoginProgress: LoginProgressMessage = message; break; case LoginActions.Validation: ValidationMessage = CanLogin ? VALID_LOGIN_MESSAGE : INVALID_LOGIN_MESSAGE; ValidationScheme = CanLogin ? SchemeManager.GetScheme ("Base") : SchemeManager.GetScheme ("Error"); break; } WeakReferenceMessenger.Default.Send (new Message { Value = loginAction });}private void ValidateLogin (){ CanLogin = !string.IsNullOrEmpty (Username) && !string.IsNullOrEmpty (Password); SendMessage (LoginActions.Validation);}...```The view's `Receive` function updates the UI based on messages from the view model. In the modern Terminal.Gui model, UI updates are automatically refreshed, so no manual `Application.Refresh()` call is needed.``` csharppublic void Receive (Message message){ switch (message.Value) { case LoginActions.LoginProgress: { loginProgressLabel.Text = ViewModel.LoginProgressMessage; break; } case LoginActions.Validation: { validationLabel.Text = ViewModel.ValidationMessage; validationLabel.SetScheme (ViewModel.ValidationScheme); break; } } SetText ();}``` \ No newline at end of file +# CommunityToolkit.MVVM Example + +This small demo gives an example of using the `CommunityToolkit.MVVM` framework's `ObservableObject`, `ObservableProperty`, and `IRecipient` in conjunction with `Microsoft.Extensions.DependencyInjection`. Right away we use IoC to load our views and view models. + +```csharp +// As a public property for access further in the application if needed. +public static IServiceProvider? Services { get; private set; } +... +// In Main. Configuration (themes, schemes, settings) is applied automatically at assembly load. +Services = ConfigureServices (); +... +private static IServiceProvider ConfigureServices () +{ + ServiceCollection services = new (); + services.AddTransient (); + services.AddTransient (); + + return services.BuildServiceProvider (); +} +``` + +Now, we start the app using the modern Terminal.Gui model and get our main view. + +```csharp +using IApplication app = Application.Create (); +app.Init (); +using LoginView loginView = Services.GetRequiredService (); +app.Run (loginView); +``` + +Our view implements `IRecipient` to demonstrate the use of the `WeakReferenceMessenger`. The binding of the view events is then created. + +```csharp +internal partial class LoginView : IRecipient> +{ + public LoginView (LoginViewModel viewModel) + { + // Initialize our Receive method + WeakReferenceMessenger.Default.Register (this); + ... + ViewModel = viewModel; + ... + passwordInput.TextChanged += (_, _) => + { + ViewModel.Password = passwordInput.Text; + }; + + loginButton.Accepting += (_, e) => + { + if (!ViewModel.CanLogin) { return; } + ViewModel.LoginCommand.Execute (null); + + // When Accepting is handled, set e.Handled to true to prevent further processing. + e.Handled = true; + }; + ... + // Let the view model know the view is initialized. + Initialized += (_, _) => { ViewModel.Initialized (); }; + } + ... +} +``` + +Momentarily slipping over to the view model, all bindable properties use some form of `ObservableProperty` with the class deriving from `ObservableObject`. Commands are of the `RelayCommand` type. The use of `ObservableProperty` generates the code for handling `INotifyPropertyChanged` and `INotifyPropertyChanging`. + +```csharp +internal partial class LoginViewModel : ObservableObject +{ + ... + [ObservableProperty] + private bool _canLogin; + + private string _password; + ... + public LoginViewModel () + { + ... + Password = string.Empty; + ... + LoginCommand = new (Execute); + Clear (); + + return; + + async void Execute () { await Login (); } + } + ... + public RelayCommand LoginCommand { get; } + + public string Password + { + get => _password; + set + { + SetProperty (ref _password, value); + PasswordLengthMessage = $"_Password ({_password.Length} characters):"; + ValidateLogin (); + } + } +``` + +The use of `WeakReferenceMessenger` provides one method of signaling the view from the view model. It's just one way to handle cross-thread messaging in this framework. + +```csharp +... +private async Task Login () +{ + SendMessage (LoginActions.LoginProgress, LOGGING_IN_PROGRESS_MESSAGE); + await Task.Delay (TimeSpan.FromSeconds (1)); + Clear (); +} + +private void SendMessage (LoginActions loginAction, string message = "") +{ + switch (loginAction) + { + case LoginActions.LoginProgress: + LoginProgressMessage = message; + + break; + case LoginActions.Validation: + ValidationMessage = CanLogin ? VALID_LOGIN_MESSAGE : INVALID_LOGIN_MESSAGE; + ValidationScheme = CanLogin ? SchemeManager.GetScheme ("Base") : SchemeManager.GetScheme ("Error"); + + break; + } + + WeakReferenceMessenger.Default.Send (new Message { Value = loginAction }); +} + +private void ValidateLogin () +{ + CanLogin = !string.IsNullOrEmpty (Username) && !string.IsNullOrEmpty (Password); + SendMessage (LoginActions.Validation); +} +... +``` + +The view's `Receive` function updates the UI based on messages from the view model. In the modern Terminal.Gui model, UI updates are automatically refreshed, so no manual `Application.Refresh()` call is needed. + +```csharp +public void Receive (Message message) +{ + switch (message.Value) + { + case LoginActions.LoginProgress: + { + loginProgressLabel.Text = ViewModel.LoginProgressMessage; + + break; + } + case LoginActions.Validation: + { + validationLabel.Text = ViewModel.ValidationMessage; + validationLabel.SetScheme (ViewModel.ValidationScheme); + + break; + } + } + + SetText (); +} +``` diff --git a/Config/README.md b/Config/README.md index 530e92b..3cdc739 100644 --- a/Config/README.md +++ b/Config/README.md @@ -1 +1,97 @@ -# Terminal.Gui Key Binding Config ExamplesThis folder contains example `config.json` files that override Terminal.Gui's defaultkey bindings to match platform conventions.## How to UseCopy the desired file to `~/.tui/config.json` (the global Terminal.Gui config location).| OS | Want macOS feel? | Want Windows feel? ||----|------------------|--------------------|| **Windows** | Copy `macos.json` ΓåÆ `~/.tui/config.json` | (already default) || **macOS** | (already default) | Copy `windows.json` ΓåÆ `~/.tui/config.json` |On Windows `~` expands to `C:\Users\`. On macOS/Linux `~` expands to `/home/` (or `/Users/` on macOS).## What Each File Changes### `macos.json` ΓÇö macOS-style bindings (for Windows users)Overrides Terminal.Gui's default key bindings to match macOS conventions:| What changes | Default (Windows) | With `macos.json` ||---|---|---|| Quit app | `Esc` | `Esc` or `Ctrl+Q` || Suspend app to background | *(not available)* | `Ctrl+Z` || Undo | `Ctrl+Z` | `Ctrl+Z` or `Ctrl+/` || Redo | `Ctrl+Y` | `Ctrl+Y` or `Ctrl+Shift+Z` || Delete char right | `Delete` | `Delete` or `Ctrl+D` |Note: Emacs navigation shortcuts (`Ctrl+B`/`Ctrl+F` for left/right in text fields,`Ctrl+N`/`Ctrl+P` for up/down in text views and lists) are already available on allplatforms ΓÇö no override needed.### `windows.json` ΓÇö Windows-style bindings (for macOS users)Overrides Terminal.Gui's default key bindings to match Windows conventions:| What changes | Default (macOS) | With `windows.json` ||---|---|---|| Quit app | `Esc` or `Ctrl+Q` | `Esc` only || Suspend app to background | `Ctrl+Z` | *(disabled)* || Undo | `Ctrl+Z` or `Ctrl+/` | `Ctrl+Z` only || Redo | `Ctrl+Y` or `Ctrl+Shift+Z` | `Ctrl+Y` only || Delete char right | `Delete` or `Ctrl+D` | `Delete` only |**Limitation:** Emacs navigation shortcuts built into text views (`Ctrl+B`, `Ctrl+F`,`Ctrl+N`, `Ctrl+P`, `Ctrl+K`, etc.) are set in C# code and cannot be removed via`config.json`. They remain available alongside the standard keys.## How It WorksTerminal.Gui's `ConfigurationManager` loads `~/.tui/config.json` and uses it toreplace the values of three key binding properties:- **`Application.DefaultKeyBindings`** ΓÇö app-level commands (Quit, Suspend, Tab navigation)- **`View.DefaultKeyBindings`** ΓÇö shared commands across all views (navigation, clipboard, editing)- **`View.ViewKeyBindings`** ΓÇö per-view overrides (keyed by view type name, e.g. `"TextField"`)The JSON format maps command names to `PlatformKeyBinding` objects:```json{ "Application.DefaultKeyBindings": { "Quit": { "All": ["Esc", "Ctrl+Q"] } }, "View.DefaultKeyBindings": { "Undo": { "All": ["Ctrl+Z"], "Linux": ["Ctrl+/"], "Macos": ["Ctrl+/"] } }, "View.ViewKeyBindings": { "TextField": { "WordLeft": { "All": ["Ctrl+CursorLeft"] } } }}```Each `PlatformKeyBinding` has four optional fields:| Field | Applies to ||-------|-----------|| `All` | Every platform || `Windows` | Windows only (added to `All`) || `Linux` | Linux only (added to `All`) || `Macos` | macOS only (added to `All`) |**Important:** When you override a property (e.g. `View.DefaultKeyBindings`), yourJSON replaces the entire default dictionary. Any command you omit reverts tohaving no binding from that layer. Always include all commands you want active. \ No newline at end of file +# Terminal.Gui Key Binding Config Examples + +This folder contains example `config.json` files that override Terminal.Gui's default key bindings to match platform conventions. + +## How to Use + +Copy the desired file to `~/.tui/config.json` (the global Terminal.Gui config location) or `./.tui/config.json` (resolved against the app's current directory). + +| OS | Want macOS feel? | Want Windows feel? | +|----|------------------|--------------------| +| **Windows** | Copy `macos.json` → `~/.tui/config.json` | (already default) | +| **macOS** | (already default) | Copy `windows.json` → `~/.tui/config.json` | + +On Windows `~` expands to `C:\Users\`. On macOS/Linux `~` expands to `/Users/` / `/home/`. + +## JSON Shape (2.5+) + +As of Terminal.Gui 2.5, configuration is loaded via Microsoft.Extensions.Configuration (`TuiConfigurationBuilder`) and uses **nested objects**, not dotted keys. Configuration is applied automatically at assembly load — `ConfigurationManager.Enable ()` no longer exists. + +To convert a pre-2.5 config (dotted keys, `Themes`/`Schemes` arrays), run the migrator from the Terminal.Gui repo: + +```bash +dotnet run --project Tools/MigrateConfig -- ./.tui/config.json ./.tui/config.json +``` + +See [Migrating ConfigurationManager to TuiConfigurationBuilder](https://github.com/tui-cs/Terminal.Gui/blob/develop/docfx/docs/migrate-cm-to-mec.md). + +## What Each File Changes + +### `macos.json` — macOS-style bindings (for Windows users) + +| What changes | Default (Windows) | With `macos.json` | +|---|---|---| +| Quit app | `Esc` | `Esc` or `Ctrl+Q` | +| Suspend app to background | *(not available)* | `Ctrl+Z` | +| Undo | `Ctrl+Z` | `Ctrl+Z` or `Ctrl+/` | +| Redo | `Ctrl+Y` | `Ctrl+Y` or `Ctrl+Shift+Z` | +| Delete char right | `Delete` | `Delete` or `Ctrl+D` | + +### `windows.json` — Windows-style bindings (for macOS users) + +| What changes | Default (macOS) | With `windows.json` | +|---|---|---| +| Quit app | `Esc` or `Ctrl+Q` | `Esc` only | +| Suspend app to background | `Ctrl+Z` | *(disabled)* | +| Undo | `Ctrl+Z` or `Ctrl+/` | `Ctrl+Z` only | +| Redo | `Ctrl+Y` or `Ctrl+Shift+Z` | `Ctrl+Y` only | +| Delete char right | `Delete` or `Ctrl+D` | `Delete` only | + +## How It Works + +Terminal.Gui overlays three nested key-binding sections onto the hard-coded defaults: + +- **`Application:DefaultKeyBindings`** — app-level commands (Quit, Suspend, Tab navigation) +- **`View:DefaultKeyBindings`** — shared commands across all views (navigation, clipboard, editing) +- **`View:ViewKeyBindings`** — per-view overrides (keyed by view type name, e.g. `"TextField"`) + +The JSON format maps command names to `PlatformKeyBinding` objects: + +```json +{ + "Application": { + "DefaultKeyBindings": { + "Quit": { "All": ["Esc", "Ctrl+Q"] } + } + }, + "View": { + "DefaultKeyBindings": { + "Undo": { "All": ["Ctrl+Z"], "Linux": ["Ctrl+/"], "Macos": ["Ctrl+/"] } + }, + "ViewKeyBindings": { + "TextField": { + "WordLeft": { "All": ["Ctrl+CursorLeft"] } + } + } + } +} +``` + +Each `PlatformKeyBinding` has four optional fields: + +| Field | Applies to | +|-------|-----------| +| `All` | Every platform | +| `Windows` | Windows only (added to `All`) | +| `Linux` | Linux only (added to `All`) | +| `Macos` | macOS only (added to `All`) | + +Bindings overlay **per command**: a command you set replaces that command's default binding entirely (include every key you want active for it), while commands you omit keep their compile-time defaults. + +Key bindings can also be changed in code, before `Application.Create ()`: + +```csharp +Application.SetDefaultKeyBinding (Command.Quit, Bind.All (Key.Esc, Key.Q.WithCtrl)); +``` + +`example_config.json` is a fuller sample that also sets a custom theme, glyphs, and view defaults. diff --git a/Config/example_config.json b/Config/example_config.json index dc8710d..d321658 100644 --- a/Config/example_config.json +++ b/Config/example_config.json @@ -1 +1,156 @@ -{ "$schema": "https://tui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json", "ConfigurationManager.ThrowOnJsonErrors": false, "Application.DefaultKeyBindings": { "Quit": { "All": ["Esc"] }, "Suspend": { "Linux": ["Ctrl+Z"], "Macos": ["Ctrl+Z"] }, "Arrange": { "All": ["Ctrl+F5"] }, "NextTabStop": { "All": ["Tab"] }, "PreviousTabStop": { "All": ["Shift+Tab"] }, "NextTabGroup": { "All": ["F6"] }, "PreviousTabGroup": { "All": ["Shift+F6"] }, "Refresh": { "All": ["F5"] } }, "Driver.Force16Colors": false, "Application.ForceDriver": "", "Application.IsMouseDisabled": false, "Key.Separator": "+", "MenuBar.DefaultKey": "F10", "PopoverMenu.DefaultKey": "Shift+F10", "FileDialog.MaxSearchResults": 10000, "FileDialogStyle.DefaultUseColors": false, "FileDialogStyle.DefaultUseUnicodeCharacters": false, "Theme": "Gruntled", "Themes": [ { "Gruntled": { "Glyphs": { "CheckStateChecked": "Γ£ô" }, "Schemes": [ { "TopLevel": { "Normal": { "Foreground": "OrangeRed", "Background": "AntiqueWhite" }, "Focus": { "Foreground": "White", "Background": "Maroon" }, "HotNormal": { "Foreground": "DarkRed", "Background": "AntiqueWhite" }, "HotFocus": { "Foreground": "Yellow", "Background": "Maroon" }, "Disabled": { "Foreground": "LightPink", "Background": "AntiqueWhite" } } }, { "Base": { "Normal": { "Foreground": "White", "Background": "OrangeRed" }, "Focus": { "Foreground": "FireBrick", "Background": "AntiqueWhite" }, "HotNormal": { "Foreground": "Gold", "Background": "OrangeRed" }, "HotFocus": { "Foreground": "FireBrick", "Background": "AntiqueWhite" }, "Disabled": { "Foreground": "LightPink", "Background": "OrangeRed" } } }, { "Dialog": { "Normal": { "Foreground": "FireBrick", "Background": "GhostWhite" }, "Focus": { "Foreground": "DarkGray", "Background": "LightGray" }, "HotNormal": { "Foreground": "FireBrick", "Background": "GhostWhite" }, "HotFocus": { "Foreground": "FireBrick", "Background": "GhostWhite" }, "Disabled": { "Foreground": "Gray", "Background": "DarkGray" } } }, { "Menu": { "Normal": { "Foreground": "White", "Background": "FireBrick" }, "Focus": { "Foreground": "White", "Background": "DarkRed" }, "HotNormal": { "Foreground": "Yellow", "Background": "FireBrick" }, "HotFocus": { "Foreground": "Yellow", "Background": "DarkRed" }, "Disabled": { "Foreground": "Gray", "Background": "DarkGray" } } }, { "Error": { "Normal": { "Foreground": "Yellow", "Background": "DarkRed" }, "Focus": { "Foreground": "DarkSalmon", "Background": "Brown" }, "HotNormal": { "Foreground": "Black", "Background": "DarkRed" }, "HotFocus": { "Foreground": "FireBrick", "Background": "Brown" }, "Disabled": { "Foreground": "DarkGray", "Background": "DarkRed" } } } ] } } ]} \ No newline at end of file +{ + "$schema": "https://tui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json", + "Application": { + "DefaultKeyBindings": { + "Quit": { "All": [ "Esc" ] }, + "Suspend": { "Linux": [ "Ctrl+Z" ], "Macos": [ "Ctrl+Z" ] }, + "Arrange": { "All": [ "Ctrl+F5" ] }, + "NextTabStop": { "All": [ "Tab" ] }, + "PreviousTabStop": { "All": [ "Shift+Tab" ] }, + "NextTabGroup": { "All": [ "F6" ] }, + "PreviousTabGroup": { "All": [ "Shift+F6" ] }, + "Refresh": { "All": [ "F5" ] } + }, + "ForceDriver": "", + "IsMouseDisabled": false + }, + "Driver": { + "Force16Colors": false + }, + "Key": { + "Separator": "+" + }, + "MenuBar": { + "DefaultKey": "F10" + }, + "PopoverMenu": { + "DefaultKey": "Shift+F10" + }, + "FileDialog": { + "MaxSearchResults": 10000 + }, + "FileDialogStyle": { + "DefaultUseColors": false, + "DefaultUseUnicodeCharacters": false + }, + "Theme": "Gruntled", + "Themes": { + "Gruntled": { + "Glyphs": { + "CheckStateChecked": "✓" + }, + "Schemes": { + "TopLevel": { + "Normal": { + "Foreground": "OrangeRed", + "Background": "AntiqueWhite" + }, + "Focus": { + "Foreground": "White", + "Background": "Maroon" + }, + "HotNormal": { + "Foreground": "DarkRed", + "Background": "AntiqueWhite" + }, + "HotFocus": { + "Foreground": "Yellow", + "Background": "Maroon" + }, + "Disabled": { + "Foreground": "LightPink", + "Background": "AntiqueWhite" + } + }, + "Base": { + "Normal": { + "Foreground": "White", + "Background": "OrangeRed" + }, + "Focus": { + "Foreground": "FireBrick", + "Background": "AntiqueWhite" + }, + "HotNormal": { + "Foreground": "Gold", + "Background": "OrangeRed" + }, + "HotFocus": { + "Foreground": "FireBrick", + "Background": "AntiqueWhite" + }, + "Disabled": { + "Foreground": "LightPink", + "Background": "OrangeRed" + } + }, + "Dialog": { + "Normal": { + "Foreground": "FireBrick", + "Background": "GhostWhite" + }, + "Focus": { + "Foreground": "DarkGray", + "Background": "LightGray" + }, + "HotNormal": { + "Foreground": "FireBrick", + "Background": "GhostWhite" + }, + "HotFocus": { + "Foreground": "FireBrick", + "Background": "GhostWhite" + }, + "Disabled": { + "Foreground": "Gray", + "Background": "DarkGray" + } + }, + "Menu": { + "Normal": { + "Foreground": "White", + "Background": "FireBrick" + }, + "Focus": { + "Foreground": "White", + "Background": "DarkRed" + }, + "HotNormal": { + "Foreground": "Yellow", + "Background": "FireBrick" + }, + "HotFocus": { + "Foreground": "Yellow", + "Background": "DarkRed" + }, + "Disabled": { + "Foreground": "Gray", + "Background": "DarkGray" + } + }, + "Error": { + "Normal": { + "Foreground": "Yellow", + "Background": "DarkRed" + }, + "Focus": { + "Foreground": "DarkSalmon", + "Background": "Brown" + }, + "HotNormal": { + "Foreground": "Black", + "Background": "DarkRed" + }, + "HotFocus": { + "Foreground": "FireBrick", + "Background": "Brown" + }, + "Disabled": { + "Foreground": "DarkGray", + "Background": "DarkRed" + } + } + } + } + } +} \ No newline at end of file diff --git a/Config/macos.json b/Config/macos.json index e26deb9..5936b80 100644 --- a/Config/macos.json +++ b/Config/macos.json @@ -1 +1,209 @@ -{ "$schema": "https://tui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json", "Application.DefaultKeyBindings": { "Quit": { "All": ["Esc", "Ctrl+Q"] }, "Suspend": { "All": ["Ctrl+Z"] }, "Arrange": { "All": ["Ctrl+F5"] }, "NextTabStop": { "All": ["Tab"] }, "PreviousTabStop": { "All": ["Shift+Tab"] }, "NextTabGroup": { "All": ["F6"] }, "PreviousTabGroup":{ "All": ["Shift+F6"] }, "Refresh": { "All": ["F5"] } }, "View.DefaultKeyBindings": { "Left": { "All": ["CursorLeft"] }, "Right": { "All": ["CursorRight"] }, "Up": { "All": ["CursorUp"] }, "Down": { "All": ["CursorDown"] }, "PageUp": { "All": ["PageUp"] }, "PageDown": { "All": ["PageDown"] }, "LeftStart": { "All": ["Home"] }, "RightEnd": { "All": ["End"] }, "Start": { "All": ["Ctrl+Home"] }, "End": { "All": ["Ctrl+End"] }, "LeftExtend": { "All": ["Shift+CursorLeft"] }, "RightExtend": { "All": ["Shift+CursorRight"] }, "UpExtend": { "All": ["Shift+CursorUp"] }, "DownExtend": { "All": ["Shift+CursorDown"] }, "PageUpExtend": { "All": ["Shift+PageUp"] }, "PageDownExtend": { "All": ["Shift+PageDown"] }, "LeftStartExtend": { "All": ["Shift+Home"] }, "RightEndExtend": { "All": ["Shift+End"] }, "StartExtend": { "All": ["Ctrl+Shift+Home"] }, "EndExtend": { "All": ["Ctrl+Shift+End"] }, "Copy": { "All": ["Ctrl+C"] }, "Cut": { "All": ["Ctrl+X"] }, "Paste": { "All": ["Ctrl+V"] }, "Undo": { "All": ["Ctrl+Z", "Ctrl+/"] }, "Redo": { "All": ["Ctrl+Y", "Ctrl+Shift+Z"] }, "SelectAll": { "All": ["Ctrl+A"] }, "DeleteCharLeft": { "All": ["Backspace"] }, "DeleteCharRight": { "All": ["Delete", "Ctrl+D"] } }, "View.ViewKeyBindings": { "TextField": { "CutToEndOfLine": { "All": ["Ctrl+K"] }, "KillWordRight": { "All": ["Ctrl+W"] } } }} \ No newline at end of file +{ + "$schema": "https://tui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json", + "Application": { + "DefaultKeyBindings": { + "Quit": { + "All": [ + "Esc", + "Ctrl+Q" + ] + }, + "Suspend": { + "All": [ + "Ctrl+Z" + ] + }, + "Arrange": { + "All": [ + "Ctrl+F5" + ] + }, + "NextTabStop": { + "All": [ + "Tab" + ] + }, + "PreviousTabStop": { + "All": [ + "Shift+Tab" + ] + }, + "NextTabGroup": { + "All": [ + "F6" + ] + }, + "PreviousTabGroup": { + "All": [ + "Shift+F6" + ] + }, + "Refresh": { + "All": [ + "F5" + ] + } + } + }, + "View": { + "DefaultKeyBindings": { + "Left": { + "All": [ + "CursorLeft" + ] + }, + "Right": { + "All": [ + "CursorRight" + ] + }, + "Up": { + "All": [ + "CursorUp" + ] + }, + "Down": { + "All": [ + "CursorDown" + ] + }, + "PageUp": { + "All": [ + "PageUp" + ] + }, + "PageDown": { + "All": [ + "PageDown" + ] + }, + "LeftStart": { + "All": [ + "Home" + ] + }, + "RightEnd": { + "All": [ + "End" + ] + }, + "Start": { + "All": [ + "Ctrl+Home" + ] + }, + "End": { + "All": [ + "Ctrl+End" + ] + }, + "LeftExtend": { + "All": [ + "Shift+CursorLeft" + ] + }, + "RightExtend": { + "All": [ + "Shift+CursorRight" + ] + }, + "UpExtend": { + "All": [ + "Shift+CursorUp" + ] + }, + "DownExtend": { + "All": [ + "Shift+CursorDown" + ] + }, + "PageUpExtend": { + "All": [ + "Shift+PageUp" + ] + }, + "PageDownExtend": { + "All": [ + "Shift+PageDown" + ] + }, + "LeftStartExtend": { + "All": [ + "Shift+Home" + ] + }, + "RightEndExtend": { + "All": [ + "Shift+End" + ] + }, + "StartExtend": { + "All": [ + "Ctrl+Shift+Home" + ] + }, + "EndExtend": { + "All": [ + "Ctrl+Shift+End" + ] + }, + "Copy": { + "All": [ + "Ctrl+C" + ] + }, + "Cut": { + "All": [ + "Ctrl+X" + ] + }, + "Paste": { + "All": [ + "Ctrl+V" + ] + }, + "Undo": { + "All": [ + "Ctrl+Z", + "Ctrl+/" + ] + }, + "Redo": { + "All": [ + "Ctrl+Y", + "Ctrl+Shift+Z" + ] + }, + "SelectAll": { + "All": [ + "Ctrl+A" + ] + }, + "DeleteCharLeft": { + "All": [ + "Backspace" + ] + }, + "DeleteCharRight": { + "All": [ + "Delete", + "Ctrl+D" + ] + } + }, + "ViewKeyBindings": { + "TextField": { + "CutToEndOfLine": { + "All": [ + "Ctrl+K" + ] + }, + "KillWordRight": { + "All": [ + "Ctrl+W" + ] + } + } + } + } +} \ No newline at end of file diff --git a/Config/windows.json b/Config/windows.json index 44df4d9..662ab86 100644 --- a/Config/windows.json +++ b/Config/windows.json @@ -1 +1,210 @@ -{ "$schema": "https://tui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json", "Application.DefaultKeyBindings": { "Quit": { "All": ["Esc"] }, "Arrange": { "All": ["Ctrl+F5"] }, "NextTabStop": { "All": ["Tab"] }, "PreviousTabStop": { "All": ["Shift+Tab"] }, "NextTabGroup": { "All": ["F6"] }, "PreviousTabGroup":{ "All": ["Shift+F6"] }, "Refresh": { "All": ["F5"] } }, "View.DefaultKeyBindings": { "Left": { "All": ["CursorLeft"] }, "Right": { "All": ["CursorRight"] }, "Up": { "All": ["CursorUp"] }, "Down": { "All": ["CursorDown"] }, "PageUp": { "All": ["PageUp"] }, "PageDown": { "All": ["PageDown"] }, "LeftStart": { "All": ["Home"] }, "RightEnd": { "All": ["End"] }, "Start": { "All": ["Ctrl+Home"] }, "End": { "All": ["Ctrl+End"] }, "LeftExtend": { "All": ["Shift+CursorLeft"] }, "RightExtend": { "All": ["Shift+CursorRight"] }, "UpExtend": { "All": ["Shift+CursorUp"] }, "DownExtend": { "All": ["Shift+CursorDown"] }, "PageUpExtend": { "All": ["Shift+PageUp"] }, "PageDownExtend": { "All": ["Shift+PageDown"] }, "LeftStartExtend": { "All": ["Shift+Home"] }, "RightEndExtend": { "All": ["Shift+End"] }, "StartExtend": { "All": ["Ctrl+Shift+Home"] }, "EndExtend": { "All": ["Ctrl+Shift+End"] }, "Copy": { "All": ["Ctrl+C"] }, "Cut": { "All": ["Ctrl+X"] }, "Paste": { "All": ["Ctrl+V"] }, "Undo": { "All": ["Ctrl+Z"] }, "Redo": { "All": ["Ctrl+Y"] }, "SelectAll": { "All": ["Ctrl+A"] }, "DeleteCharLeft": { "All": ["Backspace"] }, "DeleteCharRight": { "All": ["Delete"] } }, "View.ViewKeyBindings": { "TextField": { "WordLeft": { "All": ["Ctrl+CursorLeft"] }, "WordRight": { "All": ["Ctrl+CursorRight"] }, "WordLeftExtend": { "All": ["Ctrl+Shift+CursorLeft"] }, "WordRightExtend":{ "All": ["Ctrl+Shift+CursorRight"] } } }} \ No newline at end of file +{ + "$schema": "https://tui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json", + "Application": { + "DefaultKeyBindings": { + "Quit": { + "All": [ + "Esc" + ] + }, + "Arrange": { + "All": [ + "Ctrl+F5" + ] + }, + "NextTabStop": { + "All": [ + "Tab" + ] + }, + "PreviousTabStop": { + "All": [ + "Shift+Tab" + ] + }, + "NextTabGroup": { + "All": [ + "F6" + ] + }, + "PreviousTabGroup": { + "All": [ + "Shift+F6" + ] + }, + "Refresh": { + "All": [ + "F5" + ] + } + } + }, + "View": { + "DefaultKeyBindings": { + "Left": { + "All": [ + "CursorLeft" + ] + }, + "Right": { + "All": [ + "CursorRight" + ] + }, + "Up": { + "All": [ + "CursorUp" + ] + }, + "Down": { + "All": [ + "CursorDown" + ] + }, + "PageUp": { + "All": [ + "PageUp" + ] + }, + "PageDown": { + "All": [ + "PageDown" + ] + }, + "LeftStart": { + "All": [ + "Home" + ] + }, + "RightEnd": { + "All": [ + "End" + ] + }, + "Start": { + "All": [ + "Ctrl+Home" + ] + }, + "End": { + "All": [ + "Ctrl+End" + ] + }, + "LeftExtend": { + "All": [ + "Shift+CursorLeft" + ] + }, + "RightExtend": { + "All": [ + "Shift+CursorRight" + ] + }, + "UpExtend": { + "All": [ + "Shift+CursorUp" + ] + }, + "DownExtend": { + "All": [ + "Shift+CursorDown" + ] + }, + "PageUpExtend": { + "All": [ + "Shift+PageUp" + ] + }, + "PageDownExtend": { + "All": [ + "Shift+PageDown" + ] + }, + "LeftStartExtend": { + "All": [ + "Shift+Home" + ] + }, + "RightEndExtend": { + "All": [ + "Shift+End" + ] + }, + "StartExtend": { + "All": [ + "Ctrl+Shift+Home" + ] + }, + "EndExtend": { + "All": [ + "Ctrl+Shift+End" + ] + }, + "Copy": { + "All": [ + "Ctrl+C" + ] + }, + "Cut": { + "All": [ + "Ctrl+X" + ] + }, + "Paste": { + "All": [ + "Ctrl+V" + ] + }, + "Undo": { + "All": [ + "Ctrl+Z" + ] + }, + "Redo": { + "All": [ + "Ctrl+Y" + ] + }, + "SelectAll": { + "All": [ + "Ctrl+A" + ] + }, + "DeleteCharLeft": { + "All": [ + "Backspace" + ] + }, + "DeleteCharRight": { + "All": [ + "Delete" + ] + } + }, + "ViewKeyBindings": { + "TextField": { + "WordLeft": { + "All": [ + "Ctrl+CursorLeft" + ] + }, + "WordRight": { + "All": [ + "Ctrl+CursorRight" + ] + }, + "WordLeftExtend": { + "All": [ + "Ctrl+Shift+CursorLeft" + ] + }, + "WordRightExtend": { + "All": [ + "Ctrl+Shift+CursorRight" + ] + } + } + } + } +} \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index f36b966..7f4964c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,7 @@ true - + diff --git a/Example/Example.cs b/Example/Example.cs index 604739b..b0ff6f2 100644 --- a/Example/Example.cs +++ b/Example/Example.cs @@ -9,9 +9,11 @@ using Terminal.Gui.ViewBase; using Terminal.Gui.Views; -// Override the default configuration for the application to use the Amber Phosphor theme -ConfigurationManager.RuntimeConfig = """{ "Theme": "Amber Phosphor" }"""; -ConfigurationManager.Enable (ConfigLocations.All); +// Override the default configuration for the application to use the Amber Phosphor theme. +// Configuration is applied automatically at assembly load; setting RuntimeConfig and +// re-applying overlays the override on top. +TuiConfigurationBuilder.Shared.RuntimeConfig = """{ "Theme": "Amber Phosphor" }"""; +TuiConfigurationBuilder.Shared.ApplyToStaticFacades (); IApplication app = Application.Create ().Init (); diff --git a/Examples.sln b/Examples.sln index eb4e515..545e064 100644 --- a/Examples.sln +++ b/Examples.sln @@ -27,56 +27,170 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WideCharRepro", "WideCharRe EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Examples.SmokeTests", "tests\Examples.SmokeTests\Examples.SmokeTests.csproj", "{A1B2C3D4-000D-0000-0000-00000000000D}" EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharpExample", "FSharpExample\FSharpExample.fsproj", "{167437AD-7BE5-46D7-8BC5-A0AB37DB2C6C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {A1B2C3D4-0001-0000-0000-000000000001}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1B2C3D4-0001-0000-0000-000000000001}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-0001-0000-0000-000000000001}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1B2C3D4-0001-0000-0000-000000000001}.Debug|x64.Build.0 = Debug|Any CPU + {A1B2C3D4-0001-0000-0000-000000000001}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1B2C3D4-0001-0000-0000-000000000001}.Debug|x86.Build.0 = Debug|Any CPU {A1B2C3D4-0001-0000-0000-000000000001}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-0001-0000-0000-000000000001}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-0001-0000-0000-000000000001}.Release|x64.ActiveCfg = Release|Any CPU + {A1B2C3D4-0001-0000-0000-000000000001}.Release|x64.Build.0 = Release|Any CPU + {A1B2C3D4-0001-0000-0000-000000000001}.Release|x86.ActiveCfg = Release|Any CPU + {A1B2C3D4-0001-0000-0000-000000000001}.Release|x86.Build.0 = Release|Any CPU {A1B2C3D4-0002-0000-0000-000000000002}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1B2C3D4-0002-0000-0000-000000000002}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-0002-0000-0000-000000000002}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1B2C3D4-0002-0000-0000-000000000002}.Debug|x64.Build.0 = Debug|Any CPU + {A1B2C3D4-0002-0000-0000-000000000002}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1B2C3D4-0002-0000-0000-000000000002}.Debug|x86.Build.0 = Debug|Any CPU {A1B2C3D4-0002-0000-0000-000000000002}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-0002-0000-0000-000000000002}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-0002-0000-0000-000000000002}.Release|x64.ActiveCfg = Release|Any CPU + {A1B2C3D4-0002-0000-0000-000000000002}.Release|x64.Build.0 = Release|Any CPU + {A1B2C3D4-0002-0000-0000-000000000002}.Release|x86.ActiveCfg = Release|Any CPU + {A1B2C3D4-0002-0000-0000-000000000002}.Release|x86.Build.0 = Release|Any CPU {A1B2C3D4-0003-0000-0000-000000000003}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1B2C3D4-0003-0000-0000-000000000003}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-0003-0000-0000-000000000003}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1B2C3D4-0003-0000-0000-000000000003}.Debug|x64.Build.0 = Debug|Any CPU + {A1B2C3D4-0003-0000-0000-000000000003}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1B2C3D4-0003-0000-0000-000000000003}.Debug|x86.Build.0 = Debug|Any CPU {A1B2C3D4-0003-0000-0000-000000000003}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-0003-0000-0000-000000000003}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-0003-0000-0000-000000000003}.Release|x64.ActiveCfg = Release|Any CPU + {A1B2C3D4-0003-0000-0000-000000000003}.Release|x64.Build.0 = Release|Any CPU + {A1B2C3D4-0003-0000-0000-000000000003}.Release|x86.ActiveCfg = Release|Any CPU + {A1B2C3D4-0003-0000-0000-000000000003}.Release|x86.Build.0 = Release|Any CPU {A1B2C3D4-0004-0000-0000-000000000004}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1B2C3D4-0004-0000-0000-000000000004}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-0004-0000-0000-000000000004}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1B2C3D4-0004-0000-0000-000000000004}.Debug|x64.Build.0 = Debug|Any CPU + {A1B2C3D4-0004-0000-0000-000000000004}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1B2C3D4-0004-0000-0000-000000000004}.Debug|x86.Build.0 = Debug|Any CPU {A1B2C3D4-0004-0000-0000-000000000004}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-0004-0000-0000-000000000004}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-0004-0000-0000-000000000004}.Release|x64.ActiveCfg = Release|Any CPU + {A1B2C3D4-0004-0000-0000-000000000004}.Release|x64.Build.0 = Release|Any CPU + {A1B2C3D4-0004-0000-0000-000000000004}.Release|x86.ActiveCfg = Release|Any CPU + {A1B2C3D4-0004-0000-0000-000000000004}.Release|x86.Build.0 = Release|Any CPU {A1B2C3D4-0005-0000-0000-000000000005}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1B2C3D4-0005-0000-0000-000000000005}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-0005-0000-0000-000000000005}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1B2C3D4-0005-0000-0000-000000000005}.Debug|x64.Build.0 = Debug|Any CPU + {A1B2C3D4-0005-0000-0000-000000000005}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1B2C3D4-0005-0000-0000-000000000005}.Debug|x86.Build.0 = Debug|Any CPU {A1B2C3D4-0005-0000-0000-000000000005}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-0005-0000-0000-000000000005}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-0005-0000-0000-000000000005}.Release|x64.ActiveCfg = Release|Any CPU + {A1B2C3D4-0005-0000-0000-000000000005}.Release|x64.Build.0 = Release|Any CPU + {A1B2C3D4-0005-0000-0000-000000000005}.Release|x86.ActiveCfg = Release|Any CPU + {A1B2C3D4-0005-0000-0000-000000000005}.Release|x86.Build.0 = Release|Any CPU {A1B2C3D4-0006-0000-0000-000000000006}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1B2C3D4-0006-0000-0000-000000000006}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-0006-0000-0000-000000000006}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1B2C3D4-0006-0000-0000-000000000006}.Debug|x64.Build.0 = Debug|Any CPU + {A1B2C3D4-0006-0000-0000-000000000006}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1B2C3D4-0006-0000-0000-000000000006}.Debug|x86.Build.0 = Debug|Any CPU {A1B2C3D4-0006-0000-0000-000000000006}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-0006-0000-0000-000000000006}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-0006-0000-0000-000000000006}.Release|x64.ActiveCfg = Release|Any CPU + {A1B2C3D4-0006-0000-0000-000000000006}.Release|x64.Build.0 = Release|Any CPU + {A1B2C3D4-0006-0000-0000-000000000006}.Release|x86.ActiveCfg = Release|Any CPU + {A1B2C3D4-0006-0000-0000-000000000006}.Release|x86.Build.0 = Release|Any CPU {A1B2C3D4-0007-0000-0000-000000000007}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1B2C3D4-0007-0000-0000-000000000007}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-0007-0000-0000-000000000007}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1B2C3D4-0007-0000-0000-000000000007}.Debug|x64.Build.0 = Debug|Any CPU + {A1B2C3D4-0007-0000-0000-000000000007}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1B2C3D4-0007-0000-0000-000000000007}.Debug|x86.Build.0 = Debug|Any CPU {A1B2C3D4-0007-0000-0000-000000000007}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-0007-0000-0000-000000000007}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-0007-0000-0000-000000000007}.Release|x64.ActiveCfg = Release|Any CPU + {A1B2C3D4-0007-0000-0000-000000000007}.Release|x64.Build.0 = Release|Any CPU + {A1B2C3D4-0007-0000-0000-000000000007}.Release|x86.ActiveCfg = Release|Any CPU + {A1B2C3D4-0007-0000-0000-000000000007}.Release|x86.Build.0 = Release|Any CPU {A1B2C3D4-0008-0000-0000-000000000008}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1B2C3D4-0008-0000-0000-000000000008}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-0008-0000-0000-000000000008}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1B2C3D4-0008-0000-0000-000000000008}.Debug|x64.Build.0 = Debug|Any CPU + {A1B2C3D4-0008-0000-0000-000000000008}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1B2C3D4-0008-0000-0000-000000000008}.Debug|x86.Build.0 = Debug|Any CPU {A1B2C3D4-0008-0000-0000-000000000008}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-0008-0000-0000-000000000008}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-0008-0000-0000-000000000008}.Release|x64.ActiveCfg = Release|Any CPU + {A1B2C3D4-0008-0000-0000-000000000008}.Release|x64.Build.0 = Release|Any CPU + {A1B2C3D4-0008-0000-0000-000000000008}.Release|x86.ActiveCfg = Release|Any CPU + {A1B2C3D4-0008-0000-0000-000000000008}.Release|x86.Build.0 = Release|Any CPU {A1B2C3D4-0009-0000-0000-000000000009}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1B2C3D4-0009-0000-0000-000000000009}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-0009-0000-0000-000000000009}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1B2C3D4-0009-0000-0000-000000000009}.Debug|x64.Build.0 = Debug|Any CPU + {A1B2C3D4-0009-0000-0000-000000000009}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1B2C3D4-0009-0000-0000-000000000009}.Debug|x86.Build.0 = Debug|Any CPU {A1B2C3D4-0009-0000-0000-000000000009}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-0009-0000-0000-000000000009}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-0009-0000-0000-000000000009}.Release|x64.ActiveCfg = Release|Any CPU + {A1B2C3D4-0009-0000-0000-000000000009}.Release|x64.Build.0 = Release|Any CPU + {A1B2C3D4-0009-0000-0000-000000000009}.Release|x86.ActiveCfg = Release|Any CPU + {A1B2C3D4-0009-0000-0000-000000000009}.Release|x86.Build.0 = Release|Any CPU {A1B2C3D4-000A-0000-0000-00000000000A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1B2C3D4-000A-0000-0000-00000000000A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-000A-0000-0000-00000000000A}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1B2C3D4-000A-0000-0000-00000000000A}.Debug|x64.Build.0 = Debug|Any CPU + {A1B2C3D4-000A-0000-0000-00000000000A}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1B2C3D4-000A-0000-0000-00000000000A}.Debug|x86.Build.0 = Debug|Any CPU {A1B2C3D4-000A-0000-0000-00000000000A}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-000A-0000-0000-00000000000A}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-000A-0000-0000-00000000000A}.Release|x64.ActiveCfg = Release|Any CPU + {A1B2C3D4-000A-0000-0000-00000000000A}.Release|x64.Build.0 = Release|Any CPU + {A1B2C3D4-000A-0000-0000-00000000000A}.Release|x86.ActiveCfg = Release|Any CPU + {A1B2C3D4-000A-0000-0000-00000000000A}.Release|x86.Build.0 = Release|Any CPU {A1B2C3D4-000B-0000-0000-00000000000B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1B2C3D4-000B-0000-0000-00000000000B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-000B-0000-0000-00000000000B}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1B2C3D4-000B-0000-0000-00000000000B}.Debug|x64.Build.0 = Debug|Any CPU + {A1B2C3D4-000B-0000-0000-00000000000B}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1B2C3D4-000B-0000-0000-00000000000B}.Debug|x86.Build.0 = Debug|Any CPU {A1B2C3D4-000B-0000-0000-00000000000B}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-000B-0000-0000-00000000000B}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-000B-0000-0000-00000000000B}.Release|x64.ActiveCfg = Release|Any CPU + {A1B2C3D4-000B-0000-0000-00000000000B}.Release|x64.Build.0 = Release|Any CPU + {A1B2C3D4-000B-0000-0000-00000000000B}.Release|x86.ActiveCfg = Release|Any CPU + {A1B2C3D4-000B-0000-0000-00000000000B}.Release|x86.Build.0 = Release|Any CPU + {A1B2C3D4-000D-0000-0000-00000000000D}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1B2C3D4-000D-0000-0000-00000000000D}.Debug|x64.Build.0 = Debug|Any CPU + {A1B2C3D4-000D-0000-0000-00000000000D}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1B2C3D4-000D-0000-0000-00000000000D}.Debug|x86.Build.0 = Debug|Any CPU + {A1B2C3D4-000D-0000-0000-00000000000D}.Release|x64.ActiveCfg = Release|Any CPU + {A1B2C3D4-000D-0000-0000-00000000000D}.Release|x64.Build.0 = Release|Any CPU + {A1B2C3D4-000D-0000-0000-00000000000D}.Release|x86.ActiveCfg = Release|Any CPU + {A1B2C3D4-000D-0000-0000-00000000000D}.Release|x86.Build.0 = Release|Any CPU + {167437AD-7BE5-46D7-8BC5-A0AB37DB2C6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {167437AD-7BE5-46D7-8BC5-A0AB37DB2C6C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {167437AD-7BE5-46D7-8BC5-A0AB37DB2C6C}.Debug|x64.ActiveCfg = Debug|Any CPU + {167437AD-7BE5-46D7-8BC5-A0AB37DB2C6C}.Debug|x64.Build.0 = Debug|Any CPU + {167437AD-7BE5-46D7-8BC5-A0AB37DB2C6C}.Debug|x86.ActiveCfg = Debug|Any CPU + {167437AD-7BE5-46D7-8BC5-A0AB37DB2C6C}.Debug|x86.Build.0 = Debug|Any CPU + {167437AD-7BE5-46D7-8BC5-A0AB37DB2C6C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {167437AD-7BE5-46D7-8BC5-A0AB37DB2C6C}.Release|Any CPU.Build.0 = Release|Any CPU + {167437AD-7BE5-46D7-8BC5-A0AB37DB2C6C}.Release|x64.ActiveCfg = Release|Any CPU + {167437AD-7BE5-46D7-8BC5-A0AB37DB2C6C}.Release|x64.Build.0 = Release|Any CPU + {167437AD-7BE5-46D7-8BC5-A0AB37DB2C6C}.Release|x86.ActiveCfg = Release|Any CPU + {167437AD-7BE5-46D7-8BC5-A0AB37DB2C6C}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/FSharpExample/Program.fs b/FSharpExample/Program.fs index d68a0c8..8d358bd 100644 --- a/FSharpExample/Program.fs +++ b/FSharpExample/Program.fs @@ -1,48 +1,65 @@ -open Terminal.Gui +// A simple Terminal.Gui example in F#. +// For the full range of functionality see the UICatalog project in the Terminal.Gui repo. + +open Terminal.Gui.App +open Terminal.Gui.Input +open Terminal.Gui.ViewBase +open Terminal.Gui.Views + +// Defines a top-level window with border and title +type ExampleWindow () as this = + inherit Window () -type ExampleWindow() as this = - inherit Window() - do this.Title <- sprintf "Example App (%O to quit)" (Application.GetDefaultKey (Command.Quit)) // Create input components and labels - let usernameLabel = new Label(Text = "Username:") + let usernameLabel = new Label (Text = "Username:") - let userNameText = new TextField(X = Pos.Right(usernameLabel) + Pos.op_Implicit(1), Width = Dim.Fill()) + let userNameText = + new TextField (X = Pos.Right (usernameLabel) + Pos.op_Implicit (1), Width = Dim.Fill ()) - let passwordLabel = new Label(Text = "Password:", X = Pos.Left(usernameLabel), Y = Pos.Bottom(usernameLabel) + Pos.op_Implicit(1)) + let passwordLabel = + new Label (Text = "Password:", X = Pos.Left (usernameLabel), Y = Pos.Bottom (usernameLabel) + Pos.op_Implicit (1)) - let passwordText = new TextField(Secret = true, X = Pos.Left(userNameText), Y = Pos.Top(passwordLabel), Width = Dim.Fill()) + let passwordText = + new TextField (Secret = true, X = Pos.Left (userNameText), Y = Pos.Top (passwordLabel), Width = Dim.Fill ()) // Create login button - let btnLogin = new Button(Text = "Login", Y = Pos.Bottom(passwordLabel) + Pos.op_Implicit(1), X = Pos.Center(), IsDefault = true) + let btnLogin = + new Button (Text = "Login", Y = Pos.Bottom (passwordLabel) + Pos.op_Implicit (1), X = Pos.Center (), IsDefault = true) // When login button is clicked display a message popup - btnLogin.Accepting.Add(fun _ -> - if userNameText.Text = "admin" && passwordText.Text = "password" then - MessageBox.Query("Logging In", "Login Successful", "Ok") |> ignore - ExampleWindow.UserName <- userNameText.Text.ToString() - Application.RequestStop() - else - MessageBox.ErrorQuery("Logging In", "Incorrect username or password", "Ok") |> ignore - ) + btnLogin.Accepting.Add (fun e -> + match this.App with + | null -> () + | app -> + if userNameText.Text = "admin" && passwordText.Text = "password" then + MessageBox.Query (app, "Logging In", "Login Successful", "Ok") |> ignore + ExampleWindow.UserName <- userNameText.Text + app.RequestStop () + else + MessageBox.ErrorQuery (app, "Logging In", "Incorrect username or password", "Ok") |> ignore + + // When Accepting is handled, set e.Handled to true to prevent further processing. + e.Handled <- true) // Add the views to the Window - this.Add(usernameLabel, userNameText, passwordLabel, passwordText, btnLogin) + this.Add (usernameLabel, userNameText, passwordLabel, passwordText, btnLogin) static member val UserName = "" with get, set [] let main argv = - Application.Init() - Application.Run().Dispose() - - // Before the application exits, reset Terminal.Gui for clean shutdown - Application.Shutdown() - - // To see this output on the screen it must be done after shutdown, + // Configuration (themes, schemes, settings) is applied automatically at assembly load. + let app = Application.Create().Init () + app.Run () |> ignore + + // Dispose the application to free resources and restore the previous screen + app.Dispose () + + // To see this output on the screen it must be done after Dispose, // which restores the previous screen. printfn "Username: %s" ExampleWindow.UserName - + 0 // return an integer exit code diff --git a/FSharpExample/README.md b/FSharpExample/README.md index d1a3547..bd24a78 100644 --- a/FSharpExample/README.md +++ b/FSharpExample/README.md @@ -1,8 +1,15 @@ # F# Example -> **⚠️ NOT YET PORTED TO v2** -> -> This example uses the Terminal.Gui v1 API and needs to be rewritten -> for the v2 API. It is excluded from the solution build until ported. -> -> See the C# examples for v2 API patterns. \ No newline at end of file +A simple Terminal.Gui v2 login window written in F#, mirroring the C# `Example` project. + +```bash +cd FSharpExample +dotnet run +``` + +Notes for F# consumers of Terminal.Gui v2: + +- Open the v2 namespaces (`Terminal.Gui.App`, `Terminal.Gui.Input`, `Terminal.Gui.ViewBase`, `Terminal.Gui.Views`) — there is no flat `Terminal.Gui` namespace. +- `Pos` arithmetic with integers needs an explicit conversion: `Pos.Right (label) + Pos.op_Implicit (1)`. +- With `enable`, F# 9 nullness checking applies to Terminal.Gui's annotations — match nullable members like `View.App` against `null` before use. +- Configuration (themes, schemes, settings) is applied automatically at assembly load. diff --git a/PromptExample/Program.cs b/PromptExample/Program.cs index a76394f..ef6e47c 100644 --- a/PromptExample/Program.cs +++ b/PromptExample/Program.cs @@ -4,7 +4,6 @@ // NOTE: predictable exit codes, and full keyboard/mouse support. Works for humans and AI agents alike. using Terminal.Gui.App; -using Terminal.Gui.Configuration; using Terminal.Gui.Drawing; using Terminal.Gui.Drivers; using Terminal.Gui.Editor; @@ -19,14 +18,13 @@ if (smokeTest) { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication smokeApp = Application.Create ().Init (); Console.WriteLine ("Smoke test passed."); return; } -ConfigurationManager.Enable (ConfigLocations.All); +// Configuration (themes, schemes, settings) is applied automatically at assembly load. using IApplication app = Application.Create ().Init (DriverRegistry.Names.DOTNET); // Create a main window to host the prompts diff --git a/ReactiveExample/LoginView.cs b/ReactiveExample/LoginView.cs index ef6c96f..29dee5d 100644 --- a/ReactiveExample/LoginView.cs +++ b/ReactiveExample/LoginView.cs @@ -49,9 +49,10 @@ public LoginView (LoginViewModel viewModel) .BindTo (unInput, x => x.Text) .DisposeWith (_disposable); - unInput - .Events () - .TextChanged + // TextField hides View.TextChanging with a different delegate type, which the + // ObservableEvents source generator cannot wrap; subscribe via FromEventPattern. + Observable + .FromEventPattern (h => unInput.TextChanged += h, h => unInput.TextChanged -= h) .Select (_ => unInput.Text) .DistinctUntilChanged () .BindTo (ViewModel, x => x.Username) @@ -80,9 +81,8 @@ public LoginView (LoginViewModel viewModel) .BindTo (pwInput, x => x.Text) .DisposeWith (_disposable); - pwInput - .Events () - .TextChanged + Observable + .FromEventPattern (h => pwInput.TextChanged += h, h => pwInput.TextChanged -= h) .Select (_ => pwInput.Text) .DistinctUntilChanged () .BindTo (ViewModel, x => x.Password) diff --git a/ReactiveExample/Program.cs b/ReactiveExample/Program.cs index 3fecafc..06979ff 100644 --- a/ReactiveExample/Program.cs +++ b/ReactiveExample/Program.cs @@ -2,7 +2,6 @@ using System.Reactive.Concurrency; using ReactiveUI.Builder; using Terminal.Gui.App; -using Terminal.Gui.Configuration; namespace ReactiveExample; @@ -14,7 +13,7 @@ private static async Task Main (string[] args) { var smokeTest = args.Length > 0 && args [0] == "--smoke-test"; - ConfigurationManager.Enable (ConfigLocations.All); + // Configuration (themes, schemes, settings) is applied automatically at assembly load. using IApplication app = Application.Create (); app.Init (); _rxApp = RxAppBuilder.CreateReactiveUIBuilder (); diff --git a/ReactiveExample/README.md b/ReactiveExample/README.md index c844d9e..0e618cb 100644 --- a/ReactiveExample/README.md +++ b/ReactiveExample/README.md @@ -1 +1,53 @@ -This is a sample app that shows how to use `System.Reactive` and `ReactiveUI` with `Terminal.Gui`. The app uses the MVVM architecture that may seem familiar to folks coming from WPF, Xamarin Forms, UWP, Avalonia, or Windows Forms. In this app, we implement the data bindings using ReactiveUI `WhenAnyValue` syntax and [ObservableEvents](https://github.com/reactivemarbles/ObservableEvents) ΓÇö a Source Generator that turns events into observable wrappers.### SchedulingIn order to use reactive extensions scheduling, copy-paste the `TerminalScheduler.cs` file into your project, and add the following lines to the composition root of your `Terminal.Gui` application:```csConfigurationManager.Enable (ConfigLocations.All);using IApplication app = Application.Create ();app.Init ();RxApp.MainThreadScheduler = new TerminalScheduler (app);RxApp.TaskpoolScheduler = TaskPoolScheduler.Default;var loginView = new LoginView (new ());app.Run (loginView);loginView.Dispose ();```From now on, you can use `.ObserveOn(RxApp.MainThreadScheduler)` to return to the main loop from a background thread. This is useful when you have a `IObservable` updated from a background thread, and you wish to update the UI with `TValue`s received from that observable.### Data BindingsIf you wish to implement `OneWay` data binding, then use the `WhenAnyValue` [ReactiveUI extension method](https://www.reactiveui.net../docs/handbook/when-any/) that listens to `INotifyPropertyChanged` events of the specified property, and converts that events into `IObservable`:```cs// 'usernameInput' is 'TextField' ViewModel .WhenAnyValue (x => x.Username) .BindTo (usernameInput, x => x.Text);```Note that your view model should implement `INotifyPropertyChanged` or inherit from a `ReactiveObject`. If you wish to implement `OneWayToSource` data binding, then install [Pharmacist.MSBuild](https://github.com/reactiveui/pharmacist) into your project and listen to e.g. `TextChanged` event of a `TextField`:```cs// 'usernameInput' is 'TextField'usernameInput .Events () // The Events() extension is generated by Pharmacist. .TextChanged .Select (old => usernameInput.Text) .DistinctUntilChanged () .BindTo (ViewModel, x => x.Username);```If you combine `OneWay` and `OneWayToSource` data bindings, you get `TwoWay` data binding. Also be sure to use the `string` type instead of the `string` type. Invoking commands should be as simple as this:```cs// 'clearButton' is 'Button'clearButton .Events () .Accepting .InvokeCommand (ViewModel, x => x.Clear);``` \ No newline at end of file +This is a sample app that shows how to use `System.Reactive` and `ReactiveUI` with `Terminal.Gui`. The app uses the MVVM architecture that may seem familiar to folks coming from WPF, Xamarin Forms, UWP, Avalonia, or Windows Forms. In this app, we implement the data bindings using ReactiveUI `WhenAnyValue` syntax and [ObservableEvents](https://github.com/reactivemarbles/ObservableEvents) — a Source Generator that turns events into observable wrappers. + + + +### Scheduling + +In order to use reactive extensions scheduling, copy-paste the `TerminalScheduler.cs` file into your project, and add the following lines to the composition root of your `Terminal.Gui` application: + +```cs +// Configuration (themes, schemes, settings) is applied automatically at assembly load. +using IApplication app = Application.Create (); +app.Init (); +RxApp.MainThreadScheduler = new TerminalScheduler (app); +RxApp.TaskpoolScheduler = TaskPoolScheduler.Default; + +LoginView loginView = new (new ()); +app.Run (loginView); +loginView.Dispose (); +``` + +From now on, you can use `.ObserveOn(RxApp.MainThreadScheduler)` to return to the main loop from a background thread. This is useful when you have a `IObservable` updated from a background thread, and you wish to update the UI with `TValue`s received from that observable. + +### Data Bindings + +If you wish to implement `OneWay` data binding, then use the `WhenAnyValue` [ReactiveUI extension method](https://www.reactiveui.net/docs/handbook/when-any/) that listens to `INotifyPropertyChanged` events of the specified property, and converts that events into `IObservable`: + +```cs +// 'usernameInput' is 'TextField' +ViewModel + .WhenAnyValue (x => x.Username) + .BindTo (usernameInput, x => x.Text); +``` + +Note that your view model should implement `INotifyPropertyChanged` or inherit from a `ReactiveObject`. If you wish to implement `OneWayToSource` data binding, listen to e.g. the `TextChanged` event of a `TextField` via `Observable.FromEventPattern` (the generated `.Events ()` wrappers work too, but as of Terminal.Gui 2.5 the ObservableEvents source generator cannot wrap `TextField`, which hides `View.TextChanging` with a different delegate type): + +```cs +// 'usernameInput' is 'TextField' +Observable + .FromEventPattern (h => usernameInput.TextChanged += h, h => usernameInput.TextChanged -= h) + .Select (_ => usernameInput.Text) + .DistinctUntilChanged () + .BindTo (ViewModel, x => x.Username); +``` + +If you combine `OneWay` and `OneWayToSource` data bindings, you get `TwoWay` data binding. Invoking commands should be as simple as this: + +```cs +// 'clearButton' is 'Button' +clearButton + .Events () + .Accepting + .InvokeCommand (ViewModel, x => x.Clear); +``` diff --git a/SelfContained/Program.cs b/SelfContained/Program.cs index ff861d5..f3fb8d4 100644 --- a/SelfContained/Program.cs +++ b/SelfContained/Program.cs @@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using Terminal.Gui.App; -using Terminal.Gui.Configuration; using Terminal.Gui.Drawing; using Terminal.Gui.Input; using Terminal.Gui.ViewBase; @@ -20,7 +19,6 @@ private static async Task Main (string[] args) if (smokeTest) { - ConfigurationManager.Enable (ConfigLocations.All); Application.AppModel = AppModel.Inline; IApplication app = Application.Create (); app.Init (); @@ -42,7 +40,7 @@ private static async Task Main (string[] args) [RequiresUnreferencedCode ("Calls Terminal.Gui.Application.Run(Func, IDriver)")] private static void Run () { - ConfigurationManager.Enable (ConfigLocations.All); + // Configuration (themes, schemes, settings) is applied automatically at assembly load. // Use Inline mode — renders below the shell prompt without alternate screen buffer Application.AppModel = AppModel.Inline; diff --git a/SelfContained/README.md b/SelfContained/README.md index af596e8..09f085d 100644 --- a/SelfContained/README.md +++ b/SelfContained/README.md @@ -1 +1,34 @@ -# Terminal.Gui C# SelfContainedThis project aims to test the `Terminal.Gui` library to create a simple `self-contained` `single-file` GUI application in C#, ensuring that all its features are available.## Modern Terminal.Gui APIThis example uses the modern Terminal.Gui application model:```csharpConfigurationManager.Enable (ConfigLocations.All);IApplication app = Application.Create ();app.Init ();using ExampleWindow exampleWindow = new ();string? userName = app.Run (exampleWindow) as string;app.Dispose ();Console.WriteLine ($@"Username: {userName}");```Key aspects of the modern model:- Use `Application.Create()` to create an `IApplication` instance- Call `app.Init()` to initialize the application- Use `app.Run(view)` to run views with proper resource management- Call `app.Dispose()` to clean up resources and restore the terminal- Event handling uses `Accepting` event instead of legacy `Accept` event- Set `e.Handled = true` in event handlers to prevent further processingWith `Debug` the `.csproj` is used and with `Release` the latest `nuget package` is used, either in `Solution Configurations` or in `Profile Publish`.To publish the self-contained single file in `Debug` or `Release` mode, it is not necessary to select it in the `Solution Configurations`, just choose the `Debug` or `Release` configuration in the `Publish Profile`.When executing the file directly from the self-contained single file and needing to debug it, it will be necessary to attach it to the debugger, just like any other standalone application. However, when trying to attach the file running on `Linux` or `macOS` to the debugger, it will issue the error "`Failed to attach to process: Unknown Error: 0x80131c3c`". This issue has already been reported on [Developer Community](https://developercommunity.visualstudio.com/t/Failed-to-attach-to-process:-Unknown-Err/10694351). Maybe it would be a good idea to vote in favor of this fix because I think `Visual Studio for macOS` is going to be discontinued and we need this fix to remotely attach a process running on `Linux` or `macOS` to `Windows 11`. \ No newline at end of file +# Terminal.Gui C# SelfContained + +This project aims to test the `Terminal.Gui` library to create a simple `self-contained` `single-file` GUI application in C#, ensuring that all its features are available. + +## Modern Terminal.Gui API + +This example uses the modern Terminal.Gui application model: + +```csharp +// Configuration (themes, schemes, settings) is applied automatically at assembly load. +IApplication app = Application.Create (); +app.Init (); + +using ExampleWindow exampleWindow = new (); +string? userName = app.Run (exampleWindow) as string; + +app.Dispose (); +Console.WriteLine ($@"Username: {userName}"); +``` + +Key aspects of the modern model: + +- Use `Application.Create()` to create an `IApplication` instance +- Call `app.Init()` to initialize the application +- Use `app.Run(view)` to run views with proper resource management +- Call `app.Dispose()` to clean up resources and restore the terminal +- Event handling uses `Accepting` event instead of legacy `Accept` event +- Set `e.Handled = true` in event handlers to prevent further processing + +With `Debug` the `.csproj` is used and with `Release` the latest `nuget package` is used, either in `Solution Configurations` or in `Profile Publish`. + +To publish the self-contained single file in `Debug` or `Release` mode, it is not necessary to select it in the `Solution Configurations`, just choose the `Debug` or `Release` configuration in the `Publish Profile`. + +When executing the file directly from the self-contained single file and needing to debug it, it will be necessary to attach it to the debugger, just like any other standalone application. However, when trying to attach the file running on `Linux` or `macOS` to the debugger, it will issue the error "`Failed to attach to process: Unknown Error: 0x80131c3c`". This issue has already been reported on [Developer Community](https://developercommunity.visualstudio.com/t/Failed-to-attach-to-process:-Unknown-Err/10694351). Maybe it would be a good idea to vote in favor of this fix because I think `Visual Studio for macOS` is going to be discontinued and we need this fix to remotely attach a process running on `Linux` or `macOS` to `Windows 11`. diff --git a/ShortcutTest/ShortcutTest.cs b/ShortcutTest/ShortcutTest.cs index 259e7fa..18c4f0d 100644 --- a/ShortcutTest/ShortcutTest.cs +++ b/ShortcutTest/ShortcutTest.cs @@ -3,14 +3,12 @@ using System.Collections.ObjectModel; using Terminal.Gui.App; -using Terminal.Gui.Configuration; using Terminal.Gui.Drawing; using Terminal.Gui.Input; using Terminal.Gui.ViewBase; using Terminal.Gui.Views; -ConfigurationManager.Enable (ConfigLocations.All); - +// Configuration (themes, schemes, settings) is applied automatically at assembly load. using IApplication app = Application.Create ().Init (); var smokeTest = args.Length > 0 && args [0] == "--smoke-test"; diff --git a/Themes/code-dark.config.json b/Themes/code-dark.config.json index 71a1dbc..97a9842 100644 --- a/Themes/code-dark.config.json +++ b/Themes/code-dark.config.json @@ -1 +1,21 @@ -{ "$schema": "https://tui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json", "Themes": [ { "Dark": { "Schemes": [ { "Base": { "CodeKeyword": { "Foreground": "#ff79c6", "Background": "None", "Style": "Bold" }, "CodeString": { "Foreground": "#f1fa8c", "Background": "None", "Style": "None" } } } ] } } ]} \ No newline at end of file +{ + "$schema": "https://tui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json", + "Themes": { + "Dark": { + "Schemes": { + "Base": { + "CodeKeyword": { + "Foreground": "#ff79c6", + "Background": "None", + "Style": "Bold" + }, + "CodeString": { + "Foreground": "#f1fa8c", + "Background": "None", + "Style": "None" + } + } + } + } + } +} \ No newline at end of file From 29fdea4375ab0d1c7e4982bb60ed19168101d723 Mon Sep 17 00:00:00 2001 From: Tig Date: Sat, 22 Aug 2026 17:51:09 -0600 Subject: [PATCH 2/2] Address code-review findings: honest key-binding docs, minimal overlays, F# smoke test. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Config/macos.json and windows.json: trim to only the commands they actually change (2.5's per-command overlay keeps defaults for omitted commands); drop windows.json's Quit row (it restated the compile-time default). - Config/README.md: correct the default-binding tables against the 2.5 source (Quit is Esc on all platforms; DeleteCharRight includes Ctrl+D by default), and document that a binding cannot be removed via config — windows.json cannot disable Suspend; use Application.RemoveDefaultKeyBinding in code. - Examples.sln: revert the auto-generated x64/x86 platform churn (~120 lines); add FSharpExample and the previously missing Examples.SmokeTests rows as plain Any CPU entries, so solution builds stop silently skipping the tests. - SelfContained/README.md: snippet now matches the code (Accepted event, and the AppModel.Inline line the example exists to demonstrate). - ReactiveExample/README.md: scope the .Events () caveat correctly (works for other views, never for TextField) and show the IValue ValueChanged binding. - ReactiveExample/LoginView.cs: replace the two duplicated FromEventPattern pipelines with one ObserveText helper over IValue.ValueChanged. - FSharpExample: add a --smoke-test mode and cover it in Examples.SmokeTests (now 10 tests, all passing); fix a space-before-parens miss. The 2.5.0-develop.* floating version is intentionally left as the merge gate until Terminal.Gui#5416 publishes packages. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01NGq38GbuZPAEa5wZHkmPiZ --- Config/README.md | 17 +- Config/macos.json | 201 +---------------- Config/windows.json | 208 +----------------- Examples.sln | 112 +--------- FSharpExample/Program.fs | 27 ++- ReactiveExample/LoginView.cs | 20 +- ReactiveExample/README.md | 9 +- SelfContained/README.md | 8 +- .../Examples.SmokeTests/ExampleSmokeTests.cs | 1 + 9 files changed, 72 insertions(+), 531 deletions(-) diff --git a/Config/README.md b/Config/README.md index 3cdc739..820a153 100644 --- a/Config/README.md +++ b/Config/README.md @@ -17,6 +17,8 @@ On Windows `~` expands to `C:\Users\`. On macOS/Linux `~` expands to ` As of Terminal.Gui 2.5, configuration is loaded via Microsoft.Extensions.Configuration (`TuiConfigurationBuilder`) and uses **nested objects**, not dotted keys. Configuration is applied automatically at assembly load — `ConfigurationManager.Enable ()` no longer exists. +Bindings overlay **per command**: a command you set replaces that command's default binding entirely (include every key you want active for it), while commands you omit keep their compile-time defaults. That is why these files only list the commands they actually change. + To convert a pre-2.5 config (dotted keys, `Themes`/`Schemes` arrays), run the migrator from the Terminal.Gui repo: ```bash @@ -27,6 +29,8 @@ See [Migrating ConfigurationManager to TuiConfigurationBuilder](https://github.c ## What Each File Changes +For reference, the compile-time defaults are: Quit = `Esc` (all platforms); Suspend = `Ctrl+Z` (macOS/Linux only); Undo = `Ctrl+Z` everywhere plus `Ctrl+/` on macOS/Linux; Redo = `Ctrl+Y` everywhere plus `Ctrl+Shift+Z` on macOS/Linux; Delete char right = `Delete` or `Ctrl+D` (all platforms). + ### `macos.json` — macOS-style bindings (for Windows users) | What changes | Default (Windows) | With `macos.json` | @@ -35,17 +39,22 @@ See [Migrating ConfigurationManager to TuiConfigurationBuilder](https://github.c | Suspend app to background | *(not available)* | `Ctrl+Z` | | Undo | `Ctrl+Z` | `Ctrl+Z` or `Ctrl+/` | | Redo | `Ctrl+Y` | `Ctrl+Y` or `Ctrl+Shift+Z` | -| Delete char right | `Delete` | `Delete` or `Ctrl+D` | +| Kill word right (TextField) | `Ctrl+Delete` | `Ctrl+W` | ### `windows.json` — Windows-style bindings (for macOS users) | What changes | Default (macOS) | With `windows.json` | |---|---|---| -| Quit app | `Esc` or `Ctrl+Q` | `Esc` only | -| Suspend app to background | `Ctrl+Z` | *(disabled)* | | Undo | `Ctrl+Z` or `Ctrl+/` | `Ctrl+Z` only | | Redo | `Ctrl+Y` or `Ctrl+Shift+Z` | `Ctrl+Y` only | | Delete char right | `Delete` or `Ctrl+D` | `Delete` only | +| Word left/right (TextField) | `Ctrl+←/→` or `Ctrl+↑/↓` | `Ctrl+←/→` only | + +**Limitation:** a binding cannot be *removed* via `config.json` — an omitted command keeps its default, and empty entries are dropped. So `windows.json` cannot disable Suspend (`Ctrl+Z` on macOS/Linux); to remove a binding entirely, do it in code: + +```csharp +Application.RemoveDefaultKeyBinding (Command.Suspend); +``` ## How It Works @@ -86,8 +95,6 @@ Each `PlatformKeyBinding` has four optional fields: | `Linux` | Linux only (added to `All`) | | `Macos` | macOS only (added to `All`) | -Bindings overlay **per command**: a command you set replaces that command's default binding entirely (include every key you want active for it), while commands you omit keep their compile-time defaults. - Key bindings can also be changed in code, before `Application.Create ()`: ```csharp diff --git a/Config/macos.json b/Config/macos.json index 5936b80..bf9e94d 100644 --- a/Config/macos.json +++ b/Config/macos.json @@ -2,208 +2,19 @@ "$schema": "https://tui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json", "Application": { "DefaultKeyBindings": { - "Quit": { - "All": [ - "Esc", - "Ctrl+Q" - ] - }, - "Suspend": { - "All": [ - "Ctrl+Z" - ] - }, - "Arrange": { - "All": [ - "Ctrl+F5" - ] - }, - "NextTabStop": { - "All": [ - "Tab" - ] - }, - "PreviousTabStop": { - "All": [ - "Shift+Tab" - ] - }, - "NextTabGroup": { - "All": [ - "F6" - ] - }, - "PreviousTabGroup": { - "All": [ - "Shift+F6" - ] - }, - "Refresh": { - "All": [ - "F5" - ] - } + "Quit": { "All": [ "Esc", "Ctrl+Q" ] }, + "Suspend": { "All": [ "Ctrl+Z" ] } } }, "View": { "DefaultKeyBindings": { - "Left": { - "All": [ - "CursorLeft" - ] - }, - "Right": { - "All": [ - "CursorRight" - ] - }, - "Up": { - "All": [ - "CursorUp" - ] - }, - "Down": { - "All": [ - "CursorDown" - ] - }, - "PageUp": { - "All": [ - "PageUp" - ] - }, - "PageDown": { - "All": [ - "PageDown" - ] - }, - "LeftStart": { - "All": [ - "Home" - ] - }, - "RightEnd": { - "All": [ - "End" - ] - }, - "Start": { - "All": [ - "Ctrl+Home" - ] - }, - "End": { - "All": [ - "Ctrl+End" - ] - }, - "LeftExtend": { - "All": [ - "Shift+CursorLeft" - ] - }, - "RightExtend": { - "All": [ - "Shift+CursorRight" - ] - }, - "UpExtend": { - "All": [ - "Shift+CursorUp" - ] - }, - "DownExtend": { - "All": [ - "Shift+CursorDown" - ] - }, - "PageUpExtend": { - "All": [ - "Shift+PageUp" - ] - }, - "PageDownExtend": { - "All": [ - "Shift+PageDown" - ] - }, - "LeftStartExtend": { - "All": [ - "Shift+Home" - ] - }, - "RightEndExtend": { - "All": [ - "Shift+End" - ] - }, - "StartExtend": { - "All": [ - "Ctrl+Shift+Home" - ] - }, - "EndExtend": { - "All": [ - "Ctrl+Shift+End" - ] - }, - "Copy": { - "All": [ - "Ctrl+C" - ] - }, - "Cut": { - "All": [ - "Ctrl+X" - ] - }, - "Paste": { - "All": [ - "Ctrl+V" - ] - }, - "Undo": { - "All": [ - "Ctrl+Z", - "Ctrl+/" - ] - }, - "Redo": { - "All": [ - "Ctrl+Y", - "Ctrl+Shift+Z" - ] - }, - "SelectAll": { - "All": [ - "Ctrl+A" - ] - }, - "DeleteCharLeft": { - "All": [ - "Backspace" - ] - }, - "DeleteCharRight": { - "All": [ - "Delete", - "Ctrl+D" - ] - } + "Undo": { "All": [ "Ctrl+Z", "Ctrl+/" ] }, + "Redo": { "All": [ "Ctrl+Y", "Ctrl+Shift+Z" ] } }, "ViewKeyBindings": { "TextField": { - "CutToEndOfLine": { - "All": [ - "Ctrl+K" - ] - }, - "KillWordRight": { - "All": [ - "Ctrl+W" - ] - } + "KillWordRight": { "All": [ "Ctrl+W" ] } } } } -} \ No newline at end of file +} diff --git a/Config/windows.json b/Config/windows.json index 662ab86..1338fbf 100644 --- a/Config/windows.json +++ b/Config/windows.json @@ -1,210 +1,18 @@ { "$schema": "https://tui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json", - "Application": { - "DefaultKeyBindings": { - "Quit": { - "All": [ - "Esc" - ] - }, - "Arrange": { - "All": [ - "Ctrl+F5" - ] - }, - "NextTabStop": { - "All": [ - "Tab" - ] - }, - "PreviousTabStop": { - "All": [ - "Shift+Tab" - ] - }, - "NextTabGroup": { - "All": [ - "F6" - ] - }, - "PreviousTabGroup": { - "All": [ - "Shift+F6" - ] - }, - "Refresh": { - "All": [ - "F5" - ] - } - } - }, "View": { "DefaultKeyBindings": { - "Left": { - "All": [ - "CursorLeft" - ] - }, - "Right": { - "All": [ - "CursorRight" - ] - }, - "Up": { - "All": [ - "CursorUp" - ] - }, - "Down": { - "All": [ - "CursorDown" - ] - }, - "PageUp": { - "All": [ - "PageUp" - ] - }, - "PageDown": { - "All": [ - "PageDown" - ] - }, - "LeftStart": { - "All": [ - "Home" - ] - }, - "RightEnd": { - "All": [ - "End" - ] - }, - "Start": { - "All": [ - "Ctrl+Home" - ] - }, - "End": { - "All": [ - "Ctrl+End" - ] - }, - "LeftExtend": { - "All": [ - "Shift+CursorLeft" - ] - }, - "RightExtend": { - "All": [ - "Shift+CursorRight" - ] - }, - "UpExtend": { - "All": [ - "Shift+CursorUp" - ] - }, - "DownExtend": { - "All": [ - "Shift+CursorDown" - ] - }, - "PageUpExtend": { - "All": [ - "Shift+PageUp" - ] - }, - "PageDownExtend": { - "All": [ - "Shift+PageDown" - ] - }, - "LeftStartExtend": { - "All": [ - "Shift+Home" - ] - }, - "RightEndExtend": { - "All": [ - "Shift+End" - ] - }, - "StartExtend": { - "All": [ - "Ctrl+Shift+Home" - ] - }, - "EndExtend": { - "All": [ - "Ctrl+Shift+End" - ] - }, - "Copy": { - "All": [ - "Ctrl+C" - ] - }, - "Cut": { - "All": [ - "Ctrl+X" - ] - }, - "Paste": { - "All": [ - "Ctrl+V" - ] - }, - "Undo": { - "All": [ - "Ctrl+Z" - ] - }, - "Redo": { - "All": [ - "Ctrl+Y" - ] - }, - "SelectAll": { - "All": [ - "Ctrl+A" - ] - }, - "DeleteCharLeft": { - "All": [ - "Backspace" - ] - }, - "DeleteCharRight": { - "All": [ - "Delete" - ] - } + "Undo": { "All": [ "Ctrl+Z" ] }, + "Redo": { "All": [ "Ctrl+Y" ] }, + "DeleteCharRight": { "All": [ "Delete" ] } }, "ViewKeyBindings": { "TextField": { - "WordLeft": { - "All": [ - "Ctrl+CursorLeft" - ] - }, - "WordRight": { - "All": [ - "Ctrl+CursorRight" - ] - }, - "WordLeftExtend": { - "All": [ - "Ctrl+Shift+CursorLeft" - ] - }, - "WordRightExtend": { - "All": [ - "Ctrl+Shift+CursorRight" - ] - } + "WordLeft": { "All": [ "Ctrl+CursorLeft" ] }, + "WordRight": { "All": [ "Ctrl+CursorRight" ] }, + "WordLeftExtend": { "All": [ "Ctrl+Shift+CursorLeft" ] }, + "WordRightExtend": { "All": [ "Ctrl+Shift+CursorRight" ] } } } } -} \ No newline at end of file +} diff --git a/Examples.sln b/Examples.sln index 545e064..e459b8a 100644 --- a/Examples.sln +++ b/Examples.sln @@ -32,165 +32,61 @@ EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {A1B2C3D4-0001-0000-0000-000000000001}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1B2C3D4-0001-0000-0000-000000000001}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A1B2C3D4-0001-0000-0000-000000000001}.Debug|x64.ActiveCfg = Debug|Any CPU - {A1B2C3D4-0001-0000-0000-000000000001}.Debug|x64.Build.0 = Debug|Any CPU - {A1B2C3D4-0001-0000-0000-000000000001}.Debug|x86.ActiveCfg = Debug|Any CPU - {A1B2C3D4-0001-0000-0000-000000000001}.Debug|x86.Build.0 = Debug|Any CPU {A1B2C3D4-0001-0000-0000-000000000001}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-0001-0000-0000-000000000001}.Release|Any CPU.Build.0 = Release|Any CPU - {A1B2C3D4-0001-0000-0000-000000000001}.Release|x64.ActiveCfg = Release|Any CPU - {A1B2C3D4-0001-0000-0000-000000000001}.Release|x64.Build.0 = Release|Any CPU - {A1B2C3D4-0001-0000-0000-000000000001}.Release|x86.ActiveCfg = Release|Any CPU - {A1B2C3D4-0001-0000-0000-000000000001}.Release|x86.Build.0 = Release|Any CPU {A1B2C3D4-0002-0000-0000-000000000002}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1B2C3D4-0002-0000-0000-000000000002}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A1B2C3D4-0002-0000-0000-000000000002}.Debug|x64.ActiveCfg = Debug|Any CPU - {A1B2C3D4-0002-0000-0000-000000000002}.Debug|x64.Build.0 = Debug|Any CPU - {A1B2C3D4-0002-0000-0000-000000000002}.Debug|x86.ActiveCfg = Debug|Any CPU - {A1B2C3D4-0002-0000-0000-000000000002}.Debug|x86.Build.0 = Debug|Any CPU {A1B2C3D4-0002-0000-0000-000000000002}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-0002-0000-0000-000000000002}.Release|Any CPU.Build.0 = Release|Any CPU - {A1B2C3D4-0002-0000-0000-000000000002}.Release|x64.ActiveCfg = Release|Any CPU - {A1B2C3D4-0002-0000-0000-000000000002}.Release|x64.Build.0 = Release|Any CPU - {A1B2C3D4-0002-0000-0000-000000000002}.Release|x86.ActiveCfg = Release|Any CPU - {A1B2C3D4-0002-0000-0000-000000000002}.Release|x86.Build.0 = Release|Any CPU {A1B2C3D4-0003-0000-0000-000000000003}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1B2C3D4-0003-0000-0000-000000000003}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A1B2C3D4-0003-0000-0000-000000000003}.Debug|x64.ActiveCfg = Debug|Any CPU - {A1B2C3D4-0003-0000-0000-000000000003}.Debug|x64.Build.0 = Debug|Any CPU - {A1B2C3D4-0003-0000-0000-000000000003}.Debug|x86.ActiveCfg = Debug|Any CPU - {A1B2C3D4-0003-0000-0000-000000000003}.Debug|x86.Build.0 = Debug|Any CPU {A1B2C3D4-0003-0000-0000-000000000003}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-0003-0000-0000-000000000003}.Release|Any CPU.Build.0 = Release|Any CPU - {A1B2C3D4-0003-0000-0000-000000000003}.Release|x64.ActiveCfg = Release|Any CPU - {A1B2C3D4-0003-0000-0000-000000000003}.Release|x64.Build.0 = Release|Any CPU - {A1B2C3D4-0003-0000-0000-000000000003}.Release|x86.ActiveCfg = Release|Any CPU - {A1B2C3D4-0003-0000-0000-000000000003}.Release|x86.Build.0 = Release|Any CPU {A1B2C3D4-0004-0000-0000-000000000004}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1B2C3D4-0004-0000-0000-000000000004}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A1B2C3D4-0004-0000-0000-000000000004}.Debug|x64.ActiveCfg = Debug|Any CPU - {A1B2C3D4-0004-0000-0000-000000000004}.Debug|x64.Build.0 = Debug|Any CPU - {A1B2C3D4-0004-0000-0000-000000000004}.Debug|x86.ActiveCfg = Debug|Any CPU - {A1B2C3D4-0004-0000-0000-000000000004}.Debug|x86.Build.0 = Debug|Any CPU {A1B2C3D4-0004-0000-0000-000000000004}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-0004-0000-0000-000000000004}.Release|Any CPU.Build.0 = Release|Any CPU - {A1B2C3D4-0004-0000-0000-000000000004}.Release|x64.ActiveCfg = Release|Any CPU - {A1B2C3D4-0004-0000-0000-000000000004}.Release|x64.Build.0 = Release|Any CPU - {A1B2C3D4-0004-0000-0000-000000000004}.Release|x86.ActiveCfg = Release|Any CPU - {A1B2C3D4-0004-0000-0000-000000000004}.Release|x86.Build.0 = Release|Any CPU {A1B2C3D4-0005-0000-0000-000000000005}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1B2C3D4-0005-0000-0000-000000000005}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A1B2C3D4-0005-0000-0000-000000000005}.Debug|x64.ActiveCfg = Debug|Any CPU - {A1B2C3D4-0005-0000-0000-000000000005}.Debug|x64.Build.0 = Debug|Any CPU - {A1B2C3D4-0005-0000-0000-000000000005}.Debug|x86.ActiveCfg = Debug|Any CPU - {A1B2C3D4-0005-0000-0000-000000000005}.Debug|x86.Build.0 = Debug|Any CPU {A1B2C3D4-0005-0000-0000-000000000005}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-0005-0000-0000-000000000005}.Release|Any CPU.Build.0 = Release|Any CPU - {A1B2C3D4-0005-0000-0000-000000000005}.Release|x64.ActiveCfg = Release|Any CPU - {A1B2C3D4-0005-0000-0000-000000000005}.Release|x64.Build.0 = Release|Any CPU - {A1B2C3D4-0005-0000-0000-000000000005}.Release|x86.ActiveCfg = Release|Any CPU - {A1B2C3D4-0005-0000-0000-000000000005}.Release|x86.Build.0 = Release|Any CPU {A1B2C3D4-0006-0000-0000-000000000006}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1B2C3D4-0006-0000-0000-000000000006}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A1B2C3D4-0006-0000-0000-000000000006}.Debug|x64.ActiveCfg = Debug|Any CPU - {A1B2C3D4-0006-0000-0000-000000000006}.Debug|x64.Build.0 = Debug|Any CPU - {A1B2C3D4-0006-0000-0000-000000000006}.Debug|x86.ActiveCfg = Debug|Any CPU - {A1B2C3D4-0006-0000-0000-000000000006}.Debug|x86.Build.0 = Debug|Any CPU {A1B2C3D4-0006-0000-0000-000000000006}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-0006-0000-0000-000000000006}.Release|Any CPU.Build.0 = Release|Any CPU - {A1B2C3D4-0006-0000-0000-000000000006}.Release|x64.ActiveCfg = Release|Any CPU - {A1B2C3D4-0006-0000-0000-000000000006}.Release|x64.Build.0 = Release|Any CPU - {A1B2C3D4-0006-0000-0000-000000000006}.Release|x86.ActiveCfg = Release|Any CPU - {A1B2C3D4-0006-0000-0000-000000000006}.Release|x86.Build.0 = Release|Any CPU {A1B2C3D4-0007-0000-0000-000000000007}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1B2C3D4-0007-0000-0000-000000000007}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A1B2C3D4-0007-0000-0000-000000000007}.Debug|x64.ActiveCfg = Debug|Any CPU - {A1B2C3D4-0007-0000-0000-000000000007}.Debug|x64.Build.0 = Debug|Any CPU - {A1B2C3D4-0007-0000-0000-000000000007}.Debug|x86.ActiveCfg = Debug|Any CPU - {A1B2C3D4-0007-0000-0000-000000000007}.Debug|x86.Build.0 = Debug|Any CPU {A1B2C3D4-0007-0000-0000-000000000007}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-0007-0000-0000-000000000007}.Release|Any CPU.Build.0 = Release|Any CPU - {A1B2C3D4-0007-0000-0000-000000000007}.Release|x64.ActiveCfg = Release|Any CPU - {A1B2C3D4-0007-0000-0000-000000000007}.Release|x64.Build.0 = Release|Any CPU - {A1B2C3D4-0007-0000-0000-000000000007}.Release|x86.ActiveCfg = Release|Any CPU - {A1B2C3D4-0007-0000-0000-000000000007}.Release|x86.Build.0 = Release|Any CPU {A1B2C3D4-0008-0000-0000-000000000008}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1B2C3D4-0008-0000-0000-000000000008}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A1B2C3D4-0008-0000-0000-000000000008}.Debug|x64.ActiveCfg = Debug|Any CPU - {A1B2C3D4-0008-0000-0000-000000000008}.Debug|x64.Build.0 = Debug|Any CPU - {A1B2C3D4-0008-0000-0000-000000000008}.Debug|x86.ActiveCfg = Debug|Any CPU - {A1B2C3D4-0008-0000-0000-000000000008}.Debug|x86.Build.0 = Debug|Any CPU {A1B2C3D4-0008-0000-0000-000000000008}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-0008-0000-0000-000000000008}.Release|Any CPU.Build.0 = Release|Any CPU - {A1B2C3D4-0008-0000-0000-000000000008}.Release|x64.ActiveCfg = Release|Any CPU - {A1B2C3D4-0008-0000-0000-000000000008}.Release|x64.Build.0 = Release|Any CPU - {A1B2C3D4-0008-0000-0000-000000000008}.Release|x86.ActiveCfg = Release|Any CPU - {A1B2C3D4-0008-0000-0000-000000000008}.Release|x86.Build.0 = Release|Any CPU {A1B2C3D4-0009-0000-0000-000000000009}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1B2C3D4-0009-0000-0000-000000000009}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A1B2C3D4-0009-0000-0000-000000000009}.Debug|x64.ActiveCfg = Debug|Any CPU - {A1B2C3D4-0009-0000-0000-000000000009}.Debug|x64.Build.0 = Debug|Any CPU - {A1B2C3D4-0009-0000-0000-000000000009}.Debug|x86.ActiveCfg = Debug|Any CPU - {A1B2C3D4-0009-0000-0000-000000000009}.Debug|x86.Build.0 = Debug|Any CPU {A1B2C3D4-0009-0000-0000-000000000009}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-0009-0000-0000-000000000009}.Release|Any CPU.Build.0 = Release|Any CPU - {A1B2C3D4-0009-0000-0000-000000000009}.Release|x64.ActiveCfg = Release|Any CPU - {A1B2C3D4-0009-0000-0000-000000000009}.Release|x64.Build.0 = Release|Any CPU - {A1B2C3D4-0009-0000-0000-000000000009}.Release|x86.ActiveCfg = Release|Any CPU - {A1B2C3D4-0009-0000-0000-000000000009}.Release|x86.Build.0 = Release|Any CPU {A1B2C3D4-000A-0000-0000-00000000000A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1B2C3D4-000A-0000-0000-00000000000A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A1B2C3D4-000A-0000-0000-00000000000A}.Debug|x64.ActiveCfg = Debug|Any CPU - {A1B2C3D4-000A-0000-0000-00000000000A}.Debug|x64.Build.0 = Debug|Any CPU - {A1B2C3D4-000A-0000-0000-00000000000A}.Debug|x86.ActiveCfg = Debug|Any CPU - {A1B2C3D4-000A-0000-0000-00000000000A}.Debug|x86.Build.0 = Debug|Any CPU {A1B2C3D4-000A-0000-0000-00000000000A}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-000A-0000-0000-00000000000A}.Release|Any CPU.Build.0 = Release|Any CPU - {A1B2C3D4-000A-0000-0000-00000000000A}.Release|x64.ActiveCfg = Release|Any CPU - {A1B2C3D4-000A-0000-0000-00000000000A}.Release|x64.Build.0 = Release|Any CPU - {A1B2C3D4-000A-0000-0000-00000000000A}.Release|x86.ActiveCfg = Release|Any CPU - {A1B2C3D4-000A-0000-0000-00000000000A}.Release|x86.Build.0 = Release|Any CPU {A1B2C3D4-000B-0000-0000-00000000000B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1B2C3D4-000B-0000-0000-00000000000B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A1B2C3D4-000B-0000-0000-00000000000B}.Debug|x64.ActiveCfg = Debug|Any CPU - {A1B2C3D4-000B-0000-0000-00000000000B}.Debug|x64.Build.0 = Debug|Any CPU - {A1B2C3D4-000B-0000-0000-00000000000B}.Debug|x86.ActiveCfg = Debug|Any CPU - {A1B2C3D4-000B-0000-0000-00000000000B}.Debug|x86.Build.0 = Debug|Any CPU {A1B2C3D4-000B-0000-0000-00000000000B}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-000B-0000-0000-00000000000B}.Release|Any CPU.Build.0 = Release|Any CPU - {A1B2C3D4-000B-0000-0000-00000000000B}.Release|x64.ActiveCfg = Release|Any CPU - {A1B2C3D4-000B-0000-0000-00000000000B}.Release|x64.Build.0 = Release|Any CPU - {A1B2C3D4-000B-0000-0000-00000000000B}.Release|x86.ActiveCfg = Release|Any CPU - {A1B2C3D4-000B-0000-0000-00000000000B}.Release|x86.Build.0 = Release|Any CPU - {A1B2C3D4-000D-0000-0000-00000000000D}.Debug|x64.ActiveCfg = Debug|Any CPU - {A1B2C3D4-000D-0000-0000-00000000000D}.Debug|x64.Build.0 = Debug|Any CPU - {A1B2C3D4-000D-0000-0000-00000000000D}.Debug|x86.ActiveCfg = Debug|Any CPU - {A1B2C3D4-000D-0000-0000-00000000000D}.Debug|x86.Build.0 = Debug|Any CPU - {A1B2C3D4-000D-0000-0000-00000000000D}.Release|x64.ActiveCfg = Release|Any CPU - {A1B2C3D4-000D-0000-0000-00000000000D}.Release|x64.Build.0 = Release|Any CPU - {A1B2C3D4-000D-0000-0000-00000000000D}.Release|x86.ActiveCfg = Release|Any CPU - {A1B2C3D4-000D-0000-0000-00000000000D}.Release|x86.Build.0 = Release|Any CPU + {A1B2C3D4-000D-0000-0000-00000000000D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-000D-0000-0000-00000000000D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-000D-0000-0000-00000000000D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-000D-0000-0000-00000000000D}.Release|Any CPU.Build.0 = Release|Any CPU {167437AD-7BE5-46D7-8BC5-A0AB37DB2C6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {167437AD-7BE5-46D7-8BC5-A0AB37DB2C6C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {167437AD-7BE5-46D7-8BC5-A0AB37DB2C6C}.Debug|x64.ActiveCfg = Debug|Any CPU - {167437AD-7BE5-46D7-8BC5-A0AB37DB2C6C}.Debug|x64.Build.0 = Debug|Any CPU - {167437AD-7BE5-46D7-8BC5-A0AB37DB2C6C}.Debug|x86.ActiveCfg = Debug|Any CPU - {167437AD-7BE5-46D7-8BC5-A0AB37DB2C6C}.Debug|x86.Build.0 = Debug|Any CPU {167437AD-7BE5-46D7-8BC5-A0AB37DB2C6C}.Release|Any CPU.ActiveCfg = Release|Any CPU {167437AD-7BE5-46D7-8BC5-A0AB37DB2C6C}.Release|Any CPU.Build.0 = Release|Any CPU - {167437AD-7BE5-46D7-8BC5-A0AB37DB2C6C}.Release|x64.ActiveCfg = Release|Any CPU - {167437AD-7BE5-46D7-8BC5-A0AB37DB2C6C}.Release|x64.Build.0 = Release|Any CPU - {167437AD-7BE5-46D7-8BC5-A0AB37DB2C6C}.Release|x86.ActiveCfg = Release|Any CPU - {167437AD-7BE5-46D7-8BC5-A0AB37DB2C6C}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/FSharpExample/Program.fs b/FSharpExample/Program.fs index 8d358bd..c126c12 100644 --- a/FSharpExample/Program.fs +++ b/FSharpExample/Program.fs @@ -1,6 +1,8 @@ // A simple Terminal.Gui example in F#. // For the full range of functionality see the UICatalog project in the Terminal.Gui repo. +open System +open System.Threading open Terminal.Gui.App open Terminal.Gui.Input open Terminal.Gui.ViewBase @@ -53,13 +55,24 @@ type ExampleWindow () as this = let main argv = // Configuration (themes, schemes, settings) is applied automatically at assembly load. let app = Application.Create().Init () - app.Run () |> ignore - // Dispose the application to free resources and restore the previous screen - app.Dispose () + let smokeTest = argv.Length > 0 && argv.[0] = "--smoke-test" - // To see this output on the screen it must be done after Dispose, - // which restores the previous screen. - printfn "Username: %s" ExampleWindow.UserName + if smokeTest then + // Start, render, and exit cleanly after 2 seconds (used by tests/Examples.SmokeTests). + use cts = new CancellationTokenSource (TimeSpan.FromSeconds 2.0) + app.RunAsync(cts.Token).GetAwaiter().GetResult () |> ignore + app.Dispose () + printfn "Smoke test passed." + 0 + else + app.Run () |> ignore - 0 // return an integer exit code + // Dispose the application to free resources and restore the previous screen + app.Dispose () + + // To see this output on the screen it must be done after Dispose, + // which restores the previous screen. + printfn "Username: %s" ExampleWindow.UserName + + 0 // return an integer exit code diff --git a/ReactiveExample/LoginView.cs b/ReactiveExample/LoginView.cs index 29dee5d..dbfb7e2 100644 --- a/ReactiveExample/LoginView.cs +++ b/ReactiveExample/LoginView.cs @@ -49,12 +49,7 @@ public LoginView (LoginViewModel viewModel) .BindTo (unInput, x => x.Text) .DisposeWith (_disposable); - // TextField hides View.TextChanging with a different delegate type, which the - // ObservableEvents source generator cannot wrap; subscribe via FromEventPattern. - Observable - .FromEventPattern (h => unInput.TextChanged += h, h => unInput.TextChanged -= h) - .Select (_ => unInput.Text) - .DistinctUntilChanged () + ObserveText (unInput) .BindTo (ViewModel, x => x.Username) .DisposeWith (_disposable); }); @@ -81,10 +76,7 @@ public LoginView (LoginViewModel viewModel) .BindTo (pwInput, x => x.Text) .DisposeWith (_disposable); - Observable - .FromEventPattern (h => pwInput.TextChanged += h, h => pwInput.TextChanged -= h) - .Select (_ => pwInput.Text) - .DistinctUntilChanged () + ObserveText (pwInput) .BindTo (ViewModel, x => x.Password) .DisposeWith (_disposable); }) @@ -149,6 +141,14 @@ public LoginView (LoginViewModel viewModel) public LoginViewModel ViewModel { get; set; } + // TextField hides View.TextChanging with a different delegate type, which the ObservableEvents + // source generator cannot wrap; observe IValue.ValueChanged instead, which delivers the + // new value directly and only fires on real changes. + private static IObservable ObserveText (TextField field) => + Observable + .FromEventPattern> (h => field.ValueChanged += h, h => field.ValueChanged -= h) + .Select (e => e.EventArgs.NewValue ?? string.Empty); + object IViewFor.ViewModel { get => ViewModel; diff --git a/ReactiveExample/README.md b/ReactiveExample/README.md index 0e618cb..a42e5e6 100644 --- a/ReactiveExample/README.md +++ b/ReactiveExample/README.md @@ -31,14 +31,15 @@ ViewModel .BindTo (usernameInput, x => x.Text); ``` -Note that your view model should implement `INotifyPropertyChanged` or inherit from a `ReactiveObject`. If you wish to implement `OneWayToSource` data binding, listen to e.g. the `TextChanged` event of a `TextField` via `Observable.FromEventPattern` (the generated `.Events ()` wrappers work too, but as of Terminal.Gui 2.5 the ObservableEvents source generator cannot wrap `TextField`, which hides `View.TextChanging` with a different delegate type): +Note that your view model should implement `INotifyPropertyChanged` or inherit from a `ReactiveObject`. If you wish to implement `OneWayToSource` data binding, listen to the view's change event via `Observable.FromEventPattern`. For `TextField` specifically, the generated `.Events ()` wrappers do **not** work — as of Terminal.Gui 2.5 the ObservableEvents source generator cannot wrap `TextField`, which hides `View.TextChanging` with a different delegate type (`.Events ()` remains fine for other views, like the `Button` below). `TextField` implements `IValue`, so its `ValueChanged` event delivers the new value directly and only fires on real changes: ```cs // 'usernameInput' is 'TextField' Observable - .FromEventPattern (h => usernameInput.TextChanged += h, h => usernameInput.TextChanged -= h) - .Select (_ => usernameInput.Text) - .DistinctUntilChanged () + .FromEventPattern> ( + h => usernameInput.ValueChanged += h, + h => usernameInput.ValueChanged -= h) + .Select (e => e.EventArgs.NewValue ?? string.Empty) .BindTo (ViewModel, x => x.Username); ``` diff --git a/SelfContained/README.md b/SelfContained/README.md index 09f085d..d0d8215 100644 --- a/SelfContained/README.md +++ b/SelfContained/README.md @@ -8,6 +8,10 @@ This example uses the modern Terminal.Gui application model: ```csharp // Configuration (themes, schemes, settings) is applied automatically at assembly load. + +// Use Inline mode — renders below the shell prompt without alternate screen buffer +Application.AppModel = AppModel.Inline; + IApplication app = Application.Create (); app.Init (); @@ -20,12 +24,12 @@ Console.WriteLine ($@"Username: {userName}"); Key aspects of the modern model: +- Set `Application.AppModel = AppModel.Inline` before `Application.Create()` for inline (non-fullscreen) rendering — the feature this example demonstrates - Use `Application.Create()` to create an `IApplication` instance - Call `app.Init()` to initialize the application - Use `app.Run(view)` to run views with proper resource management - Call `app.Dispose()` to clean up resources and restore the terminal -- Event handling uses `Accepting` event instead of legacy `Accept` event -- Set `e.Handled = true` in event handlers to prevent further processing +- This example subscribes to the button's `Accepted` event (a fire-and-forget side-effect); use `Accepting` with `e.Handled = true` only when you need to inspect or cancel the in-flight action With `Debug` the `.csproj` is used and with `Release` the latest `nuget package` is used, either in `Solution Configurations` or in `Profile Publish`. diff --git a/tests/Examples.SmokeTests/ExampleSmokeTests.cs b/tests/Examples.SmokeTests/ExampleSmokeTests.cs index 1278fe6..492901b 100644 --- a/tests/Examples.SmokeTests/ExampleSmokeTests.cs +++ b/tests/Examples.SmokeTests/ExampleSmokeTests.cs @@ -24,6 +24,7 @@ public class ExampleSmokeTests [InlineData ("SelfContained")] [InlineData ("InlineCLI")] [InlineData ("PromptExample")] + [InlineData ("FSharpExample")] public async Task Example_StartsAndExitsCleanly (string projectName) { var projectPath = Path.Combine (SolutionRoot, projectName);