For AI agents helping users build apps with Terminal.Gui. This guide focuses on app development, not library contribution.
When a user says "I want to build a terminal.gui app that does XYZ":
- Is this a new project? Use the templates (see Setup below)
- Adding to existing project? Add NuGet package
- What UI patterns are needed? See Common Patterns below
dotnet new install Terminal.Gui.Templates@2.*
dotnet new tui-simple -n ProjectName
cd ProjectName
dotnet rundotnet add package Terminal.Guiusing Terminal.Gui.App; // Application, IApplication
using Terminal.Gui.Views; // All controls (Button, Label, etc.)
using Terminal.Gui.ViewBase; // View, Pos, Dim
using Terminal.Gui.Drawing; // Colors, Attribute, LineStyle
using Terminal.Gui.Input; // Key, KeyCode, MouseFlags
using Terminal.Gui.Configuration; // ConfigurationManager (optional)using Terminal.Gui.App;
using Terminal.Gui.Views;
// Create and initialize application
IApplication app = Application.Create ().Init ();
// Run the main window
app.Run<MainWindow> ();
// Clean up
app.Dispose ();
// Main window class
public sealed class MainWindow : Runnable
{
public MainWindow ()
{
Title = "My App (Esc to quit)";
// Add controls here
Button button = new () { Text = "Click Me", X = Pos.Center (), Y = Pos.Center () };
button.Accepted += (_, _) =>
{
MessageBox.Query (App!, "Hello", "Button clicked!", "OK");
};
Add (button);
}
}// Usage:
IApplication app = Application.Create ().Init ();
string? username = app.Run<LoginWindow> ().GetResult<string> ();
app.Dispose ();
public sealed class LoginWindow : Runnable<string?>
{
public LoginWindow ()
{
TextField usernameField = new () { X = 1, Y = 1, Width = 20 };
Button loginButton = new () { Text = "Login", X = 1, Y = 3 };
loginButton.Accepted += (_, _) =>
{
Result = usernameField.Text; // Set return value
App!.RequestStop (); // Close window
};
Add (usernameField, loginButton);
}
}X = 5; // Absolute: 5 from left
X = Pos.Center (); // Centered horizontally
X = Pos.Right (otherView) + 1; // 1 right of another view
X = Pos.Left (otherView); // Aligned with left of another view
X = Pos.Percent (25); // 25% from left
X = Pos.AnchorEnd (10); // 10 from right edgeWidth = 20; // Absolute: 20 characters
Width = Dim.Fill (); // Fill remaining width
Width = Dim.Fill (1); // Fill minus 1 character margin
Width = Dim.Auto (); // Size to content
Width = Dim.Percent (50); // 50% of container
Width = Dim.Width (otherView); // Same width as another viewConsult these compressed API files for available types:
| File | Contents |
|---|---|
docfx/apispec/namespace-app.md |
Application, IApplication, Clipboard |
docfx/apispec/namespace-views.md |
All UI controls (Button, Label, etc.) |
docfx/apispec/namespace-viewbase.md |
View, Pos, Dim, Adornments |
docfx/apispec/namespace-drawing.md |
Colors, LineStyle, Attribute |
docfx/apispec/namespace-input.md |
Key, KeyCode, Mouse handling |
docfx/apispec/namespace-text.md |
Text manipulation, autocomplete |
See .claude/cookbook/common-patterns.md for recipes including:
- Form with validation
- List with selection
- Menu bar and dialogs
- Split views and tabs
- File dialogs
- Progress indicators
| Example | Location | Description |
|---|---|---|
| All Controls | Examples/UICatalog/ |
Comprehensive demo app |
| Scenario automation | Examples/ScenarioRunner/ |
Run UICatalog scenarios from the CLI |
| Additional samples | tui-cs/Examples |
Standalone example applications |
// Simple side-effect handler — use Accepted (post-event)
button.Accepted += (_, _) =>
{
// Handle the click
};
// Use Accepting (pre-event) ONLY to inspect or cancel the in-flight action
button.Accepting += (_, e) =>
{
if (usernameField.Text.Length == 0)
{
e.Handled = true; // Cancel — prevents Accepted from firing
}
};textField.TextChanged += (_, _) =>
{
// React to text changes
};// Typed views expose their data via IValue<T> — ListView is IValue<int?> (the selected index)
listView.ValueChanged += (_, e) =>
{
int? selectedIndex = e.NewValue;
};// In your View subclass: add a command handler, then bind a key to it.
// (AddCommand is protected — call it from inside the view, not on an instance.)
AddCommand (Command.Refresh, () =>
{
// Reload data here
return true;
});
KeyBindings.Add (Key.F5, Command.Refresh);// MessageBox makes the last button the default (Enter-activated), so put affirmative last.
int? result = MessageBox.Query (App!, "Title", "Message", "No", "Yes");
// result: 0 = No, 1 = Yes, null = dismissed without choosingMessageBox.ErrorQuery (App!, "Error", "Something went wrong", "OK");The last Dialog button added is the default (Enter-activated), just like MessageBox. Add Cancel before OK and avoid setting IsDefault manually unless you intentionally override that default.
Dialog dialog = new ()
{
Title = "Custom Dialog",
Width = 40,
Height = 10,
// Dialog makes the last button the default; order cancel/destructive choices first.
Buttons = [new Button { Text = "Cancel" }, new Button { Text = "OK" }]
};
// Add controls to dialog...
app.Run (dialog);OpenDialog openDialog = new () { Title = "Open File" };
app.Run (openDialog);
if (!openDialog.Canceled)
{
string path = openDialog.FilePaths.First ();
}view.BorderStyle = LineStyle.Rounded; // Rounded corners
view.BorderStyle = LineStyle.Single; // Single line
view.BorderStyle = LineStyle.Double; // Double line
view.BorderStyle = LineStyle.None; // No border// Set theme at startup
TuiConfigurationBuilder config = new ();
config.RuntimeConfig = """{ "Theme": "Dark" }""";
config.ApplyToStaticFacades ();Available themes: Default, Dark, Light, Amber Phosphor, Green Phosphor, Blue Phosphor
You cannot see a TUI from a build log. Before declaring an app done, run it and observe it with tuirec — it spawns the app in a PTY, injects keystrokes, and records the terminal output:
dotnet build -c Release
$ks = 'wait:1000,Tab,Enter,wait:800,Escape'
tuirec record `
--binary dotnet `
--args "./bin/Release/net10.0/MyApp.dll" `
--name MyApp `
--keystrokes $ks `
--startup-delay 2000 --drain 1500 `
--cols 120 --rows 30Then verify the output yourself:
- Read
artifacts/MyApp.cast— it is asciinema v2 JSON (plain text). Inspect the frames to confirm the UI rendered what you expect (controls visible, focus moved, dialog appeared). - Grep the cast for failures:
Select-String -Path artifacts/MyApp.cast -Pattern "error|exception|usage:". - Check
artifacts/MyApp.gifexists and is > 100KB (a blank recording is typically < 50KB).
See Scripts/tuirec/README.md for the full keystroke syntax, validation checklist, and troubleshooting table. For in-process assertions (no PTY), use InputInjector and VirtualTimeProvider — see docfx/docs/input-injection.md.
- Project setup with correct packages
- Main window class inheriting from
RunnableorRunnable<T> - Application lifecycle: Create -> Init -> Run -> Dispose
- Layout using Pos/Dim (not hardcoded positions)
-
-edevents (Accepted) for side effects;-ingevents (Accepting) only to cancel - Proper cleanup with
Disposepattern - Behavior verified by running the app (tuirec recording or input injection), not just by compiling
- Don't use
Application.Init()/Application.Shutdown()(legacy static API) - Don't hardcode sizes - use
Dim.Fill(),Dim.Auto(),Dim.Percent() - Don't use
Acceptingfor fire-and-forget side effects - useAccepted; reserveAccepting(withe.Handled = true) for canceling - Don't block the main thread - use
Application.AddTimeoutfor async work