From ef0f8ce3daa7fff71dbd7ea550a1de81c194d3b8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 27 Jul 2026 13:30:27 -0700 Subject: [PATCH] terminal: bind notification and progress effects Update the pinned Ghostty revision to the latest main commit. The new revision introduces desktop notification and progress report effects. Expose both effects through creation options and live setters. Copy borrowed notification strings into Go memory, translate progress states, and validate sized callback structs before access. Cover OSC 9, OSC 777, and OSC 9;4 callback behavior, clearing, split writes, and all progress states. --- CMakeLists.txt | 2 +- doc.go | 5 +- examples/effects/main.go | 23 ++++++- terminal.go | 140 ++++++++++++++++++++++++++++++--------- terminal_effect.go | 97 ++++++++++++++++++++++++--- terminal_opt.go | 14 ++++ terminal_opt_test.go | 95 ++++++++++++++++++++++++++ 7 files changed, 329 insertions(+), 47 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index bb90aca..8092d5d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ project(go-libghostty LANGUAGES C) include(FetchContent) FetchContent_Declare(ghostty GIT_REPOSITORY https://github.com/ghostty-org/ghostty.git - GIT_TAG 5a35415a5d59a117e654735ca5a01f876dec5841 + GIT_TAG 2dd79f3bc6af649e68422b08e21ad0300fd8b391 ) FetchContent_MakeAvailable(ghostty) diff --git a/doc.go b/doc.go index fbb6b83..bbd2cfe 100644 --- a/doc.go +++ b/doc.go @@ -47,8 +47,9 @@ // // The terminal communicates side-effects back to the host through // effect callbacks. Register them at creation time with functional -// options like [WithWritePty], [WithBell], and [WithEnquiry], or -// on a live terminal with [Terminal.SetEffectWritePty] and friends. +// options like [WithWritePty], [WithDesktopNotification], and +// [WithProgressReport], or on a live terminal with +// [Terminal.SetEffectWritePty] and friends. // // Effect callbacks run synchronously during [Terminal.VTWrite]. They // must not call [Terminal.VTWrite] on the same terminal and should avoid diff --git a/examples/effects/main.go b/examples/effects/main.go index 982330f..8e660dc 100644 --- a/examples/effects/main.go +++ b/examples/effects/main.go @@ -1,8 +1,9 @@ // Example program demonstrating terminal effect callbacks. // -// It registers write_pty, bell, title_changed, and clipboard_write effect -// handlers, then feeds VT sequences that trigger each one. Output shows how -// the callbacks fire and how terminal state can be queried from within them. +// It registers a representative set of effects, including desktop +// notifications and progress reports, then feeds VT sequences that trigger +// each one. Output shows how the callbacks fire and how terminal state can be +// queried from within them. package main import ( @@ -39,6 +40,16 @@ func main() { return ghostty.ClipboardWriteSuccess }), + // desktop_notification: called for OSC 9 and OSC 777 requests. + ghostty.WithDesktopNotification(func(_ *ghostty.Terminal, notification ghostty.TerminalDesktopNotification) { + fmt.Printf("desktop_notification: title=%q body=%q\n", notification.Title, notification.Body) + }), + + // progress_report: called for ConEmu OSC 9;4 progress updates. + ghostty.WithProgressReport(func(_ *ghostty.Terminal, report ghostty.TerminalProgressReport) { + fmt.Printf("progress_report: state=%d progress=%d\n", report.State, report.Progress) + }), + // title_changed: called when the terminal title changes via OSC 0/2. // The terminal is passed directly as a parameter. ghostty.WithTitleChanged(func(t *ghostty.Terminal) { @@ -66,6 +77,12 @@ func main() { // OSC 52 (set clipboard) → triggers clipboard_write with decoded data. term.VTWrite([]byte("\x1b]52;c;SGVsbG8gY2xpcGJvYXJk\x1b\\")) + // OSC 777 (desktop notification) → triggers desktop_notification. + term.VTWrite([]byte("\x1b]777;notify;Build;Complete\x1b\\")) + + // OSC 9;4 (42% progress) → triggers progress_report. + term.VTWrite([]byte("\x1b]9;4;1;42\x1b\\")) + // Another BEL → triggers bell handler again. term.VTWrite([]byte{0x07}) diff --git a/terminal.go b/terminal.go index 0f0eaa3..6f2d5c1 100644 --- a/terminal.go +++ b/terminal.go @@ -28,16 +28,18 @@ type Terminal struct { // dispatch to the appropriate Go effect handler. handle cgo.Handle - onWritePty WritePtyFn - onBell BellFn - onClipboardWrite ClipboardWriteFn - onTitleChanged TitleChangedFn - onPwdChanged PwdChangedFn - onEnquiry EnquiryFn - onXtversion XtversionFn - onSize SizeFn - onColorScheme ColorSchemeFn - onDeviceAttributes DeviceAttributesFn + onWritePty WritePtyFn + onBell BellFn + onClipboardWrite ClipboardWriteFn + onDesktopNotification DesktopNotificationFn + onTitleChanged TitleChangedFn + onPwdChanged PwdChangedFn + onProgressReport ProgressReportFn + onEnquiry EnquiryFn + onXtversion XtversionFn + onSize SizeFn + onColorScheme ColorSchemeFn + onDeviceAttributes DeviceAttributesFn // effectBuf holds C-allocated memory for the most recent response // returned by an effect trampoline (e.g. enquiry, xtversion). @@ -69,16 +71,18 @@ type TerminalConfig struct { MaxScrollbackLines *uint // Effect handlers applied after terminal creation. - onWritePty WritePtyFn - onBell BellFn - onClipboardWrite ClipboardWriteFn - onTitleChanged TitleChangedFn - onPwdChanged PwdChangedFn - onEnquiry EnquiryFn - onXtversion XtversionFn - onSize SizeFn - onColorScheme ColorSchemeFn - onDeviceAttributes DeviceAttributesFn + onWritePty WritePtyFn + onBell BellFn + onClipboardWrite ClipboardWriteFn + onDesktopNotification DesktopNotificationFn + onTitleChanged TitleChangedFn + onPwdChanged PwdChangedFn + onProgressReport ProgressReportFn + onEnquiry EnquiryFn + onXtversion XtversionFn + onSize SizeFn + onColorScheme ColorSchemeFn + onDeviceAttributes DeviceAttributesFn } // WritePtyFn is called when the terminal writes data back to the pty @@ -168,6 +172,24 @@ const ( // C: GhosttyTerminalClipboardWriteFn type ClipboardWriteFn func(t *Terminal, write ClipboardWrite) ClipboardWriteResult +// TerminalDesktopNotification is a request from the running program to show a +// desktop notification. Title is empty for protocols such as OSC 9 that +// provide only a body. Both strings are copied into Go-owned memory before +// the callback runs and may be retained. +// C: GhosttyTerminalDesktopNotification +type TerminalDesktopNotification struct { + // Title is the notification title, or empty when the protocol omits it. + Title string + + // Body is the notification body. + Body string +} + +// DesktopNotificationFn is called synchronously when the terminal receives +// a desktop notification request via OSC 9 or OSC 777. +// C: GhosttyTerminalDesktopNotificationFn +type DesktopNotificationFn func(t *Terminal, notification TerminalDesktopNotification) + // TitleChangedFn is called when the terminal title changes via OSC 0/2. // The parameter is the terminal that triggered the effect. // C: GhosttyTerminalTitleChangedFn @@ -179,6 +201,44 @@ type TitleChangedFn func(t *Terminal) // C: GhosttyTerminalPwdChangedFn type PwdChangedFn func(t *Terminal) +// TerminalProgressState identifies the state of a progress report emitted by +// the running program. +// C: GhosttyTerminalProgressState +type TerminalProgressState int + +const ( + // TerminalProgressStateRemove requests removal of any progress indication. + TerminalProgressStateRemove TerminalProgressState = C.GHOSTTY_TERMINAL_PROGRESS_STATE_REMOVE + + // TerminalProgressStateSet reports determinate progress. + TerminalProgressStateSet TerminalProgressState = C.GHOSTTY_TERMINAL_PROGRESS_STATE_SET + + // TerminalProgressStateError reports failed progress. + TerminalProgressStateError TerminalProgressState = C.GHOSTTY_TERMINAL_PROGRESS_STATE_ERROR + + // TerminalProgressStateIndeterminate reports progress without a percentage. + TerminalProgressStateIndeterminate TerminalProgressState = C.GHOSTTY_TERMINAL_PROGRESS_STATE_INDETERMINATE + + // TerminalProgressStatePause reports paused progress. + TerminalProgressStatePause TerminalProgressState = C.GHOSTTY_TERMINAL_PROGRESS_STATE_PAUSE +) + +// TerminalProgressReport is a progress update emitted by the running program +// via OSC 9;4. +// C: GhosttyTerminalProgressReport +type TerminalProgressReport struct { + // State is the literal progress state reported by the program. + State TerminalProgressState + + // Progress is a percentage from 0 through 100, or -1 when omitted. + Progress int8 +} + +// ProgressReportFn is called synchronously when the terminal receives a +// progress report via OSC 9;4. +// C: GhosttyTerminalProgressReportFn +type ProgressReportFn func(t *Terminal, report TerminalProgressReport) + // EnquiryFn is called when the terminal receives ENQ (0x05). // The first parameter is the terminal that triggered the effect. // Return the response bytes; nil or empty means no response. @@ -261,6 +321,14 @@ func WithClipboardWrite(fn ClipboardWriteFn) TerminalOption { } } +// WithDesktopNotification registers an effect handler invoked for desktop +// notification requests received via OSC 9 or OSC 777. +func WithDesktopNotification(fn DesktopNotificationFn) TerminalOption { + return func(c *TerminalConfig) { + c.onDesktopNotification = fn + } +} + // WithTitleChanged registers an effect handler invoked when the // terminal title changes via OSC 0 or OSC 2. func WithTitleChanged(fn TitleChangedFn) TerminalOption { @@ -277,6 +345,14 @@ func WithPwdChanged(fn PwdChangedFn) TerminalOption { } } +// WithProgressReport registers an effect handler invoked for progress reports +// received via OSC 9;4. +func WithProgressReport(fn ProgressReportFn) TerminalOption { + return func(c *TerminalConfig) { + c.onProgressReport = fn + } +} + // WithEnquiry registers an effect handler invoked when the terminal // receives an ENQ character (0x05). Return the response bytes; nil // or empty means no response. @@ -368,17 +444,19 @@ func NewTerminal(opts ...TerminalOption) (*Terminal, error) { } t := &Terminal{ - ptr: cterm, - onWritePty: cfg.onWritePty, - onBell: cfg.onBell, - onClipboardWrite: cfg.onClipboardWrite, - onTitleChanged: cfg.onTitleChanged, - onPwdChanged: cfg.onPwdChanged, - onEnquiry: cfg.onEnquiry, - onXtversion: cfg.onXtversion, - onSize: cfg.onSize, - onColorScheme: cfg.onColorScheme, - onDeviceAttributes: cfg.onDeviceAttributes, + ptr: cterm, + onWritePty: cfg.onWritePty, + onBell: cfg.onBell, + onClipboardWrite: cfg.onClipboardWrite, + onDesktopNotification: cfg.onDesktopNotification, + onTitleChanged: cfg.onTitleChanged, + onPwdChanged: cfg.onPwdChanged, + onProgressReport: cfg.onProgressReport, + onEnquiry: cfg.onEnquiry, + onXtversion: cfg.onXtversion, + onSize: cfg.onSize, + onColorScheme: cfg.onColorScheme, + onDeviceAttributes: cfg.onDeviceAttributes, } // Always set userdata to our handle so trampolines can find us. diff --git a/terminal_effect.go b/terminal_effect.go index e59ccda..f441851 100644 --- a/terminal_effect.go +++ b/terminal_effect.go @@ -15,8 +15,10 @@ package libghostty extern void goWritePtyTrampoline(GhosttyTerminal, void*, uint8_t*, size_t); extern void goBellTrampoline(GhosttyTerminal, void*); extern GhosttyClipboardWriteResult goClipboardWriteTrampoline(GhosttyTerminal, void*, GhosttyClipboardWrite*); +extern void goDesktopNotificationTrampoline(GhosttyTerminal, void*, GhosttyTerminalDesktopNotification*); extern void goTitleChangedTrampoline(GhosttyTerminal, void*); extern void goPwdChangedTrampoline(GhosttyTerminal, void*); +extern void goProgressReportTrampoline(GhosttyTerminal, void*, GhosttyTerminalProgressReport*); extern GhosttyString goEnquiryTrampoline(GhosttyTerminal, void*); extern GhosttyString goXtversionTrampoline(GhosttyTerminal, void*); extern bool goSizeTrampoline(GhosttyTerminal, void*, GhosttySizeReportSize*); @@ -35,12 +37,18 @@ static inline GhosttyResult set_bell(GhosttyTerminal t) { static inline GhosttyResult set_clipboard_write(GhosttyTerminal t) { return ghostty_terminal_set(t, GHOSTTY_TERMINAL_OPT_CLIPBOARD_WRITE, (const void*)goClipboardWriteTrampoline); } +static inline GhosttyResult set_desktop_notification(GhosttyTerminal t) { + return ghostty_terminal_set(t, GHOSTTY_TERMINAL_OPT_DESKTOP_NOTIFICATION, (const void*)goDesktopNotificationTrampoline); +} static inline GhosttyResult set_title_changed(GhosttyTerminal t) { return ghostty_terminal_set(t, GHOSTTY_TERMINAL_OPT_TITLE_CHANGED, (const void*)goTitleChangedTrampoline); } static inline GhosttyResult set_pwd_changed(GhosttyTerminal t) { return ghostty_terminal_set(t, GHOSTTY_TERMINAL_OPT_PWD_CHANGED, (const void*)goPwdChangedTrampoline); } +static inline GhosttyResult set_progress_report(GhosttyTerminal t) { + return ghostty_terminal_set(t, GHOSTTY_TERMINAL_OPT_PROGRESS_REPORT, (const void*)goProgressReportTrampoline); +} static inline GhosttyResult set_enquiry(GhosttyTerminal t) { return ghostty_terminal_set(t, GHOSTTY_TERMINAL_OPT_ENQUIRY, (const void*)goEnquiryTrampoline); } @@ -87,6 +95,11 @@ func (t *Terminal) syncEffects() { } else { C.clear_effect(t.ptr, C.GHOSTTY_TERMINAL_OPT_CLIPBOARD_WRITE) } + if t.onDesktopNotification != nil { + C.set_desktop_notification(t.ptr) + } else { + C.clear_effect(t.ptr, C.GHOSTTY_TERMINAL_OPT_DESKTOP_NOTIFICATION) + } if t.onTitleChanged != nil { C.set_title_changed(t.ptr) } else { @@ -97,6 +110,11 @@ func (t *Terminal) syncEffects() { } else { C.clear_effect(t.ptr, C.GHOSTTY_TERMINAL_OPT_PWD_CHANGED) } + if t.onProgressReport != nil { + C.set_progress_report(t.ptr) + } else { + C.clear_effect(t.ptr, C.GHOSTTY_TERMINAL_OPT_PROGRESS_REPORT) + } if t.onEnquiry != nil { C.set_enquiry(t.ptr) } else { @@ -160,7 +178,7 @@ func goClipboardWriteTrampoline(_ C.GhosttyTerminal, userdata unsafe.Pointer, wr return C.GHOSTTY_CLIPBOARD_WRITE_RESULT_INVALID_DATA } - count, ok := clipboardSizeToInt(write.contents_len) + count, ok := ghosttySizeToInt(write.contents_len) if !ok || (count > 0 && write.contents == nil) { return C.GHOSTTY_CLIPBOARD_WRITE_RESULT_INVALID_DATA } @@ -169,11 +187,11 @@ func goClipboardWriteTrampoline(_ C.GhosttyTerminal, userdata unsafe.Pointer, wr if count > 0 { cContents := unsafe.Slice(write.contents, count) for i, content := range cContents { - mime, valid := copyClipboardString(content.mime) + mime, valid := copyGhosttyString(content.mime) if !valid { return C.GHOSTTY_CLIPBOARD_WRITE_RESULT_INVALID_DATA } - data, valid := copyClipboardString(content.data) + data, valid := copyGhosttyString(content.data) if !valid { return C.GHOSTTY_CLIPBOARD_WRITE_RESULT_INVALID_DATA } @@ -192,20 +210,20 @@ func goClipboardWriteTrampoline(_ C.GhosttyTerminal, userdata unsafe.Pointer, wr return C.GhosttyClipboardWriteResult(result) } -// clipboardSizeToInt converts a C size to a Go slice length without allowing +// ghosttySizeToInt converts a C size to a Go slice length without allowing // an overflowing conversion to produce an invalid unsafe.Slice length. -func clipboardSizeToInt(size C.size_t) (int, bool) { +func ghosttySizeToInt(size C.size_t) (int, bool) { if uint64(size) > uint64(^uint(0)>>1) { return 0, false } return int(size), true } -// copyClipboardString copies a borrowed, binary-safe GhosttyString into Go -// memory. For zero-length strings the pointer is intentionally ignored because -// libghostty does not require it to be valid. -func copyClipboardString(value C.GhosttyString) ([]byte, bool) { - length, ok := clipboardSizeToInt(value.len) +// copyGhosttyString copies a borrowed, binary-safe GhosttyString into Go +// memory. For zero-length strings the pointer is intentionally ignored +// because libghostty does not require it to be valid. +func copyGhosttyString(value C.GhosttyString) ([]byte, bool) { + length, ok := ghosttySizeToInt(value.len) if !ok { return nil, false } @@ -219,6 +237,41 @@ func copyClipboardString(value C.GhosttyString) ([]byte, bool) { return append([]byte(nil), unsafe.Slice((*byte)(unsafe.Pointer(value.ptr)), length)...), true } +//export goDesktopNotificationTrampoline +func goDesktopNotificationTrampoline( + _ C.GhosttyTerminal, + userdata unsafe.Pointer, + notification *C.GhosttyTerminalDesktopNotification, +) { + t := terminalFromUserdata(userdata) + if t.onDesktopNotification == nil { + return + } + + // GhosttyTerminalDesktopNotification is a sized struct so newer + // libghostty versions can extend it without breaking existing callbacks. + // Ignore descriptors that do not contain the full layout this binding + // knows how to read. + if notification == nil || + notification.size < C.size_t(C.sizeof_GhosttyTerminalDesktopNotification) { + return + } + + title, ok := copyGhosttyString(notification.title) + if !ok { + return + } + body, ok := copyGhosttyString(notification.body) + if !ok { + return + } + + t.onDesktopNotification(t, TerminalDesktopNotification{ + Title: string(title), + Body: string(body), + }) +} + //export goTitleChangedTrampoline func goTitleChangedTrampoline(_ C.GhosttyTerminal, userdata unsafe.Pointer) { t := terminalFromUserdata(userdata) @@ -235,6 +288,30 @@ func goPwdChangedTrampoline(_ C.GhosttyTerminal, userdata unsafe.Pointer) { } } +//export goProgressReportTrampoline +func goProgressReportTrampoline( + _ C.GhosttyTerminal, + userdata unsafe.Pointer, + report *C.GhosttyTerminalProgressReport, +) { + t := terminalFromUserdata(userdata) + if t.onProgressReport == nil { + return + } + + // GhosttyTerminalProgressReport is also a sized struct. Only read the + // fields when the caller supplied the complete layout known here. + if report == nil || + report.size < C.size_t(C.sizeof_GhosttyTerminalProgressReport) { + return + } + + t.onProgressReport(t, TerminalProgressReport{ + State: TerminalProgressState(report.state), + Progress: int8(report.progress), + }) +} + //export goEnquiryTrampoline func goEnquiryTrampoline(_ C.GhosttyTerminal, userdata unsafe.Pointer) C.GhosttyString { t := terminalFromUserdata(userdata) diff --git a/terminal_opt.go b/terminal_opt.go index 902cb9e..caf244e 100644 --- a/terminal_opt.go +++ b/terminal_opt.go @@ -31,6 +31,13 @@ func (t *Terminal) SetEffectClipboardWrite(fn ClipboardWriteFn) { t.syncEffects() } +// SetEffectDesktopNotification registers (or clears) the desktop-notification +// effect on a live terminal. Pass nil to clear. +func (t *Terminal) SetEffectDesktopNotification(fn DesktopNotificationFn) { + t.onDesktopNotification = fn + t.syncEffects() +} + // SetEffectTitleChanged registers (or clears) the title-changed effect // on a live terminal. Pass nil to clear. func (t *Terminal) SetEffectTitleChanged(fn TitleChangedFn) { @@ -45,6 +52,13 @@ func (t *Terminal) SetEffectPwdChanged(fn PwdChangedFn) { t.syncEffects() } +// SetEffectProgressReport registers (or clears) the progress-report effect on +// a live terminal. Pass nil to clear. +func (t *Terminal) SetEffectProgressReport(fn ProgressReportFn) { + t.onProgressReport = fn + t.syncEffects() +} + // SetEffectEnquiry registers (or clears) the enquiry effect on a live // terminal. Pass nil to clear. func (t *Terminal) SetEffectEnquiry(fn EnquiryFn) { diff --git a/terminal_opt_test.go b/terminal_opt_test.go index c2b8499..80143a8 100644 --- a/terminal_opt_test.go +++ b/terminal_opt_test.go @@ -242,6 +242,101 @@ func TestTerminalSetEffectClipboardWrite(t *testing.T) { } } +func TestTerminalDesktopNotificationEffect(t *testing.T) { + var notifications []TerminalDesktopNotification + term, err := NewTerminal( + WithSize(80, 24), + WithDesktopNotification(func(_ *Terminal, notification TerminalDesktopNotification) { + notifications = append(notifications, notification) + }), + ) + if err != nil { + t.Fatal(err) + } + defer term.Close() + + // OSC 777 preserves its separate title and body, including when the + // sequence arrives across multiple VT writes. + term.VTWrite([]byte("\x1b]777;notify;Codex;")) + if len(notifications) != 0 { + t.Fatalf("expected incomplete notification to remain buffered, got %d", len(notifications)) + } + term.VTWrite([]byte("Needs attention\x1b\\")) + if len(notifications) != 1 { + t.Fatalf("expected 1 desktop notification, got %d", len(notifications)) + } + if got := notifications[0]; got.Title != "Codex" || got.Body != "Needs attention" { + t.Fatalf("unexpected desktop notification: %+v", got) + } + + // OSC 9 has no title and preserves its body. + term.VTWrite([]byte("\x1b]9;Build complete\x07")) + if len(notifications) != 2 { + t.Fatalf("expected 2 desktop notifications, got %d", len(notifications)) + } + if got := notifications[1]; got.Title != "" || got.Body != "Build complete" { + t.Fatalf("unexpected OSC 9 desktop notification: %+v", got) + } + + // Callback strings are copied and remain valid after the parser reuses its + // borrowed input buffer for later terminal writes. + if got := notifications[0]; got.Title != "Codex" || got.Body != "Needs attention" { + t.Fatalf("expected retained desktop notification, got %+v", got) + } + + term.SetEffectDesktopNotification(nil) + term.VTWrite([]byte("\x1b]9;Ignored\x07")) + if len(notifications) != 2 { + t.Fatalf("expected callback to remain cleared, got %d notifications", len(notifications)) + } +} + +func TestTerminalProgressReportEffect(t *testing.T) { + var reports []TerminalProgressReport + term, err := NewTerminal( + WithSize(80, 24), + WithProgressReport(func(_ *Terminal, report TerminalProgressReport) { + reports = append(reports, report) + }), + ) + if err != nil { + t.Fatal(err) + } + defer term.Close() + + tests := []struct { + sequence string + state TerminalProgressState + progress int8 + }{ + {"\x1b]9;4;0;\x1b\\", TerminalProgressStateRemove, -1}, + {"\x1b]9;4;1;42\x07", TerminalProgressStateSet, 42}, + {"\x1b]9;4;2;7\x1b\\", TerminalProgressStateError, 7}, + {"\x1b]9;4;3\x1b\\", TerminalProgressStateIndeterminate, -1}, + {"\x1b]9;4;4;75\x1b\\", TerminalProgressStatePause, 75}, + } + for i, test := range tests { + midpoint := len(test.sequence) / 2 + term.VTWrite([]byte(test.sequence[:midpoint])) + if len(reports) != i { + t.Fatalf("expected split report %d to remain buffered, got %d reports", i, len(reports)) + } + term.VTWrite([]byte(test.sequence[midpoint:])) + if len(reports) != i+1 { + t.Fatalf("expected report %d, got %d reports", i, len(reports)) + } + if got := reports[i]; got.State != test.state || got.Progress != test.progress { + t.Fatalf("unexpected progress report %d: %+v", i, got) + } + } + + term.SetEffectProgressReport(nil) + term.VTWrite([]byte("\x1b]9;4;1;90\x1b\\")) + if len(reports) != len(tests) { + t.Fatalf("expected callback to remain cleared, got %d reports", len(reports)) + } +} + func TestTerminalWithWritePty(t *testing.T) { var received []byte term, err := NewTerminal(WithSize(80, 24), WithWritePty(func(_ *Terminal, data []byte) { -- 2.51.2