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?>
{
    private TextField _pathField;
    
    public FileDialog()
    {
        Title = "Select File";
        _pathField = new TextField { Width = Dim.Fill() };
        Add(_pathField);
        
        var okButton = new Button { Text = "OK", IsDefault = true };
        okButton.Accepting += (s, e) => {
            Result = _pathField.Text;
            Application.RequestStop();
        };
        AddButton(okButton);
    }
    
    protected override bool OnIsRunningChanging(bool oldValue, bool newValue)
    {
        if (!newValue)  // Stopping - extract result before disposal
        {
            Result = _pathField?.Text;
        }
        return base.OnIsRunningChanging(oldValue, newValue);
    }
}

// Use with fluent API
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<MyDialog>(); // Dialog disposed automatically
    var result = app.GetResult<MyResult>();
}

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.MouseClick += (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
};

Highlight Event

v2 adds a Highlight event for visual feedback:

view.Highlight += (s, e) =>
{
    // Provide visual feedback on mouse hover
};
view.HighlightStyle = HighlightStyle.Hover;

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<string> OtherEvent;
event Action<EventArgs> ThirdEvent;

v2:

// Consistent pattern
event EventHandler<EventArgs>? SomeEvent;
event EventHandler<EventArgs<string>>? OtherEvent;
event EventHandler<CancelEventArgs<bool>>? ThirdEvent;

Benefits:

  • Named parameters
  • Cancellable events via CancelEventArgs
  • Future-proof (new properties can be added)

View-Specific Changes

CheckBox

v1:

var cb = new CheckBox("_Checkbox", true);
cb.Toggled += (e) => { };
cb.Toggle();

v2:

var cb = new CheckBox 
{ 
    Title = "_Checkbox",
    CheckState = CheckState.Checked
};
cb.CheckStateChanging += (s, e) => 
{
    e.Cancel = preventChange;
};
cb.AdvanceCheckState();

StatusBar

v1:

var statusBar = new StatusBar(
    new StatusItem[]
    {
        new StatusItem(Application.QuitKey, "Quit", () => Quit())
    }
);

v2:

var statusBar = new StatusBar(
    new Shortcut[] 
    { 
        new Shortcut(Application.QuitKey, "Quit", Quit) 
    }
);

PopoverMenu

v2 replaces ContextMenu with PopoverMenu:

v1:

var contextMenu = new ContextMenu();

v2:

var popoverMenu = new PopoverMenu();

v1:

new MenuItem(
    "Copy",
    "",
    CopyGlyph,
    null,
    null,
    (KeyCode)Key.G.WithCtrl
)

v2:

new MenuItem(
    "Copy",
    "",
    CopyGlyph,
    Key.G.WithCtrl
)

Disposal and Resource Management

v2 implements proper IDisposable throughout.

View Disposal

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

// v2 - Explicit disposal required
var view = new View();
app.Run(view);
view.Dispose();
app.Dispose();

Disposal Patterns

// ✅ Best practice - using statement
using (var app = Application.Create().Init())
{
    using (var view = new View())
    {
        app.Run(view);
    }
}

// ✅ Alternative - explicit try/finally
var app = Application.Create();
try
{
    app.Init();
    var view = new View();
    try
    {
        app.Run(view);
    }
    finally
    {
        view.Dispose();
    }
}
finally
{
    app.Dispose();
}

SubView Disposal

When a View is disposed, it automatically disposes all SubViews:

var container = new View();
var child1 = new View();
var child2 = new View();

container.Add(child1, child2);

// Disposes container, child1, and child2
container.Dispose();

See Resource Management for complete details.


API Terminology Changes

v2 modernizes terminology for clarity:

Application.Top → Application.TopRunnable

v1:

Application.Top.SetNeedsDraw();

v2:

// Use TopRunnable (or TopRunnableView for View reference)
app.TopRunnable?.SetNeedsDraw();
app.TopRunnableView?.SetNeedsDraw();

// From within a view
App?.TopRunnableView?.SetNeedsDraw();

Why "TopRunnable"?

  • Clearly indicates it's the top of the runnable session stack
  • Aligns with IRunnable architecture
  • Works with any IRunnable, not just Toplevel

Application.TopLevels → Application.SessionStack

v1:

foreach (var tl in Application.TopLevels)
{
    // Process
}

v2:

foreach (var token in app.SessionStack)
{
    var runnable = token.Runnable;
    // Process
}

// Count of sessions
int sessionCount = app.SessionStack.Count;

Why "SessionStack"?

  • Describes both content (sessions) and structure (stack)
  • Aligns with SessionToken terminology
  • Follows .NET naming patterns

View Arrangement

v1:

view.SendSubViewToBack();
view.SendSubViewBackward();
view.SendSubViewToFront();
view.SendSubViewForward();

v2:

// Fixed naming (methods worked opposite to their names in v1)
view.MoveSubViewToStart();
view.MoveSubViewTowardsStart();
view.MoveSubViewToEnd();
view.MoveSubViewTowardsEnd();

Mdi → ViewArrangement.Overlapped

v1:

Application.MdiTop = true;
toplevel.IsMdiContainer = true;

v2:

view.Arrangement = ViewArrangement.Overlapped;

// Additional flags
view.Arrangement = ViewArrangement.Movable | ViewArrangement.Resizable;

See Arrangement Deep Dive for complete details.


Complete Migration Example

Here's a complete v1 to v2 migration:

v1:

using NStack;
using Terminal.Gui;

Application.Init();

var win = new Window(new Rect(0, 0, 50, 20), "Hello");

var label = new Label(1, 1, "Name:");

var textField = new TextField(10, 1, 30, "");

var button = new Button(10, 3, "OK");
button.Clicked += () =>
{
    MessageBox.Query(50, 7, "Info", $"Hello, {textField.Text}", "Ok");
};

win.Add(label, textField, button);

Application.Top.Add(win);
Application.Run();
Application.Shutdown();

v2:

using System;
using Terminal.Gui;

using (var app = Application.Create().Init())
{
    var win = new Window
    {
        Title = "Hello",
        Width = 50,
        Height = 20
    };

    var label = new Label
    {
        Text = "Name:",
        X = 1,
        Y = 1
    };

    var textField = new TextField
    {
        X = 10,
        Y = 1,
        Width = 30
    };

    var button = new Button
    {
        Text = "OK",
        X = 10,
        Y = 3
    };
    button.Accepting += (s, e) =>
    {
        MessageBox.Query(app, "Info", $"Hello, {textField.Text}", "Ok");
    };

    win.Add(label, textField, button);

    app.Run(win);
    win.Dispose();
}

Summary of Major Breaking Changes

Category v1 v2
Application Static Application IApplication instances via Application.Create()
Disposal Automatic Explicit (IDisposable pattern)
View Construction Constructors with Rect Initializers with X, Y, Width, Height
Layout Absolute/Computed distinction Unified Pos/Dim system
Colors Limited palette 24-bit TrueColor default
Types Rect, NStack.ustring Rectangle, System.String
Keyboard KeyEvent, hard-coded keys Key, configurable bindings
Mouse Screen-relative Viewport-relative
Scrolling ScrollView Built-in on all Views
Focus CanFocus default true CanFocus default false
Navigation Enter/Leave events HasFocusChanging/HasFocusChanged
Events Mixed patterns Standard EventHandler<EventArgs>
Terminology Application.Top, TopLevels TopRunnable, SessionStack

Additional Resources


Getting Help