From 183d2bc56a8aad37a198fcab3196e1dd2f18a5c4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 4 Oct 2026 06:50:24 -0700 Subject: [PATCH] lib: update libghostty and bind screen checksum reports Update the libghostty pin from 33da6848d63b to 0e0ff282f72c and bind the new checksum options. Upstream added DECRQCRA, which asks the terminal for a checksum of part of the screen, and XTCHECKSUM, which changes how that checksum is calculated. Test suites such as vttest and esctest use them to check what a terminal displays. Replies are off by default because a program can request one cell at a time and work out everything on the screen, including output from other programs. Terminal.SetChecksumReport and WithChecksumReport turn replies on or off. Terminal.SetChecksumFlags and WithChecksumFlags set how checksums are calculated. The C option takes a raw uint8 whose bits are only listed in a comment, so the Go API uses a ChecksumFlags bit set with a named constant per bit. These are the only constants in the package not defined by their C name, because the C header has none. The flag docs follow upstream's implementation, which notes that xterm's docs disagree with xterm's code about which cells are skipped. libghostty now zeroes the PNG decode output struct before calling the callback, so the trampoline no longer clears it first (see 1090baa). The SysImage and SysDecodePngFn docs now spell out the pixel layout that upstream clarified. Upstream also redocumented allocator alignment as a power of two rather than a byte count. This package has no custom allocator, so nothing changes. The other upstream changes (tmux, legacy key encoding, macOS and CLI fixes) need no binding changes. --- CMakeLists.txt | 2 +- sys.go | 32 ++++++++++--------- terminal.go | 74 ++++++++++++++++++++++++++++++++++++++++++++ terminal_opt.go | 46 +++++++++++++++++++++++++++ terminal_opt_test.go | 57 ++++++++++++++++++++++++++++++++++ 5 files changed, 195 insertions(+), 16 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index dcf08e7..4b0b8b7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,7 +11,7 @@ set(GHOSTTY_ZIG_BUILD_FLAGS "-Demit-xcframework=false" CACHE STRING FetchContent_Declare(ghostty GIT_REPOSITORY https://github.com/ghostty-org/ghostty.git - GIT_TAG 33da6848d63b3bba2b4f31ab1531d618f2795192 + GIT_TAG 0e0ff282f72c92d930d41f4703547d3bc9618ba2 ) FetchContent_MakeAvailable(ghostty) diff --git a/sys.go b/sys.go index a460fc3..c0d10d9 100644 --- a/sys.go +++ b/sys.go @@ -75,26 +75,30 @@ import "C" import "unsafe" -// SysImage holds the result of decoding an image (e.g. PNG) into raw -// RGBA pixel data. Returned by the user-supplied decode callback. +// SysImage is a decoded image, returned by a [SysDecodePngFn]. +// // C: GhosttySysImage type SysImage struct { - // Width of the decoded image in pixels. + // Width is the image width in pixels. Width uint32 - // Height of the decoded image in pixels. + // Height is the image height in pixels. Height uint32 - // Data is the decoded RGBA pixel data (4 bytes per pixel). + // Data holds the pixels as 8-bit RGBA, four bytes per pixel. Rows + // are stored in order starting from the top-left corner, with no + // padding between them, so a complete image is Width*Height*4 bytes. Data []byte } -// SysDecodePngFn is the Go callback type for PNG decoding. It receives -// raw PNG data and must return a decoded SysImage. The returned pixel -// data will be copied into library-managed memory; the caller does not -// need to keep the slice alive after returning. +// SysDecodePngFn decodes the PNG image in data and returns its pixels. +// Return a non-nil error if the image cannot be decoded. The image is then +// rejected. +// +// data is only valid until the function returns, so copy it if you need +// it later. The pixels in the returned [SysImage] are copied into memory +// owned by libghostty, so the function does not need to keep them alive. // -// Return a non-nil error to indicate decode failure. // C: GhosttySysDecodePngFn type SysDecodePngFn func(data []byte) (*SysImage, error) @@ -275,11 +279,9 @@ func goSysDecodePngTrampoline( // Copy decoded pixels into the library-owned buffer. copy(unsafe.Slice((*byte)(unsafe.Pointer(buf)), int(pixelLen)), img.Data) - // libghostty does not initialize out before calling us, and out.data - // is a pointer. Zero the struct as bytes before filling it in. See - // allocZeroed. - clear(unsafe.Slice((*byte)(unsafe.Pointer(out)), C.sizeof_GhosttySysImage)) - + // out.data is a pointer, and Go may only store a pointer into C + // memory that is already zeroed (see allocZeroed). libghostty zeroes + // out before calling us, so the fields can be set directly. out.width = C.uint32_t(img.Width) out.height = C.uint32_t(img.Height) out.data = buf diff --git a/terminal.go b/terminal.go index 3e8ca5b..27690c3 100644 --- a/terminal.go +++ b/terminal.go @@ -90,6 +90,15 @@ type TerminalConfig struct { // retains the secure disabled default. TitleReport *bool + // ChecksumReport optionally enables replies to screen checksum + // requests. Nil leaves them disabled, which is the default. See + // [Terminal.SetChecksumReport]. + ChecksumReport *bool + + // ChecksumFlags optionally sets how screen checksums are calculated. + // Nil leaves the default of zero. See [Terminal.SetChecksumFlags]. + ChecksumFlags *ChecksumFlags + // ResizePullScrollback optionally controls whether a resize may pull // rows out of scrollback back into the active area. Nil retains the // default of true. @@ -127,6 +136,42 @@ type TerminalConfig struct { onReset ResetFunc } +// ChecksumFlags controls how the terminal calculates screen checksums. Flags +// can be combined with bitwise OR. The zero value calculates checksums the +// way a real DEC terminal does, which is what most programs expect. +// +// The bits match the parameter of the XTCHECKSUM sequence (CSI Ps # y) and +// xterm's checksumExtension resource. The C API has no named constants for +// them, so the values are defined here. +// +// See [Terminal.SetChecksumReport] and [Terminal.SetChecksumFlags]. +// +// C: uint8_t (GHOSTTY_TERMINAL_OPT_XT_CHECKSUM_EXTENSION) +type ChecksumFlags uint8 + +const ( + // ChecksumNoNegate reports the sum as is. By default the reported + // value is the negated sum. + ChecksumNoNegate ChecksumFlags = 1 << iota + + // ChecksumNoAttributes leaves out text attributes such as bold and + // underline. By default each cell adds a value for its attributes. + ChecksumNoAttributes + + // ChecksumKeepSpaces counts every space. By default a plain space is + // only counted when it is the first cell of the area. + ChecksumKeepSpaces + + // ChecksumUnwrittenAsSpace counts cells that were never written to as + // spaces. By default those cells are skipped. + ChecksumUnwrittenAsSpace + + // ChecksumFullCodepoint sums the full Unicode code point of each + // character. By default characters are reduced to the 8-bit values a + // DEC terminal uses. + ChecksumFullCodepoint +) + // WritePtyFn is called when the terminal writes data back to the pty, such as // query and mode reports, clipboard replies, and terminal paste output. The // data is only valid for the call duration and consecutive chunks must be @@ -736,6 +781,23 @@ func WithTitleReport(enabled bool) TerminalOption { } } +// WithChecksumReport enables or disables replies to screen checksum +// requests. Replies are disabled by default. See [Terminal.SetChecksumReport] +// for why. +func WithChecksumReport(enabled bool) TerminalOption { + return func(c *TerminalConfig) { + c.ChecksumReport = &enabled + } +} + +// WithChecksumFlags sets how screen checksums are calculated. See +// [Terminal.SetChecksumFlags] for details. +func WithChecksumFlags(flags ChecksumFlags) TerminalOption { + return func(c *TerminalConfig) { + c.ChecksumFlags = &flags + } +} + // WithResizePullScrollback controls whether a resize may pull rows out of // scrollback back into the active area. See Terminal.SetResizePullScrollback // for details. The default is true. @@ -996,6 +1058,18 @@ func NewTerminal(opts ...TerminalOption) (*Terminal, error) { return nil, err } } + if cfg.ChecksumReport != nil { + if err := t.SetChecksumReport(*cfg.ChecksumReport); err != nil { + t.Close() + return nil, err + } + } + if cfg.ChecksumFlags != nil { + if err := t.SetChecksumFlags(*cfg.ChecksumFlags); err != nil { + t.Close() + return nil, err + } + } if cfg.ResizePullScrollback != nil { if err := t.SetResizePullScrollback(*cfg.ResizePullScrollback); err != nil { t.Close() diff --git a/terminal_opt.go b/terminal_opt.go index 9632679..f90c5a0 100644 --- a/terminal_opt.go +++ b/terminal_opt.go @@ -503,6 +503,52 @@ func (t *Terminal) SetUnknownMaxBytes(limit uint) error { )) } +// SetChecksumReport enables or disables replies to screen checksum requests. +// +// A program requests a checksum of part of the screen with the DECRQCRA +// sequence (CSI Pi ; Pg ; Pt ; Pl ; Pb ; Pr * y). Test suites such as +// vttest and esctest use these replies to check what a terminal displays. +// +// Replies are disabled by default because they leak screen contents. A +// program can request the checksum of one cell at a time and work out +// every character on the screen, including output from other programs. +// Enable replies only when every program that can write to the terminal +// is trusted. +// +// While replies are disabled, programs also cannot change how checksums +// are calculated. See [Terminal.SetChecksumFlags]. +// +// C: GHOSTTY_TERMINAL_OPT_XT_CHECKSUM_REPORT +func (t *Terminal) SetChecksumReport(enabled bool) error { + v := C.bool(enabled) + return resultError(C.ghostty_terminal_set( + t.ptr, + C.GHOSTTY_TERMINAL_OPT_XT_CHECKSUM_REPORT, + unsafe.Pointer(&v), + )) +} + +// SetChecksumFlags sets how screen checksums are calculated. It changes the +// current calculation and the one the terminal returns to after a full reset. +// Zero, the default, calculates checksums the way a real DEC terminal does. +// +// Running programs can change the calculation themselves with the +// XTCHECKSUM sequence (CSI Ps # y). Their change lasts until the next full +// reset, which restores flags. +// +// SetChecksumFlags returns an error matching [ErrInvalidValue] if flags +// contains bits other than the defined [ChecksumFlags] constants. +// +// C: GHOSTTY_TERMINAL_OPT_XT_CHECKSUM_EXTENSION +func (t *Terminal) SetChecksumFlags(flags ChecksumFlags) error { + v := C.uint8_t(flags) + return resultError(C.ghostty_terminal_set( + t.ptr, + C.GHOSTTY_TERMINAL_OPT_XT_CHECKSUM_EXTENSION, + unsafe.Pointer(&v), + )) +} + // setStringOption stages a Go string through C-owned memory before passing a // GhosttyString descriptor to cgo. ghostty_terminal_set copies string options // synchronously, so the temporary allocation can be released on return. diff --git a/terminal_opt_test.go b/terminal_opt_test.go index 4f449eb..43fb612 100644 --- a/terminal_opt_test.go +++ b/terminal_opt_test.go @@ -2,6 +2,7 @@ package libghostty import ( "bytes" + "errors" "slices" "testing" ) @@ -912,6 +913,62 @@ func TestTerminalTitleReport(t *testing.T) { } } +func TestTerminalChecksumReport(t *testing.T) { + var received []byte + term, err := NewTerminal( + WithSize(80, 24), + WithChecksumReport(true), + WithChecksumFlags(ChecksumNoNegate|ChecksumFullCodepoint), + WithWritePty(func(_ *Terminal, data []byte) { + received = append(received, data...) + }), + ) + if err != nil { + t.Fatal(err) + } + defer term.Close() + + // DECRQCRA replies with DCS Pi ! ~ <4 hex digits> ST. + const query = "\x1b[7;1;1;1;24;80*y" + term.VTWrite([]byte("hello")) + term.VTWrite([]byte(query)) + if !bytes.HasPrefix(received, []byte("\x1bP7!~")) || + !bytes.HasSuffix(received, []byte("\x1b\\")) || + len(received) != len("\x1bP7!~0000\x1b\\") { + t.Fatalf("unexpected checksum report %q", received) + } + + received = nil + if err := term.SetChecksumReport(false); err != nil { + t.Fatal(err) + } + term.VTWrite([]byte(query)) + if received != nil { + t.Fatalf("expected disabled checksum report to be ignored, got %q", received) + } +} + +func TestTerminalChecksumFlags(t *testing.T) { + term, err := NewTerminal(WithSize(80, 24)) + if err != nil { + t.Fatal(err) + } + defer term.Close() + + // Every defined flag at once is accepted. + all := ChecksumNoNegate | ChecksumNoAttributes | ChecksumKeepSpaces | + ChecksumUnwrittenAsSpace | ChecksumFullCodepoint + if err := term.SetChecksumFlags(all); err != nil { + t.Fatal(err) + } + + // The next bit up is not defined and is rejected. + undefined := ChecksumFullCodepoint << 1 + if err := term.SetChecksumFlags(undefined); !errors.Is(err, ErrInvalidValue) { + t.Fatalf("expected ErrInvalidValue for undefined flag, got %v", err) + } +} + func TestTerminalTerminfoName(t *testing.T) { var received []byte term, err := NewTerminal( -- 2.51.2