Table of Contents

Migrating From v1 To v2

This document provides a comprehensive guide for migrating applications from Terminal.Gui v1 to v2.

For detailed breaking change documentation, check out this Discussion: https://github.com/gui-cs/Terminal.Gui/discussions/2448

Table of Contents


Overview of Major Changes

Terminal.Gui v2 represents a major architectural evolution with these key improvements:

  1. Instance-Based Application Model - Move from static Application to IApplication instances
  2. IRunnable Architecture - Interface-based runnable pattern with type-safe results
  3. Simplified Layout - Removed Absolute/Computed distinction, improved adornments
  4. 24-bit TrueColor - Full color support by default
  5. Enhanced Input - Better keyboard and mouse APIs
  6. Built-in Scrolling - All views support scrolling inherently
  7. Fluent API - Method chaining for elegant code
  8. Proper Disposal - IDisposable pattern throughout

Application Architecture

Instance-Based Application Model

v1 Pattern (Static):

// v1 - static Application
Application.Init();
var top = Application.Top;
top.Add(myView);
Application.Run();
Application.Shutdown();

v2 Recommended Pattern (Instance-Based):

// v2 - instance-based with using statement
using (var app = Application.Create().Init())
{
    var top = new Window();
    top.Add(myView);
    app.Run(top);
    top.Dispose();
} // app.Dispose() called automatically

v2 Legacy Pattern (Still Works):

// v2 - legacy static (marked obsolete but functional)
Application.Init();
var top = new Window();
top.Add(myView);
Application.Run(top);
top.Dispose();
Application.Shutdown(); // Obsolete - use Dispose() instead

IRunnable Architecture

v2 introduces IRunnable<TResult> for type-safe, runnable views:

// Create a dialog that returns a typed result
public class FileDialog : Runnable<string>
{
    // Implementation
}

// Use it
using (var app = Application.Create().Init())
{
    app.Run<FileDialog>();
    string? result = app.GetResult<string>();
    
    if (result is { })
    {
        OpenFile(result);
    }
}

Key Benefits:

  • Type-safe results (no casting)
  • Automatic disposal of framework-created runnables
  • CWP-compliant lifecycle events
  • Works with any View (not just Toplevel)

Disposal and Resource Management

v2 requires explicit disposal:

// ❌ v1 - Application.Shutdown() disposed everything
Application.Init();
var top = new Window();
Application.Run(top);
Application.Shutdown(); // Disposed top automatically

// ✅ v2 - Dispose views explicitly
using (var app = Application.Create().Init())
{
    var top = new Window();
    app.Run(top);
    top.Dispose(); // Must dispose
}

// ✅ v2 - Framework-created runnables disposed automatically
using (var app = Application.Create().Init())
{
    app.Run<ColorPickerDialog>();
    var result = app.GetResult<Color>();
}

Disposal Rules:

  • "Whoever creates it, owns it"
  • Run<TRunnable>(): Framework creates → Framework disposes
  • Run(IRunnable): Caller creates → Caller disposes
  • Always dispose IApplication (use using statement)

View.App Property

Views now have an App property for accessing the application context:

// ❌ v1 - Direct static reference
Application.Driver.Move(x, y);

// ✅ v2 - Use View.App
App?.Driver.Move(x, y);

// ✅ v2 - Dependency injection
public class MyView : View
{
    private readonly IApplication _app;
    
    public MyView(IApplication app)
    {
        _app = app;
    }
}

View Construction and Initialization

Constructors → Initializers

v1:

var myView = new View(new Rect(10, 10, 40, 10));

v2:

var myView = new View 
{ 
    X = 10, 
    Y = 10, 
    Width = 40, 
    Height = 10 
};

Initialization Pattern

v2 uses ISupportInitializeNotification:

// v1 - No explicit initialization
var view = new View();
Application.Run(view);

// v2 - Automatic initialization via BeginInit/EndInit
var view = new View();
// BeginInit() called automatically when added to SuperView
// EndInit() called automatically
// Initialized event raised after EndInit()

Layout System Changes

Removed LayoutStyle Distinction

v1 had Absolute and Computed layout styles. v2 removed this distinction.

v1:

view.LayoutStyle = LayoutStyle.Computed;

v2:

// No LayoutStyle - all layout is declarative via Pos/Dim
view.X = Pos.Center();
view.Y = Pos.Center();
view.Width = Dim.Percent(50);
view.Height = Dim.Fill();

Frame vs Bounds

