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>
{
// 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 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.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.MouseClickevent has been removed- Use
MouseBindingsto map mouse events toCommands - Default mouse bindings invoke
Command.Activatewhich raises theActivatingevent - For custom behavior, override
OnActivatingor subscribe to theActivatingevent - For low-level mouse handling, use
MouseEventdirectly
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.
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<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
- Whoever creates it, owns it - If you create a View, you must dispose it
- Framework-created instances - The framework disposes what it creates
- Always use
usingstatements - ForIApplicationinstances
// ✅ 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: