@@ -13,6 +13,8 @@ package fliptree
1313
1414import (
1515 "bufio"
16+ "bytes"
17+ "fmt"
1618 "io"
1719 "slices"
1820 "strings"
@@ -24,6 +26,18 @@ import (
2426// DefaultNodeMarker is the marker used for each node in the tree.
2527var DefaultNodeMarker = ui .NewStyle ().SetString ("□" )
2628
29+ // DefaultScrollUpMarker is the marker shown
30+ // when content is scrolled out above the viewport.
31+ var DefaultScrollUpMarker = ui .NewStyle ().
32+ Foreground (ui .Gray ).
33+ SetString ("▲▲▲" )
34+
35+ // DefaultScrollDownMarker is the marker shown
36+ // when content is scrolled out below the viewport.
37+ var DefaultScrollDownMarker = ui .NewStyle ().
38+ Foreground (ui .Gray ).
39+ SetString ("▼▼▼" )
40+
2741// Graph defines a directed graph.
2842type Graph [T any ] struct {
2943 // Values specifies the value for each node in the graph.
@@ -49,6 +63,15 @@ type Graph[T any] struct {
4963type Options [T any ] struct {
5064 Theme ui.Theme
5165 Style * Style [T ]
66+
67+ // Offset states the number of lines to skip before rendering the tree,
68+ // and Height states the maximum number of lines to render after that.
69+ //
70+ // Use these together to render a view of a larger tree.
71+ //
72+ // A height <= 0 indicates no limit.
73+ // Scroll markers are rendered for a height > 0.
74+ Offset , Height int
5275}
5376
5477// Style configures the visual appearance of the tree.
@@ -61,6 +84,14 @@ type Style[T any] struct {
6184 //
6285 // By default, all nodes are marked with [DefaultNodeMarker].
6386 NodeMarker func (T ) ui.Style
87+
88+ // ScrollUpMarker is shown above the viewport
89+ // when content exists above it.
90+ ScrollUpMarker ui.Style
91+
92+ // ScrollDownMarker is shown below the viewport
93+ // when content exists below it.
94+ ScrollDownMarker ui.Style
6495}
6596
6697// DefaultStyle returns the default style for rendering trees.
@@ -70,6 +101,8 @@ func DefaultStyle[T any]() *Style[T] {
70101 NodeMarker : func (T ) ui.Style {
71102 return DefaultNodeMarker
72103 },
104+ ScrollUpMarker : DefaultScrollUpMarker ,
105+ ScrollDownMarker : DefaultScrollDownMarker ,
73106 }
74107}
75108
@@ -80,15 +113,23 @@ func Write[T any](w io.Writer, g Graph[T], opts Options[T]) error {
80113 }
81114
82115 tw := treeWriter [T ]{
83- w : bufio .NewWriter (w ),
84- g : g ,
85- style : newTreeStyle (opts .Style , opts .Theme ),
116+ w : bufio .NewWriter (w ),
117+ g : g ,
118+ style : newTreeStyle (opts .Style , opts .Theme ),
119+ offset : max (0 , opts .Offset ),
120+ height : opts .Height ,
86121 }
87122 for _ , root := range g .Roots {
88123 if err := tw .writeTree (root , nil , nil ); err != nil {
89124 return err
90125 }
91126 }
127+
128+ if tw .truncatedBelow {
129+ if _ , err := fmt .Fprintln (tw .w , tw .style .ScrollDownMarker .String ()); err != nil {
130+ return err
131+ }
132+ }
92133 return tw .w .Flush ()
93134}
94135
@@ -97,12 +138,38 @@ type treeWriter[T any] struct {
97138 g Graph [T ]
98139
99140 lineNum int
100- style treeStyle [T ]
141+
142+ // Number of rendered tree lines to skip before starting the viewport.
143+ offset int
144+ // Maximum number of tree content lines to write.
145+ // Zero or negative means no viewport limit.
146+ height int
147+
148+ // Number of tree content lines written to the viewport,
149+ // excluding scroll markers.
150+ wroteLines int
151+
152+ // Whether the top scroll marker has already been emitted.
153+ // This is shown once when offset > 0 and the first viewport line is written.
154+ wroteScrollUp bool
155+
156+ // Whether tree content was cut off at the bottom.
157+ // This is true if height > 0,
158+ // and the number of lines to write exceeds the height limit.
159+ //
160+ // This informs the caller whether a bottom scroll marker
161+ // should be emitted after the tree is fully rendered.
162+ truncatedBelow bool
163+ // TODO: maybe we can fold this into the render loop
164+
165+ style treeStyle [T ]
101166}
102167
103168type treeStyle [T any ] struct {
104- Joint lipgloss.Style
105- NodeMarker func (T ) lipgloss.Style
169+ Joint lipgloss.Style
170+ NodeMarker func (T ) lipgloss.Style
171+ ScrollUpMarker lipgloss.Style
172+ ScrollDownMarker lipgloss.Style
106173}
107174
108175func newTreeStyle [T any ](s * Style [T ], theme ui.Theme ) treeStyle [T ] {
@@ -111,6 +178,8 @@ func newTreeStyle[T any](s *Style[T], theme ui.Theme) treeStyle[T] {
111178 NodeMarker : func (v T ) lipgloss.Style {
112179 return s .NodeMarker (v ).Resolve (theme )
113180 },
181+ ScrollUpMarker : s .ScrollUpMarker .Resolve (theme ),
182+ ScrollDownMarker : s .ScrollDownMarker .Resolve (theme ),
114183 }
115184}
116185
@@ -225,25 +294,58 @@ func (tw *treeWriter[T]) writeTree(nodeIdx int, path []int, pathNodeIxes []int)
225294 lastJoint = string (_verticalRight ) + string (_horizontal )
226295 }
227296
228- lines := strings .Split (tw .g .View (nodeValue ), "\n " )
229- for idx , line := range lines {
297+ var lineBuffer bytes.Buffer
298+ firstLine := true
299+ for line := range strings .SplitSeq (tw .g .View (nodeValue ), "\n " ) {
300+ lineBuffer .Reset ()
301+
230302 // The text may be multi-line.
231303 // Only the first line has a title marker.
232- if idx == 0 {
233- tw .pipes (path , lastJoint , titlePrefix )
304+ if firstLine {
305+ firstLine = false
306+ tw .pipes (& lineBuffer , path , lastJoint , titlePrefix )
234307 } else {
235- tw .pipes (path , string (_vertical )+ " " , bodyPrefix )
308+ tw .pipes (& lineBuffer , path , string (_vertical )+ " " , bodyPrefix )
236309 }
237310
238- _ , _ = tw .w .WriteString (line )
239- _ , _ = tw .w .WriteString ("\n " )
240- tw .lineNum ++
311+ lineBuffer .WriteString (line )
312+ tw .writeLine (lineBuffer .Bytes ())
241313 }
242314
243315 return nil
244316}
245317
246- func (tw * treeWriter [T ]) pipes (path []int , joint string , marker string ) {
318+ // writeLine writes a line of the tree to the output,
319+ // respecting the scroll offset and height limits.
320+ //
321+ // This performs necessary bookkeeping internally.
322+ func (tw * treeWriter [T ]) writeLine (line []byte ) {
323+ lineNum := tw .lineNum
324+ tw .lineNum ++
325+
326+ if lineNum < tw .offset {
327+ return
328+ }
329+
330+ if tw .height > 0 && tw .wroteLines >= tw .height {
331+ tw .truncatedBelow = true
332+ return
333+ }
334+
335+ // If content above the viewport is getting cut off,
336+ // we need to add a scroll marker.
337+ if ! tw .wroteScrollUp && tw .offset > 0 {
338+ _ , _ = fmt .Fprintln (tw .w , tw .style .ScrollUpMarker .String ())
339+ tw .wroteScrollUp = true
340+ }
341+
342+ _ , _ = tw .w .Write (line )
343+ _ = tw .w .WriteByte ('\n' )
344+
345+ tw .wroteLines ++
346+ }
347+
348+ func (tw * treeWriter [T ]) pipes (buf * bytes.Buffer , path []int , joint string , marker string ) {
247349 if len (path ) == 0 {
248350 return
249351 }
@@ -254,15 +356,16 @@ func (tw *treeWriter[T]) pipes(path []int, joint string, marker string) {
254356 // needs just connecting pipes.
255357 for _ , pos := range path [:len (path )- 1 ] {
256358 if pos > 0 {
257- _ , _ = tw . w .WriteString (
359+ buf .WriteString (
258360 style .Render (string (_vertical ) + " " ),
259361 )
260362 } else {
261- _ , _ = tw . w .WriteString (" " )
363+ buf .WriteString (" " )
262364 }
263365 }
264366
265- _ , _ = tw .w .WriteString (style .Render (joint ) + marker )
367+ buf .WriteString (style .Render (joint ))
368+ buf .WriteString (marker )
266369}
267370
268371// CycleError is returned when a cycle is detected in the tree.
0 commit comments