v1:

  • Frame - Position/size in SuperView coordinates
  • Bounds - Always {0, 0, Width, Height} (location always empty)

v2:

  • Frame - Position/size in SuperView coordinates (same as v1)
  • Viewport - Visible area in content coordinates (replaces Bounds)
    • Important: Viewport.Location can now be non-zero for scrolling
// ❌ v1
var size = view.Bounds.Size;
Debug.Assert(view.Bounds.Location == Point.Empty); // Always true

// ✅ v2
var visibleArea = view.Viewport;
var contentSize = view.GetContentSize();

// Viewport.Location can be non-zero when scrolled
view.ScrollVertical(10);
Debug.Assert(view.Viewport.Location.Y == 10);

Pos and Dim API Changes

v1 v2
Pos.At(x) Pos.Absolute(x)
Dim.Sized(width) Dim.Absolute(width)
Pos.Anchor() Pos.GetAnchor()
Dim.Anchor() Dim.GetAnchor()
// ❌ v1
view.X = Pos.At(10);
view.Width = Dim.Sized(20);

// ✅ v2
view.X = Pos.Absolute(10);
view.Width = Dim.Absolute(20);

View.AutoSize Removed

v1:

view.AutoSize = true;

v2:

view.Width = Dim.Auto();
view.Height = Dim.Auto();

See Dim.Auto Deep Dive for details.


Adornments

v2 adds Border, Margin, and Padding as built-in adornments.

v1:

// Custom border drawing
view.Border = new Border { /* ... */ };

v2:

// Built-in Border adornment
view.BorderStyle = LineStyle.Single;
view.Border.Thickness = new Thickness(1);
view.Title = "My View";

// Built-in Margin and Padding
view.Margin.Thickness = new Thickness(2);
view.Padding.Thickness = new Thickness(1);

See Layout Deep Dive for complete details.


Color and Attribute Changes

24-bit TrueColor Default

v2 uses 24-bit color by default.

// v1 - Limited color palette
var color = Color.Brown;

// v2 - ANSI-compliant names + TrueColor
var color = Color.Yellow; // Brown renamed
var customColor = new Color(0xFF, 0x99, 0x00); // 24-bit RGB

Attribute.Make Removed

v1:

var attr = Attribute.Make(Color.BrightMagenta, Color.Blue);

v2:

var attr = new Attribute(Color.BrightMagenta, Color.Blue);

Color Name Changes

v1 v2
Color.Brown Color.Yellow

Type Changes

Low-Level Types

v1 v2
Rect Rectangle
Point Point
Size Size
// ❌ v1
Rect rect = new Rect(0, 0, 10, 10);

// ✅ v2
Rectangle rect = new Rectangle(0, 0, 10, 10);

Unicode and Text

NStack.ustring Removed

v1:

using NStack;
ustring text = "Hello";
var width = text.Sum(c => Rune.ColumnWidth(c));

v2:

using System.Text;
string text = "Hello";
var width = text.GetColumns(); // Extension method

Rune Changes

v1:

// Implicit cast
myView.AddRune(col, row, '▄');

// Width
var width = Rune.ColumnWidth(rune);

v2:

// Explicit constructor
myView.AddRune(col, row, new Rune('▄'));

// Width
var width = rune.GetColumns();

See Unicode for details.


Keyboard API

v2 has a completely redesigned keyboard API.

Key Class

v1:

KeyEvent keyEvent;
if (keyEvent.KeyCode == KeyCode.Enter) { }

v2:

Key key;
if (key == Key.Enter) { }

// Modifiers
if (key.Shift) { }
if (key.Ctrl) { }

// With modifiers
Key ctrlC = Key.C.WithCtrl;
Key shiftF1 = Key.F1.WithShift;

Key Bindings

v1:

// Override OnKeyPress
protected override bool OnKeyPress(KeyEvent keyEvent)
{
    if (keyEvent.KeyCode == KeyCode.Enter)
    {
        // Handle
        return true;
    }
    return base.OnKeyPress(keyEvent);
}

v2:

// Use KeyBindings + Commands
AddCommand(Command.Accept, HandleAccept);
KeyBindings.Add(Key.Enter, Command.Accept);

private bool HandleAccept()
{
    // Handle
    return true;
}

Application-Wide Keys

v1:

// Hard-coded Ctrl+Q
if (keyEvent.Key == Key.CtrlMask | Key.Q)
{
    Application.RequestStop();
}

v2:

// Configurable quit key
if (key == Application.QuitKey)
{
    Application.RequestStop();
}

