Skip to content

Commit cbf1ab2

Browse files
committed
fix(display): integrate helpers and fix scrolling at screen bottom
- Re-integrated hint and completion rendering into the new Refresh cycle. - Implemented robust terminal height probing to handle scrolling at the screen bottom. - Cleaned up unused legacy display methods from the engine. - Exported and standardized default multiline indicator styles.
1 parent 2b1810a commit cbf1ab2

4 files changed

Lines changed: 159 additions & 122 deletions

File tree

internal/display/engine.go

Lines changed: 0 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -66,43 +66,6 @@ func Init(e *Engine, highlighter func([]rune) string) {
6666
e.highlighter = highlighter
6767
}
6868

69-
// refreshOld recomputes and redisplays the entire readline interface, except
70-
// the first lines of the primary prompt when the latter is a multiline one.
71-
func (e *Engine) refreshOld() {
72-
fmt.Print(term.HideCursor)
73-
74-
// Go back to the first column, and if the primary prompt
75-
// was not printed yet, back up to the line's beginning row.
76-
term.MoveCursorBackwards(term.GetWidth())
77-
78-
if !e.primaryPrinted {
79-
term.MoveCursorUp(e.cursorRow)
80-
}
81-
82-
// Print either all or the last line of the prompt.
83-
e.prompt.LastPrint()
84-
85-
// Get all positions required for the redisplay to come:
86-
// prompt end (thus indentation), cursor positions, etc.
87-
e.computeCoordinates(true)
88-
89-
// Print the line, and any of the secondary and right prompts.
90-
e.displayLine()
91-
e.displayMultilinePrompts()
92-
93-
// Display hints and completions, go back
94-
// to the start of the line, then to cursor.
95-
helpersMoved := e.displayHelpers()
96-
if helpersMoved {
97-
e.cursorHintToLineStart()
98-
e.lineStartToCursorPos()
99-
} else {
100-
e.lineEndToCursorPos()
101-
}
102-
103-
fmt.Print(term.ShowCursor)
104-
}
105-
10669
// PrintPrimaryPrompt redraws the primary prompt.
10770
// There are relatively few cases where you want to use this.
10871
// It is currently only used when using clear-screen commands.
@@ -275,71 +238,6 @@ func (e *Engine) displayLine() {
275238
}
276239
}
277240

