Skip to content

Terminal(终端) ​

terminal 插件是为原生 UI应用准备的终端:一个视图,在 伪终端中运行用户的 shell 或任何程序,使用 Ghostty 的终端模拟器 libghostty-vt。它全部是 Go,没有 npm 包:模拟器是一个原生库, 应用在运行时加载它,无需 cgo,并且 mygo build 和 mygo dev 会把它放进 应用。

设置 ​

创建一个终端(它会启动它的程序),并显示在一个窗口中:

go
import (
	"github.com/egoist/mygo"
	"github.com/egoist/mygo/plugins/terminal"
	"github.com/egoist/mygo/ui"
)

func main() {
	mygo.App.WhenReady(func() {
		term, err := terminal.New(terminal.Options{})
		if err != nil {
			log.Fatal(err)
		}
		win := mygo.NewWindow(mygo.WindowOptions{
			Title: "Terminal",
			Content: ui.View(func(c *ui.Context) {
				terminal.View(c, term).Fill().AutoFocus()
			}),
		})
		win.OnClosed(func() { term.Close() })
	})
	if err := mygo.App.Run(); err != nil {
		log.Fatal(err)
	}
}

终端示例展示 了这个应用。它的窗口使用 shell 的标题,并在 shell 退出时关闭。

terminal.View 是和其他元素一样的元素:用 Fill 或 Grow 设置它的大小, 并把其他元素放在它旁边,例如编辑器的一个窗格。它的大小决定终端的大小, 以它的字体的单元格计,点击会把它获得键盘焦点。一个终端一次显示在一个视图 中。它的程序在某个视图首次显示它时启动,以便按视图的大小绘制它的第一屏, 或者在没有视图显示时,在 New 后半秒以 80×24 启动。

终端做终端应用做的事:

  • 颜色(16、256 和 24 位)、粗体、斜体、微弱、反色、删除线、上划线和 下划线(单、双、波浪、点、虚线,各自的颜色);
  • Unicode:宽字符、emoji 和字素簇占据与其宽度相当的单元格,与 Ghostty 一样,系统的字体替补终端字体缺少的字符;
  • 制表符画线、方块和 Powerline 的分隔符是直接绘制的,而不是取自字体, 以便全屏程序的线条相接;
  • 终端调整大小时会重排的回滚缓冲区(scrollback),以及全屏程序的备用 屏幕;
  • 按程序要求提供的键盘:传统序列、xterm 的 modifyOtherKeys 或 Kitty 键盘 协议,以及输入法,它们在光标处组合;
  • 鼠标报告、焦点报告、括号粘贴、同步输出、超链接(OSC 8)、标题(OSC 0 和 2)、工作目录(OSC 7),按应用允许向剪贴板复制和读取剪贴板(OSC 52、 OSC 5522,参见 ClipboardRead),以及 Kitty 剪贴板协议的粘贴事件 (OSC 5522,模式 5522),让程序可以获取粘贴的图像。