// Change the quit key
Application.QuitKey = Key.Esc;

v2 has consistent, configurable navigation keys:

Key Purpose
Tab Next TabStop
Shift+Tab Previous TabStop
F6 Next TabGroup
Shift+F6 Previous TabGroup
// Configurable
Application.NextTabStopKey = Key.Tab;
Application.PrevTabStopKey = Key.Tab.WithShift;
Application.NextTabGroupKey = Key.F6;
Application.PrevTabGroupKey = Key.F6.WithShift;

See Keyboard Deep Dive for complete details.


Mouse API

MouseEventEventArgs → MouseEventArgs

v1:

void HandleMouse(MouseEventEventArgs args) { }

v2:

void HandleMouse(object? sender, MouseEventArgs args) { }

Mouse Coordinates

v1:

  • Mouse coordinates were screen-relative

v2:

  • Mouse coordinates are now Viewport-relative
// v2 - Viewport-relative coordinates
view.MouseEvent += (s, e) =>
{
    // e.Position is relative to view's Viewport
    var x = e.Position.X; // 0 = left edge of viewport
    var y = e.Position.Y; // 0 = top edge of viewport
};

Mouse Click Handling

v1:

// v1 - MouseClick event
view.MouseClick += (mouseEvent) =>
{
    // Handle click
    DoSomething();
};

v2:

// v2 - Use MouseBindings + Commands + Activating event
view.MouseBindings.Add(MouseFlags.Button1Clicked, Command.Activate);
view.Activating += (s, e) =>
{
    // Handle selection (called when Button1Clicked)
    DoSomething();
};

// Alternative: Use MouseEvent for low-level handling
view.MouseEvent += (s, e) =>
{
    if (e.Flags.HasFlag(MouseFlags.Button1Clicked))
    {
        DoSomething();
        e.Handled = true;
    }
};

Key Changes:

  • View.MouseClick event has been removed
  • Use MouseBindings to map mouse events to Commands
  • Default mouse bindings invoke Command.Activate which raises the Activating event
  • For custom behavior, override OnActivating or subscribe to the Activating event
  • For low-level mouse handling, use MouseEvent directly

Migration Pattern:

// ❌ v1 - OnMouseClick override
protected override bool OnMouseClick(MouseEventArgs mouseEvent)
{
    if (mouseEvent.Flags.HasFlag(MouseFlags.Button1Clicked))
    {
        PerformAction();
        return true;
    }
    return base.OnMouseClick(mouseEvent);
}

// ✅ v2 - OnActivating override
protected override bool OnActivating(CommandEventArgs args)
{
    if (args.Context is CommandContext { Binding.MouseEventArgs: { } mouseArgs })
    {
        // Access mouse position and flags via context
        if (mouseArgs.Flags.HasFlag(MouseFlags.Button1Clicked))
        {
            PerformAction();
            return true;
        }
    }
    return base.OnActivating(args);
}

// ✅ v2 - Activating event (simpler)
view.Activating += (s, e) =>
{
    PerformAction();
    e.Handled = true;
};

Accessing Mouse Position in Activating Event:

view.Activating += (s, e) =>
{
    // Extract mouse event args from command context
    if (e.Context is CommandContext { Binding.MouseEventArgs: { } mouseArgs })
    {
        Point position = mouseArgs.Position;
        MouseFlags flags = mouseArgs.Flags;
        
        // Use position and flags for custom logic
        HandleClick(position, flags);
        e.Handled = true;
    }
};

Mouse State and Highlighting

v2 adds enhanced mouse state tracking:

// Configure which mouse states trigger highlighting
view.HighlightStates = MouseState.In | MouseState.Pressed;

// React to mouse state changes
view.MouseStateChanged += (s, e) =>
{
    switch (e.Value)
    {
        case MouseState.In:
            // Mouse entered view
            break;
        case MouseState.Pressed:
            // Mouse button pressed in view
            break;
    }
};

See Mouse Deep Dive for complete details.


Focus Properties

v1:

view.CanFocus = true; // Default was true

v2:

view.CanFocus = true; // Default is FALSE - must opt-in

Important: In v2, CanFocus defaults to false. Views that want focus must explicitly set it.

Focus Changes

v1:

// HasFocus was read-only
bool hasFocus = view.HasFocus;

v2:

// HasFocus can be set
view.HasFocus = true; // Equivalent to SetFocus()
view.HasFocus = false; // Equivalent to SuperView.AdvanceFocus()

TabStop Behavior

v1:

