mcpField
D
dotnet-terminal-gui

dotnet-terminal-gui

github.com/wshaddix开发8677 次安装35 周前更新
代码仓库

关于 dotnet-terminal-gui

dotnet-terminal-gui 是面向 .NET 开发者的 Terminal.Gui v2 技能,涵盖跨平台终端图形界面的完整开发流程,包括生命周期、布局、视图和事件处理。

主要功能

  1. 01管理应用生命周期与资源
  2. 02使用 Pos 和 Dim 进行布局
  3. 03构建视图、菜单及对话框
  4. 04处理键盘与鼠标事件
  5. 05配置主题、颜色和边框

适用场景

  1. 01构建跨平台终端界面应用
  2. 02制作包含表单和列表的 TUI
  3. 03为终端应用添加菜单与快捷键

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/wshaddix/dotnet-skills --skill dotnet-terminal-gui

安装后在 Claude Code 中输入/dotnet-terminal-gui确认已加载。

详细介绍

dotnet-terminal-gui

Terminal.Gui v2 用于构建完整的终端用户界面,支持窗口、菜单、对话框、视图、布局、事件处理、颜色主题和鼠标操作。可跨平台运行于 Windows、macOS 和 Linux 终端。

**版本假设:**基线为 .NET 8.0+。Terminal.Gui 2.0.0-alpha(v2 Alpha 是新项目的活跃开发版本——API 稳定且功能全面;Beta 发布前仍可能发生重大变更,但核心架构已十分稳固)。v1.x(1.19.0)处于维护模式,不再添加新功能。

**范围边界:**本技能涵盖使用 Terminal.Gui 进行完整的 TUI 应用开发,包括应用程序生命周期、布局、视图、菜单、对话框、事件处理和主题。富控制台输出(表格、进度条、提示、标记)由 [skill:dotnet-spectre-console] 负责。CLI 命令行解析由 [skill:dotnet-system-commandline] 负责。CLI 应用架构和分发由 [skill:dotnet-cli-architecture] 和 [skill:dotnet-cli-distribution] 负责。

交叉引用:[skill:dotnet-spectre-console] 提供富控制台输出替代方案;[skill:dotnet-csharp-async-patterns] 提供异步 TUI 模式;[skill:dotnet-native-aot] 介绍 AOT 编译注意事项;[skill:dotnet-system-commandline] 介绍 CLI 解析;[skill:dotnet-csharp-dependency-injection] 介绍 TUI 应用中的 DI;[skill:dotnet-accessibility] 介绍 TUI 的无障碍限制和屏幕阅读器注意事项。


包引用

<ItemGroup>
  
  <PackageReference Include="Terminal.Gui" Version="2.0.0-alpha.*" />
</ItemGroup>

Terminal.Gui v2 面向 .NET 8+ 和 .NET Standard 2.0/2.1。对于 v1 维护项目,请使用 Version="1.19.*"。


应用程序生命周期

Terminal.Gui v2 使用基于实例的模型,通过 IApplication 和 IDisposable 正确清理资源。这取代了 v1 的静态 Application.Init() / Application.Run() / Application.Shutdown() 模式。

基本应用程序
using Terminal.Gui;

// Create and initialize the application (instance-based in v2)
using IApplication app = Application.Create().Init();

var window = new Window
{
    Title = "My TUI App",
    Width = Dim.Fill(),
    Height = Dim.Fill()
};

var label = new Label
{
    Text = "Hello, Terminal.Gui!",
    X = Pos.Center(),
    Y = Pos.Center()
};
window.Add(label);

app.Run(window);
返回类型化结果的应用程序
using IApplication app = Application.Create().Init();

// Run a dialog and get a typed result
app.Run<MyInputDialog>();
string? result = app.GetResult<string>();
生命周期事件
// IsRunningChanging -- cancellable, fires before state change
// IsRunningChanged  -- non-cancellable, fires after state change
window.IsRunningChanged += (sender, args) =>
{
    if (!args.NewValue)
    {
        // Window is closing -- clean up resources
    }
};

布局系统

Terminal.Gui v2 将布局统一为单一模型(移除了 v1 的 Absolute/Computed 区分)。位置由 Pos(X、Y)控制,大小由 Dim(宽度、高度)控制;两者都相对于 SuperView 的内容区域。

Pos 类型(定位)
// Absolute -- fixed coordinate
view.X = 5;                          // Pos.Absolute(5)

