package vt import renderer "github.com/bnema/vev-vt/core" func (s *Screen) putRune(r rune) { if r < 0x20 || (r >= 0x7f && r <= 0x9f) { // REP only repeats a graphic character that directly precedes it. s.lastPrinted = 0 } switch r { case '\a': if s.OnBell != nil { s.OnBell() } return case 0x0e: s.shiftOut = true return case 0x0f: s.shiftOut = false return case 0x00, 0x7f: return case '\r': s.Col = 0 case '\n', '\v', '\f': if s.frame.Width > 0 && s.Col >= s.frame.Width { s.Col = s.frame.Width - 1 } s.buffer.hard(s.Row) s.index() case '\b': if s.Col > 0 { s.Col-- } case '\t': // HT moves to the next tab stop without modifying the frame. Clamp at // the right edge rather than triggering deferred wrap or a scroll. if s.frame.Width > 0 { s.Col = min(s.Col+8-s.Col%8, s.frame.Width-1) } default: // Drop C1 control characters (U+0080-U+009F). if r >= 0x80 && r <= 0x9F { return } if r >= 0x20 { s.putPrintable(s.translateCharset(r)) } } } // relinkWrap restores the soft link a deferred wrap just set when index() // scrolled the row out from under it. buffer.scrollUp severs the last moved // row on purpose — a linefeed there really does stop preceding anything — but // a wrap is the one case where that row flowed into the row below it, so the // link has to be put back after the rotation moved it to s.Row-1. movedUp is // false when nothing survived the scroll, and then there is no row to link. func (s *Screen) relinkWrap(movedUp bool) { if movedUp { s.buffer.continueRow(s.Row - 1) } } func (s *Screen) putPrintable(r rune) { w := renderer.RuneWidth(r) // Skip combining marks and zero-width characters. if w == 0 { return } if s.frame.Width == 0 || s.frame.Height == 0 { return } s.lastPrinted = r // Deferred wrap: cursor sits past the last column. Disabling DECAWM // cancels a pending wrap and leaves subsequent output at the right margin. if s.Col >= s.frame.Width { if s.autoWrapMode { s.buffer.soft(s.Row) s.Col = 0 s.relinkWrap(s.index()) } else { s.Col = s.frame.Width - 1 } } if s.Row >= s.frame.Height { s.Row = s.frame.Height - 1 } // A wide rune must never straddle the right edge. If it does not fit on the // current line, clear the abandoned last cell and wrap to the next line. if w == 2 && s.Col+1 >= s.frame.Width { if s.frame.Width < 2 || !s.autoWrapMode { // A wide rune cannot be represented at the right margin without an // orphaned continuation cell. Use the narrow replacement glyph. r = '\uFFFD' w = renderer.RuneWidth(r) } else { // The abandoned cell may itself be the continuation of a pair whose // left half sits one column back; report damage over the full span. cx := s.Col if s.Col > 0 && s.frame.At(s.Col, s.Row).Continuation { cx = s.Col - 1 } s.clearWidePairAt(s.Col, s.Row) s.frame.Set(s.Col, s.Row, renderer.BlankCell()) s.buffer.truncate(s.Row, cx) s.buffer.continueRow(s.Row) s.record(renderer.Damage{Kind: renderer.DamageText, X: cx, Y: s.Row, Width: s.Col - cx + 1, Height: 1, Count: 1}) s.Col = 0 s.relinkWrap(s.index()) if s.Row >= s.frame.Height { s.Row = s.frame.Height - 1 } } } insertDamageX := s.Col insertDamageWidth := 0 if s.insertMode { leftSplit := s.Col > 0 && s.frame.IsContinuation(s.Col, s.Row) s.frame.CopyRow(s.Row, s.Col+w, s.Col, s.frame.Width-s.Col-w) s.frame.FillRow(s.Row, s.Col, s.Col+w, renderer.BlankCell()) s.repairRow(s.Row) s.buffer.insert(s.Row, s.Col, w) if leftSplit { insertDamageX = s.Col - 1 } insertDamageWidth = s.frame.Width - insertDamageX } // Determine the range of cells actually modified, extending over any wide // pair the write lands on so no orphaned half is left behind. // Cells inside [Col, Col+w) are overwritten below, so only the orphaned // halves just outside it need blanking. lo, hi := s.Col, s.Col+w-1 if s.frame.IsContinuation(s.Col, s.Row) { lo = s.Col - 1 s.frame.Set(lo, s.Row, renderer.BlankCell()) } if right := s.Col + w; right < s.frame.Width && s.frame.IsContinuation(right, s.Row) { hi = right s.frame.Set(right, s.Row, renderer.BlankCell()) } s.frame.Set(s.Col, s.Row, renderer.Cell{Rune: r, Style: s.Style}) s.buffer.content(s.Row, s.Col+w) if w == 2 { s.frame.Set(s.Col+1, s.Row, renderer.Cell{Continuation: true, Style: s.Style}) } if insertDamageWidth > 0 { lo = min(lo, insertDamageX) hi = max(hi, insertDamageX+insertDamageWidth-1) } s.record(renderer.Damage{Kind: renderer.DamageText, X: lo, Y: s.Row, Width: hi - lo + 1, Height: 1, Count: 1}) s.Col += w if !s.autoWrapMode && s.Col >= s.frame.Width { s.Col = s.frame.Width - 1 } } // clearWidePairAt blanks both halves of a wide-character pair when the cell at // (x,y) is either the left (wide) half or the right (continuation) half. It is // O(1) and assumes the pair invariant (a continuation is preceded by its wide // left half) holds — which the writer maintains. func (s *Screen) clearWidePairAt(x, y int) { if x < 0 || x >= s.frame.Width || y < 0 || y >= s.frame.Height { return } if s.frame.At(x, y).Continuation { if x-1 >= 0 { s.frame.Set(x-1, y, renderer.BlankCell()) } s.frame.Set(x, y, renderer.BlankCell()) return } if x+1 < s.frame.Width && s.frame.At(x+1, y).Continuation { s.frame.Set(x, y, renderer.BlankCell()) s.frame.Set(x+1, y, renderer.BlankCell()) } } func (s *Screen) eraseCell() renderer.Cell { style := renderer.DefaultStyle() if s.Style.HasBackgroundRGB { style.HasBackgroundRGB = true style.BackgroundRGB = s.Style.BackgroundRGB } else { style.Background = s.Style.Background } return renderer.Cell{Rune: ' ', Style: style} } // clearRow blanks cells [x0,x1) on row y, extending the range to swallow either // half of a wide pair that straddles a boundary so no orphan half is left. It // returns the actual modified span [start, start+width). func (s *Screen) clearRow(y, x0, x1 int) (start, width int) { if y < 0 || y >= s.frame.Height { return x0, 0 } if x0 < 0 { x0 = 0 } if x1 > s.frame.Width { x1 = s.frame.Width } if x0 >= x1 { return x0, 0 } // Left boundary: a continuation at x0 means its wide left half sits at x0-1. if x0 > 0 && s.frame.At(x0, y).Continuation { x0-- } // Right boundary: a continuation at x1 means its wide left half (at x1-1) is // inside the range and will be blanked, so swallow the continuation too. if x1 < s.frame.Width && s.frame.At(x1, y).Continuation { x1++ } fullRow := x0 <= 0 && x1 >= s.frame.Width blank := s.eraseCell() s.frame.FillRow(y, x0, x1, blank) s.buffer.clear(y, x0, x1) if fullRow { s.buffer.rowIDs[y] = s.nextRowIDValue() } return x0, x1 - x0 } // repairRow blanks any orphaned wide-character halves on row y: a continuation // cell not preceded by its wide left half, or a wide rune not followed by its // continuation. Used after erase/insert/delete operations that may split a pair // at a boundary or by shifting cells. func (s *Screen) repairRow(y int) { repairFrameRow(s.frame, y) } func repairFrameRow(frame renderer.Frame, y int) { if y < 0 || y >= frame.Height { return } w := frame.Width for x := range w { c := frame.At(x, y) if c.Continuation { if x == 0 { frame.Set(x, y, renderer.BlankCell()) continue } left := frame.At(x-1, y) if left.Continuation || renderer.RuneWidth(left.Rune) != 2 { frame.Set(x, y, renderer.BlankCell()) } continue } if renderer.RuneWidth(c.Rune) == 2 { if x+1 >= w || !frame.At(x+1, y).Continuation { frame.Set(x, y, renderer.BlankCell()) } } } }