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..820a153 100644 --- a/Config/README.md +++ b/Config/README.md @@ -1 +1,104 @@ -# 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. + +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 +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 + +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` | +|---|---|---| +| 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` | +| Kill word right (TextField) | `Ctrl+Delete` | `Ctrl+W` | + +### `windows.json` — Windows-style bindings (for macOS users) + +| What changes | Default (macOS) | With `windows.json` | +|---|---|---| +| 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 + +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`) | + +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..bf9e94d 100644 --- a/Config/macos.json +++ b/Config/macos.json @@ -1 +1,20 @@ -{ "$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" ] } + } + }, + "View": { + "DefaultKeyBindings": { + "Undo": { "All": [ "Ctrl+Z", "Ctrl+/" ] }, + "Redo": { "All": [ "Ctrl+Y", "Ctrl+Shift+Z" ] } + }, + "ViewKeyBindings": { + "TextField": { + "KillWordRight": { "All": [ "Ctrl+W" ] } + } + } + } +} diff --git a/Config/windows.json b/Config/windows.json index 44df4d9..1338fbf 100644 --- a/Config/windows.json +++ b/Config/windows.json @@ -1 +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"] } }, "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", + "View": { + "DefaultKeyBindings": { + "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" ] } + } + } + } +} 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..e459b8a 100644 --- a/Examples.sln +++ b/Examples.sln @@ -27,6 +27,8 @@ 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 @@ -77,6 +79,14 @@ Global {A1B2C3D4-000B-0000-0000-00000000000B}.Debug|Any CPU.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-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}.Release|Any CPU.ActiveCfg = Release|Any CPU + {167437AD-7BE5-46D7-8BC5-A0AB37DB2C6C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/FSharpExample/Program.fs b/FSharpExample/Program.fs index d68a0c8..c126c12 100644 --- a/FSharpExample/Program.fs +++ b/FSharpExample/Program.fs @@ -1,48 +1,78 @@ -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 System +open System.Threading +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, - // which restores the previous screen. - printfn "Username: %s" ExampleWindow.UserName - - 0 // return an integer exit code + // Configuration (themes, schemes, settings) is applied automatically at assembly load. + let app = Application.Create().Init () + + let smokeTest = argv.Length > 0 && argv.[0] = "--smoke-test" + + 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 + + // 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..dbfb7e2 100644 --- a/ReactiveExample/LoginView.cs +++ b/ReactiveExample/LoginView.cs @@ -49,11 +49,7 @@ public LoginView (LoginViewModel viewModel) .BindTo (unInput, x => x.Text) .DisposeWith (_disposable); - unInput - .Events () - .TextChanged - .Select (_ => unInput.Text) - .DistinctUntilChanged () + ObserveText (unInput) .BindTo (ViewModel, x => x.Username) .DisposeWith (_disposable); }); @@ -80,11 +76,7 @@ public LoginView (LoginViewModel viewModel) .BindTo (pwInput, x => x.Text) .DisposeWith (_disposable); - pwInput - .Events () - .TextChanged - .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/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..a42e5e6 100644 --- a/ReactiveExample/README.md +++ b/ReactiveExample/README.md @@ -1 +1,54 @@ -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 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.ValueChanged += h, + h => usernameInput.ValueChanged -= h) + .Select (e => e.EventArgs.NewValue ?? string.Empty) + .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..d0d8215 100644 --- a/SelfContained/README.md +++ b/SelfContained/README.md @@ -1 +1,38 @@ -# 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. + +// Use Inline mode — renders below the shell prompt without alternate screen buffer +Application.AppModel = AppModel.Inline; + +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: + +- 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 +- 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`. + +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 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);