Skip to content

Commit

Permalink
More reliable newline behavior && terminal low-level access (#48)
Browse files Browse the repository at this point in the history
* Load default options

* fix stupid

* Fix clearline after input buffer

* Change UNIX newlines with returned newlines in display (more reliable)

* Always use original terminal file descriptor for low-level cursor
requests, and potentially most other virtual terminal sequences.
  • Loading branch information
maxlandon authored Jun 5, 2023
1 parent 84fbbef commit 915d7e5
Show file tree
Hide file tree
Showing 9 changed files with 67 additions and 50 deletions.
12 changes: 6 additions & 6 deletions internal/completion/display.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,14 @@ func (e *Engine) cutCompletionsBelow(scanner *bufio.Scanner, maxRows int) (strin
for scanner.Scan() {
line := scanner.Text()
if count < maxRows-1 {
cropped += line + "\n"
cropped += line + term.NewlineReturn
count++
} else {
break
}
}

cropped = strings.TrimSuffix(cropped, "\n")
cropped = strings.TrimSuffix(cropped, term.NewlineReturn)

// Add hint for remaining completions, if any.
_, used := e.completionCount()
Expand All @@ -95,7 +95,7 @@ func (e *Engine) cutCompletionsBelow(scanner *bufio.Scanner, maxRows int) (strin
return cropped, count - 1
}

cropped += fmt.Sprintf("\n"+color.Dim+color.FgYellow+" %d more completion rows... (scroll down to show)"+color.Reset, remain)
cropped += fmt.Sprintf(term.NewlineReturn+color.Dim+color.FgYellow+" %d more completion rows... (scroll down to show)"+color.Reset, remain)

return cropped, count
}
Expand All @@ -116,14 +116,14 @@ func (e *Engine) cutCompletionsAboveBelow(scanner *bufio.Scanner, maxRows, absPo
}

if count > cutAbove && count <= absPos {
cropped += line + "\n"
cropped += line + term.NewlineReturn
count++
} else {
break
}
}

cropped = strings.TrimSuffix(cropped, "\n")
cropped = strings.TrimSuffix(cropped, term.NewlineReturn)
count -= cutAbove + 1

// Add hint for remaining completions, if any.
Expand All @@ -134,7 +134,7 @@ func (e *Engine) cutCompletionsAboveBelow(scanner *bufio.Scanner, maxRows, absPo
return cropped, count - 1
}

cropped += fmt.Sprintf("\n"+color.Dim+color.FgYellow+" %d more completion rows... (scroll down to show)"+color.Reset, remain)
cropped += fmt.Sprintf(term.NewlineReturn+color.Dim+color.FgYellow+" %d more completion rows... (scroll down to show)"+color.Reset, remain)

return cropped, count
}
12 changes: 3 additions & 9 deletions internal/completion/group.go
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,7 @@ func (g *group) writeComps(eng *Engine) (comp string) {
}

if g.tag != "" {
comp += fmt.Sprintf("%s%s%s %s\n", color.Bold, color.FgYellow, g.tag, color.Reset) + term.ClearLineAfter
comp += fmt.Sprintf("%s%s%s %s", color.Bold, color.FgYellow, g.tag, color.Reset) + term.ClearLineAfter + term.NewlineReturn
eng.usedY++
}

Expand All @@ -561,7 +561,7 @@ func (g *group) writeComps(eng *Engine) (comp string) {
// Generate the completion string for this row (comp/aliases
// and/or descriptions), and apply any styles and isearch
// highlighting with pattern replacement,
comp += g.writeRow(eng, columns) + term.ClearLineAfter
comp += g.writeRow(eng, columns)

columns++
rows++
Expand All @@ -571,12 +571,6 @@ func (g *group) writeComps(eng *Engine) (comp string) {
}
}

// Always add a newline to the group if
// the end if not punctuated with one.
if !strings.HasSuffix(strings.TrimSuffix(comp, term.ClearLineAfter), "\n") {
comp += "\n"
}

eng.usedY += rows

return comp
Expand Down Expand Up @@ -609,7 +603,7 @@ func (g *group) writeRow(eng *Engine, row int) (comp string) {
}
}

comp += "\r\n"
comp += term.ClearLineAfter + term.NewlineReturn

return
}
Expand Down
7 changes: 4 additions & 3 deletions internal/completion/hint.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"strings"