// Percent -- percentage of parent
view.X = Pos.Percent(25);            // 25% from left

// Center -- centered in parent
view.X = Pos.Center();

// AnchorEnd -- anchored from right/bottom edge
view.X = Pos.AnchorEnd(10);          // 10 from right edge

// Relative to another view
view.X = Pos.Right(otherView) + 1;   // 1 right of otherView
view.Y = Pos.Bottom(otherView) + 1;  // 1 below otherView
view.X = Pos.Left(otherView);        // aligned left with otherView
view.Y = Pos.Top(otherView);         // aligned top with otherView

// Align -- align groups of views
view.X = Pos.Align(Alignment.End);   // right-align (e.g., dialog buttons)

// Func -- custom function
view.X = Pos.Func(() => CalculateX());

// Arithmetic
view.X = Pos.Center() - 10;
view.Y = Pos.Bottom(label) + 2;
Dim 类型(尺寸)
// Absolute -- fixed size
view.Width = 40;                       // Dim.Absolute(40)

// Percent -- percentage of parent
view.Width = Dim.Percent(50);          // 50% of parent width

// Fill -- fill remaining space
view.Width = Dim.Fill();               // fill to right edge
view.Width = Dim.Fill(2);              // fill minus 2 (margin)

// Auto -- size based on content (replaces v1's AutoSize)
view.Width = Dim.Auto();
view.Width = Dim.Auto(minimumContentDim: 20);

// Relative to another view
view.Width = Dim.Width(otherView);
view.Height = Dim.Height(otherView);

// Func -- custom function
view.Width = Dim.Func(() => CalculateWidth());

// Arithmetic
view.Width = Dim.Fill() - 10;
view.Height = Dim.Height(label) + 2;
Frame 与 Viewport
  • Frame ——最外层矩形:相对于 SuperView 的位置和大小
  • Viewport ——内容区域的可见部分:相当于通往视图内容的可滚动窗口
// Set content size larger than viewport to enable scrolling
view.SetContentSize(new Size(200, 100));
// Viewport automatically provides scroll behavior

核心视图

容器视图
// Window -- top-level container with title bar and border
var window = new Window
{
    Title = "Main Window",
    Width = Dim.Fill(),
    Height = Dim.Fill()
};

// FrameView -- bordered container without title bar behavior
var frame = new FrameView
{
    Title = "Settings",
    X = 1, Y = 1,
    Width = Dim.Fill(1),
    Height = 10
};
window.Add(frame);
文本和输入视图
// Label -- static text display
var label = new Label
{
    Text = "Username:",
    X = 1, Y = 1
};

// TextField -- single-line text input
var textField = new TextField
{
    X = Pos.Right(label) + 1,
    Y = Pos.Top(label),
    Width = 30,
    Text = ""
};

// TextView -- multi-line text editor
var textView = new TextView
{
    X = 1, Y = 3,
    Width = Dim.Fill(1),
    Height = Dim.Fill(1),
    Text = "Multi-line\nediting area"
};
按钮
var button = new Button
{
    Text = "OK",
    X = Pos.Center(),
    Y = Pos.Bottom(textField) + 1
};

// Accept event (v2 replaces v1's Clicked)
button.Accepting += (sender, args) =>
{
    MessageBox.Query(button.App!, "Info", $"You entered: {textField.Text}", "OK");
    args.Handled = true; // prevent event bubbling
};
ListView 和 TableView
// ListView -- scrollable list
var items = new List<string> { "Item 1", "Item 2", "Item 3" };
var listView = new ListView
{
    X = 1, Y = 1,
    Width = Dim.Fill(1),
    Height = Dim.Fill(1),
    Source = new ListWrapper<string>(new ObservableCollection<string>(items))
};

listView.SelectedItemChanged += (sender, args) =>
{
    // args.Value is the selected item index
};
CheckBox 和 RadioGroup
var checkbox = new CheckBox
{
    Text = "Enable notifications",
    X = 1, Y = 1
};

checkbox.CheckedStateChanging += (sender, args) =>
{
    // args.NewValue is the new CheckState
};

var radioGroup = new RadioGroup
{
    X = 1, Y = 3,
    RadioLabels = ["Option A", "Option B", "Option C"]
};

radioGroup.SelectedItemChanged += (sender, args) =>
{
    // args.SelectedItem is the selected index
};
其他 v2 视图
// DatePicker -- calendar-based date input
var datePicker = new DatePicker
{
    X = 1, Y = 1,
    Date = DateTime.Today
};

