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(