"github.com/reeflective/readline/internal/color"
"github.com/reeflective/readline/internal/term"
)

func (e *Engine) hintCompletions(comps Values) {
Expand All @@ -13,18 +14,18 @@ func (e *Engine) hintCompletions(comps Values) {
// and only if we don't have completions.
if len(comps.values) == 0 || e.config.GetBool("usage-hint-always") {
if comps.Usage != "" {
hint += color.Dim + comps.Usage + color.Reset + "\n"
hint += color.Dim + comps.Usage + color.Reset + term.NewlineReturn
}
}

// And all further messages
hint += strings.Join(comps.Messages.Get(), "\n")
hint += strings.Join(comps.Messages.Get(), term.NewlineReturn)

if e.Matches() == 0 && hint == "" && !e.auto {
hint = e.hintNoMatches()
}

hint = strings.TrimSuffix(hint, "\n")
hint = strings.TrimSuffix(hint, term.NewlineReturn)
if hint == "" {
return
}
Expand Down
2 changes: 1 addition & 1 deletion internal/core/line.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ func DisplayLine(l *Line, indent int) {
if len(line)+indent < term.GetWidth() {
line += term.ClearLineAfter
}
line += "\n"
line += term.NewlineReturn
}

fmt.Print(line)
Expand Down
14 changes: 8 additions & 6 deletions internal/display/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type Engine struct {
hintRows int
compRows int
primaryPrinted bool
termFd uintptr

// UI components
keys *core.Keys
Expand All @@ -50,6 +51,7 @@ type Engine struct {
// NewEngine is a required constructor for the display engine.
func NewEngine(k *core.Keys, s *core.Selection, h *history.Sources, p *ui.Prompt, i *ui.Hint, c *completion.Engine, opts *inputrc.Config) *Engine {
return &Engine{
termFd: os.Stdout.Fd(),
keys: k,
selection: s,
histories: h,
Expand Down Expand Up @@ -143,7 +145,7 @@ func (e *Engine) AcceptLine() {

// Go below this non-suggested line and clear everything.
term.MoveCursorBackwards(term.GetWidth())
fmt.Println()
fmt.Print(term.NewlineReturn)
}

// RefreshTransient goes back to the first line of the input buffer
Expand All @@ -160,7 +162,7 @@ func (e *Engine) RefreshTransient() {
// And redisplay the transient/primary/line.
e.prompt.TransientPrint()
e.displayLine()
fmt.Println()
fmt.Print(term.NewlineReturn)
}

// CursorToLineStart moves the cursor just after the primary prompt.
Expand All @@ -179,7 +181,7 @@ func (e *Engine) CursorToLineStart() {
func (e *Engine) CursorBelowLine() {
term.MoveCursorUp(e.cursorRow)
term.MoveCursorDown(e.lineRows)
fmt.Println()
fmt.Print(term.NewlineReturn)
}

// lineStartToCursorPos can be used if the cursor is currently
Expand Down Expand Up @@ -265,7 +267,7 @@ func (e *Engine) displayLine() {

// Adjust the cursor if the line fits exactly in the terminal width.
if e.lineCol == 0 {
fmt.Println()
fmt.Print(term.NewlineReturn)
fmt.Print(term.ClearLineAfter)
}
}
Expand All @@ -274,7 +276,7 @@ func (e *Engine) displayLine() {
// It assumes that the cursor is on the last line of input,
// and goes back to this same line after displaying this.
func (e *Engine) displayHelpers() {
fmt.Println()
fmt.Print(term.NewlineReturn)

// Recompute completions and hints if autocompletion is on.
e.completer.Autocomplete()
Expand All @@ -294,7 +296,7 @@ func (e *Engine) displayHelpers() {
// AvailableHelperLines returns the number of lines available below the hint section.
// It returns half the terminal space if we currently have less than 1/3rd of it below.
func (e *Engine) AvailableHelperLines() int {
_, termHeight, _ := term.GetSize(int(os.Stdout.Fd()))
_, termHeight, _ := term.GetSize(int(e.termFd))
compLines := termHeight - e.startRows - e.lineRows - e.hintRows

if compLines < (termHeight / oneThirdTerminalHeight) {
Expand Down
4 changes: 4 additions & 0 deletions internal/strutil/split.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package strutil
import (
"bytes"
"errors"
"regexp"
"strings"
"unicode/utf8"
)
Expand All @@ -21,6 +22,9 @@ var (
doubleEscapeChars = "$`\"\n\\"
)

// NewlineMatcher is a regular expression matching all newlines or returned newlines.
var NewlineMatcher = regexp.MustCompile(`\r\n`)

// Split splits a string according to /bin/sh's word-splitting rules. It
// supports backslash-escapes, single-quotes, and double-quotes. Notably it does
// not support the $” style of quoting. It also doesn't attempt to perform any
Expand Down
2 changes: 2 additions & 0 deletions internal/term/codes.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package term

// Terminal control sequences.
const (
NewlineReturn = "\r\n"

ClearLineAfter = "\x1b[0K"
ClearLineBefore = "\x1b[1K"
ClearLine = "\x1b[2K"
Expand Down
19 changes: 17 additions & 2 deletions internal/term/term.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,28 @@ import (
"golang.org/x/term"
)

// Those variables are very important to realine low-level code: all virtual terminal
// escape sequences should always be sent and read through the raw terminal file, even
// if people start using io.MultiWriters and os.Pipes involving basic IO.
var (
stdoutTerm *os.File
stdinTerm *os.File
stderrTerm *os.File
)

func init() {
stdoutTerm = os.Stdout
stdoutTerm = os.Stderr
stderrTerm = os.Stdin
}

// fallback terminal width when we can't get it through query.
var defaultTermWidth = 80

// GetWidth returns the width of Stdout or 80 if the width cannot be established.
func GetWidth() (termWidth int) {
var err error
fd := int(os.Stdout.Fd())
fd := int(stdoutTerm.Fd())
termWidth, _, err = GetSize(fd)

if err != nil {
Expand All @@ -26,7 +41,7 @@ func GetWidth() (termWidth int) {
// GetLength returns the length of the terminal
// (Y length), or 80 if it cannot be established.
func GetLength() int {
width, _, err := term.GetSize(0)
width, _, err := term.GetSize(int(stdoutTerm.Fd()))

if err != nil || width == 0 {
return defaultTermWidth
Expand Down
45 changes: 22 additions & 23 deletions internal/ui/hint.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,53 +86,52 @@ func DisplayHint(hint *Hint) {
return
}

var text string

// Add the various hints.
if len(hint.persistent) > 0 {
text += string(hint.persistent) + "\n"
}

if len(hint.text) > 0 {
text += string(hint.text) + "\n"
}
text := hint.renderHint()

if strutil.RealLength(text) == 0 {
return
}

text = strings.Join(strings.Split(text, "\n"), term.ClearLineAfter+"\n")

text = "\r" + text + term.ClearLineAfter + color.Reset
text += term.ClearLineAfter + color.Reset

if len(text) > 0 {
fmt.Print(text)
}
}

// CoordinatesHint returns the number of terminal rows used by the hint.
func CoordinatesHint(hint *Hint) int {
var text string
func (h *Hint) renderHint() (text string) {
if len(h.persistent) > 0 {
text += string(h.persistent) + term.NewlineReturn
}

// Add the various hints.
if len(hint.persistent) > 0 {
text += string(hint.persistent) + "\n"
if len(h.text) > 0 {
text += string(h.text) + term.NewlineReturn
}

if len(hint.text) > 0 {
text += string(hint.text)
if strutil.RealLength(text) == 0 {
return
}

// Ensure cross-platform, real display newline.
text = strings.ReplaceAll(text, term.NewlineReturn, term.ClearLineAfter+term.NewlineReturn)

return text
}

// CoordinatesHint returns the number of terminal rows used by the hint.
func CoordinatesHint(hint *Hint) int {
text := hint.renderHint()

// Nothing to do if no real text
text = strings.TrimSuffix(text, "\n")
text = strings.TrimSuffix(text, term.ClearLineAfter+term.NewlineReturn)

if strutil.RealLength(text) == 0 {
return 0
}

// Otherwise compute the real length/span.
usedY := 0
lines := strings.Split(text, "\n")
lines := strings.Split(text, term.ClearLineAfter)

for i, line := range lines {
x, y := strutil.LineSpan([]rune(line), i, 0)
Expand Down

0 comments on commit 915d7e5

Please sign in to comment.