Skip to content

Commit f0ae515

Browse files
committed
Fix terminal resize race conditions
The runebuf, search and complete data structures all maintain their own cache of the terminal width and height. When the terminal is resized, a signal is sent to a go routine which calls a function that sets the new width and height to each data structure instance. However, this races with the main thread reading the sizes. Instead of introducing more locks, it makes sense that the terminal itself caches it's width and height and the other structures just get it as necessary. This removes all the racing. As part of this change, search, complete and runebuf constructor changes to no longer require the initial sizes. Also each structure needs a reference to the Terminal so they can get the width/height. As the io.Writer parameter is actually the terminal anyway, the simpliest option was just to change the type from the io.Writer to Terminal. I don't believe that anyone would be calling these functions directly so the signature changes should be ok. I also removed the no longer used OnWidthChange() and OnSizeChange() functions from these three structures.
1 parent 4fda9f0 commit f0ae515

5 files changed

Lines changed: 75 additions & 96 deletions

File tree

complete.go

Lines changed: 25 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"bufio"
55
"bytes"
66
"fmt"
7-
"io"
87
)
98

109
type AutoCompleter interface {
@@ -25,10 +24,8 @@ func (t *TabCompleter) Do([]rune, int) ([][]rune, int) {
2524
}
2625

2726
type opCompleter struct {
28-
w io.Writer
27+
w *Terminal
2928
op *Operation
30-
width int
31-
height int
3229

3330
inCompleteMode bool
3431
inSelectMode bool
@@ -42,12 +39,10 @@ type opCompleter struct {
4239
candidateColWidth int // width of candidate columns
4340
}
4441

45-
func newOpCompleter(w io.Writer, op *Operation, width, height int) *opCompleter {
42+
func newOpCompleter(w *Terminal, op *Operation) *opCompleter {
4643
return &opCompleter{
4744
w: w,
4845
op: op,
49-
width: width,
50-
height: height,
5146
}
5247
}
5348

@@ -73,7 +68,8 @@ func (o *opCompleter) nextCandidate(i int) {
7368
// when tab pressed if cannot do complete for reason such as width unknown
7469
// or no candidates available.
7570
func (o *opCompleter) OnComplete() (ringBell bool) {
76-
if o.width == 0 || o.height < 3 {
71+
tWidth, tHeight := o.w.GetWidthHeight()
72+
if tWidth == 0 || tHeight < 3 {
7773
return false
7874
}
7975
if o.IsInCompleteSelectMode() {
@@ -103,7 +99,7 @@ func (o *opCompleter) OnComplete() (ringBell bool) {
10399
if len(newLines) == 0 || (len(newLines) == 1 && len(newLines[0]) == 0) {
104100
o.ExitCompleteMode(false)
105101
return false // will ring bell on initial tab press
106-
}
102+
}
107103
if o.candidateOff > offset {
108104
// part of buffer we are completing has changed. Example might be that we were completing "ls" and
109105
// user typed space so we are no longer completing "ls" but now we are completing an argument of
@@ -246,15 +242,6 @@ func (o *opCompleter) getMatrixSize() int {
246242
return line * colNum
247243
}
248244

249-
func (o *opCompleter) OnWidthChange(newWidth int) {
250-
o.width = newWidth
251-
}
252-
253-
func (o *opCompleter) OnSizeChange(newWidth, newHeight int) {
254-
o.width = newWidth
255-
o.height = newHeight
256-
}
257-
258245
// setColumnInfo calculates column width and number of columns required
259246
// to present the list of candidates on the terminal.
260247
func (o *opCompleter) setColumnInfo() {
@@ -270,8 +257,10 @@ func (o *opCompleter) setColumnInfo() {
270257
}
271258
colWidth++ // whitespace between cols
272259

260+
tWidth, _ := o.w.GetWidthHeight()
261+
273262
// -1 to avoid end of line issues
274-
width := o.width - 1
263+
width := tWidth - 1
275264
colNum := width / colWidth
276265
if colNum != 0 {
277266
colWidth += (width - (colWidth * colNum)) / colNum
@@ -283,8 +272,9 @@ func (o *opCompleter) setColumnInfo() {
283272

284273
// needPagerMode returns true if number of candidates would go off the page
285274
func (o *opCompleter) needPagerMode() bool {
275+
tWidth, tHeight := o.w.GetWidthHeight()
286276
buflineCnt := o.op.buf.LineCount() // lines taken by buffer content
287-
linesAvail := o.height - buflineCnt // lines available without scrolling buffer off screen
277+
linesAvail := tHeight - buflineCnt // lines available without scrolling buffer off screen
288278
if o.candidateColNum > 0 {
289279
// Normal case where each candidate at least fits on a line
290280
maxOrPage := linesAvail * o.candidateColNum // max candiates without needing to page
@@ -299,9 +289,9 @@ func (o *opCompleter) needPagerMode() bool {
299289
for _, c := range o.candidate {
300290
cWidth := sameWidth + runes.WidthAll(c)
301291
cLines := 1
302-
if o.width > 0 {
303-
cLines = cWidth / o.width
304-
if cWidth % o.width > 0 {
292+
if tWidth > 0 {
293+
cLines = cWidth / tWidth
294+
if cWidth % tWidth > 0 {
305295
cLines++
306296
}
307297
}
@@ -326,6 +316,7 @@ func (o *opCompleter) CompleteRefresh() {
326316
buf.WriteString("\033[J")
327317

328318
same := o.op.buf.RuneSlice(-o.candidateOff)
319+
tWidth, _ := o.w.GetWidthHeight()
329320

330321
colIdx := 0
331322
lines := 0
@@ -334,13 +325,13 @@ func (o *opCompleter) CompleteRefresh() {
334325
inSelect := idx == o.candidateChoise && o.IsInCompleteSelectMode()
335326
cWidth := sameWidth + runes.WidthAll(c)
336327
cLines := 1
337-
if o.width > 0 {
328+
if tWidth > 0 {
338329
sWidth := 0
339330
if isWindows && inSelect {
340331
sWidth = 1 // adjust for hightlighting on Windows
341332
}
342-
cLines = (cWidth + sWidth) / o.width
343-
if (cWidth + sWidth) % o.width > 0 {
333+
cLines = (cWidth + sWidth) / tWidth
334+
if (cWidth + sWidth) % tWidth > 0 {
344335
cLines++
345336
}
346337
}
@@ -403,25 +394,26 @@ func (o *opCompleter) pagerRefresh() (stayInMode bool) {
403394
} else {
404395
// after first page, redraw over --More--
405396
buf.WriteString("\r")
406-
}
397+
}
407398
buf.WriteString("\033[J") // clear anything below
408399

409400
same := o.op.buf.RuneSlice(-o.candidateOff)
410401
sameWidth := runes.WidthAll(same)
402+
tWidth, tHeight := o.w.GetWidthHeight()
411403

412404
colIdx := 0
413405
lines := 1
414406
for ; o.candidateChoise < len(o.candidate) ; o.candidateChoise++ {
415407
c := o.candidate[o.candidateChoise]
416408
cWidth := sameWidth + runes.WidthAll(c)
417409
cLines := 1
418-
if o.width > 0 {
419-
cLines = cWidth / o.width
420-
if cWidth % o.width > 0 {
410+
if tWidth > 0 {
411+
cLines = cWidth / tWidth
412+
if cWidth % tWidth > 0 {
421413
cLines++
422414
}
423415
}
424-
if lines > 1 && lines + cLines > o.height {
416+
if lines > 1 && lines + cLines > tHeight {
425417
break // won't fit on page, stop early.
426418
}
427419
buf.WriteString(string(same))
@@ -460,7 +452,8 @@ func (o *opCompleter) pagerRefresh() (stayInMode bool) {
460452
// we rewrite the prompt it does not over write the page content. The code to rewrite
461453
// the prompt assumes the cursor is at the index line, so we add enough blank lines.
462454
func (o *opCompleter) scrollOutOfPagerMode() {
463-
lineCnt := o.op.buf.IdxLine(o.width)
455+
tWidth, _ := o.w.GetWidthHeight()
456+
lineCnt := o.op.buf.IdxLine(tWidth)
464457
if lineCnt > 0 {
465458
buf := bufio.NewWriter(o.w)
466459
buf.Write(bytes.Repeat([]byte("\n"), lineCnt))

operation.go

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -67,24 +67,18 @@ func (w *wrapWriter) Write(b []byte) (int, error) {
6767
}
6868

6969
func NewOperation(t *Terminal, cfg *Config) *Operation {
70-
width, height := cfg.FuncGetSize()
7170
op := &Operation{
7271
t: t,
73-
buf: NewRuneBuffer(t, cfg.Prompt, cfg, width, height),
72+
buf: NewRuneBuffer(t, cfg.Prompt, cfg),
7473
outchan: make(chan []rune),
7574
errchan: make(chan error, 1),
7675
}
7776
op.w = op.buf.w
7877
op.SetConfig(cfg)
7978
op.opVim = newVimMode(op)
80-
op.opCompleter = newOpCompleter(op.buf.w, op, width, height)
79+
op.opCompleter = newOpCompleter(op.buf.w, op)
8180
op.opPassword = newOpPassword(op)
82-
op.cfg.FuncOnWidthChanged(func() {
83-
newWidth, newHeight := cfg.FuncGetSize()
84-
op.opCompleter.OnSizeChange(newWidth, newHeight)
85-
op.opSearch.OnSizeChange(newWidth, newHeight)
86-
op.buf.OnSizeChange(newWidth, newHeight)
87-
})
81+
op.cfg.FuncOnWidthChanged(t.OnSizeChange)
8882
go op.ioloop()
8983
return op
9084
}
@@ -410,7 +404,7 @@ func (o *Operation) Runes() ([]rune, error) {
410404
// maybe existing text on the same line that ideally we don't
411405
// want to overwrite and cause prompt to jump left. Note that
412406
// this is not perfect but works the majority of the time.
413-
o.buf.getAndSetOffset(o.t)
407+
o.buf.getAndSetOffset()
414408
o.buf.Print() // print prompt & buffer contents
415409
o.t.KickRead()
416410

@@ -494,21 +488,20 @@ func (op *Operation) SetConfig(cfg *Config) (*Config, error) {
494488
op.SetPrompt(cfg.Prompt)
495489
op.SetMaskRune(cfg.MaskRune)
496490
op.buf.SetConfig(cfg)
497-
width, height := op.cfg.FuncGetSize()
498491

499492
if cfg.opHistory == nil {
500493
op.SetHistoryPath(cfg.HistoryFile)
501494
cfg.opHistory = op.history
502-
cfg.opSearch = newOpSearch(op.buf.w, op.buf, op.history, cfg, width, height)
495+
cfg.opSearch = newOpSearch(op.buf.w, op.buf, op.history, cfg)
503496
}
504497
op.history = cfg.opHistory
505498

506499
// SetHistoryPath will close opHistory which already exists
507500
// so if we use it next time, we need to reopen it by `InitHistory()`
508501
op.history.Init()
509502

510-
if op.cfg.AutoComplete != nil {
511-
op.opCompleter = newOpCompleter(op.buf.w, op, width, height)
503+
if op.cfg.AutoComplete != nil && op.opCompleter == nil {
504+
op.opCompleter = newOpCompleter(op.buf.w, op)
512505
}
513506

514507
op.opSearch = cfg.opSearch

runebuf.go

Lines changed: 18 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,11 @@ type RuneBuffer struct {
1818
buf []rune
1919
idx int
2020
prompt []rune
21-
w io.Writer
21+
w *Terminal
2222

2323
interactive bool
2424
cfg *Config
2525

26-
width int
27-
height int
28-
2926
bck *runeBufferBck
3027

3128
offset string // is offset useful? scrolling means row varies
@@ -40,19 +37,6 @@ func (r *RuneBuffer) pushKill(text []rune) {
4037
r.lastKill = append([]rune{}, text...)
4138
}
4239

43-
func (r *RuneBuffer) OnWidthChange(newWidth int) {
44-
r.Lock()
45-
r.width = newWidth
46-
r.Unlock()
47-
}
48-
49-
func (r *RuneBuffer) OnSizeChange(newWidth, newHeight int) {
50-
r.Lock()
51-
r.width = newWidth
52-
r.height = newHeight
53-
r.Unlock()
54-
}
55-
5640
func (r *RuneBuffer) Backup() {
5741
r.Lock()
5842
r.bck = &runeBufferBck{r.buf, r.idx}
@@ -69,13 +53,11 @@ func (r *RuneBuffer) Restore() {
6953
})
7054
}
7155

72-
func NewRuneBuffer(w io.Writer, prompt string, cfg *Config, width int, height int) *RuneBuffer {
56+
func NewRuneBuffer(w *Terminal, prompt string, cfg *Config) *RuneBuffer {
7357
rb := &RuneBuffer{
7458
w: w,
7559
interactive: cfg.useInteractive(),
7660
cfg: cfg,
77-
width: width,
78-
height: height,
7961
}
8062
rb.SetPrompt(prompt)
8163
return rb
@@ -102,9 +84,8 @@ func (r *RuneBuffer) CurrentWidth(x int) int {
10284

10385
func (r *RuneBuffer) PromptLen() int {
10486
r.Lock()
105-
width := r.promptLen()
106-
r.Unlock()
107-
return width
87+
defer r.Unlock()
88+
return r.promptLen()
10889
}
10990

11091
func (r *RuneBuffer) promptLen() int {
@@ -448,12 +429,13 @@ func (r *RuneBuffer) isInLineEdge() bool {
448429
}
449430

450431
func (r *RuneBuffer) getSplitByLine(rs []rune, nextWidth int) [][]rune {
432+
tWidth, _ := r.w.GetWidthHeight()
451433
if r.cfg.EnableMask {
452434
w := runes.Width(r.cfg.MaskRune)
453435
masked := []rune(strings.Repeat(string(r.cfg.MaskRune), len(rs)))
454-
return SplitByLine(runes.ColorFilter(r.prompt), masked, r.ppos, r.width, w)
436+
return SplitByLine(runes.ColorFilter(r.prompt), masked, r.ppos, tWidth, w)
455437
} else {
456-
return SplitByLine(runes.ColorFilter(r.prompt), rs, r.ppos, r.width, nextWidth)
438+
return SplitByLine(runes.ColorFilter(r.prompt), rs, r.ppos, tWidth, nextWidth)
457439
}
458440
}
459441

@@ -476,7 +458,8 @@ func (r *RuneBuffer) idxLine(width int) int {
476458
}
477459

478460
func (r *RuneBuffer) CursorLineCount() int {
479-
return r.LineCount() - r.IdxLine(r.width)
461+
tWidth, _ := r.w.GetWidthHeight()
462+
return r.LineCount() - r.IdxLine(tWidth)
480463
}
481464

482465
func (r *RuneBuffer) Refresh(f func()) {
@@ -506,7 +489,7 @@ func (r *RuneBuffer) refresh(f func()) {
506489
// will write the offset back to us via stdin and there may already be
507490
// other data in the stdin buffer ahead of it.
508491
// This function is called at the start of readline each time.
509-
func (r *RuneBuffer) getAndSetOffset(t *Terminal) {
492+
func (r *RuneBuffer) getAndSetOffset() {
510493
if !r.interactive {
511494
return
512495
}
@@ -517,7 +500,7 @@ func (r *RuneBuffer) getAndSetOffset(t *Terminal) {
517500
// at the beginning of the next line.
518501
r.w.Write([]byte(" \b"))
519502
}
520-
t.GetOffset(r.SetOffset)
503+
r.w.GetOffset(r.SetOffset)
521504
}
522505

523506
func (r *RuneBuffer) SetOffset(offset string) {
@@ -528,8 +511,9 @@ func (r *RuneBuffer) SetOffset(offset string) {
528511

529512
func (r *RuneBuffer) setOffset(offset string) {
530513
r.offset = offset
531-
if _, c, ok := (&escapeKeyPair{attr:offset}).Get2(); ok && c > 0 && c < r.width {
532-
r.ppos = c - 1 // c should be 1..width
514+
tWidth, _ := r.w.GetWidthHeight()
515+
if _, c, ok := (&escapeKeyPair{attr:offset}).Get2(); ok && c > 0 && c < tWidth {
516+
r.ppos = c - 1 // c should be 1..tWidth
533517
} else {
534518
r.ppos = 0
535519
}
@@ -703,7 +687,8 @@ func (r *RuneBuffer) SetPrompt(prompt string) {
703687
func (r *RuneBuffer) cleanOutput(w io.Writer, idxLine int) {
704688
buf := bufio.NewWriter(w)
705689

706-
if r.width == 0 {
690+
tWidth, _ := r.w.GetWidthHeight()
691+
if tWidth == 0 {
707692
buf.WriteString(strings.Repeat("\r\b", len(r.buf)+r.promptLen()))
708693
buf.Write([]byte("\033[J"))
709694
} else {
@@ -724,7 +709,8 @@ func (r *RuneBuffer) Clean() {
724709
}
725710

726711
func (r *RuneBuffer) clean() {
727-
r.cleanWithIdxLine(r.idxLine(r.width))
712+
tWidth, _ := r.w.GetWidthHeight()
713+
r.cleanWithIdxLine(r.idxLine(tWidth))
728714
}
729715

730716
func (r *RuneBuffer) cleanWithIdxLine(idxLine int) {

0 commit comments

Comments
 (0)