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.

TUITerminalC#.NETCross-Platform

GitHub Overview

gui-cs/Terminal.Gui

Cross Platform Terminal UI toolkit for .NET

Stars10,210
Watchers143
Forks720
Created:December 11, 2017
Language:C#
License:MIT License

Topics

consoleconsole-applicationcross-platformcsharpcursesdotnetterminalterminal-basedterminal-uitui

Star History

gui-cs/Terminal.Gui Star History
Data as of: 7/25/2025, 11:09 AM

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

AspectTerminal.GuiConsoloniaConsole Framework
ApproachNative TUIAvalonia-basedWPF-like
XAML Support
MaturityVery HighMediumLow
Component CountVery ManyMediumBasic
PerformanceHighMediumMedium

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.