From 8aaf5148ab17b33a72ff3f946c57029c33bdb4d0 Mon Sep 17 00:00:00 2001 From: Chen Asraf Date: Sun, 25 Jan 2026 00:42:51 +0200 Subject: [PATCH] feat: stream new output in-place without resetting existing output --- internal/runner/runner.go | 52 ++++++++++++++++++++++++++++++--------- internal/ui/ui.go | 12 +++++++-- 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/internal/runner/runner.go b/internal/runner/runner.go index 83ebe55..a3a3961 100755 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -178,11 +178,13 @@ func (r *Runner) Run(ctx context.Context) (Result, error) { // StreamingResult holds the state of a streaming command type StreamingResult struct { - Lines *[]Line - ExitCode int - Done bool - Error error - mu sync.RWMutex + Lines *[]Line + ExitCode int + Done bool + Error error + PrevLineCount int // Number of lines from previous run (for trimming) + CurrentLineCount int // Number of lines written by current run + mu sync.RWMutex } // GetLines returns a copy of the current lines (thread-safe) @@ -214,14 +216,27 @@ func (s *StreamingResult) IsDone() bool { return s.Done } +// GetCurrentLineCount returns the number of lines written by the current run (thread-safe) +func (s *StreamingResult) GetCurrentLineCount() int { + s.mu.RLock() + defer s.mu.RUnlock() + return s.CurrentLineCount +} + // RunStreaming executes the command and streams output lines in the background. // Returns a StreamingResult that can be polled for updates. // The command runs until ctx is cancelled or it completes naturally. -func (r *Runner) RunStreaming(ctx context.Context) *StreamingResult { +// If prevLines is provided, lines are updated in place rather than starting fresh. +func (r *Runner) RunStreaming(ctx context.Context, prevLines []Line) *StreamingResult { + // Copy previous lines to allow in-place updates + lines := make([]Line, len(prevLines)) + copy(lines, prevLines) + result := &StreamingResult{ - Lines: &[]Line{}, - ExitCode: -1, - Done: false, + Lines: &lines, + ExitCode: -1, + Done: false, + PrevLineCount: len(prevLines), } go func() { @@ -268,14 +283,27 @@ func (r *Runner) RunStreaming(ctx context.Context) *StreamingResult { for scanner.Scan() { lineNumMu.Lock() currentLineNum := lineNum + lineIdx := lineNum - 1 // 0-indexed lineNum++ lineNumMu.Unlock() - result.mu.Lock() - *result.Lines = append(*result.Lines, Line{ + newLine := Line{ Number: currentLineNum, Content: sanitizeLine(scanner.Text()), - }) + } + + result.mu.Lock() + if lineIdx < len(*result.Lines) { + // Update existing line in place + (*result.Lines)[lineIdx] = newLine + } else { + // Append new line + *result.Lines = append(*result.Lines, newLine) + } + // Track how many lines this run has produced + if currentLineNum > result.CurrentLineCount { + result.CurrentLineCount = currentLineNum + } result.mu.Unlock() } } diff --git a/internal/ui/ui.go b/internal/ui/ui.go index de9b26d..eec415c 100755 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -157,10 +157,11 @@ func (m *model) startStreaming() tea.Cmd { } m.ctx, m.cancel = context.WithCancel(context.Background()) - m.streamResult = m.runner.RunStreaming(m.ctx) + // Pass previous lines for in-place updates + m.streamResult = m.runner.RunStreaming(m.ctx, m.lines) m.streaming = true m.loading = true - m.lastLineCount = 0 + m.lastLineCount = len(m.lines) m.exitCode = -1 m.errorMsg = "" m.userScrolled = false @@ -223,6 +224,13 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.errorMsg = m.streamResult.Error.Error() } + // Trim excess lines from previous run + currentCount := m.streamResult.GetCurrentLineCount() + if currentCount < len(m.lines) { + m.lines = m.lines[:currentCount] + m.updateFiltered() + } + // If auto-refresh is enabled, schedule the next run if m.config.RefreshInterval > 0 { return m, m.tickCmd()