diff --git a/lib/auth/recordingmetadata/recordingmetadatav1/recordingmetadata.go b/lib/auth/recordingmetadata/recordingmetadatav1/recordingmetadata.go index be38d9921c45e..db2233a1ce8f8 100644 --- a/lib/auth/recordingmetadata/recordingmetadatav1/recordingmetadata.go +++ b/lib/auth/recordingmetadata/recordingmetadatav1/recordingmetadata.go @@ -130,9 +130,15 @@ func (s *RecordingMetadataService) ProcessSessionRecording(ctx context.Context, sampler := newThumbnailBucketSampler(maxThumbnails, thumbnailInterval) recordThumbnail := func(start time.Time) { - state := vt.DumpState() - - sampler.add(&state, start) + cols, rows := vt.Size() + + sampler.add(&thumbnailState{ + svg: terminal.VtToSvg(vt), + cols: cols, + rows: rows, + cursorVisible: vt.CursorVisible(), + cursor: vt.Cursor(), + }, start) } var hasSeenPrintEvent bool @@ -333,12 +339,12 @@ func (s *RecordingMetadataService) upload(ctx context.Context, sessionID session func thumbnailEntryToProto(t *thumbnailEntry) *pb.SessionRecordingThumbnail { return &pb.SessionRecordingThumbnail{ - Svg: terminal.VtStateToSvg(t.state), - Cols: int32(t.state.Cols), - Rows: int32(t.state.Rows), - CursorX: int32(t.state.CursorX), - CursorY: int32(t.state.CursorY), - CursorVisible: t.state.CursorVisible, + Svg: t.state.svg, + Cols: int32(t.state.cols), + Rows: int32(t.state.rows), + CursorX: int32(t.state.cursor.X), + CursorY: int32(t.state.cursor.Y), + CursorVisible: t.state.cursorVisible, StartOffset: durationpb.New(t.startOffset), EndOffset: durationpb.New(t.endOffset), } diff --git a/lib/auth/recordingmetadata/recordingmetadatav1/thumbnails.go b/lib/auth/recordingmetadata/recordingmetadatav1/thumbnails.go index 3f0b8a3d94d2a..c2f8c565f50aa 100644 --- a/lib/auth/recordingmetadata/recordingmetadatav1/thumbnails.go +++ b/lib/auth/recordingmetadata/recordingmetadatav1/thumbnails.go @@ -37,8 +37,15 @@ type thumbnailBucketSampler struct { startTime time.Time } +type thumbnailState struct { + svg []byte + cols, rows int + cursorVisible bool + cursor vt10x.Cursor +} + type thumbnailEntry struct { - state *vt10x.TerminalState + state *thumbnailState startOffset time.Duration endOffset time.Duration timestamp time.Time @@ -56,7 +63,7 @@ func (s *thumbnailBucketSampler) shouldCapture(timestamp time.Time) bool { return !timestamp.Before(s.nextTimestamp) } -func (s *thumbnailBucketSampler) add(state *vt10x.TerminalState, timestamp time.Time) { +func (s *thumbnailBucketSampler) add(state *thumbnailState, timestamp time.Time) { if s.startTime.IsZero() { s.startTime = timestamp } diff --git a/lib/auth/recordingmetadata/recordingmetadatav1/thumbnails_test.go b/lib/auth/recordingmetadata/recordingmetadatav1/thumbnails_test.go index 0898ed5570736..36edee48cf92d 100644 --- a/lib/auth/recordingmetadata/recordingmetadatav1/thumbnails_test.go +++ b/lib/auth/recordingmetadata/recordingmetadatav1/thumbnails_test.go @@ -22,7 +22,6 @@ import ( "testing" "time" - "github.com/hinshun/vt10x" "github.com/stretchr/testify/require" ) @@ -32,7 +31,7 @@ func TestThumbnailBucketSampler_ShouldCapture(t *testing.T) { require.True(t, sampler.shouldCapture(baseTime)) - sampler.add(&vt10x.TerminalState{}, baseTime) + sampler.add(&thumbnailState{}, baseTime) require.False(t, sampler.shouldCapture(baseTime.Add(500*time.Millisecond))) require.False(t, sampler.shouldCapture(baseTime.Add(999*time.Millisecond))) @@ -44,9 +43,9 @@ func TestThumbnailBucketSampler_BasicAdd(t *testing.T) { sampler := newThumbnailBucketSampler(10, time.Second) baseTime := time.Now() - sampler.add(&vt10x.TerminalState{}, baseTime) - sampler.add(&vt10x.TerminalState{}, baseTime.Add(time.Second)) - sampler.add(&vt10x.TerminalState{}, baseTime.Add(2*time.Second)) + sampler.add(&thumbnailState{}, baseTime) + sampler.add(&thumbnailState{}, baseTime.Add(time.Second)) + sampler.add(&thumbnailState{}, baseTime.Add(2*time.Second)) result := sampler.result() require.Len(t, result, 3) @@ -69,13 +68,13 @@ func TestThumbnailBucketSampler_AdaptInterval(t *testing.T) { baseTime := time.Now() for i := 0; i < 4; i++ { - sampler.add(&vt10x.TerminalState{}, baseTime.Add(time.Duration(i)*time.Second)) + sampler.add(&thumbnailState{}, baseTime.Add(time.Duration(i)*time.Second)) } require.Len(t, sampler.entries, 4) require.Equal(t, time.Second, sampler.interval) - sampler.add(&vt10x.TerminalState{}, baseTime.Add(4*time.Second)) + sampler.add(&thumbnailState{}, baseTime.Add(4*time.Second)) require.Len(t, sampler.entries, 3) require.Equal(t, 2*time.Second, sampler.interval) @@ -115,7 +114,7 @@ func TestThumbnailBucketSampler_MultipleAdaptations(t *testing.T) { // Add 1500ms (4 entries) for i := 0; i < 16; i++ { - sampler.add(&vt10x.TerminalState{}, baseTime.Add(time.Duration(i)*100*time.Millisecond)) + sampler.add(&thumbnailState{}, baseTime.Add(time.Duration(i)*100*time.Millisecond)) } require.Equal(t, 6400*time.Millisecond, sampler.interval) diff --git a/lib/terminal/svg.go b/lib/terminal/svg.go index 579a3ecd85130..b8c4fe24a8d3f 100644 --- a/lib/terminal/svg.go +++ b/lib/terminal/svg.go @@ -31,11 +31,11 @@ const ( charRatio = 0.602 // Menlo mono font character width/height ratio ) -// VtStateToSvg converts a terminal state to an SVG representation -func VtStateToSvg(state *vt10x.TerminalState) []byte { +// VtToSvg converts a terminal state to an SVG representation +func VtToSvg(terminal vt10x.Terminal) []byte { var buf bytes.Buffer - cols, rows := state.Cols, state.Rows + cols, rows := terminal.Size() charWidthPx := fontSize * charRatio rowHeightPx := fontSize * lineHeight @@ -45,12 +45,13 @@ func VtStateToSvg(state *vt10x.TerminalState) []byte { closeHeader := writeSVGHeader(&buf, pixelWidth, pixelHeight) var cursor *cursorPos - if state.CursorVisible { - cursor = &cursorPos{x: state.CursorX, y: state.CursorY} + if terminal.CursorVisible() { + c := terminal.Cursor() + cursor = &cursorPos{x: c.X, y: c.Y} } - renderBackgrounds(&buf, state.PrimaryBuffer, cols, rows, charWidthPx, rowHeightPx, cursor) - renderText(&buf, state.PrimaryBuffer, cols, rows, charWidthPx, rowHeightPx, cursor) + renderBackgrounds(&buf, terminal, charWidthPx, rowHeightPx, cursor) + renderText(&buf, terminal, charWidthPx, rowHeightPx, cursor) buf.WriteString(closeHeader()) @@ -73,23 +74,20 @@ func writeSVGHeader(buf *bytes.Buffer, width, height int) func() string { } } -func renderBackgrounds(buf *bytes.Buffer, buffer [][]vt10x.Glyph, cols, rows int, charWidthPx, rowHeightPx float64, cursor *cursorPos) { +func renderBackgrounds(buf *bytes.Buffer, terminal vt10x.Terminal, charWidthPx, rowHeightPx float64, cursor *cursorPos) { type bgRect struct { x, y, w, h float64 color vt10x.Color } var rects []bgRect - for y := 0; y < len(buffer); y++ { - if y >= len(buffer) { - continue - } - + cols, rows := terminal.Size() + for y := range rows { yPos := float64(y) * rowHeightPx var currentRect *bgRect - for x := 0; x < len(buffer[y]); x++ { - glyph := buffer[y][x] + for x := range cols { + glyph := terminal.Cell(x, y) attrs := getTextAttrs(glyph, cursor, x, y) if attrs.background == vt10x.DefaultBG { @@ -133,14 +131,15 @@ func renderBackgrounds(buf *bytes.Buffer, buffer [][]vt10x.Glyph, cols, rows int } } -func renderText(buf *bytes.Buffer, buffer [][]vt10x.Glyph, cols, rows int, charWidthPx, rowHeightPx float64, cursor *cursorPos) { +func renderText(buf *bytes.Buffer, terminal vt10x.Terminal, charWidthPx, rowHeightPx float64, cursor *cursorPos) { buf.WriteString(``) - for y := 0; y < len(buffer); y++ { + cols, rows := terminal.Size() + for y := range rows { // Check if the entire row has any non-space content hasContent := false - for x := 0; x < len(buffer[y]); x++ { - glyph := buffer[y][x] + for x := range cols { + glyph := terminal.Cell(x, y) isCursor := cursor != nil && cursor.x == x && cursor.y == y if (glyph.Char != 0 && glyph.Char != ' ') || isCursor { hasContent = true @@ -156,8 +155,8 @@ func renderText(buf *bytes.Buffer, buffer [][]vt10x.Glyph, cols, rows int, charW fmt.Fprintf(buf, ``, yPos) col := 0 - for col < len(buffer[y]) { - glyph := buffer[y][col] + for col < cols { + glyph := terminal.Cell(col, y) isCursor := cursor != nil && cursor.x == col && cursor.y == y // Skip spaces completely (unless it's the cursor position) @@ -171,8 +170,8 @@ func renderText(buf *bytes.Buffer, buffer [][]vt10x.Glyph, cols, rows int, charW var text strings.Builder // Collect consecutive non-space characters with same attributes - for col < len(buffer[y]) { - currentGlyph := buffer[y][col] + for col < cols { + currentGlyph := terminal.Cell(col, y) currentIsCursor := cursor != nil && cursor.x == col && cursor.y == y // Stop if we hit a space or null (unless cursor) diff --git a/lib/terminal/svg_test.go b/lib/terminal/svg_test.go index 82fb48a73b2c6..8d8749d17903b 100644 --- a/lib/terminal/svg_test.go +++ b/lib/terminal/svg_test.go @@ -253,8 +253,7 @@ func TestTerminalStateToSVG(t *testing.T) { tt.setup(vt) - state := vt.DumpState() - svg := string(VtStateToSvg(&state)) + svg := string(VtToSvg(vt)) if golden.ShouldSet() { golden.Set(t, []byte(svg))