// NumericUpDown -- numeric spinner
var spinner = new NumericUpDown<int>
{
    X = 1, Y = 3,
    Value = 42
};

// ColorPicker -- TrueColor selection
var colorPicker = new ColorPicker
{
    X = 1, Y = 5,
    SelectedColor = new Color(0, 120, 215)
};

菜单和状态栏

MenuBar

在 v2 中,MenuBar 的构造函数参数为 MenuBarItem[]。MenuItem 同时支持位置参数构造函数和对象初始化语法。

var menuBar = new MenuBar([
    new MenuBarItem("_File",
    [
        new MenuItem("_New", "Create new file", () => NewFile()),
        new MenuItem("_Open", "Open existing file", () => OpenFile()),
        new MenuBarItem("_Recent",
        [
            new MenuItem("file1.txt", "", () => Open("file1.txt")),
            new MenuItem("file2.txt", "", () => Open("file2.txt"))
        ]),
        null,  // separator
        new MenuItem
        {
            Title = "_Quit",
            HelpText = "Exit application",
            Key = Application.QuitKey,
            Command = Command.Quit
        }
    ]),
    new MenuBarItem("_Edit",
    [
        new MenuItem("_Copy", "", () => Copy(), Key.C.WithCtrl),
        new MenuItem("_Paste", "", () => Paste(), Key.V.WithCtrl)
    ]),
    new MenuBarItem("_Help",
    [
        new MenuItem("_About", "About this app", () =>
            MessageBox.Query(app, "", "My TUI App v1.0", "OK"))
    ])
]);
window.Add(menuBar);
StatusBar

在 v2 中,StatusBar 使用 Shortcut 对象,而不是 v1 中已移除的 StatusItem。通过 statusBar.Add() 添加快捷键。

var statusBar = new StatusBar();

var helpShortcut = new Shortcut
{
    Title = "Help",
    Key = Key.F1,
    CanFocus = false
};
helpShortcut.Accepting += (sender, args) =>
{
    ShowHelp();
    args.Handled = true;
};

var saveShortcut = new Shortcut
{
    Title = "Save",
    Key = Key.F2,
    CanFocus = false
};
saveShortcut.Accepting += (sender, args) =>
{
    Save();
    args.Handled = true;
};

var quitShortcut = new Shortcut
{
    Title = "Quit",
    Key = Application.QuitKey,
    CanFocus = false
};

statusBar.Add(helpShortcut, saveShortcut, quitShortcut);
window.Add(statusBar);

对话框和 MessageBox

Dialog
// Dialog with buttons
var dialog = new Dialog
{
    Title = "Confirm",
    Width = 50,
    Height = 10
};

var label = new Label
{
    Text = "Are you sure?",
    X = Pos.Center(),
    Y = 1
};
dialog.Add(label);

var okButton = new Button { Text = "OK" };
okButton.Accepting += (sender, args) =>
{
    dialog.RequestStop();
    args.Handled = true;
};

var cancelButton = new Button { Text = "Cancel" };
cancelButton.Accepting += (sender, args) =>
{
    dialog.RequestStop();
    args.Handled = true;
};

dialog.AddButton(okButton);
dialog.AddButton(cancelButton);

app.Run(dialog);
MessageBox

在 v2 中,MessageBox.Query 和 MessageBox.ErrorQuery 的第一个参数为 IApplication。

// Simple query dialog (returns button index)
// In v2, pass the app instance as first parameter
int result = MessageBox.Query(app, "Confirm Delete",
    "Delete this file permanently?",
    "Yes", "No");

if (result == 0)
{
    // User clicked "Yes"
}

// Error message
MessageBox.ErrorQuery(app, "Error",
    "Failed to save file.\nCheck permissions.",
    "OK");
FileDialog
var fileDialog = new FileDialog
{
    Title = "Open File",
    AllowedTypes = [new AllowedType("C# Files", ".cs", ".csx")],
    MustExist = true
};

app.Run(fileDialog);

if (!fileDialog.Canceled)
{
    string selectedPath = fileDialog.FilePath;
    // Process the selected file
}

事件处理和按键绑定

使用命令进行按键绑定

Terminal.Gui v2 使用命令模式进行按键绑定。视图声明支持的命令,然后将按键映射到这些命令。