278-
func (e *Engine) displayMultilinePrompts() {
279-
// If we have more than one line, write the columns.
280-
if e.line.Lines() > 1 {
281-
term.MoveCursorUp(e.lineRows)
282-
term.MoveCursorBackwards(term.GetWidth())
283-
e.prompt.MultilineColumnPrint()
284-
}
285-
286-
// Then if we have a line at all, rewrite the last column
287-
// character with any secondary prompt available.
288-
if e.line.Lines() > 0 {
289-
term.MoveCursorBackwards(term.GetWidth())
290-
e.prompt.SecondaryPrint()
291-
term.MoveCursorBackwards(term.GetWidth())
292-
term.MoveCursorForwards(e.lineCol)
293-
}
294-
295-
// Then prompt the right-sided prompt if possible.
296-
e.prompt.RightPrint(e.lineCol, true)
297-
}
298-
299-
// displayHelpers renders the hint and completion sections.
300-
// It assumes that the cursor is on the last line of input,
301-
// and goes back to this same line after displaying this.
302-
func (e *Engine) displayHelpers() bool {
303-
// Recompute completions and hints if autocompletion is on.
304-
e.completer.Autocomplete()
305-
306-
hintRows := ui.CoordinatesHint(e.hint)
307-
compMatches := e.completer.Matches()
308-
compSkip := e.completer.DisplaySkipped()
309-
310-
if e.hintRows == 0 && e.compRows == 0 && hintRows == 0 && (compMatches == 0 || compSkip) {
311-
return false
312-
}
313-
314-
fmt.Print(term.NewlineReturn)
315-
316-
prevHintRows := e.hintRows
317-
prevCompRows := e.compRows
318-
319-
// Display hint and completions.
320-
ui.DisplayHint(e.hint)
321-
322-
e.hintRows = ui.CoordinatesHint(e.hint)
323-
if compMatches > 0 && !compSkip {
324-
completion.Display(e.completer, e.AvailableHelperLines())
325-
e.compRows = completion.Coordinates(e.completer)
326-
} else {
327-
e.completer.ResetUsedRows()
328-
e.compRows = 0
329-
}
330-
331-
if e.hintRows+e.compRows < prevHintRows+prevCompRows {
332-
fmt.Print(term.ClearScreenBelow)
333-
}
334-
335-
// Go back to the first line below the input line.
336-
term.MoveCursorBackwards(term.GetWidth())
337-
term.MoveCursorUp(e.compRows)
338-
term.MoveCursorUp(e.hintRows)
339-
340-
return true
341-
}
342-
343241
// lineEndToCursorPos moves the cursor from the end of the input line
344242
// to the current cursor position.
345243
func (e *Engine) lineEndToCursorPos() {

internal/display/refresh.go

Lines changed: 124 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import (
55
"strconv"
66

77
"github.com/reeflective/readline/internal/color"
8+
"github.com/reeflective/readline/internal/completion"
89
"github.com/reeflective/readline/internal/core"
910
"github.com/reeflective/readline/internal/strutil"
1011
"github.com/reeflective/readline/internal/term"
12+
"github.com/reeflective/readline/internal/ui"
1113
)
1214

1315
// Refresh recomputes and redisplays the entire readline interface, except
@@ -43,9 +45,9 @@ func (e *Engine) Refresh() {
4345
if e.prompt.LastUsed() == 0 && e.line.Lines() > 0 {
4446
var indicator string
4547
if e.opts.GetBool("multiline-column-numbered") {
46-
indicator = fmt.Sprintf("\x1b[1;30m%d\x1b[0m ", 1)
48+
indicator = fmt.Sprintf(color.FgBlackBright+"%d"+color.Reset+" ", 1)
4749
} else {
48-
indicator = "\x1b[1;30m\U00002502 \x1b[0m"
50+
indicator = ui.DefaultMultilineColumn
4951
}
5052

5153
e.startCols += indicatorWidth
@@ -67,10 +69,53 @@ func (e *Engine) Refresh() {
6769
e.lineCol, e.lineRows = core.CoordinatesLine(e.line, e.startCols)
6870
}
6971

70-
// 3. Input Area Rendering
72+
// Ensure that we have enough space to print the line.
73+
// We probe the terminal to verify that we are not at the bottom of the screen.
74+
// If we are, we scroll the screen to make space for the line.
75+
if e.lineRows > 1 {
76+
// 1. Probe the terminal height.
77+
// We move the cursor down to the last line of the input line,
78+
// and check if the cursor is at the expected position.
79+
term.MoveCursorDown(e.lineRows - 1)
80+
_, actualRow := e.keys.GetCursorPos()
81+
term.MoveCursorUp(e.lineRows - 1)
82+
83+
// 2. Calculate the overshoot.
84+
expectedRow := e.startRows + e.lineRows - 1
85+
overshoot := expectedRow - actualRow
86+
87+
// 3. Scroll the screen if needed.
88+
if overshoot > 0 {
89+
// Move to the bottom of the terminal.
90+
term.MoveCursorDown(actualRow - e.startRows)
91+
92+
// Scroll the screen by printing newlines.
93+
for i := 0; i < overshoot; i++ {
94+
fmt.Print("\n")
95+
}
96+
97+
// Update the start row to reflect the scrolling.
98+
e.startRows -= overshoot
99+
100+
// Move the cursor back up to the new start position.
101+
term.MoveCursorUp(e.lineRows - 1)
102+
term.MoveCursorForwards(e.startCols)
103+
}
104+
}
71105

106+
// 3. Input Area Rendering
72107
e.renderInputArea()
108+
73109
// 4. Helpers Rendering
110+
// We clear everything below the input area to ensure that no artifacts
111+
// from previous renders (like longer lines or helpers) remain visible.
112+
term.MoveCursorDown(1)
113+
term.MoveCursorBackwards(term.GetWidth())
114+
fmt.Print(term.ClearScreenBelow)
115+
term.MoveCursorUp(1)
116+
term.MoveCursorForwards(e.lineCol)
117+
118+
e.renderHelpers()
74119

75120
// 5. Final Cursor Positioning
76121
// The cursor is currently at the end of the input line (lineRows, lineCol).
@@ -88,6 +133,68 @@ func (e *Engine) Refresh() {
88133
func (e *Engine) renderInputArea() {
89134
e.displayLineRefactored()
90135
e.renderMultilineIndicators()
136+
e.renderRightPrompt()
137+
}
138+
139+
func (e *Engine) renderHelpers() {
140+
e.completer.Autocomplete()
141+
142+
// 1. Check if we have anything to print.
143+
hintRows := ui.CoordinatesHint(e.hint)
144+
compMatches := e.completer.Matches()
145+
compSkip := e.completer.DisplaySkipped()
146+
147+
// 2. Clear below the input line to remove artifacts,
148+
// unless we are at the bottom of the screen.
149+
termHeight := term.GetLength()
150+
if (e.startRows + e.lineRows) < termHeight {
151+
term.MoveCursorDown(1)
152+
term.MoveCursorBackwards(term.GetWidth())
153+
fmt.Print(term.ClearScreenBelow)
154+
term.MoveCursorUp(1)
155+
term.MoveCursorForwards(e.lineCol)
156+
}
157+
158+
if hintRows == 0 && (compMatches == 0 || compSkip) {
159+
e.hintRows = 0
160+
e.compRows = 0
161+
162+
return
163+
}
164+
165+
fmt.Print(term.NewlineReturn)
166+
167+
// 3. Display Hints
168+
ui.DisplayHint(e.hint)
169+
e.hintRows = ui.CoordinatesHint(e.hint)
170+
171+
// 4. Display Completions
172+
if compMatches > 0 && !compSkip {
173+
completion.Display(e.completer, e.AvailableHelperLines())
174+
e.compRows = completion.Coordinates(e.completer)
175+
} else {
176+
e.completer.ResetUsedRows()
177+
e.compRows = 0
178+
}
179+
180+
// 5. Restore Cursor to the "bottom of input area"
181+
// The cursor is currently at the bottom of the helpers.
182+
// We need to move it back up to the line just below the input text.
183+
term.MoveCursorUp(e.compRows)
184+
term.MoveCursorUp(e.hintRows)
185+
term.MoveCursorUp(1)
186+
187+
// We are now on the same row as the end of the input line,
188+
// but at column 0. We need to move to e.lineCol.
189+
term.MoveCursorForwards(e.lineCol)
190+
}
191+
192+
func (e *Engine) renderRightPrompt() {
193+
e.prompt.RightPrint(e.lineCol, true)
194+
195+
// Restore cursor to the end of the input line.
196+
term.MoveCursorBackwards(term.GetWidth())
197+
term.MoveCursorForwards(e.lineCol)
91198
}
92199

93200
func (e *Engine) displayLineRefactored() {
@@ -121,43 +228,49 @@ func (e *Engine) renderMultilineIndicators() {
121228
if e.line.Lines() == 0 {
122229
return
123230
}
231+
124232
// 1. Determine if we need to print columns.
125233
columns := e.opts.GetBool("multiline-column") ||
126234
e.opts.GetBool("multiline-column-numbered") ||
127235
e.opts.GetString("multiline-column-custom") != ""
128236
promptEmpty := e.prompt.LastUsed() == 0
237+
129238
// If no columns are requested and the prompt is not empty, we have nothing to do.
130239
if !columns && !promptEmpty {
131240
return
132241
}
242+
133243
// 2. Move to the top of the input area (first line).
134244
term.MoveCursorUp(e.lineRows)
135245
term.MoveCursorBackwards(term.GetWidth())
246+
136247
// 3. Print the indicators for subsequent lines (1..N).
137248
printedLines := 0
138249
numbered := e.opts.GetBool("multiline-column-numbered")
139-
pipe := "\x1b[1;30m\U00002502 \x1b[0m"
140-
angle := "\x1b[1;30m\U00002514 \x1b[0m"
250+
251+
// Indicators
252+
pipe := ui.DefaultMultilineColumn
141253

142254
for i := 1; i <= e.line.Lines(); i++ {
143-
var indicator string
255+
fmt.Print("\n")
256+
144257
if numbered {
145-
indicator = fmt.Sprintf("\x1b[1;30m%d\x1b[0m ", i+1)
258+
fmt.Print(fmt.Sprintf(color.FgBlackBright+"%d"+color.Reset+" ", i+1))
146259
} else if i == e.line.Lines() {
147-
indicator = angle
260+
e.prompt.SecondaryPrint()
148261
} else {
149-
indicator = pipe
262+
fmt.Print(pipe)
150263
}
151264

152-
fmt.Print("\n" + indicator)
153-
154265
printedLines++
155266
}
267+
156268
// 4. Return cursor to the bottom of the input area.
157269
correction := e.lineRows - printedLines
158270
if correction > 0 {
159271
term.MoveCursorDown(correction)
160272
}
273+
161274
// 5. Restore horizontal position to the end of the input text.
162275
term.MoveCursorBackwards(term.GetWidth())
163276
term.MoveCursorForwards(e.lineCol)

internal/ui/prompt.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,18 @@ import (
66
"strings"
77

88
"github.com/reeflective/readline/inputrc"
9+
"github.com/reeflective/readline/internal/color"
910
"github.com/reeflective/readline/internal/core"
1011
"github.com/reeflective/readline/internal/keymap"
1112
"github.com/reeflective/readline/internal/strutil"
1213
"github.com/reeflective/readline/internal/term"
1314
)
1415

15-
const (
16-
secondaryPromptDefault = "\x1b[1;30m\U00002514 \x1b[0m"
17-
multilineColumnDefault = "\x1b[1;30m\U00002502 \x1b[0m"
16+
var (
17+
// DefaultSecondaryPrompt is the default prompt to use for secondary lines.
18+
DefaultSecondaryPrompt = color.FgBlackBright + "\U00002514 " + color.Reset
19+
// DefaultMultilineColumn is the default prompt to use for multiline columns.
20+
DefaultMultilineColumn = color.FgBlackBright + "\U00002502 " + color.Reset
1821
)
1922

2023
// Prompt stores all prompt rendering/generation functions and is
@@ -181,7 +184,7 @@ func (p *Prompt) SecondaryPrint() {
181184
return
182185
}
183186

184-
fmt.Print(secondaryPromptDefault)
187+
fmt.Print(DefaultSecondaryPrompt)
185188
}
186189

187190
// MultilineColumnPrint prints the multiline editor column status indicator.
@@ -195,7 +198,7 @@ func (p *Prompt) MultilineColumnPrint() {
195198
case numbered:
196199
column := ""
197200
for pos := range p.line.Lines() {
198-
column += fmt.Sprintf("\n\x1b[1;30m%d\x1b[0m ", pos+2)
201+
column += fmt.Sprintf("\n"+color.FgBlackBright+"%d"+color.Reset+" ", pos+2)
199202
}
200203
fmt.Print(column)
201204
case len(custom) > 0:
@@ -207,7 +210,7 @@ func (p *Prompt) MultilineColumnPrint() {
207210
case defaultCol:
208211
column := ""
209212
for range p.line.Lines() {
210-
column += "\n" + multilineColumnDefault
213+
column += "\n" + DefaultMultilineColumn
211214
}
212215
fmt.Print(column)
213216
}

0 commit comments

Comments
 (0)