使用 ​

  • 选择:用指针选择与 Ghostty 中一样:拖动选择单元格,双击选择单词, 三击选择行,拖过顶部或底部会滚动。Option(在其他平台是 Alt)拖动矩形。 Shift+点击从原始锚点扩展现有的选择;后续的 Shift+点击可以扩大或缩小它, 包括跨行。当程序接管鼠标时(编辑器会这样),Shift 进行选择。
  • 指针:是 Ghostty 的指针:在 shell 上方是 I 形光标,当程序接管鼠标时 是箭头(按住 Shift 时又变回 I 形,因为此时 Shift 进行选择),以及程序用 OSC 22 请求的形状(pointer、crosshair、col-resize……),直到它再次 接管或离开鼠标。
  • 复制与粘贴:在 macOS 上是 Command+C 和 Command+V(编辑菜单的角色 也会发送它们),在其他平台是 Control+Shift+C 和 Control+Shift+V,以及 Control+Insert 和 Shift+Insert,在 Windows 上还有像 Windows Terminal 一样的 Control+C 和 Control+V(参见按键)。当程序要求时,粘贴 是带括号的,以便粘贴的行不会作为命令运行。上下文菜单有复制、粘贴、全选 和清除回滚。选项可以添加选中即复制和其他次级点击(参见选项)。
  • 粘贴图像:自己读取剪贴板的程序(像接受图像的编码代理那样以模式 5522 询问的程序)会被告知一次粘贴包含什么——剪贴板的每种格式作为 MIME 类型(文本、图像作为 PNG、文件作为 text/uri-list,……)——并读取它想要 的内容,甚至通过 SSH。只有执行粘贴的那个终端应答它的读取,带着粘贴时 剪贴板的内容;显示同一程序的其他终端(例如同一会话的其他窗口)保持沉默。 其他程序只得到文本。单独一张图像,对一个要求括号粘贴的程序(如 VS Code 的终端)会粘贴一个空的括号粘贴,而 Claude Code 这样的代理把它当作自己从 剪贴板读取图像的信号;对其他程序,什么都不粘贴。
  • 读取剪贴板:程序用 OSC 52(?)或 OSC 5522 询问,默认被拒绝—— OSC 52 返回空剪贴板,OSC 5522 返回 EPERM 状态;ClipboardRead 允许它 或先询问应用(参见选项)。剪贴板在视图构建一帧时于主线程上 读取,回答也在主线程上写入,因此程序读到的是那时的剪贴板内容,而不是它 询问时的。
  • 滚动:滚轮和触控板滚动回滚缓冲区,Shift+Page Up 和 Shift+Page Down 也一样。全屏程序把滚轮当作方向键接收,或者当它们接管鼠标时作为鼠标报告 接收。打字会滚动回底部。
  • 缩放:按住 Control(在 macOS 上是 Command)滚动滚轮,以及 Control(Command)加 =、- 和 0 使文本变大、变小并恢复原大小,一步一个 DIP,与 Ghostty 一样;OnZoom 改为接管缩放。
  • 链接:Command+点击(在其他平台是 Control+点击)打开程序打印的 超链接(OSC 8),或作为文本打印的 URL,即使程序接管了鼠标;按住 Command 时,指针在它们上方变成手形。
  • 应用的快捷键优先:在 macOS 上,Command 快捷键绝不会发送给程序,而由 窗口或终端周围的元素用 Shortcut 处理的键,或在 macOS 上作为应用的 某个菜单的键等效键的键,会交给它们。

选项 ​