view.TabStop = true; // Boolean

v2:

view.TabStop = TabBehavior.TabStop; // Enum with more options

// Options:
// - NoStop: Focusable but not via Tab
// - TabStop: Normal tab navigation
// - TabGroup: Advance via F6

v1:

view.Enter += (s, e) => { }; // Gained focus
view.Leave += (s, e) => { }; // Lost focus

v2:

view.HasFocusChanging += (s, e) => 
{ 
    // Before focus changes (cancellable)
    if (preventFocusChange)
        e.Cancel = true;
};

view.HasFocusChanged += (s, e) => 
{ 
    // After focus changed
    if (e.Value)
        Console.WriteLine("Gained focus");
    else
        Console.WriteLine("Lost focus");
};

See Navigation Deep Dive for complete details.


Scrolling Changes

ScrollView Removed

v1:

var scrollView = new ScrollView
{
    ContentSize = new Size(100, 100),
    ShowHorizontalScrollIndicator = true,
    ShowVerticalScrollIndicator = true
};

v2:

// Built-in scrolling on every View
var view = new View();
view.SetContentSize(new Size(100, 100));

// Built-in scrollbars
view.VerticalScrollBar.Visible = true;
view.HorizontalScrollBar.Visible = true;
view.VerticalScrollBar.AutoShow = true;

Scrolling API

v2:

// Set content larger than viewport
view.SetContentSize(new Size(100, 100));

// Scroll by changing Viewport location
view.Viewport = view.Viewport with { Location = new Point(10, 10) };

// Or use helper methods
view.ScrollVertical(5);
view.ScrollHorizontal(3);

See Scrolling Deep Dive for complete details.


Event Pattern Changes

v2 standardizes all events to use object sender, EventArgs args pattern.

Button.Clicked → Button.Accepting

v1:

button.Clicked += () => { /* do something */ };

v2:

button.Accepting += (s, e) => { /* do something */ };

Event Signatures

v1:

// Various patterns
event Action SomeEvent;
event Action<T> OtherEvent;
event Action<T1, T2> ThirdEvent;

v2:

// Consistent pattern
event EventHandler<EventArgs>? SomeEvent;
event EventHandler<T>? OtherEvent;

View-Specific Changes

ListView

v1:

var listView = new ListView(items);
listView.SelectedChanged += () => { };

v2:

var listView = new ListView();
listView.SetSource(items);
listView.SelectedItemChanged += (s, e) => { };

TextView

v1:

var textView = new TextView
{
    Text = "Initial text"
};

v2:

var textView = new TextView
{
    Text = "Initial text"
};
// Same API, but better performance

Button

v1:

var button = new Button("Click Me");
button.Clicked += () => { };

v2:

var button = new Button { Text = "Click Me" };
button.Accepting += (s, e) => { };

Disposal and Resource Management

v2 implements IDisposable throughout the API.

Disposal Rules

  1. Whoever creates it, owns it - If you create a View, you must dispose it
  2. Framework-created instances - The framework disposes what it creates
  3. Always use using statements - For IApplication instances
// ✅ Correct disposal pattern
using (var app = Application.Create().Init())
{
    var window = new Window();
    try
    {
        app.Run(window);
    }
    finally
    {
        window.Dispose();
    }
}

// ✅ Framework disposes what it creates
using (var app = Application.Create().Init())
{
    app.Run<MyDialog>(); // Framework creates and disposes MyDialog
}

API Terminology Changes

v1 v2 Notes
Application.Top app.TopRunnable Property on IApplication instance
Application.MainLoop app.MainLoop Property on IApplication instance
Application.Driver app.Driver Property on IApplication instance
Bounds Viewport Viewport can have non-zero location for scrolling
Rect Rectangle Standard .NET type
MouseClick event Activating event Via Command.Activate
Enter/Leave events HasFocusChanged event Unified focus event
Button.Clicked Button.Accepting Consistent with Command pattern
AutoSize Dim.Auto() Part of layout system
LayoutStyle Removed All layout is now declarative

Summary

v2 represents a significant evolution of Terminal.Gui with:

  • Better Architecture - Instance-based, testable, maintainable
  • Modern APIs - Standard .NET patterns throughout
  • Enhanced Capabilities - TrueColor, built-in scrolling, better input
  • Improved Developer Experience - Fluent API, better documentation

While migration requires some effort, the result is a more robust, performant, and maintainable codebase. Start by updating your application lifecycle to use Application.Create(), then address layout and input changes incrementally.

For more details, see: