package app import ( "fmt" "strconv" "strings" "sync" ) // viewportRenderer rewrites child PTY output so it lands inside the // main viewport instead of controlling patterm's full host terminal. type viewportRenderer struct { mu sync.Mutex shifter *cursorShifter layout terminalLayout row int col int state viewportState buf []byte pending strings.Builder } type viewportState int const ( vpNormal viewportState = iota vpEsc vpCSI vpOSC vpOSCEsc vpDCS vpDCSEsc vpSOSPMAPC vpSOSPMAPCEsc ) func newViewportRenderer(l terminalLayout) *viewportRenderer { return &viewportRenderer{ shifter: newCursorShifter(int(l.mainTop) - 1), layout: l, row: 1, col: 1, } } func (vr *viewportRenderer) SetLayout(l terminalLayout) { vr.mu.Lock() defer vr.mu.Unlock() vr.layout = l vr.shifter.SetRowOffset(int(l.mainTop) - 1) } func (vr *viewportRenderer) Render(in []byte) []byte { vr.mu.Lock() defer vr.mu.Unlock() vr.pending.Reset() for _, b := range in { vr.feed(b) } return []byte(vr.pending.String()) } func (vr *viewportRenderer) feed(b byte) { switch vr.state { case vpNormal: if b == 0x1b { vr.state = vpEsc vr.buf = vr.buf[:0] vr.buf = append(vr.buf, b) return } vr.pending.WriteByte(b) vr.advancePrintable(b) case vpEsc: vr.buf = append(vr.buf, b) switch b { case '[': vr.state = vpCSI case ']': vr.state = vpOSC case 'P': vr.state = vpDCS case 'X', '^', '_': vr.state = vpSOSPMAPC default: vr.pending.Write(vr.buf) vr.state = vpNormal vr.buf = vr.buf[:0] } case vpCSI: vr.buf = append(vr.buf, b) if isCSIFinal(b) { vr.emitCSI() vr.state = vpNormal vr.buf = vr.buf[:0] } case vpOSC: vr.buf = append(vr.buf, b) switch b { case 0x07: vr.pending.Write(vr.buf) vr.state = vpNormal vr.buf = vr.buf[:0] case 0x1b: vr.state = vpOSCEsc } case vpOSCEsc: vr.buf = append(vr.buf, b) vr.pending.Write(vr.buf) vr.state = vpNormal vr.buf = vr.buf[:0] case vpDCS: vr.buf = append(vr.buf, b) if b == 0x1b { vr.state = vpDCSEsc } case vpDCSEsc: vr.buf = append(vr.buf, b) vr.pending.Write(vr.buf) vr.state = vpNormal vr.buf = vr.buf[:0] case vpSOSPMAPC: vr.buf = append(vr.buf, b) if b == 0x1b { vr.state = vpSOSPMAPCEsc } case vpSOSPMAPCEsc: vr.buf = append(vr.buf, b) vr.pending.Write(vr.buf) vr.state = vpNormal vr.buf = vr.buf[:0] } } func (vr *viewportRenderer) emitCSI() { if len(vr.buf) < 3 { vr.pending.Write(vr.buf) return } final := vr.buf[len(vr.buf)-1] params := vr.buf[2 : len(vr.buf)-1] if final == 'h' || final == 'l' { if isAltScreenMode(params) { return } } switch final { case 'J': n, ok := parseOneParam(params, 0) if !ok { vr.pending.Write(vr.shifter.Shift(vr.buf)) return } switch n { case 2, 3: vr.pending.WriteString(vr.clearViewport()) default: vr.pending.Write(vr.shifter.Shift(vr.buf)) } case 'K': n, ok := parseOneParam(params, 0) if !ok { vr.pending.Write(vr.shifter.Shift(vr.buf)) return } vr.pending.WriteString(vr.clearLine(n)) default: vr.pending.Write(vr.shifter.Shift(vr.buf)) } vr.trackCSI(final, params) } func isAltScreenMode(params []byte) bool { s := string(params) if !strings.HasPrefix(s, "?") { return false } for _, p := range strings.Split(strings.TrimPrefix(s, "?"), ";") { switch p { case "47", "1047", "1049": return true } } return false } func (vr *viewportRenderer) clearViewport() string { var b strings.Builder b.WriteString("\x1b7") for r := uint16(0); r < vr.layout.childRows(); r++ { fmt.Fprintf(&b, "\x1b[%d;%dH%s", int(vr.layout.mainTop+r), int(vr.layout.mainLeft), strings.Repeat(" ", int(vr.layout.childCols()))) } b.WriteString("\x1b8") return b.String() } func (vr *viewportRenderer) clearLine(n int) string { right := int(vr.layout.childCols()) if vr.col < 1 { vr.col = 1 } if vr.col > right { vr.col = right } switch n { case 0: return "\x1b[" + strconv.Itoa(right-vr.col+1) + "X" case 1: return "\x1b7\r\x1b[" + strconv.Itoa(vr.col) + "X\x1b8" case 2: return "\x1b7\r\x1b[" + strconv.Itoa(right) + "X\x1b8" default: return "\x1b[" + strconv.Itoa(right-vr.col+1) + "X" } } func (vr *viewportRenderer) advancePrintable(b byte) { switch b { case '\r': vr.col = 1 case '\n': vr.row++ case '\b': if vr.col > 1 { vr.col-- } case '\t': vr.col += 8 - ((vr.col - 1) % 8) default: if b >= 0x20 && b != 0x7f { vr.col++ } } vr.clampCursor() } func (vr *viewportRenderer) trackCSI(final byte, params []byte) { switch final { case 'H', 'f': r, c, ok := parseTwoParams(params) if ok { vr.row, vr.col = r, c } case 'G', '`': c, ok := parseOneParam(params, 1) if ok { vr.col = c } case 'd': r, ok := parseOneParam(params, 1) if ok { vr.row = r } case 'A': n, ok := parseOneParam(params, 1) if ok { vr.row -= n } case 'B', 'e': n, ok := parseOneParam(params, 1) if ok { vr.row += n } case 'C', 'a': n, ok := parseOneParam(params, 1) if ok { vr.col += n } case 'D': n, ok := parseOneParam(params, 1) if ok { vr.col -= n } } vr.clampCursor() } func (vr *viewportRenderer) clampCursor() { if vr.row < 1 { vr.row = 1 } if vr.col < 1 { vr.col = 1 } if max := int(vr.layout.childRows()); vr.row > max { vr.row = max } if max := int(vr.layout.childCols()); vr.col > max { vr.col = max } }