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
Terminal.Gui v2 represents a major architectural evolution with these key improvements:
Application to IApplication instancesv1 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
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:
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:
Run<TRunnable>(): Framework creates → Framework disposesRun(IRunnable): Caller creates → Caller disposesIApplication (use using statement)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;
}
}
v1:
var myView = new View(new Rect(10, 10, 40, 10));
v2:
var myView = new View
{
X = 10,
Y = 10,
Width = 40,
Height = 10
};
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()
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();
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.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);
| 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);
v1:
view.AutoSize = true;
v2:
view.Width = Dim.Auto();
view.Height = Dim.Auto();
See Dim.Auto Deep Dive for details.
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.
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
v1:
var attr = Attribute.Make(Color.BrightMagenta, Color.Blue);
v2:
var attr = new Attribute(Color.BrightMagenta, Color.Blue);
| v1 | v2 |
|---|---|
Color.Brown |
Color.Yellow |
| 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);
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
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.
v2 has a completely redesigned keyboard API.
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;
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;
}
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.
v1:
void HandleMouse(MouseEventEventArgs args) { }
v2:
void HandleMouse(object? sender, MouseEventArgs args) { }
v1:
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
};
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 removedMouseBindings to map mouse events to CommandsCommand.Activate which raises the Activating eventOnActivating or subscribe to the Activating eventMouseEvent directlyMigration 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<MouseBinding> { 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<MouseBinding> { Binding.MouseEventArgs: { } mouseArgs })
{
Point position = mouseArgs.Position;
MouseFlags flags = mouseArgs.Flags;
// Use position and flags for custom logic
HandleClick(position, flags);
e.Handled = true;
}
};
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.
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.
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()
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.
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;
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.
v2 standardizes all events to use object sender, EventArgs args pattern.
v1:
button.Clicked += () => { /* do something */ };
v2:
button.Accepting += (s, e) => { /* do something */ };
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:
CancelEventArgsv1:
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();
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)
}
);
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
)
v2 implements proper IDisposable throughout.
// 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();
// ✅ 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();
}
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.
v2 modernizes terminology for clarity:
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"?
IRunnable architectureIRunnable, not just Toplevelv1:
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"?
SessionToken terminologyv1:
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();
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.
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();
}
| 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 |