Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions vt/callbacks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
8 changes: 7 additions & 1 deletion vt/cc.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

// 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)
}
Expand All @@ -16,7 +16,7 @@
// linefeed is the same as [index], except that it respects [ansi.LNM] mode.
func (e *Emulator) linefeed() {
e.index()
if e.isModeSet(ansi.LineFeedNewLineMode) {

Check failure on line 19 in vt/cc.go

View workflow job for this annotation

GitHub Actions / lint / lint (macos-latest)

SA1019: ansi.LineFeedNewLineMode is deprecated: use [ModeLineFeedNewLine] instead. (staticcheck)
e.carriageReturn()
}
}
Expand All @@ -29,6 +29,9 @@
// 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)
}
Expand All @@ -48,6 +51,9 @@
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)
}
Expand Down
2 changes: 1 addition & 1 deletion vt/csi.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down
13 changes: 10 additions & 3 deletions vt/csi_cursor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
3 changes: 3 additions & 0 deletions vt/csi_screen.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
8 changes: 4 additions & 4 deletions vt/dcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,31 @@ 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)
}
}

// 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)
}
}

// 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)
}
}

// 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)
}
Expand Down
2 changes: 1 addition & 1 deletion vt/emulator.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@
// 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
Expand All @@ -284,7 +284,7 @@

// WriteString writes a string to the terminal output buffer.
func (e *Emulator) WriteString(s string) (n int, err error) {
return e.Write([]byte(s)) //nolint:wrapcheck

Check failure on line 287 in vt/emulator.go

View workflow job for this annotation

GitHub Actions / lint / lint (macos-latest)

directive `//nolint:wrapcheck` is unused for linter "wrapcheck" (nolintlint)
}

// InputPipe returns the terminal's input pipe.
Expand Down
2 changes: 1 addition & 1 deletion vt/esc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
71 changes: 69 additions & 2 deletions vt/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,13 +346,13 @@
func (e *Emulator) registerDefaultEscHandlers() {
e.RegisterEscHandler('=', func() bool {
// Keypad Application Mode [ansi.DECKPAM]
e.setMode(ansi.NumericKeypadMode, ansi.ModeSet)

Check failure on line 349 in vt/handlers.go

View workflow job for this annotation

GitHub Actions / lint / lint (macos-latest)

SA1019: ansi.NumericKeypadMode is deprecated: use [ModeNumericKeypad] instead. (staticcheck)
return true
})

e.RegisterEscHandler('>', func() bool {
// Keypad Numeric Mode [ansi.DECKPNM]
e.setMode(ansi.NumericKeypadMode, ansi.ModeReset)

Check failure on line 355 in vt/handlers.go

View workflow job for this annotation

GitHub Actions / lint / lint (macos-latest)

SA1019: ansi.NumericKeypadMode is deprecated: use [ModeNumericKeypad] instead. (staticcheck)
return true
})

Expand Down Expand Up @@ -461,6 +461,11 @@
// 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
})

Expand Down Expand Up @@ -551,14 +556,27 @@
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
Expand All @@ -577,12 +595,22 @@
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
}
Expand All @@ -595,6 +623,13 @@
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
})
Expand All @@ -607,6 +642,13 @@
// 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
})
Expand All @@ -615,20 +657,35 @@
// 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
})

Expand All @@ -645,6 +702,11 @@
// 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
})

Expand Down Expand Up @@ -677,6 +739,11 @@
// 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
})

Expand Down
2 changes: 1 addition & 1 deletion vt/osc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
12 changes: 8 additions & 4 deletions vt/screen.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 14 additions & 2 deletions vt/utf8.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(hasDamage bool)

if len(e.grapheme) == 0 {
return
}
Expand All @@ -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):]
}
Expand Down
Loading