// Add a custom command and bind a key to it
view.AddCommand(Command.Accept, (args) =>
{
    // Handle the accept command
    return true; // handled
});
view.KeyBindings.Add(Key.Enter, Command.Accept);

// Bind Ctrl+S to a save action
view.KeyBindings.Add(Key.S.WithCtrl, Command.Save);
view.AddCommand(Command.Save, (args) =>
{
    SaveDocument();
    return true;
});
按键事件处理
// KeyDown -- fires when a key is pressed
view.KeyDown += (sender, args) =>
{
    if (args.KeyCode == Key.F5)
    {
        RefreshData();
        args.Handled = true;
    }
};

// KeyUp -- fires when a key is released
view.KeyUp += (sender, args) =>
{
    // Handle key release
};
应用程序级按键

虽然 v2 使用基于实例的 IApplication,但 Application.QuitKey 仍是静态配置属性,需在 Init() 之前设置。这些是框架级设置,不属于实例状态。

// Configure global quit key before Init (default: Esc)
Application.QuitKey = Key.Q.WithCtrl;

IApplication app = Application.Create().Init();
// Application.QuitKey is now in effect for this app instance
鼠标事件
// Mouse events provide viewport-relative coordinates
view.MouseClick += (sender, args) =>
{
    int col = args.Position.X;
    int row = args.Position.Y;
    // Handle click at viewport-relative position
};

view.MouseEvent += (sender, args) =>
{
    if (args.Flags.HasFlag(MouseFlags.Button1DoubleClicked))
    {
        // Handle double-click
    }
};

颜色主题和样式

Terminal.Gui v2 默认使用 24 位 TrueColor;对于受限终端,会自动回退到 16 色模式。

颜色和属性
// TrueColor via RGB values
var customColor = new Color(0xFF, 0x99, 0x00); // orange

// Create an attribute (foreground + background + style)
var attr = new Attribute(
    new Color(255, 255, 255),  // foreground: white
    new Color(0, 0, 128)       // background: dark blue
);

// Apply to a view
view.ColorScheme = new ColorScheme
{
    Normal = attr,
    Focus = new Attribute(Color.Black, Color.BrightCyan),
    HotNormal = new Attribute(Color.Red, Color.Blue),
    HotFocus = new Attribute(Color.BrightRed, Color.BrightCyan)
};
文本样式
// v2 supports text effects (terminal-dependent)
// Bold, Italic, Underline, Strikethrough, Blink, Reverse, Faint
主题配置

Terminal.Gui v2 通过 ConfigurationManager 支持基于 JSON 的主题持久化。用户无需更改代码,即可自定义主题、按键绑定和视图属性。

// Set a built-in theme via runtime config before Init
ConfigurationManager.RuntimeConfig = """{ "Theme": "Amber Phosphor" }""";
ConfigurationManager.Enable(ConfigLocations.All);

IApplication app = Application.Create().Init();
// Theme is now applied

装饰(边框、外边距、内边距)

Terminal.Gui v2 提供装饰系统,用于设置视觉间距和边框。

var view = new View
{
    X = 1, Y = 1,
    Width = 40, Height = 10
};

// Border styles: Single, Double, Heavy, Rounded, Dashed, Dotted
view.Border.LineStyle = LineStyle.Rounded;
view.Border.Thickness = new Thickness(1);

// Margin -- transparent spacing outside the border
view.Margin.Thickness = new Thickness(1);

// Padding -- internal spacing inside the border
view.Padding.Thickness = new Thickness(1, 0); // top/bottom=1, left/right=0

跨平台注意事项

Terminal.Gui 支持 Windows、macOS 和 Linux 终端,并会自动选择驱动程序。

终端兼容性
功能Windows TerminalmacOS Terminal.appLinux (xterm/gnome)
真彩色(24 位)是是是(大多数)
鼠标支持是是是
Unicode/表情符号是是视情况而定
Sixel 图像部分支持否部分支持
按键修饰键完整有限完整
特定平台的注意事项
  • macOS Terminal.app ——按键修饰键支持有限;Alt 组合键可能会被终端拦截。iTerm2 和 WezTerm 提供更好的修饰键支持。
  • SSH 会话 ——终端功能取决于客户端终端,而非服务端。请通过 SSH 测试 TUI 应用,以验证渲染效果。
  • Windows Console Host ——旧版 conhost 的 Unicode 支持有限。Windows Terminal 提供完整支持。
  • tmux/screen ——可能会拦截某些组合键。将 TERM=xterm-256color 设为最佳颜色支持。
