Terminal.Gui
The most popular cross-platform .NET TUI toolkit. Provides comprehensive features including rich built-in views, flexible layout system, MVVM support, True Color and mouse support.
GitHub Overview
gui-cs/Terminal.Gui
Cross Platform Terminal UI toolkit for .NET
Topics
Star History
Terminal.Gui
Terminal.Gui is a cross-platform Terminal User Interface (TUI) toolkit for .NET. It runs on Windows, macOS, and Linux/Unix, enabling you to build rich console applications with text-based user interfaces.
Features
Cross-Platform Support
- Windows: Windows Console driver
- macOS/Linux: Curses driver
- Fallback: .NET Console driver
- Abstraction: Platform-independent through IConsoleDriver interface
Rich UI Components
- Basic Controls: Button, Label, TextField, TextView
- List Components: ListView, ComboBox, RadioGroup, CheckBox
- Data Display: TableView, TreeView, HexView
- Dialogs: MessageBox, OpenDialog, SaveDialog
- Layout: FrameView, ScrollView, TabView
Advanced Layout System
- Pos/Dim System: Absolute, relative, and percentage-based positioning
- ViewArrangement: User-interactive layout (move and resize)
- Auto Layout: Content-based sizing
- Adornments: Unified management of borders, margins, and padding
Modern Features
- True Color Support: 24-bit RGB colors (v2)
- Mouse Support: Click, drag and drop
- Clipboard: Cut, copy, paste functionality
- MVVM Support: Reactive Extensions, ReactiveUI data binding
Basic Usage
Installation
<!-- .csproj file -->
<PackageReference Include="Terminal.Gui" Version="1.19.0" />
<!-- v2 Alpha -->
<PackageReference Include="Terminal.Gui" Version="2.0.0-pre.1" />
# NuGet CLI
dotnet add package Terminal.Gui
# Package Manager Console
Install-Package Terminal.Gui
Hello World
using Terminal.Gui;
class Program
{
static void Main()
{
Application.Init();
// Create main window
var top = Application.Top;
// Create window
var win = new Window("Hello Terminal.Gui!")
{
X = 0,
Y = 1,
Width = Dim.Fill(),
Height = Dim.Fill()
};
// Add label
var label = new Label("Welcome to Terminal.Gui!")
{
X = Pos.Center(),
Y = Pos.Center() - 1
};
// Add button
var button = new Button("Quit")
{
X = Pos.Center(),
Y = Pos.Center() + 1
};
button.Clicked += () => Application.RequestStop();
// Add components to window
win.Add(label, button);
// Add window to top level
top.Add(win);
// Run application
Application.Run();
// Cleanup
Application.Shutdown();
}
}
Menu Bar and Status Bar
using Terminal.Gui;
class MainApp
{
static void Main()
{
Application.Init();
var top = Application.Top;
// Create menu bar
var menuBar = new MenuBar(new MenuBarItem[]
{
new MenuBarItem("_File", new MenuItem[]
{
new MenuItem("_New", "Create new file", NewFile),
new MenuItem("_Open", "Open existing file", OpenFile),
new MenuItem("_Save", "Save current file", SaveFile),
null, // Separator
new MenuItem("_Quit", "Exit application", () => Application.RequestStop())
}),
new MenuBarItem("_Edit", new MenuItem[]
{
new MenuItem("_Copy", "Copy to clipboard", Copy),
new MenuItem("_Paste", "Paste from clipboard", Paste)
}),
new MenuBarItem("_Help", new MenuItem[]
{
new MenuItem("_About", "About this application", About)
})
});
// Create status bar
var statusBar = new StatusBar(new StatusItem[]
{
new StatusItem(Key.F1, "~F1~ Help", () => ShowHelp()),
new StatusItem(Key.F2, "~F2~ Save", SaveFile),
new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Application.RequestStop())
});
// Main window
var mainWindow = new Window("Main Window")
{
X = 0,
Y = 1,
Width = Dim.Fill(),
Height = Dim.Fill() - 1
};
// Add text editor
var textView = new TextView()
{
X = 0,
Y = 0,
Width = Dim.Fill(),
Height = Dim.Fill(),
Text = "You can enter text here..."
};
mainWindow.Add(textView);
top.Add(menuBar, mainWindow, statusBar);
Application.Run();
Application.Shutdown();
}
static void NewFile() {
MessageBox.Query(50, 7, "New File", "Create a new file?", "Yes", "No");
}
static void OpenFile() {
var openDialog = new OpenDialog("Open File", "Select a file");
Application.Run(openDialog);
if (!openDialog.Canceled)
{
MessageBox.Query(50, 7, "File Open", $"File: {openDialog.FilePath}", "OK");
}
}
static void SaveFile() {
MessageBox.Query(50, 7, "Save", "File saved successfully", "OK");
}
static void Copy() {
MessageBox.Query(50, 7, "Copy", "Copied to clipboard", "OK");
}
static void Paste() {
MessageBox.Query(50, 7, "Paste", "Pasted from clipboard", "OK");
}
static void About() {
MessageBox.Query(50, 7, "About Application",
"Terminal.Gui Demo Application\nVersion 1.0\n\nBuilt with Terminal.Gui", "OK");
}
static void ShowHelp() {
MessageBox.Query(50, 10, "Help",
"F1: Show help\nF2: Save file\nCtrl+Q: Exit application\n\nMenus can be accessed with mouse or Alt+key", "OK");
}
}
Data Binding and MVVM
using System.ComponentModel;
using Terminal.Gui;
using System.Collections.ObjectModel;
// Model class
public class Person : INotifyPropertyChanged
{
private string _name;
private int _age;
public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged(nameof(Name));
}
}
}
public int Age
{
get => _age;
set
{
if (_age != value)
{
_age = value;
OnPropertyChanged(nameof(Age));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
// ViewModel class
public class PersonViewModel : INotifyPropertyChanged
{
private Person _selectedPerson;
public ObservableCollection<Person> People { get; } = new ObservableCollection<Person>();
public Person SelectedPerson
{
get => _selectedPerson;
set
{
if (_selectedPerson != value)
{
_selectedPerson = value;
OnPropertyChanged(nameof(SelectedPerson));
}
}
}
public PersonViewModel()
{
People.Add(new Person { Name = "John Doe", Age = 30 });
People.Add(new Person { Name = "Jane Smith", Age = 25 });
People.Add(new Person { Name = "Bob Johnson", Age = 35 });
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
// View class
class DataBindingDemo
{
static void Main()
{
Application.Init();
var viewModel = new PersonViewModel();
var top = Application.Top;
var window = new Window("Data Binding Demo")
{
X = 0,
Y = 0,
Width = Dim.Fill(),
Height = Dim.Fill()
};
// List view
var listView = new ListView()
{
X = 0,
Y = 0,
Width = 30,
Height = Dim.Fill() - 2,
AllowsMarking = false
};
// Set data source
var peopleNames = viewModel.People.Select(p => p.Name).ToList();
listView.SetSource(peopleNames);
// Detail display area
var detailFrame = new FrameView("Details")
{
X = 32,
Y = 0,
Width = Dim.Fill(),
Height = Dim.Fill() - 2
};
var nameLabel = new Label("Name:") { X = 1, Y = 1 };
var nameField = new TextField("")
{
X = 10,
Y = 1,
Width = Dim.Fill() - 1
};
var ageLabel = new Label("Age:") { X = 1, Y = 3 };
var ageField = new TextField("")
{
X = 10,
Y = 3,
Width = Dim.Fill() - 1
};
// Selection change event
listView.SelectedItemChanged += (args) =>
{
if (args.Item >= 0 && args.Item < viewModel.People.Count)
{
viewModel.SelectedPerson = viewModel.People[args.Item];
nameField.Text = viewModel.SelectedPerson.Name;
ageField.Text = viewModel.SelectedPerson.Age.ToString();
}
};
// Field change events
nameField.TextChanged += (oldText) =>
{
if (viewModel.SelectedPerson != null)
{
viewModel.SelectedPerson.Name = nameField.Text.ToString();
// Update list
peopleNames[listView.SelectedItem] = viewModel.SelectedPerson.Name;
listView.SetNeedsDisplay();
}
};
ageField.TextChanged += (oldText) =>
{
if (viewModel.SelectedPerson != null &&
int.TryParse(ageField.Text.ToString(), out int age))
{
viewModel.SelectedPerson.Age = age;
}
};
detailFrame.Add(nameLabel, nameField, ageLabel, ageField);
window.Add(listView, detailFrame);
// Initial selection
if (viewModel.People.Any())
{
viewModel.SelectedPerson = viewModel.People[0];
nameField.Text = viewModel.SelectedPerson.Name;
ageField.Text = viewModel.SelectedPerson.Age.ToString();
}
top.Add(window);
Application.Run();
Application.Shutdown();
}
}
Tables and Charts
using Terminal.Gui;
using System.Data;
class TableViewDemo
{
static void Main()
{
Application.Init();
var window = new Window("Table View Demo")
{
X = 0,
Y = 0,
Width = Dim.Fill(),
Height = Dim.Fill()
};
// Create data table
var dataTable = new DataTable();
dataTable.Columns.Add("Product", typeof(string));
dataTable.Columns.Add("Price", typeof(decimal));
dataTable.Columns.Add("Stock", typeof(int));
dataTable.Columns.Add("Category", typeof(string));
// Add sample data
dataTable.Rows.Add("Laptop", 899.00m, 15, "Electronics");
dataTable.Rows.Add("Mouse", 29.80m, 50, "Electronics");
dataTable.Rows.Add("Keyboard", 88.00m, 25, "Electronics");
dataTable.Rows.Add("Monitor", 350.00m, 8, "Electronics");
dataTable.Rows.Add("Coffee Cup", 4.50m, 100, "Beverages");
// Create table view
var tableView = new TableView()
{
X = 0,
Y = 1,
Width = Dim.Fill(),
Height = Dim.Fill() - 3
};
tableView.Table = dataTable;
// Adjust column widths
tableView.Style.ColumnStyles[0].MinWidth = 15; // Product
tableView.Style.ColumnStyles[1].MinWidth = 8; // Price
tableView.Style.ColumnStyles[2].MinWidth = 6; // Stock
tableView.Style.ColumnStyles[3].MinWidth = 10; // Category
// Filter field
var filterLabel = new Label("Filter:") { X = 0, Y = 0 };
var filterField = new TextField("")
{
X = 10,
Y = 0,
Width = 20
};
// Filter functionality
filterField.TextChanged += (oldText) =>
{
var filterText = filterField.Text.ToString().ToLower();
var view = dataTable.DefaultView;
if (string.IsNullOrEmpty(filterText))
{
view.RowFilter = string.Empty;
}
else
{
view.RowFilter = $"Product LIKE '*{filterText}*' OR Category LIKE '*{filterText}*'";
}
tableView.Update();
};
// Status label
var statusLabel = new Label("Select a row")
{
X = 0,
Y = Pos.Bottom(tableView)
};
// Selection event
tableView.SelectedCellChanged += (args) =>
{
if (tableView.Table?.Rows.Count > 0 && args.NewRow >= 0)
{
var row = tableView.Table.Rows[args.NewRow];
statusLabel.Text = $"Selected: {row["Product"]} - Price: {row["Price"]:C} - Stock: {row["Stock"]} units";
}
};
window.Add(filterLabel, filterField, tableView, statusLabel);
Application.Top.Add(window);
Application.Run();
Application.Shutdown();
}
}
Advanced Features
Custom Controls
using Terminal.Gui;
public class ProgressBar : View
{
private float _progress = 0f;
public float Progress
{
get => _progress;
set
{
_progress = Math.Max(0f, Math.Min(1f, value));
SetNeedsDisplay();
}
}
public ProgressBar()
{
Height = 1;
Width = Dim.Fill();
}
public override void Redraw(Rect bounds)
{
Driver.SetAttribute(ColorScheme.Normal);
int width = bounds.Width;
int filledWidth = (int)(width * _progress);
for (int x = 0; x < width; x++)
{
if (x < filledWidth)
{
Driver.SetAttribute(ColorScheme.HotNormal);
Driver.AddRune(x, 0, '█');
}
else
{
Driver.SetAttribute(ColorScheme.Normal);
Driver.AddRune(x, 0, '░');
}
}
// Display percentage
string percent = $"{(int)(_progress * 100)}%";
int textX = (width - percent.Length) / 2;
Driver.SetAttribute(ColorScheme.HotNormal);
for (int i = 0; i < percent.Length && textX + i < width; i++)
{
Driver.AddRune(textX + i, 0, percent[i]);
}
}
}
Theme Customization
// Custom color scheme
var customScheme = new ColorScheme
{
Normal = Attribute.Make(Color.White, Color.Blue),
Focus = Attribute.Make(Color.Yellow, Color.Blue),
HotNormal = Attribute.Make(Color.Cyan, Color.Blue),
HotFocus = Attribute.Make(Color.Yellow, Color.Red),
Disabled = Attribute.Make(Color.Gray, Color.Blue)
};
// Apply to controls
window.ColorScheme = customScheme;
button.ColorScheme = customScheme;
Key Bindings
// Global key bindings
Application.Top.KeyDown += (keyEvent) =>
{
if (keyEvent.KeyEvent.Key == Key.F5)
{
// F5 key for refresh
RefreshData();
keyEvent.Handled = true;
}
};
// Control-specific key bindings
textView.KeyDown += (keyEvent) =>
{
if (keyEvent.KeyEvent.Key == (Key.CtrlMask | Key.S))
{
// Ctrl+S for save
SaveFile();
keyEvent.Handled = true;
}
};
Ecosystem
Supported Platforms
- Windows: Windows 10/11, Windows Server
- macOS: macOS 10.15+
- Linux: Ubuntu, Debian, CentOS, RHEL, Fedora
- Unix: FreeBSD, OpenBSD, Solaris
.NET Version Support
- .NET 6.0+: Recommended version
- .NET Core 3.1+: Supported
- .NET Framework 4.8+: Windows only
- Mono: Limited support
Tools and Utilities
- UICatalog: Component demo application
- Visual Designer: Visual designer (experimental)
- NuGet: Official package distribution
Advantages
- Maturity: Most mature framework among .NET TUI libraries
- Rich Features: Comprehensive built-in components
- Cross-Platform: True cross-platform support
- Active Development: Continuous development and improvements
- Community: Large community and support
Limitations
- Learning Curve: High initial learning cost due to rich API
- Performance: Performance issues with large amounts of data
- v2 Migration: Breaking changes from v1 to v2
- Documentation: Limited Japanese documentation
Comparison with Other Libraries
Aspect | Terminal.Gui | Consolonia | Console Framework |
---|---|---|---|
Approach | Native TUI | Avalonia-based | WPF-like |
XAML Support | ✗ | ✓ | ✓ |
Maturity | Very High | Medium | Low |
Component Count | Very Many | Medium | Basic |
Performance | High | Medium | Medium |
Summary
Terminal.Gui is the most mature cross-platform TUI toolkit in the .NET ecosystem. It provides rich components, flexible layout system, MVVM support, and modern features (True Color, mouse support), making it ideal for professional console application development. The v2 Alpha offers further improvements, and its adoption is highly recommended for new projects.