Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions CommunityToolkitExample/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Terminal.Gui.App;
using Terminal.Gui.Configuration;

namespace CommunityToolkitExample;

Expand All @@ -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 ();
Expand Down
164 changes: 163 additions & 1 deletion CommunityToolkitExample/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,163 @@
# CommunityToolkit.MVVM ExampleThis small demo gives an example of using the `CommunityToolkit.MVVM` framework's `ObservableObject`, `ObservableProperty`, and `IRecipient<T>` 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<LoginView> (); services.AddTransient<LoginViewModel> (); 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<LoginView> ();app.Run (loginView);```Our view implements `IRecipient<T>` to demonstrate the use of the `WeakReferenceMessenger`. The binding of the view events is then created.``` csharpinternal partial class LoginView : IRecipient<Message<LoginActions>>{ 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<LoginActions> { 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<LoginActions> 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 ();}```
# CommunityToolkit.MVVM Example

This small demo gives an example of using the `CommunityToolkit.MVVM` framework's `ObservableObject`, `ObservableProperty`, and `IRecipient<T>` 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<LoginView> ();
services.AddTransient<LoginViewModel> ();

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<LoginView> ();
app.Run (loginView);
```

Our view implements `IRecipient<T>` to demonstrate the use of the `WeakReferenceMessenger`. The binding of the view events is then created.

```csharp
internal partial class LoginView : IRecipient<Message<LoginActions>>
{
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<LoginActions> { 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<LoginActions> 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 ();
}
```
105 changes: 104 additions & 1 deletion Config/README.md
Original file line number Diff line number Diff line change
@@ -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\<username>`. On macOS/Linux `~` expands to `/home/<username>` (or `/Users/<username>` 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.
# 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\<username>`. On macOS/Linux `~` expands to `/Users/<username>` / `/home/<username>`.

## 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.
Loading
Loading