wip
This commit is contained in:
@@ -17,6 +17,8 @@ type viewportRenderer struct {
|
||||
col int
|
||||
scrollTop int
|
||||
scrollBottom int
|
||||
originMode bool
|
||||
lrMarginMode bool
|
||||
|
||||
state viewportState
|
||||
buf []byte
|
||||
@@ -75,8 +77,40 @@ func (vr *viewportRenderer) Render(in []byte) []byte {
|
||||
vr.mu.Lock()
|
||||
defer vr.mu.Unlock()
|
||||
vr.pending.Reset()
|
||||
for _, b := range in {
|
||||
vr.feed(b)
|
||||
// Fast path: while we're in vpNormal and have a run of plain ASCII
|
||||
// printables that fit the remaining column budget, copy en bloc
|
||||
// instead of round-tripping each byte through the feed state
|
||||
// machine. UTF-8 leaders and any control byte fall back to the
|
||||
// per-byte path so the cursor/skipUTF8/clamp logic stays exact.
|
||||
for i := 0; i < len(in); {
|
||||
if vr.state == vpNormal {
|
||||
maxCol := int(vr.layout.childCols())
|
||||
if maxCol > 0 && vr.col >= 1 && vr.col <= maxCol {
|
||||
budget := maxCol - vr.col + 1
|
||||
j := i
|
||||
for j < len(in) && budget > 0 {
|
||||
b := in[j]
|
||||
// Pure ASCII printables only — any control byte
|
||||
// (0x1b ESC included), UTF-8 leader, or trailer
|
||||
// kicks back to the state machine.
|
||||
if b < 0x20 || b == 0x7f || b >= 0x80 {
|
||||
break
|
||||
}
|
||||
j++
|
||||
budget--
|
||||
}
|
||||
if j-i >= 4 {
|
||||
vr.pending.Write(in[i:j])
|
||||
vr.col += j - i
|
||||
vr.skipUTF8 = false
|
||||
vr.clampCursor()
|
||||
i = j
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
vr.feed(in[i])
|
||||
i++
|
||||
}
|
||||
return []byte(vr.pending.String())
|
||||
}
|
||||
@@ -192,12 +226,53 @@ func (vr *viewportRenderer) emitCSI() {
|
||||
params := vr.buf[2 : len(vr.buf)-1]
|
||||
|
||||
if final == 'h' || final == 'l' {
|
||||
if isOriginMode(params) {
|
||||
vr.setOriginMode(final == 'h')
|
||||
vr.emitCursorPosition(vr.row, vr.col)
|
||||
return
|
||||
}
|
||||
if isLeftRightMarginMode(params) {
|
||||
vr.lrMarginMode = final == 'h'
|
||||
return
|
||||
}
|
||||
if isAltScreenMode(params) {
|
||||
return
|
||||
}
|
||||
if isMouseTrackingMode(params) {
|
||||
// Patterm owns mouse reporting on the host so wheel events keep
|
||||
// flowing for scroll-viewport. The child's own emulator still
|
||||
// observes the mode set/reset (it processes the same bytes we
|
||||
// hand to ghostty_terminal_vt_write), so we know whether the
|
||||
// child wants mouse input — we just don't let it disarm our
|
||||
// host listener.
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if final == 's' && vr.lrMarginMode {
|
||||
return
|
||||
}
|
||||
|
||||
switch final {
|
||||
case 'H', 'f':
|
||||
r, c, ok := parseTwoParams(params)
|
||||
if !ok {
|
||||
vr.pending.Write(vr.shifter.Shift(vr.buf))
|
||||
return
|
||||
}
|
||||
vr.row = vr.originRow(r)
|
||||
vr.col = c
|
||||
vr.emitCursorPosition(vr.row, c)
|
||||
vr.clampCursor()
|
||||
case 'd':
|
||||
r, ok := parseOneParam(params, 1)
|
||||
if !ok {
|
||||
vr.pending.Write(vr.shifter.Shift(vr.buf))
|
||||
return
|
||||
}
|
||||
vr.row = vr.originRow(r)
|
||||
vr.pending.Write(vr.shifter.Shift([]byte(fmt.Sprintf("\x1b[%dd", vr.row))))
|
||||
vr.clampCursor()
|
||||
case 'J':
|
||||
n, ok := parseOneParam(params, 0)
|
||||
if !ok {
|
||||
@@ -230,10 +305,85 @@ func (vr *viewportRenderer) emitCSI() {
|
||||
// the sidebar is repainted afterwards.
|
||||
vr.pending.Write(vr.shifter.Shift(vr.buf))
|
||||
vr.scrolled = true
|
||||
case 'r':
|
||||
vr.pending.Write(vr.shifter.Shift(vr.buf))
|
||||
if vr.trackScrollRegion(params) {
|
||||
vr.emitHomeAfterScrollRegion()
|
||||
}
|
||||
case 'A', 'B', 'E', 'F':
|
||||
// Relative cursor moves: CUU (A) / CUD (B) / CNL (E) / CPL (F).
|
||||
// The cursor shifter only rewrites absolute positioning, so a
|
||||
// child that asks the cursor to "go up 50" from viewport row 1
|
||||
// would walk the host cursor into the tab bar (and the next
|
||||
// printable would write there). Clamp the step using the
|
||||
// renderer's tracked row so the host cursor stays inside the
|
||||
// viewport. E / F additionally home the column to 1.
|
||||
vr.emitRelativeRowMove(final, params)
|
||||
return
|
||||
default:
|
||||
vr.pending.Write(vr.shifter.Shift(vr.buf))
|
||||
}
|
||||
vr.trackCSI(final, params)
|
||||
if final != 'H' && final != 'f' && final != 'd' && final != 'r' {
|
||||
vr.trackCSI(final, params)
|
||||
}
|
||||
}
|
||||
|
||||
// emitRelativeRowMove rewrites CSI A / B / E / F so the resulting host
|
||||
// cursor stays within rows 1..childRows in viewport coordinates. The
|
||||
// renderer already tracks vr.row for clear-line bookkeeping; reusing
|
||||
// that here avoids a second cursor model. n is normalized — a step of
|
||||
// 0 is treated as 1 to match xterm. After clamping, if the effective
|
||||
// step is zero we drop the sequence (the cursor is already pinned to
|
||||
// the boundary). E / F also move the cursor to column 1 even when no
|
||||
// row step is emitted.
|
||||
func (vr *viewportRenderer) emitRelativeRowMove(final byte, params []byte) {
|
||||
n, ok := parseOneParam(params, 1)
|
||||
if !ok {
|
||||
vr.pending.Write(vr.shifter.Shift(vr.buf))
|
||||
return
|
||||
}
|
||||
if n <= 0 {
|
||||
n = 1
|
||||
}
|
||||
rows := int(vr.layout.childRows())
|
||||
if rows < 1 {
|
||||
rows = 1
|
||||
}
|
||||
row := vr.row
|
||||
if row < 1 {
|
||||
row = 1
|
||||
}
|
||||
if row > rows {
|
||||
row = rows
|
||||
}
|
||||
up := final == 'A' || final == 'F'
|
||||
var safe int
|
||||
if up {
|
||||
safe = row - 1
|
||||
} else {
|
||||
safe = rows - row
|
||||
}
|
||||
if safe < 0 {
|
||||
safe = 0
|
||||
}
|
||||
if n > safe {
|
||||
n = safe
|
||||
}
|
||||
if n > 0 {
|
||||
if up {
|
||||
vr.row -= n
|
||||
} else {
|
||||
vr.row += n
|
||||
}
|
||||
fmt.Fprintf(&vr.pending, "\x1b[%d%c", n, final)
|
||||
}
|
||||
if final == 'E' || final == 'F' {
|
||||
// CNL / CPL anchor the column at 1 regardless of whether the
|
||||
// row step was clamped to zero, matching xterm.
|
||||
vr.col = 1
|
||||
vr.pending.WriteByte('\r')
|
||||
}
|
||||
vr.clampCursor()
|
||||
}
|
||||
|
||||
func isAltScreenMode(params []byte) bool {
|
||||
@@ -250,6 +400,52 @@ func isAltScreenMode(params []byte) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func isOriginMode(params []byte) bool {
|
||||
s := string(params)
|
||||
if !strings.HasPrefix(s, "?") {
|
||||
return false
|
||||
}
|
||||
for _, p := range strings.Split(strings.TrimPrefix(s, "?"), ";") {
|
||||
if p == "6" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isLeftRightMarginMode(params []byte) bool {
|
||||
s := string(params)
|
||||
if !strings.HasPrefix(s, "?") {
|
||||
return false
|
||||
}
|
||||
for _, p := range strings.Split(strings.TrimPrefix(s, "?"), ";") {
|
||||
if p == "69" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isMouseTrackingMode reports whether any of the modes in a CSI ? … h/l
|
||||
// is a mouse-tracking or mouse-encoding DEC private mode. The host runs
|
||||
// with SGR mouse reporting permanently armed; we drop the child's set/
|
||||
// reset for these modes from the host stream so wheel events keep
|
||||
// reaching patterm.
|
||||
func isMouseTrackingMode(params []byte) bool {
|
||||
s := string(params)
|
||||
if !strings.HasPrefix(s, "?") {
|
||||
return false
|
||||
}
|
||||
for _, p := range strings.Split(strings.TrimPrefix(s, "?"), ";") {
|
||||
switch p {
|
||||
case "9", "1000", "1001", "1002", "1003", "1004",
|
||||
"1005", "1006", "1007", "1015", "1016":
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (vr *viewportRenderer) clearViewport() string {
|
||||
var b strings.Builder
|
||||
b.WriteString("\x1b7")
|
||||
@@ -339,6 +535,53 @@ func (vr *viewportRenderer) resetScrollRegion() {
|
||||
}
|
||||
}
|
||||
|
||||
func (vr *viewportRenderer) setOriginMode(on bool) {
|
||||
vr.originMode = on
|
||||
if on {
|
||||
vr.row = vr.scrollTop
|
||||
} else {
|
||||
vr.row = 1
|
||||
}
|
||||
vr.col = 1
|
||||
vr.clampCursor()
|
||||
}
|
||||
|
||||
func (vr *viewportRenderer) originRow(row int) int {
|
||||
if row < 1 {
|
||||
row = 1
|
||||
}
|
||||
if !vr.originMode {
|
||||
return row
|
||||
}
|
||||
row = vr.scrollTop + row - 1
|
||||
if row < vr.scrollTop {
|
||||
row = vr.scrollTop
|
||||
}
|
||||
if row > vr.scrollBottom {
|
||||
row = vr.scrollBottom
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
func (vr *viewportRenderer) homeAfterScrollRegion() {
|
||||
if vr.originMode {
|
||||
vr.row = vr.scrollTop
|
||||
} else {
|
||||
vr.row = 1
|
||||
}
|
||||
vr.col = 1
|
||||
vr.clampCursor()
|
||||
}
|
||||
|
||||
func (vr *viewportRenderer) emitHomeAfterScrollRegion() {
|
||||
vr.homeAfterScrollRegion()
|
||||
vr.emitCursorPosition(vr.row, vr.col)
|
||||
}
|
||||
|
||||
func (vr *viewportRenderer) emitCursorPosition(row, col int) {
|
||||
vr.pending.Write(vr.shifter.Shift([]byte(fmt.Sprintf("\x1b[%d;%dH", row, col))))
|
||||
}
|
||||
|
||||
func (vr *viewportRenderer) lineFeed() {
|
||||
if vr.row >= vr.scrollTop && vr.row == vr.scrollBottom {
|
||||
vr.scrolled = true
|
||||
@@ -426,7 +669,7 @@ func (vr *viewportRenderer) trackCSI(final byte, params []byte) {
|
||||
case 'H', 'f':
|
||||
r, c, ok := parseTwoParams(params)
|
||||
if ok {
|
||||
vr.row, vr.col = r, c
|
||||
vr.row, vr.col = vr.originRow(r), c
|
||||
}
|
||||
case 'G', '`':
|
||||
c, ok := parseOneParam(params, 1)
|
||||
@@ -436,7 +679,7 @@ func (vr *viewportRenderer) trackCSI(final byte, params []byte) {
|
||||
case 'd':
|
||||
r, ok := parseOneParam(params, 1)
|
||||
if ok {
|
||||
vr.row = r
|
||||
vr.row = vr.originRow(r)
|
||||
}
|
||||
case 'A':
|
||||
n, ok := parseOneParam(params, 1)
|
||||
@@ -459,19 +702,21 @@ func (vr *viewportRenderer) trackCSI(final byte, params []byte) {
|
||||
vr.col -= n
|
||||
}
|
||||
case 'r':
|
||||
vr.trackScrollRegion(params)
|
||||
if vr.trackScrollRegion(params) {
|
||||
vr.homeAfterScrollRegion()
|
||||
}
|
||||
}
|
||||
vr.clampCursor()
|
||||
}
|
||||
|
||||
func (vr *viewportRenderer) trackScrollRegion(params []byte) {
|
||||
func (vr *viewportRenderer) trackScrollRegion(params []byte) bool {
|
||||
if len(params) == 0 {
|
||||
vr.resetScrollRegion()
|
||||
return
|
||||
return true
|
||||
}
|
||||
top, bottom, ok := parseTwoParams(params)
|
||||
if !ok {
|
||||
return
|
||||
return false
|
||||
}
|
||||
maxRows := int(vr.layout.childRows())
|
||||
if maxRows < 1 {
|
||||
@@ -484,10 +729,11 @@ func (vr *viewportRenderer) trackScrollRegion(params []byte) {
|
||||
bottom = maxRows
|
||||
}
|
||||
if top >= bottom {
|
||||
return
|
||||
return false
|
||||
}
|
||||
vr.scrollTop = top
|
||||
vr.scrollBottom = bottom
|
||||
return true
|
||||
}
|
||||
|
||||
func (vr *viewportRenderer) clampCursor() {
|
||||
|
||||
Reference in New Issue
Block a user