docfx/docs/migratingfromv1.md
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:
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
v2 introduces xref:Terminal.Gui.App.IRunnable 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:
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:
Run<TRunnable>(): Framework creates → Framework disposesRun(IRunnable): Caller creates → Caller disposesusing 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)
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.GetDefaultKey (Command.Quit))
{
Application.RequestStop ();
}
// Change the quit key via DefaultKeyBindings
Application.DefaultKeyBindings[Command.Quit] = Bind.All (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 via Application.DefaultKeyBindings
Application.GetDefaultKey (Command.NextTabStop); // Key.Tab
Application.GetDefaultKey (Command.PreviousTabStop); // Key.Tab.WithShift
Application.GetDefaultKey (Command.NextTabGroup); // Key.F6
Application.GetDefaultKey (Command.PreviousTabGroup); // Key.F6.WithShift
See Keyboard Deep Dive for complete details.
v1:
void HandleMouse(MouseEventEventArgs args) { }
v2:
void HandleMouse(object? sender, MouseEventArgs args) { }
v1:
v2:
// 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 xref:Terminal.Gui.Input.CommandsOnActivating or subscribe to the xref:Terminal.Gui.ViewBase.View.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 { 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;
}
};
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, xref:Terminal.Gui.ViewBase.View.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 with automatic visibility
view.ViewportSettings |= ViewportSettingsFlags.HasVerticalScrollBar;
view.ViewportSettings |= ViewportSettingsFlags.HasHorizontalScrollBar;
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<T> OtherEvent;
event Action<T1, T2> ThirdEvent;
v2:
// Consistent pattern
event EventHandler<EventArgs>? SomeEvent;
event EventHandler<T>? OtherEvent;
Terminal.Gui v2 introduces a completely redesigned cursor system that separates the Terminal Cursor (visible indicator) from the Draw Cursor (internal rendering position).
v1 Pattern (PositionCursor Override):
// v1 - Override PositionCursor method
public override void PositionCursor ()
{
if (!HasFocus) return;
var col = _cursorPosition - _scrollOffset;
if (col < 0 || col >= Frame.Width) return;
Move (col, 0); // This was confusing - affected both cursors
}
v2 Pattern (Cursor Property):
// v2 - Set Cursor property in OnDrawContent
protected override void OnDrawContent (Rectangle viewport)
{
// ... drawing code ...
if (HasFocus)
{
int col = _cursorPosition - _scrollOffset;
if (col >= 0 && col < viewport.Width)
{
// Convert to screen coordinates and set cursor
Point screenPos = ViewportToScreen (new Point (col, 0));
Cursor = new Cursor
{
Position = screenPos,
Style = CursorStyle.BlinkingBar
};
}
else
{
// Hide cursor when outside viewport
Cursor = new Cursor { Position = null };
}
}
}
v2 uses an immutable Cursor record class:
// Immutable cursor with screen coordinates
Cursor = new Cursor
{
Position = screenPos, // Point? - null = hidden
Style = CursorStyle.BlinkingBar // ANSI-based styles
};
// Update position keeping same style
Cursor = Cursor with { Position = newScreenPos };
// Hide cursor
Cursor = new Cursor { Position = null };
v2 uses ANSI/VT terminal standards instead of Windows-based styles:
// v2 - ANSI DECSCUSR-based styles
public enum CursorStyle
{
Default = 0, // Usually BlinkingBlock
BlinkingBlock = 1, // █ (blinking)
SteadyBlock = 2, // █ (steady)
BlinkingUnderline = 3, // _ (blinking)
SteadyUnderline = 4, // _ (steady)
BlinkingBar = 5, // | (blinking) - common for text editors
SteadyBar = 6, // | (steady)
Hidden = -1 // No visible cursor
}
CRITICAL: Cursor.Position must ALWAYS be in screen-absolute coordinates.
// v2 - Always convert to screen coordinates
Point contentPos = new Point (col, row); // Your internal coordinates
Point screenPos = ContentToScreen (contentPos); // Convert to screen
Cursor = new Cursor { Position = screenPos, Style = CursorStyle.BlinkingBar };
// Or from viewport coordinates
Point viewportPos = new Point (col, row);
Point screenPos = ViewportToScreen (viewportPos);
Cursor = new Cursor { Position = screenPos, Style = CursorStyle.BlinkingBar };
When cursor moves without content changes, use SetCursorNeedsUpdate():
// v2 - Signal cursor update without full redraw
private void MoveCursorRight ()
{
_cursorPosition++;
int viewportCol = _cursorPosition - _scrollOffset;
if (viewportCol >= 0 && viewportCol < Viewport.Width)
{
Point screenPos = ViewportToScreen (new Point (viewportCol, 0));
Cursor = Cursor with { Position = screenPos };
SetCursorNeedsUpdate (); // Efficient - no redraw
}
}
v1 Confusion:
Move() affected both Draw Cursor and positioning for Terminal Cursorv2 Clarity:
Move() ONLY affects Draw Cursor (where next character renders)Cursor property ONLY affects Terminal Cursor (visible indicator)// ❌ WRONG in v2 - Don't use Move() for cursor positioning
Move (cursorCol, cursorRow); // This is for drawing, not Terminal Cursor
// ✅ CORRECT in v2 - Use Cursor property
Point screenPos = ViewportToScreen (new Point (cursorCol, cursorRow));
Cursor = new Cursor { Position = screenPos, Style = CursorStyle.BlinkingBar };
When migrating cursor code from v1 to v2:
PositionCursor() overrideOnDrawContent() or event handlersViewportToScreen() or ContentToScreen()Cursor property instead of calling Move()CursorStyle enum for cursor appearanceSetCursorNeedsUpdate() for position-only changesCursor.Position = null to hide cursorSee Cursor Management for comprehensive documentation and examples.
v1:
var listView = new ListView(items);
listView.SelectedChanged += () => { };
v2:
var listView = new ListView();
listView.SetSource(items);
listView.SelectedItemChanged += (s, e) => { };
v1:
var textView = new TextView
{
Text = "Initial text"
};
v2:
var textView = new TextView
{
Text = "Initial text"
};
// Same API, but better performance
v1:
var button = new Button("Click Me");
button.Clicked += () => { };
v2:
var button = new Button { Text = "Click Me" };
button.Accepting += (s, e) => { };
v2 implements IDisposable throughout the API.
using statements - For xref:Terminal.Gui.App.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
}
| 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 | xref:Terminal.Gui.ViewBase.View.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 |
v2 represents a significant evolution of Terminal.Gui with:
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: