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
- Application Architecture
- View Construction and Initialization
- Layout System Changes
- Color and Attribute Changes
- Type Changes
- Unicode and Text
- Keyboard API
- Mouse API
- Navigation Changes
- Scrolling Changes
- Adornments
- Event Pattern Changes
- View-Specific Changes
- Disposal and Resource Management
- API Terminology Changes
Overview of Major Changes
Terminal.Gui v2 represents a major architectural evolution with these key improvements:
- Instance-Based Application Model - Move from static
ApplicationtoIApplicationinstances - IRunnable Architecture - Interface-based runnable pattern with type-safe results
- Simplified Layout - Removed Absolute/Computed distinction, improved adornments
- 24-bit TrueColor - Full color support by default
- Enhanced Input - Better keyboard and mouse APIs
- Built-in Scrolling - All views support scrolling inherently
- Fluent API - Method chaining for elegant code
- 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 disposesRun(IRunnable): Caller creates → Caller disposes- Always dispose
IApplication(useusingstatement)
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 coordinatesBounds- 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.Locationcan now be non-zero for scrolling
- Important:
// ❌ 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;
Navigation Keys
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.
Navigation Changes
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
Navigation Events
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();
MenuItem
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
IRunnablearchitecture - Works with any
IRunnable, not justToplevel
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
SessionTokenterminology - 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
- Application Deep Dive - Complete application architecture
- View Deep Dive - View system details
- Layout Deep Dive - Comprehensive layout guide
- Keyboard Deep Dive - Keyboard input handling
- Mouse Deep Dive - Mouse input handling
- Navigation Deep Dive - Focus and navigation
- Scrolling Deep Dive - Built-in scrolling system
- Arrangement Deep Dive - Movable/resizable views
- Configuration Deep Dive - Configuration system
- What's New in v2 - New features overview