diff --git a/vt/callbacks.go b/vt/callbacks.go index d1d569da..02385e63 100644 --- a/vt/callbacks.go +++ b/vt/callbacks.go @@ -60,4 +60,8 @@ type Callbacks struct { // DisableMode callback. When set, this function is called when a mode is // disabled. DisableMode func(mode ansi.Mode) + + // Damage callback. When set, this function is called when a region of the + // terminal is damaged. + Damage func(damage Damage) } diff --git a/vt/cc.go b/vt/cc.go index 6a0fb3ce..9eb0c542 100644 --- a/vt/cc.go +++ b/vt/cc.go @@ -7,7 +7,7 @@ import ( // handleControl handles a control character. func (e *Emulator) handleControl(r byte) { - e.flushGrapheme() // Flush any pending grapheme before handling control codes. + e.flushGrapheme(true) // Flush any pending grapheme before handling control codes. if !e.handleCc(r) { e.logf("unhandled sequence: ControlCode %q", r) } @@ -29,6 +29,9 @@ func (e *Emulator) index() { // XXX: Handle scrollback whenever we add it. if y == scroll.Max.Y-1 && x >= scroll.Min.X && x < scroll.Max.X { e.scr.ScrollUp(1) + if e.cb.Damage != nil { + e.cb.Damage(ScrollDamage{Rectangle: e.scr.ScrollRegion(), Dy: -1}) + } } else if y < scroll.Max.Y-1 || !uv.Pos(x, y).In(scroll) { e.scr.moveCursor(0, 1) } @@ -48,6 +51,9 @@ func (e *Emulator) reverseIndex() { scroll := e.scr.ScrollRegion() if y == scroll.Min.Y && x >= scroll.Min.X && x < scroll.Max.X { e.scr.ScrollDown(1) + if e.cb.Damage != nil { + e.cb.Damage(ScrollDamage{Rectangle: e.scr.ScrollRegion(), Dy: 1}) + } } else { e.scr.moveCursor(0, -1) } diff --git a/vt/csi.go b/vt/csi.go index 5bfa3103..7d55f37f 100644 --- a/vt/csi.go +++ b/vt/csi.go @@ -9,7 +9,7 @@ import ( ) func (e *Emulator) handleCsi(cmd ansi.Cmd, params ansi.Params) { - e.flushGrapheme() // Flush any pending grapheme before handling CSI sequences. + e.flushGrapheme(true) // Flush any pending grapheme before handling CSI sequences. if !e.handlers.handleCsi(cmd, params) { e.logf("unhandled sequence: CSI %q", paramsString(cmd, params)) } diff --git a/vt/csi_cursor.go b/vt/csi_cursor.go index 947fef6c..f018f0d2 100644 --- a/vt/csi_cursor.go +++ b/vt/csi_cursor.go @@ -100,11 +100,18 @@ func (e *Emulator) carriageReturn() { // repeatPreviousCharacter repeats the previous character n times. This is // equivalent to typing the same character n times. This performs the same as // [ansi.REP]. -func (e *Emulator) repeatPreviousCharacter(n int) { +// It returns true if the operation was performed, false otherwise. +func (e *Emulator) repeatPreviousCharacter(n int) bool { if e.lastChar == 0 { - return + return false + } + if e.cb.Damage != nil { + x, y := e.scr.CursorPosition() + rect := uv.Rect(x, y, n, 1) + e.cb.Damage(RectDamage(rect)) } for range n { - e.handlePrint(e.lastChar) + e.handleRune(e.lastChar, false) } + return true } diff --git a/vt/csi_screen.go b/vt/csi_screen.go index 5c8e7368..023718fc 100644 --- a/vt/csi_screen.go +++ b/vt/csi_screen.go @@ -15,4 +15,7 @@ func (e *Emulator) eraseCharacter(n int) { e.scr.FillArea(e.scr.blankCell(), rect) e.atPhantom = false // ECH does not move the cursor. + if e.cb.Damage != nil { + e.cb.Damage(RectDamage(rect)) + } } diff --git a/vt/dcs.go b/vt/dcs.go index ff45bec5..74aec416 100644 --- a/vt/dcs.go +++ b/vt/dcs.go @@ -4,7 +4,7 @@ import "github.com/charmbracelet/x/ansi" // handleDcs handles a DCS escape sequence. func (e *Emulator) handleDcs(cmd ansi.Cmd, params ansi.Params, data []byte) { - e.flushGrapheme() // Flush any pending grapheme before handling DCS sequences. + e.flushGrapheme(true) // Flush any pending grapheme before handling DCS sequences. if !e.handlers.handleDcs(cmd, params, data) { e.logf("unhandled sequence: DCS %q %q", paramsString(cmd, params), data) } @@ -12,7 +12,7 @@ func (e *Emulator) handleDcs(cmd ansi.Cmd, params ansi.Params, data []byte) { // handleApc handles an APC escape sequence. func (e *Emulator) handleApc(data []byte) { - e.flushGrapheme() // Flush any pending grapheme before handling APC sequences. + e.flushGrapheme(true) // Flush any pending grapheme before handling APC sequences. if !e.handlers.handleApc(data) { e.logf("unhandled sequence: APC %q", data) } @@ -20,7 +20,7 @@ func (e *Emulator) handleApc(data []byte) { // handleSos handles an SOS escape sequence. func (e *Emulator) handleSos(data []byte) { - e.flushGrapheme() // Flush any pending grapheme before handling SOS sequences. + e.flushGrapheme(true) // Flush any pending grapheme before handling SOS sequences. if !e.handlers.handleSos(data) { e.logf("unhandled sequence: SOS %q", data) } @@ -28,7 +28,7 @@ func (e *Emulator) handleSos(data []byte) { // handlePm handles a PM escape sequence. func (e *Emulator) handlePm(data []byte) { - e.flushGrapheme() // Flush any pending grapheme before handling PM sequences. + e.flushGrapheme(true) // Flush any pending grapheme before handling PM sequences. if !e.handlers.handlePm(data) { e.logf("unhandled sequence: PM %q", data) } diff --git a/vt/emulator.go b/vt/emulator.go index 6a7b5504..93f4ff15 100644 --- a/vt/emulator.go +++ b/vt/emulator.go @@ -274,7 +274,7 @@ func (e *Emulator) Write(p []byte) (n int, err error) { // written the whole byte slice. if len(e.grapheme) > 0 { if (e.lastState == parser.GroundState && state != parser.Utf8State) || i == len(p)-1 { - e.flushGrapheme() + e.flushGrapheme(true) } } e.lastState = state diff --git a/vt/esc.go b/vt/esc.go index 73f94e05..64905fcf 100644 --- a/vt/esc.go +++ b/vt/esc.go @@ -7,7 +7,7 @@ import ( // handleEsc handles an escape sequence. func (e *Emulator) handleEsc(cmd ansi.Cmd) { - e.flushGrapheme() // Flush any pending grapheme before handling ESC sequences. + e.flushGrapheme(true) // Flush any pending grapheme before handling ESC sequences. if !e.handlers.handleEsc(int(cmd)) { var str string if inter := cmd.Intermediate(); inter != 0 { diff --git a/vt/handlers.go b/vt/handlers.go index 60cc797c..712d8a7d 100644 --- a/vt/handlers.go +++ b/vt/handlers.go @@ -461,6 +461,11 @@ func (e *Emulator) registerDefaultCsiHandlers() { // Insert Character [ansi.ICH] n, _, _ := params.Param(0, 1) e.scr.InsertCell(n) + if e.cb.Damage != nil { + x, y := e.scr.CursorPosition() + rect := uv.Rect(x, y, n, 1) + e.cb.Damage(RectDamage(rect)) + } return true }) @@ -551,14 +556,27 @@ func (e *Emulator) registerDefaultCsiHandlers() { rect2 := uv.Rect(0, y+1, width, height-y-1) // next line onwards e.scr.FillArea(e.scr.blankCell(), rect1) e.scr.FillArea(e.scr.blankCell(), rect2) + if e.cb.Damage != nil { + e.cb.Damage(RectDamage(rect1)) + e.cb.Damage(RectDamage(rect2)) + } case 1: // Erase screen above (including cursor) rect := uv.Rect(0, 0, width, y+1) e.scr.FillArea(e.scr.blankCell(), rect) + if e.cb.Damage != nil { + e.cb.Damage(RectDamage(rect)) + } case 2: // erase screen + if e.cb.Damage != nil { + e.cb.Damage(ScreenDamage{Width: width, Height: height}) + } fallthrough case 3: // erase display //nolint:godox // TODO: Scrollback buffer support? + if e.cb.Damage != nil { + e.cb.Damage(ScreenDamage{Width: width, Height: height}) + } e.scr.Clear() default: return false @@ -577,12 +595,22 @@ func (e *Emulator) registerDefaultCsiHandlers() { switch n { case 0: // Erase from cursor to end of line e.eraseCharacter(w - x) + if e.cb.Damage != nil { + rect := uv.Rect(x, y, w-x, 1) + e.cb.Damage(RectDamage(rect)) + } case 1: // Erase from start of line to cursor rect := uv.Rect(0, y, x+1, 1) e.scr.FillArea(e.scr.blankCell(), rect) + if e.cb.Damage != nil { + e.cb.Damage(RectDamage(rect)) + } case 2: // Erase entire line rect := uv.Rect(0, y, w, 1) e.scr.FillArea(e.scr.blankCell(), rect) + if e.cb.Damage != nil { + e.cb.Damage(RectDamage(rect)) + } default: return false } @@ -595,6 +623,13 @@ func (e *Emulator) registerDefaultCsiHandlers() { if e.scr.InsertLine(n) { // Move the cursor to the left margin. e.scr.setCursorX(0, true) + if e.cb.Damage != nil { + width := e.scr.Width() + height := e.scr.Height() + _, y := e.scr.CursorPosition() + rect := uv.Rect(0, y, width, height-y) + e.cb.Damage(RectDamage(rect)) + } } return true }) @@ -607,6 +642,13 @@ func (e *Emulator) registerDefaultCsiHandlers() { // left. // Move the cursor to the left margin. e.scr.setCursorX(0, true) + if e.cb.Damage != nil { + width := e.scr.Width() + height := e.scr.Height() + _, y := e.scr.CursorPosition() + rect := uv.Rect(0, y, width, height-y) + e.cb.Damage(RectDamage(rect)) + } } return true }) @@ -615,20 +657,35 @@ func (e *Emulator) registerDefaultCsiHandlers() { // Delete Character [ansi.DCH] n, _, _ := params.Param(0, 1) e.scr.DeleteCell(n) + if e.cb.Damage != nil { + x, y := e.scr.CursorPosition() + rect := uv.Rect(x, y, n, 1) + e.cb.Damage(RectDamage(rect)) + } return true }) e.RegisterCsiHandler('S', func(params ansi.Params) bool { // Scroll Up [ansi.SU] n, _, _ := params.Param(0, 1) - e.scr.ScrollUp(n) + if e.scr.ScrollUp(n) { + if e.cb.Damage != nil { + rect := e.scr.ScrollRegion() + e.cb.Damage(ScrollDamage{Rectangle: rect, Dy: -n}) + } + } return true }) e.RegisterCsiHandler('T', func(params ansi.Params) bool { // Scroll Down [ansi.SD] n, _, _ := params.Param(0, 1) - e.scr.ScrollDown(n) + if e.scr.ScrollDown(n) { + if e.cb.Damage != nil { + rect := e.scr.ScrollRegion() + e.cb.Damage(ScrollDamage{Rectangle: rect, Dy: n}) + } + } return true }) @@ -645,6 +702,11 @@ func (e *Emulator) registerDefaultCsiHandlers() { // Erase Character [ansi.ECH] n, _, _ := params.Param(0, 1) e.eraseCharacter(n) + if e.cb.Damage != nil { + x, y := e.scr.CursorPosition() + rect := uv.Rect(x, y, n, 1) + e.cb.Damage(RectDamage(rect)) + } return true }) @@ -677,6 +739,11 @@ func (e *Emulator) registerDefaultCsiHandlers() { // Repeat Previous Character [ansi.REP] n, _, _ := params.Param(0, 1) e.repeatPreviousCharacter(n) + if e.cb.Damage != nil { + x, y := e.scr.CursorPosition() + rect := uv.Rect(x, y, n, 1) + e.cb.Damage(RectDamage(rect)) + } return true }) diff --git a/vt/osc.go b/vt/osc.go index 5f2f5859..0e37b1c1 100644 --- a/vt/osc.go +++ b/vt/osc.go @@ -12,7 +12,7 @@ import ( // handleOsc handles an OSC escape sequence. func (e *Emulator) handleOsc(cmd int, data []byte) { - e.flushGrapheme() // Flush any pending grapheme before handling OSC sequences. + e.flushGrapheme(true) // Flush any pending grapheme before handling OSC sequences. if !e.handlers.handleOsc(cmd, data) { e.logf("unhandled sequence: OSC %q", data) } diff --git a/vt/screen.go b/vt/screen.go index a2eae20b..9f261505 100644 --- a/vt/screen.go +++ b/vt/screen.go @@ -253,21 +253,25 @@ func (s *Screen) DeleteCell(n int) { // ScrollUp scrolls the content up n lines within the given region. Lines // scrolled past the top margin are lost. This is equivalent to [ansi.SU] which // moves the cursor to the top margin and performs a [ansi.DL] operation. -func (s *Screen) ScrollUp(n int) { +// It returns true if the operation was successful. +func (s *Screen) ScrollUp(n int) bool { x, y := s.CursorPosition() s.setCursor(s.cur.X, 0, true) - s.DeleteLine(n) + v := s.DeleteLine(n) s.setCursor(x, y, false) + return v } // ScrollDown scrolls the content down n lines within the given region. Lines // scrolled past the bottom margin are lost. This is equivalent to [ansi.SD] // which moves the cursor to top margin and performs a [ansi.IL] operation. -func (s *Screen) ScrollDown(n int) { +// It returns true if the operation was successful. +func (s *Screen) ScrollDown(n int) bool { x, y := s.CursorPosition() s.setCursor(s.cur.X, 0, true) - s.InsertLine(n) + v := s.InsertLine(n) s.setCursor(x, y, false) + return v } // InsertLine inserts n blank lines at the cursor position Y coordinate. diff --git a/vt/utf8.go b/vt/utf8.go index d04c2a70..7083b396 100644 --- a/vt/utf8.go +++ b/vt/utf8.go @@ -9,10 +9,18 @@ import ( // handlePrint handles printable characters. func (e *Emulator) handlePrint(r rune) { + e.handleRune(r, true) +} + +func (e *Emulator) handleRune(r rune, damage bool) { if r >= ansi.SP && r < ansi.DEL { if len(e.grapheme) > 0 { // If we have a grapheme buffer, flush it before handling the ASCII character. - e.flushGrapheme() + e.flushGrapheme(damage) + } + if damage && e.cb.Damage != nil { + x, y := e.scr.CursorPosition() + e.cb.Damage(CellDamage{X: x, Y: y, Width: 1}) } e.handleGrapheme(string(r), 1) } else { @@ -22,7 +30,7 @@ func (e *Emulator) handlePrint(r rune) { // flushGrapheme flushes the current grapheme buffer, if any, and handles the // grapheme as a single unit. -func (e *Emulator) flushGrapheme() { +func (e *Emulator) flushGrapheme(damage bool) { if len(e.grapheme) == 0 { return } @@ -34,6 +42,10 @@ func (e *Emulator) flushGrapheme() { graphemes := string(e.grapheme) for len(graphemes) > 0 { cluster, width := ansi.FirstGraphemeCluster(graphemes, method) + if damage && e.cb.Damage != nil { + x, y := e.scr.CursorPosition() + e.cb.Damage(CellDamage{X: x, Y: y, Width: width}) + } e.handleGrapheme(cluster, width) graphemes = graphemes[len(cluster):] }