@@ -12,6 +12,7 @@ import (
1212
1313 tea "github.com/charmbracelet/bubbletea"
1414 "github.com/charmbracelet/lipgloss"
15+ "github.com/charmbracelet/x/ansi"
1516)
1617
1718// LogViewer component displays and tails log file with scrolling and search
@@ -142,8 +143,8 @@ func (lv *LogViewer) MinWidth() int {
142143
143144// MinHeight returns minimum height
144145func (lv * LogViewer ) MinHeight () int {
145- // Fixed 8 lines + title (1) + footer (1) + border padding (2) + spacing (1) = 13
146- return 13
146+ // Minimum: 3 log lines + title (1) + footer (1) + border (2) = 7
147+ return 7
147148}
148149
149150// Update receives messages
@@ -253,16 +254,19 @@ func (lv *LogViewer) View(w, h int) string {
253254 h = 0
254255 }
255256
256- // Account for border
257- borderWidth := 2
258- contentWidth := w - borderWidth
257+ // Account for border (left + right = 2)
258+ contentWidth := w - 2
259259 if contentWidth < 0 {
260260 contentWidth = 0
261261 }
262262
263- // Don't use MaxHeight - let border render fully
264- // The layout system already allocates the right amount of space
265- rendered := style .Width (contentWidth ).Render (content )
263+ // Set fixed height to prevent content overflow causing flicker
264+ contentHeight := h - 2 // subtract border top + bottom
265+ if contentHeight < 0 {
266+ contentHeight = 0
267+ }
268+
269+ rendered := style .Width (contentWidth ).Height (contentHeight ).Render (content )
266270 lv .UpdateCache (rendered )
267271 return rendered
268272}
@@ -300,10 +304,11 @@ func (lv *LogViewer) renderContent(w, h int) string {
300304 filteredLines = allLines
301305 }
302306
303- // Fixed 8-line display for stable log viewing
304- // This prevents the display from constantly adjusting as logs stream in
305- const fixedLogLines = 8
306- availableLines := fixedLogLines
307+ // Dynamic line count: use allocated height minus border (2), title (1), footer (1)
308+ availableLines := h - 4
309+ if availableLines < 3 {
310+ availableLines = 3
311+ }
307312
308313 // Apply scroll position
309314 totalLines := len (filteredLines )
@@ -341,6 +346,11 @@ func (lv *LogViewer) renderContent(w, h int) string {
341346 styledLines = append (styledLines , styledLine )
342347 }
343348
349+ // Pad to exact line count for stable widget height
350+ for len (styledLines ) < availableLines {
351+ styledLines = append (styledLines , "" )
352+ }
353+
344354 content := strings .Join (styledLines , "\n " )
345355
346356 // Add footer hint
@@ -349,12 +359,12 @@ func (lv *LogViewer) renderContent(w, h int) string {
349359 return fmt .Sprintf ("%s\n %s\n %s" , title , content , footer )
350360}
351361
352- // styleLogLine applies color coding based on log level
362+ // styleLogLine applies color coding based on log level and truncates to maxWidth
353363func (lv * LogViewer ) styleLogLine (line string , maxWidth int ) string {
354- // Don't truncate - let terminal handle line wrapping
355- // This allows users to see full log messages
356-
357364 if lv .noEmoji {
365+ if maxWidth > 0 {
366+ return ansi .Truncate (line , maxWidth , "…" )
367+ }
358368 return line
359369 }
360370
@@ -373,11 +383,19 @@ func (lv *LogViewer) styleLogLine(line string, maxWidth int) string {
373383 } else if strings .Contains (lowerLine , "debug" ) || strings .Contains (lowerLine , "trace" ) || strings .Contains (lowerLine , " dbg " ) {
374384 style = lipgloss .NewStyle ().Foreground (lipgloss .Color ("240" )) // Gray
375385 } else {
376- // Default color
386+ // Default - no color, just truncate
387+ if maxWidth > 0 {
388+ return ansi .Truncate (line , maxWidth , "…" )
389+ }
377390 return line
378391 }
379392
380- return style .Render (line )
393+ // Apply color then truncate (ansi.Truncate is ANSI and cell-width aware)
394+ styled := style .Render (line )
395+ if maxWidth > 0 {
396+ return ansi .Truncate (styled , maxWidth , "…" )
397+ }
398+ return styled
381399}
382400
383401// renderFooter shows control hints
0 commit comments