日志集成
// Terminal.Gui v2 supports Microsoft.Extensions.Logging
// Useful for debugging rendering and event issues without
// interfering with the TUI display

完整示例:简单编辑器

using Terminal.Gui;

using IApplication app = Application.Create().Init();

var window = new Window
{
    Title = $"Simple Editor ({Application.QuitKey} to quit)",
    Width = Dim.Fill(),
    Height = Dim.Fill()
};

// Declare textView first so menu lambda can capture it
var textView = new TextView
{
    X = 0, Y = 1,  // below menu bar
    Width = Dim.Fill(),
    Height = Dim.Fill(1),  // leave room for status bar
    Text = ""
};

// Menu bar
var menuBar = new MenuBar([
    new MenuBarItem("_File",
    [
        new MenuItem("_New", "Clear editor", () => textView.Text = ""),
        null,
        new MenuItem
        {
            Title = "_Quit",
            HelpText = "Exit",
            Key = Application.QuitKey,
            Command = Command.Quit
        }
    ])
]);

// Status bar with Shortcut objects (v2 API)
var statusBar = new StatusBar();
var helpShortcut = new Shortcut { Title = "Help", Key = Key.F1, CanFocus = false };
helpShortcut.Accepting += (s, e) =>
{
    MessageBox.Query(app, "Help", "Simple text editor.", "OK");
    e.Handled = true;
};
statusBar.Add(helpShortcut);

window.Add(menuBar, textView, statusBar);
app.Run(window);

Agent 注意事项

  1. 不要使用 v1 的静态生命周期模式。 v2 使用基于实例的 Application.Create().Init(),并实现 IDisposable。v1 的 Application.Init() / Application.Run() / Application.Shutdown() 模式已过时。始终使用 using 语句包装应用程序。
  2. 不要使用 View.AutoSize。 它已在 v2 中移除。对于基于内容的尺寸调整,请改用 Dim.Auto()。
  3. 不要混淆 Frame 和 Viewport。 Frame 是外部矩形(相对于 SuperView 的位置/大小)。Viewport 是可见的内容区域(支持滚动)。内容相对坐标请使用 Viewport。
  4. 不要使用 Button.Clicked。 它在 v2 中已由 Button.Accepting 取代。这一语义变化体现了命令模式——按钮的接受操作触发时会调用 Accepting。
  5. 不要从后台线程调用 UI 操作。 Terminal.Gui 是单线程的。请在异步代码中使用 Application.Invoke(),将调用调度回 UI 线程。异步模式请参见 [skill:dotnet-csharp-async-patterns]。
  6. 关闭窗口时不要忘记调用 RequestStop()。 直接对运行中的窗口调用 Dispose() 会破坏终端状态。请使用 RequestStop() 干净地退出运行循环,从而触发正确的清理。
  7. 不要硬编码终端尺寸。 使用 Dim.Fill()、Dim.Percent() 和 Pos.Center() 创建可响应布局,以适应终端大小变化。绝对坐标在不同终端尺寸下会失效。
  8. 应用崩溃时不要忽略终端状态。 如果应用未正确释放就崩溃,终端可能会停留在原始模式。请用 try/catch 包装 app.Run(),并确保 using 代码块释放应用程序,以恢复终端状态。
  9. 不要使用 ScrollView。 它已在 v2 中移除。现在所有视图都通过 SetContentSize() 和 Viewport 属性原生支持滚动。
  10. 不要使用 NStack.ustring。 它已在 v2 中移除。请始终使用标准的 System.String。
  11. 不要使用 StatusItem。 它已在 v2 中移除。请改用 Shortcut 对象和 StatusBar.Add()。请在状态栏快捷键上设置 CanFocus = false,并使用 args.Handled = true 处理 Accepting。

前提条件

  • NuGet 包: Terminal.Gui 2.0.0-alpha(v2)或 1.19.x(v1 维护版)
  • 目标框架: net8.0 或更高版本(也支持 netstandard2.0/2.1)
  • 终端: 支持 ANSI 转义序列的任意终端模拟器。建议使用 Windows Terminal、iTerm2 或现代 Linux 终端,以获得最佳体验(真彩色、鼠标、Unicode)。
  • 无需 GUI 运行时: Terminal.Gui 可在任意终端中运行——无需 X11、Wayland 或桌面环境。

参考资料

评论

支持 Markdown