go
term, err := terminal.New(terminal.Options{
	Command:  []string{"htop"},          // the user's login shell when empty
	Dir:      "/tmp",                     // the home directory when empty
	Env:      []string{"EDITOR=vim"},     // added to the app's environment
	Font:     terminal.Font{Family: "JetBrains Mono", Size: 14},
	Theme:    terminal.DarkTheme(),        // follows the window when nil
	Cursor:   terminal.CursorBar,          // a block when zero
	OnTitle:  func(title string) { win.SetTitle(title) },
	OnExit:   func(code int) { win.Close() },
})
  • Command 运行一个程序而不是 shell。shell 以登录 shell 运行(-zsh), 像终端应用启动它那样,以便它读取用户的 profile:$SHELL,否则在 macOS 上是 /bin/zsh,在其他平台是 /bin/sh,在 Windows 上是 PowerShell。

  • Env 添加到环境中,环境已带有 TERM=xterm-256color、 COLORTERM=truecolor,当未设置区域设定时还有 LANG=en_US.UTF-8,如同 从 Finder 启动的应用。

  • Font 设置字体:默认为系统的等宽字体,13 DIP(参见字体)。

  • Theme 设置颜色:前景、背景、光标及其下文本的颜色、选区及所选文本的 颜色,以及 16 色调色板;同时设置 DarkTheme 时,它设置深色窗口的这些 颜色(参见主题)。terminal.DarkTheme() 和 terminal.LightTheme() 是窗口的背景和文本配上 Visual Studio Code 的 调色板;没有主题时,终端随窗口的外观变化采用其主题。程序可以改变颜色 (OSC 4、10、11、12)。

  • Cursor 和 NoBlink 设置光标,程序也可以改变它。

  • Scrollback 决定屏幕之上保留多少字节的输出:默认 10 MB,负值则不保留。

  • ScrollMultiplier 缩放滚动,如同 Ghostty 的 mouse-scroll-multiplier: Discrete 是鼠标滚轮一格滚动的行数(默认为 3;在 Windows 上,它缩放 "每次滚动的行数"设置,就像该设置为 3 一样),Precision 缩放触控板的 移动距离(默认为 1)。SetScrollMultiplier 在运行中的终端里修改它。

  • MinimumContrast 是文本与其背景之间的最低对比度,如同 Ghostty 的 minimum-contrast:从 1(无,默认值)到 21 的 WCAG 比率。对比度更低的 文本,例如程序的浅灰色配浅色主题,会被加深或调浅到刚好达到该值,同时 保持其色相。SetMinimumContrast 在运行中的终端里修改它。

  • Transparent 不绘制背景,如同 Ghostty 的 background-opacity,使视图 之下的内容透出:窗口的材质,或元素的半透明背景。有自己背景色的单元格 仍然绘制它。

  • Padding 是网格周围的空间,以 DIP 计,横向(X)和纵向(Y),如同 Ghostty 的 window-padding-x 和 window-padding-y:默认横向 6、纵向 4,负值则无。PaddingColor 决定它显示什么,如同 Ghostty 的 window-padding-color:默认为背景色,或者用 terminal.PaddingExtend 用离它最近单元格的背景,使全屏程序的颜色延伸到视图的边缘而不是停在 网格处。如同 Ghostty 的 extend,它只延伸每个单元格都有自己背景的行, 因此提示符的高亮,或程序的标签页和方框,保持在它们的单元格内; terminal.PaddingExtendAlways 延伸每一行。无论哪种方式,一个反色的 孤立字符(程序自绘光标时就是这样)把它的背景保持在它的单元格内。

    go
    term, err := terminal.New(terminal.Options{
    	Padding:      terminal.Padding{X: 14, Y: 8},
    	PaddingColor: terminal.PaddingExtend,
    })
  • OptionAsAlt 使 macOS 上的 Option 成为程序的 Alt 键(Meta),而不是 输入重音字母的键。

  • CopyOnSelect: terminal.CopyOnSelectClipboard 在指针结束选择时以及 全选产生选择时,把选择复制到剪贴板,如同 Ghostty 的 copy-on-select = clipboard。Ghostty 的 primary(Linux 的选择剪贴板) 不存在,因为原生 UI 没有。SetCopyOnSelect 在运行中的终端里修改它。

  • Keybinds 把键绑定到动作,叠加在默认值之上(参见按键)。

  • RightClick 决定次级点击做什么,如同 Ghostty 的 right-click-action: 默认是上下文菜单,还有 RightClickPaste、RightClickCopy、 RightClickCopyOrPaste(复制选择,或在没有选择时粘贴,如同 Windows Terminal)或 RightClickIgnore。复制会释放选择。接管鼠标的程序仍会收到 点击,除非按住 Shift。

  • NoClipboardWrite 阻止程序写入剪贴板(OSC 52 和 OSC 5522),如同 Ghostty 的 clipboard-write = deny;读取由 ClipboardRead 管辖,而一次 粘贴向程序提供的内容仍会被应答。

  • ClipboardRead 决定程序读取剪贴板时得到什么(OSC 52 的 "?",以及 Kitty 剪贴板协议的 OSC 5522):默认为 ClipboardDeny,如同 Ghostty 的 clipboard-read = deny(Ghostty 默认询问,但会询问的终端需要一个视图), 或者 ClipboardAllow 以剪贴板的内容作答,或者 ClipboardAsk 先询问 应用。剪贴板的读取和回答的写入都在主线程上,在视图构建它的下一帧时, 这也正是终端自己的提示显示的那一帧:一个没有视图显示的终端的读取会 等待,一个已关闭终端的读取永远不会被应答。

  • OnClipboardRequest 在 ClipboardRead 为 ClipboardAsk 时,在程序读取 剪贴板之前询问应用。它在主线程上运行,带着一个 ClipboardRequest:程序 请求的 MIME 类型(Mimes)、它是否请求了可用的类型(List,Kitty 协议 的目标列举,它会不经询问得到回答)、它想要的内容(Primary)以及剪贴板 以文本保存的内容(Text),用于一个关于它的提示。Allow 和 Deny 应答 该请求,可以在钩子中或稍后、从任何 goroutine,第一个生效;钩子未应答的 请求会等待。没有钩子时,终端在视图内绘制它自己的提示,一次一个读取,带有 一行被读取内容的预览和 Allow、Deny 按钮(Enter 允许,Escape 拒绝):

    go
    OnClipboardRequest: func(req terminal.ClipboardRequest) {
    	if req.Text == "" && !req.List {
    		req.Deny() // nothing to leak
    		return
    	}
    	askTheUser(func(ok bool) {
    		if ok {
    			req.Allow()
    		} else {
    			req.Deny()
    		}
    	})
    },
  • OnCopy 在终端向剪贴板写入文本后在主线程上运行,带着促成复制的原因: CopyUser(复制键、上下文菜单、复制的次级点击)、CopySelect (CopyOnSelect)或 CopyProgram(OSC 52)。应用可以提示,如同 Ghostty 的 app-notifications = clipboard-copy 用提示条(toast)做的那样:

    go
    OnCopy: func(text string, by terminal.CopySource) {
    	if by != terminal.CopyProgram {
    		showToast("Copied to clipboard")
    	}
    },
  • OnClear 在主线程上看到用户的清除,在终端清除之前:ClearScreen,对应 ActionClearScreen 的键和上下文菜单的清除。它报告自己是否接管了这次 清除,接管后此处什么都不清除。显示服务器终端的窗口接管它,让服务器的 终端来清除(参见服务器的终端,在别处显示)。 在全屏程序的屏幕上不会被询问。

  • OnTitle、OnExit、OnBell 和 OnNotify 在终端的一个 goroutine 上 运行,当程序设置标题、退出、响铃或请求桌面通知时。它们可能在显示终端的 窗口存在之前运行,NewWindow 在 New 之后才创建窗口:使用窗口的回调要 等待它,像终端示例那样(在 win 之前 <-ready,在 NewWindow 设置 win 之后 close(ready))。Done 在程序退出后关闭,ExitCode 返回 它的退出码(无法启动的程序为 -1);终端随后显示 "[Process exited]"。

  • OpenLink 在主线程上接管 Command+点击打开的链接,也接管作为文本打印的 文件路径(/abs/path、~/notes.md、./a.go、src/app.go:12:3、 main.go:250),按打印原样:相对路径是程序的路径,即 Dir。它报告自己 是否打开了链接;它留下的超链接或 URL 由系统打开,没有 OpenLink 时所有 链接都由系统打开。应用借此在自己的预览中打开远程 shell 的路径,而不是让 系统在本地打开。

  • OnPaste 在主线程上看到一次剪贴板粘贴(通过键、粘贴菜单项或次级点击), 带着剪贴板保存的每种格式作为一个 transfer.Data,返回 true 即接管: 什么都不粘贴。驱动远程 shell 的应用借此上传剪贴板的文件(d.Files()) 或图像(d.Read(transfer.PNG)),并改用 Paste 粘贴它们的远程路径:

    go
    OnPaste: func(d transfer.Data) bool {
    	files, _ := d.Files()
    	if len(files) == 0 {
    		return false // text pastes as usual
    	}
    	go func() { term.Paste(strings.Join(upload(files), " ")) }()
    	return true
    },
  • OnPasteStatus 在终端的一个 goroutine 上告知一次向自己读取剪贴板的程序 粘贴的进行状态,带着一个 PasteProgress:PasteOffered,然后当程序读取 时 PasteSending 带着 Total 中已 Sent 的字节数(写入它的字节,按协议 发送时的编码),然后 PasteSent;或者当程序在应答写出后 5 秒仍未读取时为 PasteNotTaken(在下一次粘贴到来之前它仍可能读取),或者当一次没有文本 的粘贴(例如只有图像)发给一个既不读剪贴板也不读括号粘贴的程序时为 PasteNoText(这种程序什么都不粘贴)。应用借此在慢速链接上显示大图像的 进度,并说明为什么什么都没粘贴。

  • OnKey 在主线程上、按键绑定之前、终端发送该键之前看到一次按键,返回 true 即接管:该键和它输入的文本都不会到达程序。与视图周围的 Shortcut (它随下一帧运行)不同,它保持在它之后键入的键之间的位置,它用 Send 发送的内容也一样。只输入文本的键可能只作为文本到来(像在 Linux 上), OnKey 不会看到那些。

  • OnScroll 在主线程上、终端滚动其回滚缓冲区之前,看到滚轮把视图按整行 滚动(向上为负),返回 true 即接管:回滚缓冲区保持不变。当滚轮被程序接收 时不会被询问:程序接管鼠标时作为鼠标报告(除非按住 Shift),或在全屏程序 的屏幕上作为方向键(alternate scroll,可用 \x1b[?1007l 关闭)。一个历史 存在于另一端的 Conn(例如只流式传输其屏幕的远程多路复用器)借此把滚轮 发往那里。鼠标滚轮的一格是三行,或在 Windows 上按其"每次滚动的行数"设置 要求的行数(未更改时为 3),如同 Windows Terminal;触控板按移动的距离滚动。

  • OnZoom 在主线程上看到一次缩放:按住 Control(在 macOS 上是 Command) 的滚轮(它在 OnScroll 和接管鼠标的程序看到之前缩放),或绑定到字号动作 的键。它的 Zoom 带有 Steps 数量,向上滚动为正(滚轮的一个事件是一格; 触控板的移动累加成它们),或 Reset。返回 true 即接管缩放,像一个统一调整 所有文本大小的应用那样;没有它,或被它留下的缩放,终端自己改变其字体的 大小,一步一个 DIP,最低到 1,重置时回到 Font.Size。macOS 的捏合手势 (原生 UI 不上报它)不会缩放。

按键 ​

终端把键绑定到动作,如同 Ghostty 的 keybind 选项,带有 Ghostty 针对平台 的默认值,terminal.DefaultKeybinds 会返回它们:

动作macOSLinux 和 WindowsWindows 还可
ActionCopyCmd+CCtrl+Shift+C, Ctrl+InsertCtrl+C(可执行)
ActionPasteCmd+VCtrl+Shift+V, Shift+InsertCtrl+V(可执行)
ActionSelectAllCmd+ACtrl+Shift+A
ActionClearScreenCmd+KCtrl+Shift+K
ActionIncreaseFontSizeCmd+=, Cmd++Ctrl+=, Ctrl++
ActionDecreaseFontSizeCmd+-Ctrl+-
ActionResetFontSizeCmd+0Ctrl+0
ActionScrollPageUp、…DownShift+ 或 Cmd+Page Up/DownShift+Page Up/DownCtrl+Shift+Page Up/Down
ActionScrollToTop、…BottomCmd+Home/EndShift+Home/End(Linux)Ctrl+Shift+Home/End

清屏和滚动的键也是可执行的(performable),以便全屏程序(如编辑器)收到 它们。Options.Keybinds 绑定更多,叠加在默认值之上:一个绑定会替换其键和 修饰键的默认值,绑定到 ActionNone 会解除它们,以便程序收到该键; NoDefaultKeybinds 从零开始,如同 Ghostty 的 keybind = clear:

go
term, err := terminal.New(terminal.Options{Keybinds: []terminal.Keybind{
	// 在 Linux 上也使用 Windows Terminal 的 Control+C 和 Control+V。
	{Mods: ui.Ctrl, Key: ui.KeyC, Action: terminal.ActionCopy, Performable: true},
	{Mods: ui.Ctrl, Key: ui.KeyV, Action: terminal.ActionPaste, Performable: true},
	// 让 Control+Shift+A 到达程序。
	{Mods: ui.Ctrl | ui.Shift, Key: ui.KeyA, Action: terminal.ActionNone},
}})

一个 Performable 绑定,如同 Ghostty 的 performable: 前缀,只在它的动作 确实做了某事时才接管该键:只有有选择时才复制,只有剪贴板有内容时才粘贴, 只在不在全屏程序的屏幕上时才清屏和滚动;否则键继续传给程序,如同未绑定: 没有选择时 Control+C 会中断。可执行的复制也会释放选择,以便下一次按键按下 时中断。ActionClearScreen 清除回滚缓冲区和光标行以上的屏幕,光标行连同 shell 提示符移到顶部,如同 Kitty 的 clear_terminal to_cursor,上下文菜单 的清除也这样做;Terminal.ClearScreen 从代码做同样的事, Terminal.ClearScrollback 只清除回滚缓冲区。ActionIgnore 接管该键但不做 任何事。

一个键先交给应用的快捷键,然后到 OnKey,然后到按键绑定,然后到程序。 上下文菜单显示绑定到其项的键。

字体 ​

terminal.Font 保存 Ghostty 的字体选项所设置的内容:

go
term, err := terminal.New(terminal.Options{Font: terminal.Font{
	Family:     "JetBrains Mono, Menlo, monospace", // tried in order
	Size:       14,                                  // DIPs, 13 when zero
	Weight:     500,                                 // 400 when zero
	Features:   []string{"-calt", "+ss01"},          // as Ghostty's font-feature
	LineHeight: 1.2,                                 // as adjust-cell-height = 20%
	Thicken:    true,                                // as font-thicken, on macOS
}})
  • Family 是一个字体族列表,如同 ui.Element.Font 接受的:为空时使用系统 的等宽字体。字体缺少的字符来自系统的回退字体。应用自带的字体(而不是用户 安装的)用 ui.RegisterFont 添加,它的各种样式归在一个字体族下,以便粗体 和斜体文本取用它们:

    go
    //go:embed fonts
    var fonts embed.FS
    
    for _, name := range []string{"Regular", "Bold", "Italic", "BoldItalic"} {
    	data, _ := fonts.ReadFile("fonts/JetBrainsMono-" + name + ".ttf")
    	ui.RegisterFont(data, "JetBrains Mono")
    }
  • Weight 设置文本的粗细,从 100 到 900,取该字体族拥有的字重;粗体文本重 300,至少 700。

  • Features 开关 OpenType 特性:"ss01" 或 "+ss01" 开启,"calt=0" 或 "-calt" 关闭,"cv05=2" 是一个替换字形。编程字体大多用 calt 制作连字, 其余用 liga;一个连字跨越其字符的单元格。

  • LineHeight 使行比字体的行高更高或更矮,文本在其中居中;制表符画线仍然 相接。

  • Thicken 以更粗的笔画绘制文本,以 Core Text 字体平滑的最强档,无论颜色和 系统设置如何,如同 Ghostty。没有它,文本如同 AppKit 绘制的那样:越浅平滑得 越多,只要用户保持开启平滑。它在 Linux 和 Windows 上不改变任何东西,如同 Ghostty。

SetFont 改变运行中终端的字体;网格随后采用其单元格的新大小。

主题 ​

该插件带有 Ghostty 随附的数百个主题(来自 iTerm2-Color-Schemes), terminal.GhosttyTheme 按名字返回它们,如同 Ghostty 的 theme 选项接受的 名字:

go
mocha, err := terminal.GhosttyTheme("Catppuccin Mocha")
latte, err := terminal.GhosttyTheme("Catppuccin Latte")
term, err := terminal.New(terminal.Options{
	Theme:     latte, // in light windows
	DarkTheme: mocha, // in dark windows, as theme = light:…,dark:… in Ghostty
})

如同 Ghostty,用户位于 Ghostty 主题目录 ($XDG_CONFIG_HOME/ghostty/themes,默认 ~/.config/ghostty/themes)的主题 文件优先,绝对路径指定一个主题文件。terminal.GhosttyThemes() 列出名字—— 用户的主题连同 Ghostty 的——供选择器使用,terminal.ParseGhosttyTheme 从 字节按 Ghostty 的格式读取一个主题:

palette = 0=#45475a
palette = 1=#f38ba8
background = #1e1e2e
foreground = #cdd6f4
cursor-color = #f5e0dc
cursor-text = #1e1e2e
selection-background = #f5e0dc
selection-foreground = #1e1e2e

SetTheme(theme, dark) 改变运行中终端的主题。

搜索 ​

Search 在屏幕和回滚缓冲区中搜索文本,如同 Ghostty 的 find,使用 libghostty-vt 的搜索:匹配在找到时被高亮,一帧处理回滚缓冲区的一部分,并跟随 到来的输出;最新的匹配被选中并显示。匹配是精确的,只有 ASCII 字母忽略大小写。 SearchNext 选中向较旧输出的下一个匹配,向上进入回滚缓冲区,SearchPrevious 向较新的输出,两者都会绕回,并且除非匹配已显示,把它滚动到视图中间,避开在 边缘的查找栏;EndSearch(或 Search(""))结束搜索。终端不绘制查找栏:应用 绘制一个,并把这些方法绑定到 Ghostty 的键,在 macOS 上是 Cmd+F、Cmd+G、 Cmd+Shift+G 和 Escape。OnSearch 在主线程上告诉它搜索进行得如何,用于它的 "1 of 3":

go
term, err := terminal.New(terminal.Options{
	OnSearch: func(s terminal.SearchStatus) {
		// s.Selected of s.Total matches of s.Text; s.Searching while more
		// of the scrollback is to go through.
		bar.status = fmt.Sprintf("%d of %d", s.Selected, s.Total)
	},
})

主题的 Search 和 SearchText 为匹配及其文本着色,SearchSelected 和 SearchSelectedText 为选中的匹配着色,如同 Ghostty 的 search-background、search-foreground、search-selected-background 和 search-selected-foreground,主题文件也能设置它们:为零时是 Ghostty 的黑色 文本下的浅琥珀色和浅橙色。

方法 ​

终端的方法从任何 goroutine 调用都是安全的:

  • Send 向程序发送字节,如同键入的,Paste 粘贴文本。PasteData 粘贴一个 transfer.Data,如同粘贴键粘贴剪贴板:自己读取剪贴板的程序被提供每种格式 (作为 MIME 类型),并在它询问时读取它想要的(一个 transfer.Lazy 表示只 在那时创建,在终端的一个 goroutine 上);其他程序得到文本。Paste 是带 文本的 PasteData,这种程序把文本作为 text/plain 读取。
  • Feed 显示字节,如同程序写出的,文本和转义序列。
  • Title、Dir(shell 报告的目录)、Size(以单元格计)和 Text(屏幕 及其回滚缓冲区)读取终端。
  • HoveredLink 返回视图中按住 Command(其他平台是 Control)的指针悬停的 链接,如同 OpenLink 接到的,它是否是文件路径,以及它的单元格的矩形(相对于 视图):在 View 之后读取它,以便在视图上显示链接的预览。
  • SetFont 和 SetTheme 改变字体和颜色;SetFont 还设置缩放重置回的大小, 并取消缩放。
  • Resize 设置一个没有视图显示的终端的大小,Snapshot 以转义序列返回终端 显示的内容:它的回滚缓冲区和屏幕及其样式、光标和程序设置的模式。喂给一个 相同大小的新终端,它显示相同的内容,如同服务器为一个附加到会话的窗口保留 会话的屏幕。
  • Close 挂断程序(程序收到 SIGHUP),并释放终端。

没有程序 ​

Conn 把终端连接到任何可读写的东西,例如 SSH 会话或串口,而不是程序:终端从 它读到的会显示,键入的会写给它。一个带有 Resize(cols, rows int) error 方法 的 Conn 会被告知终端的大小:

go
term, err := terminal.New(terminal.Options{Conn: session})

一个 Conn 从不发送任何东西的终端显示应用用 Feed 写的内容,比如带颜色的 日志。

服务器的终端,在别处显示 ​

一个保持会话运行的服务器(多路复用器就是这样)保持每个会话的终端(程序的输出 去往那里),窗口从另一个进程显示它。服务器终端的一个 Mirror 框住它显示的内容: Frame 返回 VT 序列,把显示这些帧的终端带到服务器现在显示的内容。窗口的终端从 它的 Conn 读取它们,如同程序写出的,逐单元格地显示服务器的屏幕和回滚缓冲区, 带着它的光标和改变它发送内容的模式(光标键的、键盘协议的、鼠标的、括号粘贴) 以及程序设置的颜色(OSC 4、10、11、12),同时它仍然像任何终端一样选择、复制、 打开链接和组合输入。它从不自己排版程序的输出,因此不可能显示成别的东西:一个从 服务器屏幕的快照获得程序输出的窗口可能会,因为那些通过相对光标当前位置移动来 重绘的程序(代理的界面就是这样)会显示不同。

go
// 服务器,每个窗口一个 Mirror,它的第一个帧从头开始:
m, err := term.NewMirror()
frame, due := m.Frame() // after output; due: a synchronized update holds it

帧在它们所在的位置绘制行,滚出的行按顺序穿过窗口的屏幕,以便它的回滚缓冲区就是 服务器的;软换行保持为软。每个单元格以它自己的样式绘制,因此空的单元格在带颜色的 单元格之后保持为空。一帧像视图那样读取终端,取走它对变化的记录:镜像一个没有视图 显示的终端(比如服务器的),按它拥有的窗口数量建立同样多的 Mirror。程序保持同步 输出(模式 2026)时帧不会到来,最多一秒:Frame 随后报告有一帧到期。窗口的光标 在程序不动光标形状时采用它自己的 Cursor 和 NoBlink,在程序设置形状时采用 程序的。

窗口的终端设置 ResizeByConn:它的视图只向 Conn 请求它适合的大小,屏幕保持其 大小直到窗口调用 Resize,在服务器的流说服务器已调整大小时,在新大小的帧之前。

go
term, err := terminal.New(terminal.Options{Conn: stream, ResizeByConn: true})
// 服务器的流说它调整为 cols×rows 时:
term.Resize(cols, rows)

服务器的终端回答程序的问题,因此它需要只有窗口知道的东西。一个带有 ResizePixels(cols, rows, cellWidth, cellHeight int) error 的 Conn 会被告知 窗口单元格以像素计的大小,一个带有 UseTheme(theme *terminal.Theme) error 的会 被告知窗口的颜色;服务器用 ResizePixels 和 UseTheme 把它们交给它的终端。按 像素询问大小或以像素接管鼠标(模式 1016)的程序随后以窗口的单元格计数,询问颜色 (OSC 4、10、11、12)或是否为深色的程序得到窗口的。询问过的程序(模式 2031)会在 颜色从浅色变为深色或返回时被告知,如同有视图显示的终端在窗口外观改变时告诉它。

go
// 服务器,当某个窗口的流这么说时:
term.ResizePixels(cols, rows, cellWidth, cellHeight)
term.UseTheme(theme)

用户的清除要由服务器来做:只清除窗口的话,服务器从仍然满的屏幕绘制的下一帧会把 行带回错误的位置。窗口的 OnClear 接管它们并询问服务器,服务器的终端清除;下一帧 显示它。

go
// 窗口:
OnClear: func(c terminal.Clear) bool {
	stream.Clear(c) // however the app asks its server
	return true
},
// 服务器,被询问时:
term.ClearScreen()

程序对剪贴板的写入(OSC 52)随帧一起传递,由窗口的终端按它自己的策略写入。服务器 的终端设置 ClipboardRead: terminal.ClipboardForward 后,程序的读取也一并传递, 窗口的终端回答它们,像它自己会做的那样询问用户:服务器什么都不回答。服务器自己回答 程序的其他查询,作为它们写入的那个终端。

History 把服务器的回滚缓冲区和屏幕(到光标为止)按行返回,不带光标和模式,供一个 在会话的程序结束后(比如服务器重启后)接管该会话的终端使用:喂给它,光标留在它显示的 内容之下,以便下一个程序在它下面开始而不是覆盖它。全屏程序的屏幕没有历史。

库 ​

libghostty-vt 是 Ghostty 的一个原生库,为每个平台从该插件绑定的 Ghostty 版本构建, 并随 MyGo 发布。该插件的包在 mygo-plugin.json 中列出这些构建及其 SHA-256,CLI 把 每个平台的那个放进它构建的应用,在它们的资源中,名为 libghostty-vt.dylib、 libghostty-vt.so 或 ghostty-vt.dll:macOS 应用与应用一起签名它,通用应用在一个 文件中得到两种架构。

不是由 CLI 构建的程序(例如在 go run 和 go test 下)会把该库一次性下载到用户的 缓存(<cache>/mygo/natives/)——CLI 把下载的文件保存在那里——并检查它的 SHA-256。 打包的应用从不下载它。terminal.LibraryPath 返回一个程序加载的库, $MYGO_GHOSTTY_VT 指定另一个库。

从 Ghostty 的源码构建 libghostty-vt:

sh
git clone https://github.com/ghostty-org/ghostty && cd ghostty
zig build -Demit-lib-vt -Doptimize=ReleaseFast
MYGO_GHOSTTY_VT=$PWD/zig-out/lib/libghostty-vt.dylib go run ./examples/terminal

terminal.Load 加载该库,New 也会加载它:先调用它,以便在显示窗口之前报告缺失的 库。

该库需要 macOS 13 或更高版本,如同 Ghostty,因此带终端的应用把 minimumSystemVersion 设为 13.0 或更高;在 Linux 上需要 glibc 2.28 或更高 (Debian 10、Ubuntu 20.04),支持 x64 和 arm64;在 Windows 10 1809 或更高版本上, 由它的 ConPTY 运行程序,支持 x64 和 arm64。

测试 ​

ui.NewTester 无需窗口运行一个终端视图:用 Type 和 Key 向它键入,用 Text 读取程序打印的内容:

go
func TestShell(t *testing.T) {
	term, err := terminal.New(terminal.Options{Command: []string{"/bin/sh"}})
	if err != nil {
		t.Fatal(err)
	}
	defer term.Close()
	tt := ui.NewTester(func(c *ui.Context) { terminal.View(c, term).Fill().AutoFocus() }, 640, 400)
	tt.Type("echo hello")
	tt.Key(0, ui.KeyEnter)
	for !strings.Contains(term.Text(), "\nhello") {
		time.Sleep(10 * time.Millisecond)
		tt.Frame()
	}
}